diff --git a/tradein-mvp/frontend/src/app/mera-public/__tests__/landing-v3-render.test.tsx b/tradein-mvp/frontend/src/app/mera-public/__tests__/landing-v3-render.test.tsx index 1a476d84..892374e7 100644 --- a/tradein-mvp/frontend/src/app/mera-public/__tests__/landing-v3-render.test.tsx +++ b/tradein-mvp/frontend/src/app/mera-public/__tests__/landing-v3-render.test.tsx @@ -132,6 +132,40 @@ describe("витрина лэндинга v3 без данных", () => { expect(note.textContent).toContain(String(BACKTEST.priceError.sampleN)); }); + /** + * УТВЕРЖДЕНИЕ ПРО ПОЛОСУ ПРОВЕРЯЕТ САМО СЕБЯ ПО ПОКАЗАННЫМ СТРОКАМ. + * + * Границы стоят в коде фронта, строки приходят из БД от последнего прогона + * задачи, а задача в расписании не стоит. Между выкатом фронта и пересчётом + * витрины на странице лежат СТАРЫЕ строки: на проде 12 из 20 вне полосы, + * худшая +75,71 %. Утверждение «показана полоса −5…+20 %» и строка +75,7 % + * под ним — хуже, чем отсутствие утверждения. + * + * Здесь ровно этот набор: одна строка вне полосы (значение с прода) — и про + * полосу не утверждается ничего. Ломать так: сделать `allWithinBand` + * всегда-true — тест покраснеет ПО ЗНАЧЕНИЮ, найдя «отобранная полоса» над + * строкой, которая в неё не входит. + */ + it("«Точность»: строка вне полосы снимает утверждение про полосу, медиана сверки остаётся", () => { + const outOfBand: ShowcaseResponse = { + ...SHOWCASE, + deals: [ + SHOWCASE.deals[0], + { ...SHOWCASE.deals[0], err_pct: 75.71 }, + ], + }; + render(); + const note = screen.getByText(/Разброс показанных строк/u); + expect( + note.textContent, + "подпись обещает полосу, а в таблице под ней строка вне полосы", + ).not.toContain("отобранная полоса расхождения"); + expect(note.textContent).toMatch(/есть расхождения вне неё/u); + // Медиана по всей сверке остаётся в обеих ветках — она и удерживает + // страницу честной, независимо от того, пересчитана витрина или нет. + expect(note.textContent).toContain(BACKTEST.priceError.text); + }); + it("«Точность»: пустой /stats снимает плитки и таблицу, а не обнуляет их", () => { const { container } = render(); expect(screen.queryByText("расчётов сделано")).toBeNull(); @@ -202,24 +236,47 @@ describe("лента сделок: контекст разброса", () => { predicted_rub: Math.round(SHOWCASE.deals[0].fact_rub * (1 + err_pct / 100)), }); + // Значения ВНУТРИ полосы: −11,5 %, стоявшее здесь раньше, ниже её нижней + // границы (−5 %), и лента о такой ленте полосу не утверждает — проверку + // ниже это роняло по делу. Полоса несимметрична, и «маленькое отклонение» + // ещё не значит «в полосе». it("подпись печатает медиану и худшую ровно тех строк, что показаны", () => { - render(); + render(); const note = screen.getByText(/Медиана показанных строк/u); - expect(note.textContent).toContain("11,5 %"); + expect(note.textContent).toContain("3,4 %"); expect(note.textContent).toContain("19,3 %"); }); it("та же лента без худшей строки печатает ДРУГИЕ числа — они не константы", () => { - render(); + render(); const note = screen.getByText(/Медиана показанных строк/u); - expect(note.textContent).toContain("7,8 %"); + expect(note.textContent).toContain("2,3 %"); expect(note.textContent).not.toContain("19,3 %"); }); it("подпись называет полосу — иначе разброс ленты читается как точность расчёта", () => { - render(); + render(); const note = screen.getByText(/Медиана показанных строк/u); expect(note.textContent).toMatch(/от -5 % до \+20 %/u); expect(note.textContent).toContain("не вся сверка"); }); + + /** + * Та же лента с ОДНОЙ строкой вне полосы (значение с прода) — про полосу не + * утверждается ничего, остаются только посчитанные по строкам числа. Это и + * есть окно «фронт выкачен, витрина не пересчитана»: лента прокручивает + * +75,7 % и не имеет права обещать над ним −5…+20 %. + * + * Ломать так: сделать `allWithinBand` всегда-true — тест покраснеет ПО + * ЗНАЧЕНИЮ на обещании полосы рядом с 75,7 %. + */ + it("строка вне полосы снимает утверждение про полосу, числа остаются", () => { + render(); + const note = screen.getByText(/Медиана расхождения показанных строк/u); + expect( + note.textContent, + "лента обещает полосу, а сама прокручивает строку вне неё", + ).not.toMatch(/от -5 % до \+20 %/u); + expect(note.textContent).toContain("75,7 %"); + }); }); diff --git a/tradein-mvp/frontend/src/app/mera-public/_components/v3/AccuracyV3.tsx b/tradein-mvp/frontend/src/app/mera-public/_components/v3/AccuracyV3.tsx index 54db3f3f..9cf2a043 100644 --- a/tradein-mvp/frontend/src/app/mera-public/_components/v3/AccuracyV3.tsx +++ b/tradein-mvp/frontend/src/app/mera-public/_components/v3/AccuracyV3.tsx @@ -29,6 +29,15 @@ * строк по построению лучше работы расчёта, и без второго числа страница * обещала бы точность, которой никто не мерил. * + * УТВЕРЖДЕНИЕ ПРО ПОЛОСУ САМОПРОВЕРЯЕМОЕ. Границы стоят в коде фронта, строки + * приходят из БД от последнего прогона задачи, а задача в расписании не стоит + * — её запускают руками. В окне «фронт выкачен, витрина не пересчитана» + * страница утверждала бы полосу над строками прежнего правила, а под + * утверждением стояла бы строка +75,7 % (на проде сейчас 12 таких из 20). + * Поэтому обе подписи спрашивают сами строки (`allWithinBand` в `deal-view`): + * не соответствуют — про полосу не говорим, называем то, что есть, а правило + * прогона и так печатается рядом и приезжает из ТОГО ЖЕ прогона, что строки. + * * ПЛИТКИ БЭКТЕСТА ИДУТ КОМПЛЕКТОМ И ПОРОЗНЬ НЕ ПОКАЗЫВАЮТСЯ. «88 % в * коридоре» без ширины коридора (±37 %) и без разброса пересборок — это * разные способы выглядеть точнее, чем есть. Поэтому ширина коридора стоит в @@ -70,6 +79,7 @@ import styles from "../../landing-v3.module.css"; import { absPct, + allWithinBand, BAND_MAX_PCT, BAND_MIN_PCT, count, @@ -267,6 +277,18 @@ export function AccuracyV3({ : "Подпись прогона не пришла — из чего отобраны строки, сказать нечем."}

{spread && ( + // УТВЕРЖДЕНИЕ ПРО ПОЛОСУ — САМОПРОВЕРЯЕМОЕ. Границы стоят в коде + // фронта, а строки приходят из БД, из последнего прогона задачи; + // задача в расписании не стоит и запускается руками. Между + // выкатом и пересчётом страница утверждала бы полосу над + // строками, собранными до неё (на проде сейчас 12 из 20 вне + // полосы, худшая +75,7 %) — утверждение и его опровержение в + // одном экране. Поэтому про полосу говорим, только если ни одна + // показанная строка этому не противоречит, а иначе называем то, + // что есть: строки собраны прогоном с другим правилом, и это + // правило напечатано выше — оно приезжает из ТОГО ЖЕ прогона, + // что и строки, поэтому разойтись с ними не может. + // // «В ПРЕДЕЛАХ 20 % — N ИЗ N» ОТСЮДА СНЯТО, И ЭТО НЕ СОКРАЩЕНИЕ. // Полоса витрины — от −5 % до +20 %, значит |отклонение| ≤ 20 у // КАЖДОЙ показанной строки по построению фильтра: счёт всегда @@ -283,7 +305,11 @@ export function AccuracyV3({ // сторожит landing-v3-render («подпись витрины называет полосу и // держит рядом медиану по всей сверке»).

- {`Разброс показанных строк: медианное расхождение ${absPct(spread.medianAbsPct)} по ${spread.n} строкам, худшая ${absPct(spread.worstAbsPct)}. Это отобранная полоса расхождения от ${errPct(BAND_MIN_PCT)} до ${errPct(BAND_MAX_PCT)}, а не вся сверка: промахи крупнее полосы в данных есть, здесь их не видно. Медиана по всей сверке — ${BACKTEST.priceError.text} (${count(BACKTEST.priceError.sampleN)} сделок).`} + {`Разброс показанных строк: медианное расхождение ${absPct(spread.medianAbsPct)} по ${spread.n} строкам, худшая ${absPct(spread.worstAbsPct)}. ${ + allWithinBand(deals) + ? `Это отобранная полоса расхождения от ${errPct(BAND_MIN_PCT)} до ${errPct(BAND_MAX_PCT)}, а не вся сверка: промахи крупнее полосы в данных есть, здесь их не видно.` + : `Полосу расхождения от ${errPct(BAND_MIN_PCT)} до ${errPct(BAND_MAX_PCT)} эта подпись не обещает: среди показанных строк есть расхождения вне неё, то есть витрину собрал прогон с другим правилом — тем, что напечатано выше.` + } Медиана по всей сверке — ${BACKTEST.priceError.text} (${count(BACKTEST.priceError.sampleN)} сделок).`}

)}

{deals[0].note}

diff --git a/tradein-mvp/frontend/src/app/mera-public/_components/v3/DealsTickerV3.tsx b/tradein-mvp/frontend/src/app/mera-public/_components/v3/DealsTickerV3.tsx index 2ee33a49..657dd8fb 100644 --- a/tradein-mvp/frontend/src/app/mera-public/_components/v3/DealsTickerV3.tsx +++ b/tradein-mvp/frontend/src/app/mera-public/_components/v3/DealsTickerV3.tsx @@ -16,8 +16,16 @@ * пишет только сделки с расхождением от −5 % до +20 % (`select_rows`/`BAND_*` * в app/tasks/landing_showcase_deals.py), внутри полосы порядок задают полнота * и свежесть. Лента берёт ТЕ ЖЕ строки, поэтому её содержимое отобрано ровно - * так же — и «худшая» в подписи ниже теперь ограничена полосой сверху, а не - * данными. + * так же. + * + * НО УТВЕРЖДАЕМ МЫ ЭТО НЕ НА ВЕРУ. Границы живут в коде фронта, а строки + * приезжают из БД от ПОСЛЕДНЕГО прогона задачи — и задача в расписании не + * стоит, её запускают руками. Значит между выкатом фронта и пересчётом + * витрины лента утверждала бы полосу над строками, собранными по прежнему + * правилу (на проде сейчас 12 из 20 таких, худшая +75,7 %), и тут же + * прокручивала бы опровержение мимо собственной подписи. Поэтому подпись + * спрашивает сами строки (`allWithinBand`) и говорит про полосу, только если + * ни одна из них этому не противоречит. * * ПОД лентой — подпись с разбросом ПОКАЗАННЫХ строк (`shownSpread`, та же * функция, что и под таблицей сверок): медиана модуля и худшая. До полосы она @@ -44,6 +52,7 @@ import styles from "../../landing-v3.module.css"; import { absPct, + allWithinBand, BAND_MAX_PCT, BAND_MIN_PCT, dealTitle, @@ -85,8 +94,16 @@ export function DealsTickerV3({ deals }: { deals: readonly ShowcaseDeal[] }) { {spread && ( + // Про полосу говорим, только если ЛЕНТА ей соответствует. Границы + // стоят в коде, строки приходят из БД от последнего прогона задачи, а + // задача в расписании не стоит: между выкатом и пересчётом лента + // утверждала бы полосу над строками, собранными до неё, — и тут же + // прокручивала +75,7 % мимо этого утверждения. Не соответствует — + // печатаем только то, что посчитано по самим строкам.

- {`Показаны сделки с расхождением от ${errPct(BAND_MIN_PCT)} до ${errPct(BAND_MAX_PCT)} — отобранная полоса, не вся сверка. Медиана показанных строк — ${absPct(spread.medianAbsPct)}, худшая — ${absPct(spread.worstAbsPct)}`} + {allWithinBand(deals) + ? `Показаны сделки с расхождением от ${errPct(BAND_MIN_PCT)} до ${errPct(BAND_MAX_PCT)} — отобранная полоса, не вся сверка. Медиана показанных строк — ${absPct(spread.medianAbsPct)}, худшая — ${absPct(spread.worstAbsPct)}` + : `Медиана расхождения показанных строк — ${absPct(spread.medianAbsPct)}, худшая — ${absPct(spread.worstAbsPct)}`}

)} diff --git a/tradein-mvp/frontend/src/app/mera-public/_components/v3/deal-view.ts b/tradein-mvp/frontend/src/app/mera-public/_components/v3/deal-view.ts index 61653657..5bd0b185 100644 --- a/tradein-mvp/frontend/src/app/mera-public/_components/v3/deal-view.ts +++ b/tradein-mvp/frontend/src/app/mera-public/_components/v3/deal-view.ts @@ -206,6 +206,23 @@ export const WITHIN_PCT = 20; export const BAND_MIN_PCT = -5; export const BAND_MAX_PCT = 20; +/** + * Можно ли про ЭТИ строки утверждать, что они — полоса. + * + * Подпись про полосу собирается из констант, а строки приезжают из БД, из + * последнего прогона задачи. Прогон в расписании не стоит и запускается + * руками, поэтому между выкатом фронта и пересчётом витрины страница + * утверждала бы полосу над строками, собранными ДО неё: на проде 12 из 20 + * лежащих сейчас строк вне полосы, включая +75,7 % — утверждение и его + * опровержение в одном экране. + * + * Поэтому утверждение самопроверяемое: печатается, только если ни одна + * показанная строка ему не противоречит. Проверка идёт по тем же границам, + * что стоят в тексте, — соврать, не покраснев, подпись не может. + */ +export const allWithinBand = (deals: readonly ShowcaseDeal[]): boolean => + deals.every((d) => d.err_pct >= BAND_MIN_PCT && d.err_pct <= BAND_MAX_PCT); + export interface ShownSpread { readonly n: number; readonly medianAbsPct: number;