# D48-XSMOM-28D-V2 implementation acceptance review

Reviewed: `2026-07-29`

Scope: frozen V2 contract, Amendments 1–3, the existing research-data audit,
raw-signal coverage implementation, and their unit tests. This is an implementation
review only. No strategy code was changed and no candidate/no-op PnL was run.

## Verdict

**Current status: NOT READY for a V2 candidate run.**

The existing raw-signal coverage implementation is a sound V1 reference for causal
signal timing, 29-day continuity, first-full-listing-day handling, deterministic
ranking, and fail-closed snapshots. It is not yet a V2 implementation:

1. a valid-snapshot signal whose own pair is unscoreable is still returned as
   `block / xsmom_missing_pair`, rather than unchanged pass-through with
   `xsmom_unscored_passthrough`;
2. the summary schema knows only `pass` and `block`, so it cannot prove pass-through
   fidelity or separate pass-through from rank evidence;
3. no V2 candidate or no-op strategy class exists in the reviewed tree;
4. no automated isolation comparison exists for raw decisions, normalized trades,
   or wallet paths;
5. the general data audit's reported first XSMOM eligibility ignores intraday
   listing-day exclusion. Its dates for POWER/EDGE must not be used as V2 maturity
   evidence.

Passing the current 18 unit tests means the tested V1 primitives behave as written;
it does not constitute V2 acceptance.

## Acceptance matrix

| Area | Frozen acceptance rule | Current evidence | Status |
|---|---|---|---|
| Signal causality | A 5m signal is eligible only at candle open time + 5m | Raw extractor stores both times and adds 5m | Partial pass |
| Daily causality | At eligible time `t`, endpoint `d` is the prior completed UTC day | Coverage code uses `normalize(t) - 1 day` | Pass in code; boundary tests incomplete |
| Formula | Exactly 29 contiguous, positive, finite daily closes from `d-28` through `d`; score is `ln(C(d)/C(d-28))` | Implemented by exact daily reindex and endpoint log ratio | Pass |
| Listing day | Intraday listing day is partial and excluded; midnight listing may use that day | Coverage helper implements this; POWER/EDGE tests exist | Pass in coverage code |
| General data audit maturity | First eligible timestamp must apply the same listing rule | `audit_research_data.py` takes the 29th observed daily row and ignores listing time for this field | Fail; do not use this field for V2 |
| Universe | Same frozen 12-pair universe, evaluated point-in-time at each signal | Loaded from frozen config and computed per eligible timestamp | Pass, subject to frozen hash verification |
| Ties | Sort by score descending, then exact canonical pair string ascending | Implemented as `(-score, pair)` | Pass; boundary-tie tests incomplete |
| Buckets | Rank 1–4 top; ranks `N-3..N` bottom; minimum `N=10` | Implemented | Pass; exact `N=10` boundary test missing |
| Snapshot failure | Check snapshot first; if fewer than 10 scoreable pairs, block with `xsmom_snapshot_unavailable` | Implemented before signal-pair eligibility check | Pass in V1 helper |
| Missing signal pair | If snapshot is valid but the signal pair is unscoreable, preserve the baseline raw signal unchanged | Current helper blocks with `xsmom_missing_pair` | **Fail — V2 blocker** |
| Rank gate | Scoreable long only top4; scoreable short only bottom4 | Implemented | Pass |
| Audit labels | Pass-through must be distinct from rank pass and rank block | Existing output has only `pass`/`block`; no pass-through label | **Fail — V2 blocker** |
| Coverage gates | Snapshot ≥98%; common support ≥95%; post-maturity historical 100%; fresh thresholds per contract | Existing V1 artifact reports snapshot 100% and common support 97.276%, but does not emit V2 maturity/pass-through metrics | Incomplete |
| Pass-through fidelity | Every missing-pair pass-through has the same downstream decision as baseline; historical 111/111 | No baseline-decision join or fidelity field | **Fail — V2 blocker** |
| Episode exclusion | Pass-through cannot start an XSMOM `PATH_DIVERGENCE` episode | No V2 episode audit exists | **Fail — V2 blocker** |
| No-op | Same indicator/rank plumbing, but preserves every raw signal; exactly reproduces formal 331-trade baseline | No reviewed no-op implementation or comparison artifact | **Fail — V2 blocker** |
| A/B isolation | Baseline A, no-op, candidate, baseline B; A/B/no-op normalized trades and wallet paths identical | Contracted but not implemented/run | Pending; required before candidate metrics |
| First changed decision | Candidate versus no-op may first differ only at the scoreable-pair rank gate | No decision-stage trace or comparator | **Fail — V2 blocker** |

## Normative V2 decision order

The implementation and the audit must use this exact precedence:

```text
raw RiskCap signal at candle_time
  -> eligible_time = candle_time + 5m
  -> build point-in-time scores from daily endpoints d-28 and d,
     where d = UTC calendar day(eligible_time) - 1 day
  -> rank scoreable frozen-universe pairs by (-score, canonical_pair)
  -> if scoreable_count < 10:
       block; reason=xsmom_snapshot_unavailable
  -> elif signal pair is unscoreable:
       preserve raw signal; decision=passthrough;
       reason=xsmom_unscored_passthrough
  -> elif long and rank <= 4:
       pass; reason=xsmom_top4
  -> elif short and rank >= scoreable_count - 3:
       pass; reason=xsmom_bottom4
  -> else:
       block; reason=xsmom_not_top4 or xsmom_not_bottom4
  -> continue through the unchanged pre-existing RiskCap portfolio/risk path
```

The snapshot test must precede the missing-pair test. A missing signal pair in a
nine-pair snapshot is `xsmom_snapshot_unavailable`, not pass-through.

## Required row-level audit contract

Each authentic raw signal should retain the existing causal and rank fields and add
enough information to prove V2 isolation:

- `hypothesis_id=D48-XSMOM-28D-V2`, contract frozen timestamp, script version;
- signal candle time and signal eligible time;
- listing time, first full listing day, last complete UTC day, both score endpoints;
- all 29-close validity flags, scoreability, eligible universe and count;
- score, rank, bucket, side;
- `baseline_raw_decision`, `noop_raw_decision`, `candidate_raw_decision`;
- `xsmom_decision` in the closed set `pass`, `block`, `passthrough`;
- `xsmom_reason` in the closed set:
  `xsmom_top4`, `xsmom_bottom4`, `xsmom_not_top4`,
  `xsmom_not_bottom4`, `xsmom_unscored_passthrough`,
  `xsmom_snapshot_unavailable`;
- `changed_vs_noop`, `pair_rank_eligible`, `passthrough_fidelity`;
- downstream pre-existing risk/protection decision and first differing stage;
- divergence episode ID and trigger reason, nullable when no divergence occurs.

Invariants:

- `signals = pass + block + passthrough`;
- pass-through rows have null score/rank/bucket, `pair_rank_eligible=false`,
  `changed_vs_noop=false`, and `passthrough_fidelity=true`;
- pass-through rows do not increment `xsmom_pass`, changed-signal, rank-mechanism, or
  divergence-trigger counts;
- snapshot-unavailable rows are blocked even when the signal pair itself has a score;
- every changed candidate decision has `pair_rank_eligible=true` and first differing
  stage `xsmom_rank_gate`;
- historical training must show exactly 111 cold-start pass-through rows
  (EDGE 94, POWER 17), 111/111 fidelity, and zero pass-through-triggered episodes.

## Required unit and integration tests

### Causal time and daily data

- A `23:55` candle becomes eligible at next-day `00:00`, while the last usable daily
  endpoint remains the day before that new UTC date.
- Intraday eligible times never use the current UTC daily candle.
- Exactly 29 contiguous dates pass; 28 dates, a missing middle date, duplicate
  timestamps, NaN, infinity, zero, and negative close fail.
- The computed score equals the endpoint log ratio, including a known negative score.

### Listing maturity

- Listing exactly at `00:00` permits that date as the first full day.
- Intraday POWER listing `2025-12-06T09:00Z` yields first full day `2025-12-07` and
  first score eligibility `2026-01-05T00:00Z`.
- Intraday EDGE listing `2026-03-19T14:00Z` yields first full day `2026-03-20` and
  first score eligibility `2026-04-18T00:00Z`.
- A stored partial listing-day candle cannot make either pair scoreable one day early.
- Post-maturity missing/invalid daily data is counted as unexpected unscoreability,
  not cold start.

### Ranking and decisions

- Nine eligible pairs fail closed; exactly ten are valid.
- Exact ties spanning ranks 4/5 and the bottom4 boundary resolve by canonical pair
  string, with explicit expected winners/losers.
- Long top4 passes and middle/bottom blocks; short bottom4 passes and middle/top
  blocks.
- Valid snapshot plus unscoreable signal pair returns
  `passthrough/xsmom_unscored_passthrough`.
- Invalid snapshot plus unscoreable signal pair returns
  `block/xsmom_snapshot_unavailable`.
- Unscoreable pairs never receive a rank or bucket.

### Isolation and artifacts

- Candidate and no-op produce exactly the same authentic raw-signal denominator,
  signal timestamps, pair, side, and pre-gate baseline decisions.
- No-op preserves every raw signal before existing downstream controls.
- Candidate versus no-op first-difference tracing rejects any difference outside the
  scoreable-pair rank gate.
- Summary counts and reason counts reconcile exactly to row-level CSV data.
- Historical pass-through selection is exactly the frozen 111-row set.
- The run harness refuses comparison if source, config, image, data, listing, fee,
  timerange, startup, protections, or cache settings differ.
- Normalized trade-list and wallet-path hashes for baseline A, no-op, and baseline B
  are identical and match the formal 331-trade baseline.

## Implementation handoff checklist

- [ ] Version or replace the V1 coverage decision function for V2; preserve the V1
  artifact and semantics.
- [ ] Add explicit `passthrough` decision and the V2 controlled reason vocabulary.
- [ ] Add V2 coverage denominators and post-maturity classifications.
- [ ] Fix or clearly deprecate the general data audit's listing-unaware XSMOM
  first-eligibility field.
- [ ] Implement a V2 no-op class sharing the exact candidate rank plumbing.
- [ ] Implement row-level baseline/no-op/candidate decision-stage comparison.
- [ ] Add all boundary and invariant tests above.
- [ ] Generate coverage-only artifacts first and validate the frozen 111 rows.
- [ ] Freeze all identity hashes and the exact execution protocol.
- [ ] Only after every non-PnL item above passes, run the contracted sequence
  baseline A → no-op → candidate → baseline B with cache disabled.

Until then, any candidate performance output should be labeled
`INCONCLUSIVE_IMPLEMENTATION_ISOLATION`, not interpreted as strategy evidence.
