Add T0.95 late-order heat stage

Request:
- Reflect that near-final market heat can be used when the user can still place an IPO order at T0.95.

Changes:
- Added T0_95_final_heat as a separate analyst decision stage with executability and no-leakage rules.
- Added an experimental T0.95 rule overlay for late-order heat scoring and calibration discipline.
- Updated archivist guidance and the market-heat archiver so snapshots can be explicitly stored as T0_95_final_heat.
- Added market_heat_stage to the analysis dataset and refreshed the model report to show T0.95 coverage separately.

Verification:
- Ran py_compile for the modified scripts.
- Checked archive_t0_5_market_heat.py --help for the new --stage option.
- Rebuilt data/snapshots/analysis_model_v0_dataset.csv and reports/2026-06-15_analysis_model_v0.md.
- Ran git diff --check.

Next useful context:
- Current archived heat rows remain T0_5_market_heat only; there are no true T0.95 rows yet.
- external_ipo_history.public_oversubscription_times is still calibration-only unless a comparable value is archived before the executable order cutoff.
This commit is contained in:
2026-06-15 16:28:26 +00:00
parent 5e35242a76
commit 19832ac5af
8 changed files with 479 additions and 312 deletions
+18 -2
View File
@@ -415,6 +415,7 @@ def fetch_rows(conn: sqlite3.Connection) -> list[sqlite3.Row]:
p.d60_return_pct,
p.d1_turnover_hkd_m,
h.observed_at AS t0_5_observed_at,
h.stage AS market_heat_stage,
h.provider AS t0_5_provider,
h.margin_subscription_multiple AS t0_5_margin_subscription_multiple,
h.source_id AS t0_5_source_id,
@@ -487,6 +488,7 @@ def build_records(rows: list[sqlite3.Row], as_of: str) -> list[dict[str, Any]]:
"public_oversubscription_times": row["public_oversubscription_times"],
"international_oversubscription_times": row["international_oversubscription_times"],
"t0_5_observed_at": row["t0_5_observed_at"],
"market_heat_stage": row["market_heat_stage"],
"t0_5_provider": row["t0_5_provider"],
"t0_5_margin_subscription_multiple": row["t0_5_margin_subscription_multiple"],
"t0_5_source_id": row["t0_5_source_id"],
@@ -609,6 +611,7 @@ def write_dataset(records: list[dict[str, Any]], output_path: Path) -> None:
"public_oversubscription_times",
"international_oversubscription_times",
"t0_5_observed_at",
"market_heat_stage",
"t0_5_provider",
"t0_5_margin_subscription_multiple",
"t0_5_source_id",
@@ -705,8 +708,15 @@ def write_report(
total = len(records)
d1_records = [record for record in records if record["d1_return_pct"] is not None]
structured_t1 = [record for record in records if record["has_structured_t1"]]
structured_t0_5 = [record for record in records if record["t0_5_margin_subscription_multiple"] is not None]
structured_market_heat = [record for record in records if record["t0_5_margin_subscription_multiple"] is not None]
structured_t0_5 = [
record for record in structured_market_heat if record["market_heat_stage"] in {None, "", "T0_5_market_heat"}
]
structured_t0_95 = [
record for record in structured_market_heat if record["market_heat_stage"] == "T0_95_final_heat"
]
t0_5_with_d1 = [record for record in structured_t0_5 if record["d1_return_pct"] is not None]
t0_95_with_d1 = [record for record in structured_t0_95 if record["d1_return_pct"] is not None]
external_history_rows = [record for record in records if record["external_history_source_path"]]
external_oversub_rows = [record for record in records if record["external_public_oversubscription_times"] is not None]
external_oversub_with_d1 = [
@@ -746,8 +756,11 @@ def write_report(
f"- Rows with offer size: {count_present(records, 'offer_size_hkd_m')}",
f"- Rows with public oversubscription: {count_present(records, 'public_oversubscription_times')}",
f"- Rows with international oversubscription: {count_present(records, 'international_oversubscription_times')}",
f"- Rows with market heat snapshots: {len(structured_market_heat)}",
f"- Rows with T0.5 margin heat snapshots: {len(structured_t0_5)}",
f"- Rows with T0.95 late-order heat snapshots: {len(structured_t0_95)}",
f"- Rows with T0.5 margin heat and D1 labels: {len(t0_5_with_d1)}",
f"- Rows with T0.95 late-order heat and D1 labels: {len(t0_95_with_d1)}",
f"- Rows matched to external ipohk history: {len(external_history_rows)}",
f"- Rows with external final oversubscription: {len(external_oversub_rows)}",
f"- Rows with external final oversubscription and D1 labels: {len(external_oversub_with_d1)}",
@@ -769,10 +782,13 @@ def write_report(
"",
"## T0.5 Market Heat",
"",
"T0.5 uses archived subscription-period margin heat snapshots. These are non-official live signals and are kept separate from T1 allotment demand. The current archive is not yet a historical training set: it has too few rows and no D1 labels for calibration.",
"T0.5 uses archived subscription-period margin heat snapshots. T0.95 is the near-deadline subset that is still actionable before the user's order cutoff. These are non-official live signals and are kept separate from T1 allotment demand. The current archive is not yet a historical training set: it has too few rows and no D1 labels for calibration.",
"",
f"- Total market heat rows: {len(structured_market_heat)}",
f"- T0.5 margin rows: {len(structured_t0_5)}",
f"- T0.5 rows with D1 labels: {len(t0_5_with_d1)}",
f"- T0.95 late-order heat rows: {len(structured_t0_95)}",
f"- T0.95 rows with D1 labels: {len(t0_95_with_d1)}",
"",
"## External Final Heat Proxy",
"",