Thales
← research journal
Jul 25, 2026raw markdown ↗

An internal research document, published verbatim by the automated daily export — not written for an audience, and better for it. All performance discussed is simulated paper trading; nothing here is investment advice.

VRP decision-fidelity amendment — 2026-07-25

Status: AMENDMENT to the VRP pre-registration (spec S2, activated 2026-07-12). Effective: first trading day on or after 2026-07-27. Scope: the sleeve's implementation of its manage rule. No registered parameter value changes.


1. What is NOT changing

manage_dte: 21, profit_take_frac: 0.50, width: 1.0, min_credit: 0.05, risk_budget_pct: 0.02, dte_min/dte_max, target_delta — all unchanged and still keystone-pinned. The entry rule, the sizing rule, and the safety gate are untouched.

This matters because the obvious failure mode for an amendment like this is laundering a parameter change as a "bug fix". The test applied throughout: does the change make the code do what the already-registered value says, or does it change what we are asking for? Only the former is admitted here.

2. Why an amendment was needed

Two defects were found on 2026-07-25 while auditing the 2026-07-24 runner-loss incident. Both are in the implementation of a correctly-registered rule.

2.1 The profit trigger could fire on a single bad quote

manage_decision closes when 1 - (current_spread_value / entry_credit) >= 0.50. current_spread_value is mid(short) - mid(long), and its only guard was bid <= 0 or ask <= 0 or ask < bid (vrp_daily.py) — no width, staleness, or sanity check, and no requirement that the reading hold more than once.

Evidence it actually fired spuriously, from the sleeve's own committed record:

The cohort opened 2026-07-13 (expiry 2026-08-14, entry_credit 0.265, qty 2) was closed on 2026-07-22 with run_summary.action = "manage:close".

  • DTE on 2026-07-22 = 23. manage_dte is 21, so the DTE branch cannot have fired. dte < 0 would have logged manage:expired-HALT, not manage:close.
  • By elimination the profit branch fired, which required current_spread_value <= 0.50 x 0.265 = 0.1325.
  • The same run, moments later, recorded mid_close_cost: 0.225 in pessimistic_ledger.jsonl — computed by the same function.

Two reads of one spread in one run, ~70% apart, with the decision-side read landing just under the trigger.

Corroboration that 50% capture was not real: the 2026-07-23 replacement cohort's −30Δ short strike is 722 at 29 DTE, versus 735 at 32 DTE on 07-13. Same delta, near-identical DTE ⇒ spot fell ~1.8% (or IV rose). Either move makes an OTM put credit spread worth more, not less. Both direct measurements in the record put the spread at ~0.225–0.26, i.e. ~85% of the entry credit — the opposite of 50% captured.

Cost of a spurious close: a full unplanned round trip. The pessimistic twin measures 0.035–0.04/sh of drag per side against a cycle credit of ~0.18–0.27/sh, so each one burns roughly 30–45% of a cycle's gross credit.

Caveat, stated plainly: this is one observed instance. A genuine fast-tape move between the two reads is the innocent explanation — but it does not fit the direction implied by the strike migration. The mechanism (an unguarded ratio of two wide-leg mids) is not in doubt regardless of how this single instance is read, and the remedy is a guard, not a redesign.

2.2 The 50% target was really ~36%

entry_credit was persisted as credit_estimate — the decision-time mid. For a credit you fill at or below mid, so the denominator is overstated one-directionally, never understated.

Current position: mid 0.23 recorded, broker-realized fill 0.18.

registeredactual
profit triggerclose at 50% of max profitclose at 36.1% of realized max profit
— net of measured close drag~17%
defined risk / contract$82recorded $77 (understated $5)

Sizing and the safety gate are not affected — both recompute worst case from leg strikes (width x 100) and refuse to trust the declared field, so contract count and the 2%-of-equity cap were never exposed. This is a rule-fidelity defect, not a risk-limit breach.

The pessimistic-twin ledger does not cover this: it is explicitly pure observability and never feeds back into entry_credit. It measures the drag; it did not correct the rule.

3. The amendment

  1. Sanity band on spread revaluation. A vertical's cost to close is bounded by construction to [0, width]. A mid outside that is a broken quote, not an observation; it now returns None, degrading to the DTE rule — the existing fail-closed path. Catches gross corruption only.

  2. Confirmation re-read on the profit branch. The profit trigger must hold across profit_confirm_reads: 2 observations spaced profit_confirm_delay_seconds: 15. Failure to confirm ⇒ hold. This is the house's own fail-closed idiom applied one layer up: sleeve state already mutates only on a broker-confirmed fill, so a decision worth a round trip should likewise survive a second look. The DTE rule is never gated by this — it is time-based and cannot be faked by a quote. Cost: one extra quote read on the rare day the trigger fires.

    Branch detection is derived from DTE arithmetic, not by string-matching the reason text, so rewording a log message cannot silently disable it.

  3. Realized-fill reconciliation. After a broker-confirmed open, entry_credit is set from the order's realized fill. The decision-time mid is kept alongside as entry_credit_mid — the gap between them is the execution-quality series the twin exists to measure. max_loss_per_contract is recomputed from the realized credit.

    Fail-soft toward the pre-amendment behaviour: no order id, no fill price, a broker error, or a fill magnitude outside (0, width) all fall back to the mid and log. Sign convention is not hard-coded off one observation — magnitude is unambiguous, and the (0, width) band (a credit cannot be zero, nor >= width without being arbitrage) rejects a misread more reliably than a sign rule.

Both new parameters are keystone-pinned at introduction, before the amended rule produced any forward data. Loosening either silently restores the single-read trigger.

4. Forward-clock reset — and why this is not shopping

oos_monitor.since moves 2026-07-13 → 2026-07-27. The 9 trading days accrued under the old rule measure a different system and cannot be spliced onto the amended one.

The distinction from start-date shopping is the entire point, so state it explicitly:

start-date shoppingthis reset
rule unchangedrule changed
start chosen because the curve looks better after itstart forced by the change date; no discretion
taken after enough data to see what flatterstaken at day 9 — the earliest possible moment
discarded window quietly disappearsdiscarded window preserved below

Taking it now is also the cheapest it will ever be: at day 100 the same fix would cost the entire gate, and we would face a standing incentive to leave a known defect in place. That asymmetry is precisely why it is being done today rather than "watched for another instance".

execution.live_start_date does NOT move and stays 2026-07-13. It is a fact of account history (when the Alpaca account began trading), and the backfill/reconcile jurisdiction guards filter pre-live broker filler against it; moving it would re-admit that filler as real equity. Two different clocks, deliberately.

The discarded window, preserved (2026-07-13 → 2026-07-24, 9 marks):

2026-07-14  9983.84    2026-07-20  9981.84
2026-07-15  9987.84    2026-07-21  9985.84
2026-07-16  9991.84    2026-07-22  9989.84
2026-07-17  9975.84    2026-07-23  9991.76
                       2026-07-24  9989.59  (backfilled from broker truth)

Two cohorts, one full cycle: opened 07-13 at 0.265 mid (qty 2), closed 07-22 at 0.225 mid — the close this amendment argues was spurious. Net +$7.92 over the window, which is noise on $10k and carries no information either way. Nothing of value is being discarded; the window is retained here so the claim can be checked.

4b. One-time correction of the OPEN cohort

The reconciliation in §3.3 applies to new opens. The cohort open at the time of this amendment (entered 2026-07-23, expiry 2026-08-21) would otherwise be managed under the defective mid denominator for ~4 more weeks, and the reset clock would then start on days that still ran the old rule.

So data/state/vrp/vrp_position.json is corrected once, from verified broker truth — order 56123d75-0117-4f6c-8f14-0df0f497bfad, filled_avg_price -0.18, filled_qty 1.0, read back via thales --sleeve vrp portfolio orders --json:

fieldbeforeafter
entry_credit0.23 (mid)0.18 (realized)
entry_credit_mid0.23 (retained)
max_loss_per_contract$77$82 (true defined risk)
profit trigger fires at close cost0.11500.0900

This is a correction toward measured reality, not a fabrication: every value is read from the broker, and the effect is to make the position harder to close and its recorded risk larger. Nothing here is estimated or back-filled from a model.

5. What would falsify the 2.1 diagnosis

If, over the next cohorts, profit_confirm_reads: 2 never vetoes a trigger — i.e. the confirmation always agrees with the first read — then the 07-22 event was more likely a genuine fast move than quote noise, and the confirmation is harmless-but-unnecessary insurance. The veto rate is logged (manage:hold-unconfirmed) and should be reviewed after ~5 cohorts. A veto rate materially above zero confirms the diagnosis; a rate of exactly zero across many triggers argues the guard is inert and the single instance was a genuine move.

Either way the guard stays: its cost is one quote read.

6. What this amendment does NOT claim

It does not claim the VRP sleeve works. It does not improve any expected return estimate. It removes two ways the implementation departed from its own registered rule, and it makes the sleeve exit later in both cases — a strictly higher bar to close. The forward gate remains the sleeve's entire validation, now running on a rule that matches its registration, from 2026-07-27.