From d011278a2df143f98fc5f4efc5440dcc4ddf0af7 Mon Sep 17 00:00:00 2001 From: Tomasz Kowalczyk Date: Thu, 30 Jul 2026 10:32:20 +0200 Subject: [PATCH] fix(statements): republish the 2026-07-28 weekly, and render the denominator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `resolved_count` landed in #59, so an unresolved window is expressible: adding `resolved_count: 0` clears `findHitRateInconsistencies`, which is what held this file at `draft: true`. The numbers are unchanged from #58 — regenerated from the warehouse at auditmos/ogsfrompoly@554f537 over the identical window — and its summary's "0 of 263 resolved" is the count itself, so the field states what the prose already did. Publishing it would otherwise have put `Hit rate 0%` back on the live page beside this stat's own caption, "0.50 ≈ a coin flip; above 0.50 is signal" — the exact misreading auditmos/ogsfrompoly#236 exists to stop, on the most-skimmed surface. So the stat now reads `pending` when the denominator is known-empty, with wording that replaces the standing caption rather than appending a caveat to it. `formatHitRate` / `describeHitRate` key off the denominator, never the rate, so a genuine 0-in-favour-of-240 week still reads as a real 0%. Absent stays unknown, never zero: every statement published before the field existed renders exactly as before, verified across all twelve. A known non-zero denominator is now named too ("Measured over the 240 of 263 alerts that have resolved") — publishing a rate without one is what let the 0.00 pass unnoticed for a week. Refs auditmos/ogsfrompoly#236 --- src/components/brand/StatementCard.astro | 6 ++- src/content/statements/2026-07-28-weekly.md | 3 +- src/lib/og/card-model.ts | 8 ++- src/lib/statement-format/hit-rate.test.ts | 59 +++++++++++++++++++++ src/lib/statement-format/hit-rate.ts | 35 ++++++++++++ src/pages/[collection]/[...slug].astro | 9 ++-- src/pages/index.astro | 1 + src/pages/statements/index.astro | 1 + 8 files changed, 113 insertions(+), 9 deletions(-) create mode 100644 src/lib/statement-format/hit-rate.test.ts create mode 100644 src/lib/statement-format/hit-rate.ts diff --git a/src/components/brand/StatementCard.astro b/src/components/brand/StatementCard.astro index da31805..e382b83 100644 --- a/src/components/brand/StatementCard.astro +++ b/src/components/brand/StatementCard.astro @@ -1,7 +1,7 @@ --- import { formatCategoryList, type Category } from "@/lib/statement-format/categories"; import { statementHref } from "@/lib/statement-format/href"; -import { formatPercent } from "@/lib/statement-format/percent"; +import { formatHitRate } from "@/lib/statement-format/hit-rate"; import { formatPeriodLabel } from "@/lib/statement-format/period"; import { formatSignedUsd } from "@/lib/statement-format/signed-usd"; @@ -13,6 +13,8 @@ interface Props { periodStart: string; periodEnd: string; hitRate: number; + /** Denominator behind `hitRate`. Absent means unknown, never zero. */ + resolvedCount?: number; alertCount: number; hypotheticalPnlUsd: number; categories: ReadonlyArray; @@ -36,7 +38,7 @@ const href = statementHref(props.slug);
Hit rate
-
{formatPercent(props.hitRate)}
+
{formatHitRate(props.hitRate, props.resolvedCount)}
Alerts
diff --git a/src/content/statements/2026-07-28-weekly.md b/src/content/statements/2026-07-28-weekly.md index 1d840cf..1a22bb9 100644 --- a/src/content/statements/2026-07-28-weekly.md +++ b/src/content/statements/2026-07-28-weekly.md @@ -1,7 +1,7 @@ --- schema_version: 1 type: weekly -draft: true +draft: false title: Week of 2026-07-22 summary: ogsfrompoly weekly statement — 263 alerts, outcomes pending (0 of 263 resolved). period_start: '2026-07-22' @@ -9,6 +9,7 @@ period_end: '2026-07-28' bankroll_usd: 10000.0 alert_count: 263 hit_rate: 0.0 +resolved_count: 0 hypothetical_pnl_usd: 199.11 categories: - macro-finance diff --git a/src/lib/og/card-model.ts b/src/lib/og/card-model.ts index c235b91..3c0ac41 100644 --- a/src/lib/og/card-model.ts +++ b/src/lib/og/card-model.ts @@ -1,5 +1,5 @@ import { SITE_TITLE, SITE_URL } from "@/lib/site/config"; -import { formatPercent } from "@/lib/statement-format/percent"; +import { formatHitRate } from "@/lib/statement-format/hit-rate"; import { formatPeriodLabel } from "@/lib/statement-format/period"; import { formatSignedUsd } from "@/lib/statement-format/signed-usd"; import type { Statement } from "@/lib/statement-format/statement-data"; @@ -61,7 +61,11 @@ export function buildStatementCardModel(statement: Statement): CardModel { )}`, title: statement.title, stats: [ - { label: "Hit rate", value: formatPercent(statement.hit_rate), tone: "default" }, + { + label: "Hit rate", + value: formatHitRate(statement.hit_rate, statement.resolved_count), + tone: "default", + }, { label: "Hypo. PnL", value: formatSignedUsd(statement.hypothetical_pnl_usd), diff --git a/src/lib/statement-format/hit-rate.test.ts b/src/lib/statement-format/hit-rate.test.ts new file mode 100644 index 0000000..5c143be --- /dev/null +++ b/src/lib/statement-format/hit-rate.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from "vitest"; +import { describeHitRate, formatHitRate } from "./hit-rate"; + +describe("formatHitRate", () => { + it("reads as pending when nothing has resolved", () => { + // `hit_rate: 0` over an empty denominator is vacuous, not a 0% success + // rate. Rendering it as "0%" beside the stat's own "above 0.50 is signal" + // caption is what put "263 calls, all wrong" on the live site + // (auditmos/ogsfrompoly#236). + expect(formatHitRate(0, 0)).toBe("pending"); + }); + + it("still reads as a rate when outcomes genuinely all missed", () => { + // 0 in favour out of 240 RESOLVED is a real 0%. Keying the wording off the + // denominator rather than the rate is what keeps a genuinely bad week from + // hiding behind "pending". + expect(formatHitRate(0, 240)).toBe("0%"); + }); + + it("renders a percent when outcomes resolved", () => { + expect(formatHitRate(0.47, 240)).toBe("47%"); + }); + + it("renders a percent when the denominator is unknown", () => { + // Every statement published before `resolved_count` existed omits it. + // Absent means unknown, never zero — those must keep rendering as before. + expect(formatHitRate(0.51, undefined)).toBe("51%"); + expect(formatHitRate(0, undefined)).toBe("0%"); + }); +}); + +describe("describeHitRate", () => { + it("says nothing has settled, and gives the count, when the denominator is empty", () => { + // The stat's standing caption ("0.50 ≈ a coin flip; above 0.50 is signal") + // actively misreads an unresolved window. Replace it, don't append to it. + const note = describeHitRate(263, 0); + + expect(note).toContain("0 of 263"); + expect(note).not.toContain("coin flip"); + }); + + it("names the denominator when outcomes did resolve", () => { + // Publishing a rate without its denominator is what let the 0.00 pass + // unnoticed for a week, so the note carries it in both branches. + const note = describeHitRate(263, 240); + + expect(note).toContain("240 of 263"); + expect(note).toContain("coin flip"); + }); + + it("falls back to the standing note when the denominator is unknown", () => { + // The back catalogue omits `resolved_count`; those pages must read exactly + // as they did before, with no invented count. + const note = describeHitRate(1034, undefined); + + expect(note).toContain("coin flip"); + expect(note).not.toContain("1034"); + }); +}); diff --git a/src/lib/statement-format/hit-rate.ts b/src/lib/statement-format/hit-rate.ts new file mode 100644 index 0000000..9d7c216 --- /dev/null +++ b/src/lib/statement-format/hit-rate.ts @@ -0,0 +1,35 @@ +import { formatPercent } from "./percent"; + +/** + * Display value for the hit-rate stat. + * + * `hit_rate` is `in_favor / resolved` over alerts **emitted** in the period, so + * a known-empty denominator makes it vacuous rather than a 0% success rate. + * Rendering that as "0%" is what published "263 calls, all wrong" + * (auditmos/ogsfrompoly#236). + */ +export function formatHitRate(hitRate: number, resolvedCount?: number): string { + return resolvedCount === 0 ? "pending" : formatPercent(hitRate); +} + +const STANDING_NOTE = + "Share of resolved alerts that hit the predicted side. 0.50 ≈ a coin flip; above 0.50 is signal."; + +/** + * The sentence explaining what the hit-rate stat is measured over. + * + * An unresolved window gets its own wording rather than an appended caveat: the + * standing note's "above 0.50 is signal" reads as a verdict on the number beside + * it, which is exactly the misreading a vacuous rate invites. + */ +export function describeHitRate(alertCount: number, resolvedCount?: number): string { + if (resolvedCount === 0) { + return ( + `No outcomes have settled yet — 0 of ${alertCount} alerts have resolved, ` + + "so there is no rate to report. Macro markets typically resolve months " + + "after the alert fires." + ); + } + if (resolvedCount === undefined) return STANDING_NOTE; + return `${STANDING_NOTE} Measured over the ${resolvedCount} of ${alertCount} alerts that have resolved.`; +} diff --git a/src/pages/[collection]/[...slug].astro b/src/pages/[collection]/[...slug].astro index ecf2b73..8747464 100644 --- a/src/pages/[collection]/[...slug].astro +++ b/src/pages/[collection]/[...slug].astro @@ -6,7 +6,7 @@ import { ogImagePathForStatement } from "@/lib/og/paths"; import { bucketUsdToNearest, formatBucketedUsd } from "@/lib/statement-format/bucketed-usd"; import { formatCategoryList } from "@/lib/statement-format/categories"; import { formatContributionPercent } from "@/lib/statement-format/contribution-percent"; -import { formatPercent } from "@/lib/statement-format/percent"; +import { describeHitRate, formatHitRate } from "@/lib/statement-format/hit-rate"; import { formatPeriodLabel } from "@/lib/statement-format/period"; import { monthlyPnlRows, type PnlTone } from "@/lib/statement-format/monthly-pnl"; import { formatSignedUsd } from "@/lib/statement-format/signed-usd"; @@ -86,10 +86,11 @@ const ogImage =
Hit rate
-
{formatPercent(statement.hit_rate)}
+
+ {formatHitRate(statement.hit_rate, statement.resolved_count)} +

- Share of resolved alerts that hit the predicted side. - {" "}0.50 ≈ a coin flip; above 0.50 is signal. + {describeHitRate(statement.alert_count, statement.resolved_count)}

diff --git a/src/pages/index.astro b/src/pages/index.astro index 0a737a7..513c0f2 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -52,6 +52,7 @@ const latest = pickLatestStatement(statements); periodStart={latest.data.period_start} periodEnd={latest.data.period_end} hitRate={latest.data.hit_rate} + resolvedCount={latest.data.resolved_count} alertCount={latest.data.alert_count} hypotheticalPnlUsd={latest.data.hypothetical_pnl_usd} categories={latest.data.categories} diff --git a/src/pages/statements/index.astro b/src/pages/statements/index.astro index 51b2875..2a44877 100644 --- a/src/pages/statements/index.astro +++ b/src/pages/statements/index.astro @@ -50,6 +50,7 @@ const statements = sortStatementsNewestFirst( periodStart={entry.data.period_start} periodEnd={entry.data.period_end} hitRate={entry.data.hit_rate} + resolvedCount={entry.data.resolved_count} alertCount={entry.data.alert_count} hypotheticalPnlUsd={entry.data.hypothetical_pnl_usd} categories={entry.data.categories}