Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/components/brand/StatementCard.astro
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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<Category>;
Expand All @@ -36,7 +38,7 @@ const href = statementHref(props.slug);
<dl class="mt-2 grid grid-cols-3 gap-4 font-mono text-sm">
<div class="flex flex-col">
<dt class="text-eyebrow uppercase text-ink-5">Hit rate</dt>
<dd class="text-ink-8">{formatPercent(props.hitRate)}</dd>
<dd class="text-ink-8">{formatHitRate(props.hitRate, props.resolvedCount)}</dd>
</div>
<div class="flex flex-col">
<dt class="text-eyebrow uppercase text-ink-5">Alerts</dt>
Expand Down
3 changes: 2 additions & 1 deletion src/content/statements/2026-07-28-weekly.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
---
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'
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
Expand Down
8 changes: 6 additions & 2 deletions src/lib/og/card-model.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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),
Expand Down
59 changes: 59 additions & 0 deletions src/lib/statement-format/hit-rate.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
35 changes: 35 additions & 0 deletions src/lib/statement-format/hit-rate.ts
Original file line number Diff line number Diff line change
@@ -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.`;
}
9 changes: 5 additions & 4 deletions src/pages/[collection]/[...slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -86,10 +86,11 @@ const ogImage =
<dl class="not-prose mt-2 mb-10 grid grid-cols-2 gap-6 border-y border-ink-3 py-6 font-mono text-sm sm:grid-cols-4">
<div class="flex flex-col gap-1.5">
<dt class="text-eyebrow uppercase tracking-wider text-ink-5">Hit rate</dt>
<dd class="text-lg text-ink-8">{formatPercent(statement.hit_rate)}</dd>
<dd class="text-lg text-ink-8">
{formatHitRate(statement.hit_rate, statement.resolved_count)}
</dd>
<p class="font-sans text-xs leading-snug text-ink-5">
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)}
</p>
</div>
<div class="flex flex-col gap-1.5">
Expand Down
1 change: 1 addition & 0 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
1 change: 1 addition & 0 deletions src/pages/statements/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Loading