# VRD-1 — `VRPRunResult` has no `run_date`, so the exit-4 de-risking escalation silently degrades on the option sleeve: every failed CLOSE reads as NEW, every session

status: open · raised 2026-10-21 (implementer, building OSR-1 — adjacent defect found by probing the capability from its actual caller, FILED not fixed) · class: truth divergence between layers · judgement: **YES** · effort ~0.1 pd · horizon: live now; fires on any VRP session with a failed order

---

**Plain-language summary.** Two of the three sleeves return an
`ExecutionResult` from a run; the option sleeve (VRP) returns its own
`VRPRunResult`, which its docstring calls *"field-compatible with what `thales
run` reads off the daily result."* It is not quite: it has no `run_date`. The
one place `thales run` reads that field is the FOS-1 / IGD-1 escalation that
decides whether a failed de-risking order is NEW (red the run, exit 4) or
STANDING (the digest's streak line). On VRP that read raises `AttributeError`
into a `try/except` whose handler is *"unknown = new"* — so on the option
sleeve the escalation never consults history at all, and the `standing
failure` line can never print. It is fail-LOUD, so nothing is hidden; but the
NEW-vs-STANDING distinction FOS-1 exists to draw does not exist there, which
is the alarm-fatigue condition its own docstring warns about.

## Verified at HEAD `fad1d78`

- `src/thales/execution/vrp_daily.py:53-67` — `@dataclass class VRPRunResult`
  declares `equity, skipped, skip_reason, halted, halt_reasons,
  rejected_orders, orders_submitted, orders_failed, n_target_positions,
  kill_switch`. No `run_date`. `ExecutionResult`
  (`pipeline.py:27-37`) declares `run_date: date` as its FIRST field.
- `src/thales/cli.py` (the `run` command) — the only `result.run_date` read
  sits inside `if result.orders_failed:` → `if not dry_run:` → `try:`, and
  the `except` prints *"failure history unreadable … treating every failed
  de-risking order as NEW"*. On VRP that branch is reached by
  `AttributeError`, not by an unreadable log.
- Not yet observed live only because VRP has had no failed order on a session
  where this path ran; it is a latent, not a theoretical, defect.

## Fix shape (propose-only)

Add `run_date: date = <today>` to `VRPRunResult` and set it where the wrapper
builds its result, or have `cli.run` resolve the date the way the OSR-1 sweep
now does (`getattr(result, "run_date", None) or market_session_date()`). The
first is better: the docstring's claim of field compatibility should be made
true rather than worked around at each caller. A test asserting the two result
types carry the same field names — or at least that every field `cli.run`
reads exists on both — stops the next divergence.

## Test design + negative control

- VRP fixture with one failed CLOSE on two consecutive sessions → the second
  reads STANDING, not NEW (today: NEW both times). Revert the field → NEW both
  times returns.
- A structural test over the two dataclasses' field sets, so a third sleeve
  cannot repeat it. Revert → the test names the missing field.

## Kill criterion — pre-registered

If `cli.run`'s only `run_date` read is removed or the VRP wrapper is retired
before this is built, the row closes as superseded. If a reading of the live
VRP order log shows the sleeve structurally cannot produce a failed order (it
cannot — a refused mleg close is exactly the case), the row closes NO.

## Note on provenance

Found while building **OSR-1** (legs 1-3, PR for 2026-10-21). The OSR-1 build
does NOT fix this — it only avoids the same trap in its own new code, with a
comment pointing here. Filed per the implementer's scope rule: adjacent
defects get filed, never silently fixed.
