fix(forecast): §6 honesty layer — pegged-signal plaque + non-residential caveat + segment/pipeline note (#1953)
Из live-walkthrough Anton'а (66:41:0702017:131): дефицит −1.00 «затоварка» на всех
горизонтах — данные КОРРЕКТНЫ (Кировский Комфорт: 12156 доступных ÷ ~100/мес = ~155 мес,
genuine затоварка при ставке 14.5%), но презентация ложно-точная/сбивающая.
B1: deficitPegState helper — детект насыщения (|deficit_index|≥0.99 одного знака на
ВСЕХ горизонтах) → честная плашка «сигнал на пределе шкалы, −1.00 не различает
горизонты, оценка грубая» над 6.1/6.2 + маркер «НА ПРЕДЕЛЕ» вместо ложно-точного ×4.
B2: поднял product_tz.gate_caveat в заметную плашку ВВЕРХ §6 (раньше не рендерился
нигде) — «участок не под жильё → прогноз справочный».
B3: микрокопия почему future=0 (посегментный баланс vs класс-агностичный pipeline §4.3).
Edit-only, контракт не менялся. +8 unit-тестов на pegState.
This commit is contained in:
parent
86f0797499
commit
8fed54971f
5 changed files with 331 additions and 7 deletions
|
|
@ -19,15 +19,25 @@
|
|||
import { Badge } from "@/components/ui/Badge";
|
||||
import type { DemandSupplyForecast } from "@/types/forecast";
|
||||
import {
|
||||
deficitPegPlaque,
|
||||
deficitVariant,
|
||||
deficitWord,
|
||||
fmtNum,
|
||||
isDeficitPegged,
|
||||
type DeficitPegState,
|
||||
} from "./forecast-helpers";
|
||||
|
||||
interface Props {
|
||||
forecasts: DemandSupplyForecast[];
|
||||
/** Horizon (мес) to emphasise as the current target — highlights its row. */
|
||||
targetHorizon?: number;
|
||||
/**
|
||||
* B1 (#1958/#1959) — pegged-signal state (deficit_index уперся в край шкалы на
|
||||
* всех горизонтах). Считается в Section6Forecast по общему ряду и прокидывается
|
||||
* сюда + в 6.2, чтобы плашка «на пределе» не разъезжалась. Когда не задан —
|
||||
* по умолчанию «не pegged» (плашка скрыта).
|
||||
*/
|
||||
peg?: DeficitPegState;
|
||||
}
|
||||
|
||||
const TH_STYLE: React.CSSProperties = {
|
||||
|
|
@ -53,7 +63,11 @@ const TD_STYLE: React.CSSProperties = {
|
|||
|
||||
const NUM_TD: React.CSSProperties = { ...TD_STYLE, textAlign: "right" };
|
||||
|
||||
export function ForecastHorizonsBlock({ forecasts, targetHorizon }: Props) {
|
||||
export function ForecastHorizonsBlock({
|
||||
forecasts,
|
||||
targetHorizon,
|
||||
peg,
|
||||
}: Props) {
|
||||
if (forecasts.length === 0) {
|
||||
return (
|
||||
<p style={{ fontSize: 13, color: "var(--fg-tertiary)", margin: 0 }}>
|
||||
|
|
@ -68,6 +82,10 @@ export function ForecastHorizonsBlock({ forecasts, targetHorizon }: Props) {
|
|||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
{/* B1 — честная плашка, когда индекс упёрся в край шкалы на всех горизонтах:
|
||||
точное «−1.00 ×4» не различает горизонты, оценка грубая. */}
|
||||
{peg?.pegged && peg.sign != null && <DeficitPegPlaque sign={peg.sign} />}
|
||||
|
||||
<div style={{ overflowX: "auto" }}>
|
||||
<table
|
||||
style={{
|
||||
|
|
@ -103,7 +121,11 @@ export function ForecastHorizonsBlock({ forecasts, targetHorizon }: Props) {
|
|||
<td
|
||||
style={
|
||||
isTarget
|
||||
? { ...TD_STYLE, fontWeight: 600, color: "var(--accent)" }
|
||||
? {
|
||||
...TD_STYLE,
|
||||
fontWeight: 600,
|
||||
color: "var(--accent)",
|
||||
}
|
||||
: TD_STYLE
|
||||
}
|
||||
>
|
||||
|
|
@ -137,6 +159,22 @@ export function ForecastHorizonsBlock({ forecasts, targetHorizon }: Props) {
|
|||
>
|
||||
{deficitWord(di)}
|
||||
</span>
|
||||
{/* B1 — значение на краю шкалы: помечаем «на пределе», чтобы
|
||||
одинаковые −1.00 ×4 не читались как ложно-точные. */}
|
||||
{peg?.pegged && isDeficitPegged(di) && (
|
||||
<span
|
||||
style={{
|
||||
marginLeft: 8,
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
letterSpacing: "0.04em",
|
||||
textTransform: "uppercase",
|
||||
color: "var(--fg-tertiary)",
|
||||
}}
|
||||
>
|
||||
на пределе
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td style={NUM_TD}>
|
||||
{Math.round(f.projected_demand_units).toLocaleString("ru")}
|
||||
|
|
@ -165,6 +203,22 @@ export function ForecastHorizonsBlock({ forecasts, targetHorizon }: Props) {
|
|||
уверенность ≤ средней.
|
||||
</p>
|
||||
|
||||
{/* B3 — почему «Прогноз предложения» здесь может быть 0, хотя в §4.3 виден
|
||||
крупный pipeline: баланс §6 посегментный (класс/район), а §4.3 — класс-
|
||||
агностичный будущий ввод. Снимает кажущееся противоречие 4.3 ↔ 6. */}
|
||||
<p
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: 12,
|
||||
lineHeight: "16px",
|
||||
color: "var(--fg-tertiary)",
|
||||
}}
|
||||
>
|
||||
Баланс считается по сегменту (класс/район); класс-агностичный будущий
|
||||
pipeline из раздела 4.3 в посегментный баланс не входит — поэтому
|
||||
будущее предложение здесь может быть 0.
|
||||
</p>
|
||||
|
||||
{/* Rate-sensitivity phrase — shared across horizons in current data; show
|
||||
the distinct phrases once each to avoid noisy repetition. */}
|
||||
<RateSensitivityNotes forecasts={rows} />
|
||||
|
|
@ -172,6 +226,32 @@ export function ForecastHorizonsBlock({ forecasts, targetHorizon }: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* B1 (#1958/#1959) — honest plaque shown above 6.1 / inside 6.2 when the deficit
|
||||
* signal is saturated at the scale edge on every horizon. Exported so 6.2
|
||||
* (ScenariosBlock) renders the identical plaque (one visual source of truth). The
|
||||
* RU text comes from `deficitPegPlaque` (helper) — never hardcoded per block.
|
||||
* Warn pattern (--warn-soft / role="note") как у остальных caveat-плашек §6.
|
||||
*/
|
||||
export function DeficitPegPlaque({ sign }: { sign: -1 | 1 }) {
|
||||
return (
|
||||
<div
|
||||
role="note"
|
||||
style={{
|
||||
padding: "10px 14px",
|
||||
borderRadius: 8,
|
||||
background: "var(--warn-soft, #FEF3C7)",
|
||||
border: "1px solid var(--warn, #9A6700)",
|
||||
fontSize: 12,
|
||||
lineHeight: "16px",
|
||||
color: "var(--warn, #9A6700)",
|
||||
}}
|
||||
>
|
||||
{deficitPegPlaque(sign)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RateSensitivityNotes({
|
||||
forecasts,
|
||||
}: {
|
||||
|
|
|
|||
|
|
@ -21,12 +21,21 @@ import {
|
|||
deficitVariant,
|
||||
deficitWord,
|
||||
fmtNum,
|
||||
type DeficitPegState,
|
||||
} from "./forecast-helpers";
|
||||
import { DeficitPegPlaque } from "./ForecastHorizonsBlock";
|
||||
|
||||
interface Props {
|
||||
scenarios: ReportScenarios;
|
||||
scenariosSummary: ScenariosSummary | null;
|
||||
targetHorizon: number;
|
||||
/**
|
||||
* B1 (#1958/#1959) — pegged-signal state (deficit_index уперся в край шкалы на
|
||||
* всех горизонтах). Тот же объект, что у 6.1 — считается один раз в
|
||||
* Section6Forecast. Когда pegged, над карточками показываем честную плашку
|
||||
* «на пределе», т.к. −1.00 одинаков во всех сценариях.
|
||||
*/
|
||||
peg?: DeficitPegState;
|
||||
}
|
||||
|
||||
// Display order: conservative → base → aggressive (worst-to-best rate path).
|
||||
|
|
@ -80,7 +89,12 @@ export function ScenariosBlock({
|
|||
scenarios,
|
||||
scenariosSummary,
|
||||
targetHorizon,
|
||||
peg,
|
||||
}: Props) {
|
||||
const pegPlaque =
|
||||
peg?.pegged && peg.sign != null ? (
|
||||
<DeficitPegPlaque sign={peg.sign} />
|
||||
) : null;
|
||||
const present = SCENARIO_ORDER.filter(
|
||||
(k) => scenarios.by_scenario[k] != null || scenariosSummary != null,
|
||||
);
|
||||
|
|
@ -109,6 +123,7 @@ export function ScenariosBlock({
|
|||
);
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
{pegPlaque}
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
|
|
@ -157,6 +172,7 @@ export function ScenariosBlock({
|
|||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
{pegPlaque}
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
* advisory disclaimer (footer)
|
||||
*/
|
||||
|
||||
import { LineChart } from "lucide-react";
|
||||
import { AlertTriangle, LineChart } from "lucide-react";
|
||||
|
||||
import { HeadlineBar } from "@/components/ui/HeadlineBar";
|
||||
import { KpiCard } from "@/components/site-finder/KpiCard";
|
||||
|
|
@ -40,6 +40,7 @@ import { ForecastMetaLine } from "./ForecastMetaLine";
|
|||
import { ForecastExportButtons } from "./ForecastExportButtons";
|
||||
import {
|
||||
CONFIDENCE_RU,
|
||||
deficitPegState,
|
||||
deficitVariant,
|
||||
fmtNum,
|
||||
hasSupplySignal,
|
||||
|
|
@ -256,6 +257,17 @@ function ForecastReady({
|
|||
hasScoring ||
|
||||
hasFutureSupplyDetail;
|
||||
|
||||
// B1 (#1958/#1959) — pegged-signal honesty-guard. deficit_index упирается в край
|
||||
// шкалы (|value| ≥ 0.99) на ВСЕХ горизонтах того же знака → «−1.00 ×4» не
|
||||
// различает горизонты. Считаем ОДИН раз тут (источник — общий ряд горизонтов) и
|
||||
// прокидываем в 6.1 (таблица) и 6.2 (сценарии), чтобы плашки не разъезжались.
|
||||
const peg = deficitPegState(fm.forecasts_by_horizon);
|
||||
|
||||
// B2 — gate-caveat участка (см. блокеры: «Нельзя строить МКД»). Бэкенд проставляет
|
||||
// product_tz.gate_caveat, когда жильё под запретом → весь §6-прогноз справочный.
|
||||
// Поднимаем его на уровень ВСЕЙ секции (был бы закопан в 6.4) — заметная плашка.
|
||||
const gateCaveat = pt?.gate_caveat?.trim() || null;
|
||||
|
||||
// Headline subtitle = future_market summary (one sentence «откуда / что значит»).
|
||||
const subtitle = fm.summary ?? undefined;
|
||||
|
||||
|
|
@ -271,6 +283,10 @@ function ForecastReady({
|
|||
gap: 24,
|
||||
}}
|
||||
>
|
||||
{/* B2 — участок не под жильё: прогноз спроса/предложения справочный.
|
||||
Заметная плашка В ВЕРХУ всего §6 (выше KPI), а не закопанная в 6.4. */}
|
||||
{gateCaveat && <NonResidentialCaveat caveat={gateCaveat} />}
|
||||
|
||||
{/* Export / share forecast report (EPIC #959) */}
|
||||
<ForecastExportButtons cad={cad} />
|
||||
|
||||
|
|
@ -330,9 +346,7 @@ function ForecastReady({
|
|||
(exec.overall_confidence ?? kn.confidence) != null
|
||||
? CONFIDENCE_RU[
|
||||
(exec.overall_confidence ?? kn.confidence) as
|
||||
| "high"
|
||||
| "medium"
|
||||
| "low"
|
||||
"high" | "medium" | "low"
|
||||
]
|
||||
: "—"
|
||||
}
|
||||
|
|
@ -370,6 +384,7 @@ function ForecastReady({
|
|||
<ForecastHorizonsBlock
|
||||
forecasts={fm.forecasts_by_horizon}
|
||||
targetHorizon={targetHorizon}
|
||||
peg={peg}
|
||||
/>
|
||||
</SubBlock>
|
||||
)}
|
||||
|
|
@ -381,6 +396,7 @@ function ForecastReady({
|
|||
scenarios={report.scenarios}
|
||||
scenariosSummary={fm.scenarios_summary}
|
||||
targetHorizon={targetHorizon}
|
||||
peg={peg}
|
||||
/>
|
||||
</SubBlock>
|
||||
)}
|
||||
|
|
@ -457,6 +473,60 @@ function ForecastReady({
|
|||
);
|
||||
}
|
||||
|
||||
// ── NonResidentialCaveat — участок не под жильё (B2) ──────────────────────────
|
||||
//
|
||||
// Заметная warn-плашка В ВЕРХУ §6: бэкенд проставил product_tz.gate_caveat, когда
|
||||
// gate=«Нельзя строить МКД». Весь прогноз спроса/предложения тогда — справочный
|
||||
// (считается по сегменту рынка, но строить жильё на участке нельзя). Паттерн
|
||||
// --warn-soft / role="note" как у СЗЗ-баннера / VelocityBlock-caveat; иконка
|
||||
// Lucide (НЕ emoji), токены с fallback. `caveat` — готовый RU-текст от бэкенда;
|
||||
// под ним — наша микрокопия-следствие «прогноз носит справочный характер».
|
||||
|
||||
function NonResidentialCaveat({ caveat }: { caveat: string }) {
|
||||
return (
|
||||
<div
|
||||
role="note"
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 10,
|
||||
alignItems: "flex-start",
|
||||
padding: "12px 16px",
|
||||
borderRadius: 12,
|
||||
background: "var(--warn-soft, #FEF3C7)",
|
||||
border: "1px solid var(--warn, #9A6700)",
|
||||
}}
|
||||
>
|
||||
<AlertTriangle
|
||||
size={18}
|
||||
aria-hidden
|
||||
style={{ flexShrink: 0, marginTop: 1, color: "var(--warn, #9A6700)" }}
|
||||
/>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.5,
|
||||
color: "var(--warn, #9A6700)",
|
||||
}}
|
||||
>
|
||||
Участок не предназначен под жильё (см. блокеры) — прогноз спроса и
|
||||
предложения носит справочный характер.
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 12,
|
||||
lineHeight: 1.5,
|
||||
color: "var(--fg-secondary)",
|
||||
}}
|
||||
>
|
||||
{caveat}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── DeficitLegend — что значит шкала −1…+1 + связь с MOI (#1963) ─────────────
|
||||
//
|
||||
// Плоская строка-легенда под KPI: финдиректор видит «−0.42» / «116.6 мес» без
|
||||
|
|
@ -533,7 +603,9 @@ function DeficitLegend() {
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
<span style={{ fontSize: 12, lineHeight: 1.5, color: "var(--fg-tertiary)" }}>
|
||||
<span
|
||||
style={{ fontSize: 12, lineHeight: 1.5, color: "var(--fg-tertiary)" }}
|
||||
>
|
||||
«Мес. предложения» (MOI) — за сколько месяцев район распродаст текущее
|
||||
предложение при нынешнем темпе продаж: чем больше месяцев, тем сильнее
|
||||
затоварка и ниже индекс.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
/**
|
||||
* Tests for `deficitPegState` / `isDeficitPegged` — B1 honesty-guard (#1958/#1959).
|
||||
*
|
||||
* The §22 deficit_index is clamped to [−1, 1] and saturates at balance_ratio=0.5;
|
||||
* when supply massively dwarfs demand (ratio ≪ 0.5 на всех горизонтах) every
|
||||
* horizon clamps to exactly −1.00 (зеркально +1.00 для острого дефицита). Showing
|
||||
* «−1.00 ×4» reads as broken / falsely precise, so Section6Forecast detects the
|
||||
* saturated state once and renders a honest «на пределе» plaque over 6.1 / 6.2.
|
||||
*
|
||||
* The state is pegged only when EVERY present row is saturated AND same-signed —
|
||||
* a single non-pegged / opposite-sign row means there IS differentiation.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { deficitPegState, isDeficitPegged } from "../forecast-helpers";
|
||||
|
||||
function rows(...values: (number | null)[]) {
|
||||
return values.map(
|
||||
(deficit_index) =>
|
||||
({ deficit_index }) as {
|
||||
deficit_index: number;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
describe("isDeficitPegged — B1 saturation threshold (#1958/#1959)", () => {
|
||||
it("is pegged at ±1.00 and within eps of the edge", () => {
|
||||
expect(isDeficitPegged(-1)).toBe(true);
|
||||
expect(isDeficitPegged(1)).toBe(true);
|
||||
expect(isDeficitPegged(-0.99)).toBe(true);
|
||||
expect(isDeficitPegged(0.995)).toBe(true);
|
||||
});
|
||||
|
||||
it("is NOT pegged below the threshold (real differentiation)", () => {
|
||||
expect(isDeficitPegged(-0.98)).toBe(false);
|
||||
expect(isDeficitPegged(0.5)).toBe(false);
|
||||
expect(isDeficitPegged(0)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deficitPegState — across horizons (#1958/#1959)", () => {
|
||||
it("pegs at −1.00 on all horizons (затоварка) → sign −1", () => {
|
||||
expect(deficitPegState(rows(-1, -1, -1, -1))).toEqual({
|
||||
pegged: true,
|
||||
sign: -1,
|
||||
});
|
||||
});
|
||||
|
||||
it("pegs at +1.00 on all horizons (острый дефицит) → sign +1", () => {
|
||||
expect(deficitPegState(rows(1, 1, 1, 1))).toEqual({
|
||||
pegged: true,
|
||||
sign: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("does NOT peg when a row is mid-range (signal differentiates)", () => {
|
||||
expect(deficitPegState(rows(-1, -1, -0.6, -1))).toEqual({
|
||||
pegged: false,
|
||||
sign: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("does NOT peg when saturated rows have opposite signs", () => {
|
||||
expect(deficitPegState(rows(-1, 1))).toEqual({
|
||||
pegged: false,
|
||||
sign: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores null deficit_index and pegs on present saturated rows only", () => {
|
||||
expect(deficitPegState(rows(null, -1, null, -1))).toEqual({
|
||||
pegged: true,
|
||||
sign: -1,
|
||||
});
|
||||
});
|
||||
|
||||
it("is not pegged when no rows carry a deficit_index", () => {
|
||||
expect(deficitPegState(rows(null, null))).toEqual({
|
||||
pegged: false,
|
||||
sign: null,
|
||||
});
|
||||
expect(deficitPegState([])).toEqual({ pegged: false, sign: null });
|
||||
});
|
||||
});
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
import type { BadgeVariant } from "@/components/ui/Badge";
|
||||
import type {
|
||||
ConfidenceLevel,
|
||||
DemandSupplyForecast,
|
||||
FutureSupply,
|
||||
ProductMixEntry,
|
||||
} from "@/types/forecast";
|
||||
|
|
@ -14,6 +15,77 @@ import type {
|
|||
/** Below this |deficit_index| we treat the market as balanced (neutral). */
|
||||
export const DEFICIT_BALANCE_EPS = 0.05;
|
||||
|
||||
/**
|
||||
* #1958/#1959 honesty-guard — at/above this |deficit_index| the signal is
|
||||
* SATURATED («упёрся» в край шкалы). The backend clamps deficit_index to [−1, 1]
|
||||
* and saturates the index at balance_ratio=0.5; when supply massively dwarfs
|
||||
* demand (ratio ≪ 0.5 на всех горизонтах) every horizon clamps to exactly −1.00
|
||||
* (зеркально +1.00 для острого дефицита). Showing the SAME «−1.00» ×4 reads as
|
||||
* broken / falsely precise — so we mark such values «на пределе» and раскрываем
|
||||
* честную плашку. Mirrors `ForecastChart.DEGENERATE_CLAMP_EPS` (0.02 →
|
||||
* |value| ≥ 0.98) but tightened to 0.99 per the §6 brief (а у графика свой порог).
|
||||
*/
|
||||
export const DEFICIT_PEG_THRESHOLD = 0.99;
|
||||
|
||||
/** Saturated (clamp-floor) signal? `|deficit_index| ≥ DEFICIT_PEG_THRESHOLD`. */
|
||||
export function isDeficitPegged(deficitIndex: number): boolean {
|
||||
return Math.abs(deficitIndex) >= DEFICIT_PEG_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pegged-signal state across a set of horizon rows (HONESTY-GUARD, #1958/#1959).
|
||||
* The signal is «на пределе шкалы» only when EVERY present deficit_index is
|
||||
* saturated (|value| ≥ 0.99) AND all саме знака — then «−1.00 ×4» не различает
|
||||
* горизонты и плашка честнее точного числа. A single non-pegged / opposite-sign
|
||||
* row means there IS differentiation → not pegged (plain table tells the story).
|
||||
*
|
||||
* Returns `{ pegged, sign }`:
|
||||
* pegged — render the honesty plaque + mark values «на пределе»;
|
||||
* sign — −1 затоварка (supply ≫ demand) / +1 острый дефицит, или null when
|
||||
* не pegged. Drives which RU plaque text to show (zeroкально).
|
||||
* PURE (no JSX) — single source of truth shared by 6.1 (table) and 6.2 (scenarios).
|
||||
*/
|
||||
export interface DeficitPegState {
|
||||
pegged: boolean;
|
||||
sign: -1 | 1 | null;
|
||||
}
|
||||
|
||||
export function deficitPegState(
|
||||
forecasts: Pick<DemandSupplyForecast, "deficit_index">[],
|
||||
): DeficitPegState {
|
||||
const present = forecasts
|
||||
.map((f) => f.deficit_index)
|
||||
.filter((v): v is number => v != null);
|
||||
if (present.length === 0) return { pegged: false, sign: null };
|
||||
const allPegged = present.every(isDeficitPegged);
|
||||
if (!allPegged) return { pegged: false, sign: null };
|
||||
const allSameSign =
|
||||
present.every((v) => v > 0) || present.every((v) => v < 0);
|
||||
if (!allSameSign) return { pegged: false, sign: null };
|
||||
return { pegged: true, sign: present[0] > 0 ? 1 : -1 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Human RU plaque text for a pegged deficit signal (#1958/#1959). Финдиректору
|
||||
* объясняем простым языком, что точное «−1.00 ×4» — артефакт упора в край шкалы
|
||||
* (рынок перенасыщен), а не различие горизонтов. Зеркально для +1.0 — острый
|
||||
* дефицит. Источник правды для 6.1 и 6.2 (не дублировать строку в TSX).
|
||||
*/
|
||||
export function deficitPegPlaque(sign: -1 | 1): string {
|
||||
if (sign < 0) {
|
||||
return (
|
||||
"Сигнал на пределе шкалы: предложение во много раз превышает спрос на всех " +
|
||||
"горизонтах — сильная затоварка. Точное −1.00 не различает горизонты (рынок " +
|
||||
"перенасыщен), оценка грубая."
|
||||
);
|
||||
}
|
||||
return (
|
||||
"Сигнал на пределе шкалы: спрос во много раз превышает предложение на всех " +
|
||||
"горизонтах — острый дефицит. Точное +1.00 не различает горизонты (рынок " +
|
||||
"сильно недонасыщен), оценка грубая."
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deficit semantics (HARD): >0 недонасыщенность (повод строить, success);
|
||||
* <0 затоварка (warn/danger); ≈0 баланс (neutral).
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue