feat(db): record the stop a tranche was sized against (#520, #502 stage 2) - #527
Merged
Conversation
…rrent one ratchets away from it (#520) v12 adds positions.initial_stop, written at tranche open and never rewritten. The break-even arm of exit_policy.next_stop computes its threshold from the trade's ORIGINAL per-unit risk: entry + be_roll_rr * (entry - initial_stop). Live state carried entry_fill (the ledger) and open_stop:<product_id> (the CURRENT, already-ratcheted stop) and nothing else -- so the number that threshold is most sensitive to was simply absent. Substituting the current stop is not an approximation, it is a DIFFERENT POLICY. The current stop rises on every ratchet, shrinking (entry - stop), so the threshold creeps toward entry and the arm fires earlier each time, drifting further from the measured policy the longer a trade runs. Live and sim would then encode two different break-even rules while appearing to share exit_policy's functions -- the exact failure sharing them was meant to prevent. NULL means UNKNOWN and readers must switch the break-even arm OFF for that tranche rather than guess. Zero would be a real number -- a stop 100% below entry -- and a catastrophic one to compute a threshold from, so the column is nullable and _position_row_to_dict is careful not to let _text_to_dec invent a zero. The trailing arm is unaffected either way: it needs no original risk, so a tranche without one keeps trailing and simply never break-even-rolls. NO BACKFILL, deliberately: the honest value for every pre-v12 tranche is NULL. Idempotent by the v8/v11 PRAGMA table_info guard, because a database stamped at v11 got positions from v4's DDL and CREATE TABLE IF NOT EXISTS never adds a column. DCA passes None and that is legitimate, not a degraded case -- it carries no stop by design, and requiring one would refuse a real tranche. The three schema-version tripwires were bumped 11 -> 12 consciously, which is what they exist to force. Gates: pytest 4559 passed / 3 skipped; ruff check keel tests packages clean; mypy clean across 354 source files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyeggYtojNXCTHeD3JHxb6
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #520. First piece of #502 stage 2 — the named prerequisite for the live break-even arm.
The gap
exit_policy.next_stop's break-even arm computes its threshold from the trade's original per-unit risk:Live state carried
entry_fill(the ledger) andopen_stop:<product_id>(the current, already-ratcheted stop) — and nothing else. The number the threshold is most sensitive to was simply absent.Why substituting the current stop is wrong, not merely imprecise
The current stop rises on every ratchet, shrinking
(entry − stop). So the threshold creeps toward entry and the arm fires earlier each time, drifting further from the measured policy the longer a trade runs.Live and sim would then encode two different break-even rules while appearing to share
exit_policy's functions — the exact failure that sharing them was meant to prevent.What ships
positions.initial_stop, idempotent by the v8/v11PRAGMA table_infoguard (a database stamped at v11 gotpositionsfrom v4's DDL, andCREATE TABLE IF NOT EXISTSnever adds a column).agent._open_tranchefrom the entry setup's stop, and never rewritten — that separation from the ratchetingopen_stop:is the fix.Repository.open_positiontakes it;_position_row_to_dictdecodes it nullable.The nullable handling is load-bearing
Nonemeans UNKNOWN, and readers must switch the break-even arm off for that tranche rather than guess.Zero would be a real number — a stop 100% below entry — and a catastrophic one to compute a threshold from. So
_position_row_to_dictis careful not to let_text_to_decinvent one, and there's a test named for it.The trailing arm is unaffected either way: it needs no original risk, so a tranche without one keeps trailing and simply never break-even-rolls.
No backfill, deliberately. The honest value for every pre-v12 tranche is NULL. Inventing one would fabricate the single input the policy is most sensitive to.
DCA passes
None, and that is legitimate rather than degraded — it carries no stop by design, and requiring one would refuse a real tranche.Tests
Six, in
tests/data/test_initial_stop.py:Decimal;None, not zero (the load-bearing one);initial_stopis not rewritten when the live stop ratchets.The three schema-version tripwires were bumped 11 → 12 consciously, which is what they exist to force.
Verification
pytest -q— 4,559 passed, 3 skippedruff check keel tests packages— cleanmypy— clean, 354 source filesNot in this PR
The rest of stage 2: idempotency keys on bracket placement, the roll protocol's pre-flight guards and materiality floor, the executor constructing
BracketGTCvalues, and thescale_outrebuild. The crash-ledger half of the roll protocol already shipped as #522.There is no live consumer of this column yet — the break-even arm itself is stage 3. Same shape as stage 1: the data exists and is correct before anything depends on it.