fix(mera/лендинг): утверждение про полосу проверяет само себя по показанным строкам
All checks were successful
CI Trade-In / changes (pull_request) Successful in 12s
CI / changes (pull_request) Successful in 15s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m48s
CI Trade-In / backend-tests (pull_request) Successful in 6m0s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 12s
CI / changes (pull_request) Successful in 15s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m48s
CI Trade-In / backend-tests (pull_request) Successful in 6m0s
Дыра в выкате, найденная ревьюером до мержа. Подпись про полосу собиралась из констант `BAND_MIN_PCT`/`BAND_MAX_PCT` в коде фронта, а строки витрины и `rejection_rule` приезжают из БД, от ПОСЛЕДНЕГО прогона задачи `landing_showcase_deals`. Задачи нет в расписании — её запускают руками. Значит в окне «фронт выкачен, витрина не пересчитана» страница утверждала бы «показаны сделки с расхождением от −5 % до +20 %», а под утверждением лежали бы прежние двадцать строк: по замеру на проде 12 из 20 вне полосы, худшая +75,7 %. Утверждение и его опровержение в одном экране — хуже, чем было до правки. Чинится конструкцией, а не запуском задачи: `allWithinBand(deals)` в `deal-view.ts` спрашивает САМИ показанные строки теми же границами, что стоят в тексте. * Все показанные строки в полосе — печатаем прежнюю формулировку. * Хоть одна вне — про полосу НЕ утверждаем. В таблице: «Полосу расхождения от -5 % до +20 % эта подпись не обещает: среди показанных строк есть расхождения вне неё, то есть витрину собрал прогон с другим правилом — тем, что напечатано выше». Правило того прогона и так приезжает в `rejection_rule` из ТОГО ЖЕ прогона, что и строки, поэтому подпись с ними согласована по построению. В ленте остаётся только то, что посчитано по строкам: медиана и худшая. * Медиана по ВСЕЙ сверке (15,3 %, 325 сделок) печатается в обеих ветках — она и удерживает страницу честной независимо от того, пересчитана витрина. Тесты по значению в обе стороны: набор с одной строкой вне полосы (+75,71 % — реальная строка прода) → утверждения про полосу нет; все в полосе → есть. Фальсификация: `allWithinBand` обезврежен руками (всегда true) — краснеют оба новых теста, и красный текст показывает ровно тот дефект: «Это отобранная полоса расхождения от -5 % до +20 % … худшая 75,7 %». Проверка возвращена. Проверка заодно поймала мои же фикстуры ленты: −11,5 % ниже нижней границы полосы (−5 %), то есть «маленькое отклонение» ещё не значит «в полосе». Значения заменены на внутриполосные. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
2467943200
commit
db47fa0ecd
4 changed files with 126 additions and 9 deletions
|
|
@ -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(<AccuracyV3 stats={STATS} showcase={outOfBand} />);
|
||||
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(<AccuracyV3 stats={{}} showcase={null} />);
|
||||
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(<DealsTickerV3 deals={[tickerDeal(4), tickerDeal(-11.5), tickerDeal(19.3)]} />);
|
||||
render(<DealsTickerV3 deals={[tickerDeal(1.2), tickerDeal(-3.4), tickerDeal(19.3)]} />);
|
||||
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(<DealsTickerV3 deals={[tickerDeal(4), tickerDeal(-11.5)]} />);
|
||||
render(<DealsTickerV3 deals={[tickerDeal(1.2), tickerDeal(-3.4)]} />);
|
||||
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(<DealsTickerV3 deals={[tickerDeal(4), tickerDeal(-11.5)]} />);
|
||||
render(<DealsTickerV3 deals={[tickerDeal(1.2), tickerDeal(-3.4)]} />);
|
||||
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(<DealsTickerV3 deals={[tickerDeal(4), tickerDeal(75.71)]} />);
|
||||
const note = screen.getByText(/Медиана расхождения показанных строк/u);
|
||||
expect(
|
||||
note.textContent,
|
||||
"лента обещает полосу, а сама прокручивает строку вне неё",
|
||||
).not.toMatch(/от -5 % до \+20 %/u);
|
||||
expect(note.textContent).toContain("75,7 %");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
: "Подпись прогона не пришла — из чего отобраны строки, сказать нечем."}
|
||||
</p>
|
||||
{spread && (
|
||||
// УТВЕРЖДЕНИЕ ПРО ПОЛОСУ — САМОПРОВЕРЯЕМОЕ. Границы стоят в коде
|
||||
// фронта, а строки приходят из БД, из последнего прогона задачи;
|
||||
// задача в расписании не стоит и запускается руками. Между
|
||||
// выкатом и пересчётом страница утверждала бы полосу над
|
||||
// строками, собранными до неё (на проде сейчас 12 из 20 вне
|
||||
// полосы, худшая +75,7 %) — утверждение и его опровержение в
|
||||
// одном экране. Поэтому про полосу говорим, только если ни одна
|
||||
// показанная строка этому не противоречит, а иначе называем то,
|
||||
// что есть: строки собраны прогоном с другим правилом, и это
|
||||
// правило напечатано выше — оно приезжает из ТОГО ЖЕ прогона,
|
||||
// что и строки, поэтому разойтись с ними не может.
|
||||
//
|
||||
// «В ПРЕДЕЛАХ 20 % — N ИЗ N» ОТСЮДА СНЯТО, И ЭТО НЕ СОКРАЩЕНИЕ.
|
||||
// Полоса витрины — от −5 % до +20 %, значит |отклонение| ≤ 20 у
|
||||
// КАЖДОЙ показанной строки по построению фильтра: счёт всегда
|
||||
|
|
@ -283,7 +305,11 @@ export function AccuracyV3({
|
|||
// сторожит landing-v3-render («подпись витрины называет полосу и
|
||||
// держит рядом медиану по всей сверке»).
|
||||
<p className={styles.accFootnote}>
|
||||
{`Разброс показанных строк: медианное расхождение ${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)} сделок).`}
|
||||
</p>
|
||||
)}
|
||||
<p className={styles.accFootnote}>{deals[0].note}</p>
|
||||
|
|
|
|||
|
|
@ -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[] }) {
|
|||
</div>
|
||||
</div>
|
||||
{spread && (
|
||||
// Про полосу говорим, только если ЛЕНТА ей соответствует. Границы
|
||||
// стоят в коде, строки приходят из БД от последнего прогона задачи, а
|
||||
// задача в расписании не стоит: между выкатом и пересчётом лента
|
||||
// утверждала бы полосу над строками, собранными до неё, — и тут же
|
||||
// прокручивала +75,7 % мимо этого утверждения. Не соответствует —
|
||||
// печатаем только то, что посчитано по самим строкам.
|
||||
<p className={styles.tickerNote}>
|
||||
{`Показаны сделки с расхождением от ${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)}`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue