SPB Git

spb/wp39_uqo

Public
6.0 KB

# PIT_PROTOCOL.md — point-in-time rules for WP39

Frozen 2026-11-02 before any model was estimated. Any change is an amendment logged in DECISIONS.md.

# 1. Clocks

All instants are converted to America/New_York for session logic and kept in UTC in storage. Trading days follow the XNYS calendar (exchange_calendars). For firm i and fiscal quarter q:

Symbol Meaning Source
E_q fiscal period end fundamentals_facts.fiscal_period_end
A_q earnings-release date (press release) — the reveal date of quarter-q results earnings_events.event_date, first event in [E_q, F_q + 10 d]
τ_q release timing: before_open, after_close, or unknown earnings_events.release_time
F_q SEC filing date of the 10-Q/10-K fundamentals_facts.filing_date
V_q vendor-proxy availability of the structured facts available_at = F_q + 1 d 00:00 ET
c prediction cutoff (a trading day; features use information up to the close of c) design

# 2. The information set at cutoff c

I(c) contains exactly:

  1. Market bars with session_date ≤ c (regular session 09:30–16:00 ET for the main specification; the 16:00 bar is the closing print; extended hours 04:00–09:29 and 16:01–19:59 enter only the "extended" variants, never mixed).
  2. Fundamentals with available_at ≤ c 00:00 ET, i.e. F_j + 1 ≤ c. The latest such quarter is j*(c), and the model also receives the age c − A_{j*} in trading days. Because V_q > F_q ≥ A_q > c by construction (§3), quarter-q facts can never enter I(c).
  3. Earnings-release history with event_date < c (prior realized releases, used for the historical base-rate feature B0).
  4. Static crosswalk (CIK, SIC) — time-invariant by assumption; formerNames are checked for ticker reuse (Phase 2 audit).
  5. Nothing else. No consensus estimates for quarter q (vintage unknown), no index-membership backfills, no contemporaneous factors.

# 3. Cutoff rules

  • Event-time design (primary, H1–H4): for horizon h ∈ {40, 20, 10, 5, 1} trading days, c = A_q − h trading days. Admissible only if c < min(A_q, F_q) and c ≥ A_{q−1} + 1 (the previous release has already happened, so the cohort window never straddles two reveals). With τ_q = before_open, bars of A_q contain the reaction and are excluded (c ≤ A_q − 1 guarantees it); with τ_q = after_close or unknown the same rule applies — we never use A_q bars.
  • Fixed-calendar design (H1 robustness, H5): c = last trading day of each month; the target is the next release after c (first A_q > c), restricted to A_q − c ≤ 65 trading days. The identity of "the next quarter" is known ex ante; its date is not needed by the forecaster.
  • Firm-quarter exclusions (frozen): no matched release; F_q < A_q; F_q − A_q > 30 calendar days; E_q not within 60–120 days of E_{q−1} (irregular fiscal calendars); quarter q − 4 or q − 1 not available at c (expectation undefined); raw close at c < 1 USD; fewer than 15 valid sessions in the 20-day feature window.

# 4. Label construction (no price-based labels)

Primary: S_q = (X_q − E_c[X_q]) / TA_{j*}, X = op_inc (quarterly operating income), TA = total assets of the latest available quarter, E_c[X_q] = X_{q−4} + (1/4) Σ_{k=0..3} (X_{q−1−k} − X_{q−5−k}) using only quarters available at c (seasonal random walk with drift; Foster 1977). D_q = 1[S_q ≤ −0.005]. The secondary targets are in configs/research.yaml. X_q itself is taken from the first ingested revision of quarter q (the only one available), which is the vendor's current value — see LIMITATIONS §PIT.

# 5. Leakage battery (each item is a unit test in tests/)

# Risk Guard Test
L1 quarter-q facts used as inputs as-of join on available_at ≤ c test_pit_leakage.py::test_no_future_fundamentals
L2 selection by fiscal end instead of availability join key is available_at, never fiscal_period_end test_pit_leakage.py::test_join_key_is_available_at
L3 post-release bars in the feature window session_date ≤ c < A_q test_pit_leakage.py::test_no_bars_after_cutoff
L4 global standardisation / imputation / tuning on test data transformers fit on train rows only; tuning on validation period only test_train_only_transforms.py
L5 DST / timezone errors UTC → ET conversion, 09:30 and 16:00 bars identified on both DST regimes test_timestamp_dsts.py
L6 ticker reuse / identifier drift crosswalk with effective dates; SEC formerNames audit test_instrument_ids.py
L7 splits/dividends creating mechanical returns split-adjusted returns from corporate_actions (ex-date rule), raw bars immutable test_corporate_actions.py
L8 release timing assumed intraday release-day bars never used; timing enters only as a descriptive variable test_labels.py::test_release_day_excluded
L9 overlapping target windows across splits purge + 1-quarter embargo at every split boundary test_splits.py
L10 survivorship by construction universe = all fundamentals entities (documented survivor conditioning), delisted bars kept where present 01_audit_universe.py report
L11 vendor ingestion date mistaken for knowledge date observed_at is never used as a knowledge date; available_at only test_pit_leakage.py::test_observed_at_not_used
L12 randomised placebo labels shuffled within calendar quarter → gain must vanish; cutoffs shifted −45 td → H4 gain must vanish 08_run_robustness.py

# 6. Equivalence check of the local as-of join with the API

For 25 random entities and 10 random as_of instants, the set of (metric, period) pairs returned by GET /fundamentals/{entity}?as_of= must equal the set produced by the local filter available_at ≤ as_of on the full extract. Implemented in tests/test_api_contract.py::test_asof_equivalence (live test, WP39_LIVE=1).

Files · 42edd965d464895e91a65b5b6e7ea7e9f619e658
Menu

Theme