From d5f59fa2b8dd387cdd4c0fe250c7bf1048d80223 Mon Sep 17 00:00:00 2001 From: Marcos Achaval Date: Sat, 17 Oct 2026 12:09:34 -0300 Subject: [PATCH 1/2] docs(verify): keep resting orders across a restart and cover the safe exit refusals Since #500, paper rebuilds its book from the store at boot. The round trip and ghost recipes still expected the resting order to be ghosted, so the round trip read FAIL on main. Both now prove the order stays live. A real ghost is unreachable from the helper, and the file says so. The map also gains the store lock and reserved id refusals, a paper flatten, the seq_records checks, and the 10 second wait the testnet recipe was missing. Every changed recipe was re-driven live and reads PASS. Co-Authored-By: Claude Opus 5.5 --- .claude/skills/verify-tickwright/SKILL.md | 7 +- .../verify-tickwright/features/README.md | 12 ++-- .../features/config-refusals.md | 29 ++++++-- .../features/crash-recovery.md | 8 ++- .../features/ghost-reconcile.md | 71 +++++++++---------- .../features/hyperliquid-testnet.md | 12 +++- .../features/paper-round-trip.md | 25 ++++--- .../features/portfolio-strategies.md | 12 ++-- .../features/postgres-store.md | 7 +- .../verify-tickwright/features/safe-exit.md | 30 +++++++- 10 files changed, 135 insertions(+), 78 deletions(-) diff --git a/.claude/skills/verify-tickwright/SKILL.md b/.claude/skills/verify-tickwright/SKILL.md index 4630ef14..16fedf24 100644 --- a/.claude/skills/verify-tickwright/SKILL.md +++ b/.claude/skills/verify-tickwright/SKILL.md @@ -23,8 +23,9 @@ Read `features/README.md` before driving. It is the map of what to prove. - Logs are JSON lines on **stderr**, one `"event": ""` per line. The event names are the catalog in `src/tickwright/observability/catalog.py`. - State is the store: SQLite file by default, Postgres when selected. Tables: `orders`, - `positions`, `account`, `kill_switch`, `funding_marks`, `strategy_snapshots`. Order states are - stored lowercase (`filled`, `live`, `rejected`, `denied`). + `positions`, `account`, `kill_switch`, `funding_marks`, `strategy_snapshots`, `seq_records`. + `dump` prints the first five. Order states are stored lowercase (`filled`, `live`, `rejected`, + `denied`). - Under the replay feed, time is virtual. The clock jumps to each tick's `ts_event`. So a tick file with rows 90 seconds apart runs the 90 second ghost grace, and a row past the top of a UTC hour settles paper funding. When the file ends, time stops. Nothing paced by the clock fires @@ -230,7 +231,7 @@ All in `.claude/skills/verify-tickwright/scripts/`, all executable: | `verify` | `.claude/skills/verify-tickwright/scripts/verify ...` (wrapper on the venv python) | | `verify.py` | the helper itself, `verify --help` and `verify --help` | | `run_strategy.py` | run by `verify start --strategy `, never by hand | -| `strategies.py` | the four verification strategies `run_strategy.py` registers | +| `strategies.py` | the five verification strategies `run_strategy.py` registers | ## Out of scope diff --git a/.claude/skills/verify-tickwright/features/README.md b/.claude/skills/verify-tickwright/features/README.md index 8e283219..2cf0b138 100644 --- a/.claude/skills/verify-tickwright/features/README.md +++ b/.claude/skills/verify-tickwright/features/README.md @@ -40,18 +40,19 @@ Each file has an H1, one paragraph, then exactly four H2 sections in this order: ## Features - [Paper round trip and restart](./paper-round-trip.md): the quickstart path, a market fill, a - resting limit, a graceful stop, and a restart that recovers the fill and ghosts the limit. + resting limit, a graceful stop, and a restart that recovers the fill and keeps the limit. - [Crash recovery](./crash-recovery.md): `KILL` mid-run, restart, the saga and ledger converge. - [Kill switch](./kill-switch.md): `USR1` denies every new order, survives a restart, `USR2` clears it. - [Pre-trade limits](./pre-trade-limits.md): the size, value, position, and rate caps each deny one order, and a close bigger than the size and value caps still fills. -- [Ghost reconcile](./ghost-reconcile.md): a resting order the venue no longer holds is rejected - after the 90 second grace window on the wall clock, and at the first tick on replay. +- [Ghost reconcile](./ghost-reconcile.md): a restart keeps a resting order past the 90 second + grace window, on both clocks. A real ghost is unreachable from the helper today. - [Paper economics](./paper-economics.md): taker fees, hourly funding, the seeded stochastic fill model with partial fills. - [Config refusals](./config-refusals.md): the engine refuses to start on a missing genesis, a - changed genesis, and a dead leverage entry, with a readable error and exit 1. + changed genesis, a dead leverage entry, a reserved strategy id, and a held store lock, with a + readable error and exit 1. - [Portfolio strategies](./portfolio-strategies.md): four throwaway strategies that read the `Portfolio` seam, including margin at 5x and a two-symbol book. - [Kafka bus](./kafka-bus.md): the same paper run over `KafkaBus`, messages counted on the topic. @@ -60,4 +61,5 @@ Each file has an H1, one paragraph, then exactly four H2 sections in this order: - [Hyperliquid testnet](./hyperliquid-testnet.md): a real round trip on the testnet exchange, the account reconcile cadence, and key redaction. - [Safe exit](./safe-exit.md): on testnet, `cancel-all` and `flatten` leave the account flat, and - a restart after `flatten` trades nothing until the kill switch is reset. + a restart after `flatten` trades nothing until the kill switch is reset. On paper, the store + lock refuses an exit run and `flatten` works with no key. diff --git a/.claude/skills/verify-tickwright/features/config-refusals.md b/.claude/skills/verify-tickwright/features/config-refusals.md index 68234f63..a6be93aa 100644 --- a/.claude/skills/verify-tickwright/features/config-refusals.md +++ b/.claude/skills/verify-tickwright/features/config-refusals.md @@ -2,7 +2,9 @@ The engine refuses to start on a config it cannot run safely, with a readable message and exit code 1. Three refusals matter most: a paper run with no genesis collateral, a genesis that -disagrees with the store's ledger, and a leverage entry for a symbol no strategy trades. +disagrees with the store's ledger, and a leverage entry for a symbol no strategy trades. Two more +came with safe exit: the reserved strategy id `__operator__`, and a store another engine holds. +An exit run (`cancel-all`, `flatten`) refuses with exit 2 instead. See `safe-exit.md`. ## Sub-features @@ -10,6 +12,9 @@ disagrees with the store's ledger, and a leverage entry for a symbol no strategy - `refuse-mismatch` a different genesis against a store that already holds a ledger (`StoreAccountMismatch`, ADR-0043 §10). - `refuse-dead-leverage` `TICKWRIGHT_LEVERAGE` names a symbol no strategy trades (ADR-0044 §3). +- `refuse-reserved` a strategy uses the id `__operator__` (ADR-0054). +- `refuse-lock` a second engine starts on a store a running engine holds (`StoreLockHeld`, + ADR-0052). ## How to get to it (user POV) @@ -28,9 +33,15 @@ Preconditions: `$V await refuse nogenesis --exit`, then `$V check refuse nogenesis refuse-genesis --exit --expect 1`. The stderr file contains `exchange='paper' needs a starting collateral: set TICKWRIGHT_PAPER__GENESIS_COLLATERAL`. -- **Seed a ledger.** Run `$V start refuse first --preset paper-replay`, - `$V await refuse first --event engine.feed_started`, `$V signal refuse first TERM`, - `$V await refuse first --exit`, `$V dump refuse first`. +- **Seed a ledger.** Run `$V start refuse first --preset paper-replay` and + `$V await refuse first --event engine.feed_started`. Leave it running. +- **Lock held.** While `first` runs, run `$V start refuse locked --preset paper-replay`, + `$V await refuse locked --exit`, then + `$V check refuse locked refuse-lock --exit --expect 1` and + `$V check refuse locked refuse-lock --event engine.faulted --expect 1`. The `engine.faulted` + line's `error` starts with `StoreLockHeld` and names the holder's pid. +- **Stop the seed.** Run `$V signal refuse first TERM`, `$V await refuse first --exit`, + `$V dump refuse first`. - **Changed genesis.** Run `$V start refuse mismatch --preset paper-replay --env TICKWRIGHT_PAPER__GENESIS_COLLATERAL=50000`, `$V await refuse mismatch --exit`, then @@ -42,10 +53,16 @@ Preconditions: `$V await refuse deadlev --exit`, then `$V check refuse deadlev refuse-dead-leverage --exit --expect 1`. The stderr file contains `leverage names symbols no configured strategy trades: ETH`. +- **Reserved id.** Run + `$V start refuse reserved --preset paper-replay --env 'TICKWRIGHT_STRATEGIES=[{"kind":"single_shot_market","strategy_id":"__operator__","symbol":"BTC","side":"buy","quantity":"0.5"}]'`, + `$V await refuse reserved --exit`, then + `$V check refuse reserved refuse-reserved --exit --expect 1`. The stderr file contains + `__operator__ is reserved for operator orders, not a strategy id`. - **Untouched ledger.** Run `$V check refuse first refuse-mismatch --sql "select genesis_collateral from account" --expect 100000`. -- **Report.** Run `$V report refuse`. Expected verdict: `PASS (0 of 5 checks failed)`. The one - alarm event listed, `engine.faulted`, is the mismatch refusal and is expected. +- **Report.** Run `$V report refuse`. Expected verdict: `PASS (0 of 8 checks failed)`. The two + alarm events listed are both `engine.faulted`, one for the lock and one for the mismatch. Both + are expected. - **Cleanup.** Run `$V cleanup refuse`. ## Gotchas diff --git a/.claude/skills/verify-tickwright/features/crash-recovery.md b/.claude/skills/verify-tickwright/features/crash-recovery.md index 357e8033..78d8d094 100644 --- a/.claude/skills/verify-tickwright/features/crash-recovery.md +++ b/.claude/skills/verify-tickwright/features/crash-recovery.md @@ -1,8 +1,9 @@ # Crash recovery The process dies with `KILL` after a fill. The next start rebuilds the order cache and the ledger -from the store, clears the barrier, resumes the strategy seq from the saga high-water so no -signal id is ever reused, and does not place a second order for a strategy that already fired. +from the store, clears the barrier, resumes the strategy seq so no signal id is ever reused, and +does not place a second order for a strategy that already fired. The seq resumes from the higher +of the saga seqs and the `seq_records` table (#442). ## Sub-features @@ -40,7 +41,8 @@ Preconditions: `$V check crash second crash-converge --sql "select signed_size from positions" --expect 0.500`, `$V check crash second crash-converge --event order.placed --expect 0`, `$V check crash second crash-seq --sql "select count(*) from orders where signal_id='shooter:BTC:1'" --expect 1`, - `$V check crash second crash-seq --exit --expect 0`. + `$V check crash second crash-seq --exit --expect 0`, + `$V check crash second crash-seq --sql "select seq from seq_records where strategy_id='shooter'" --expect 1`. - **Report.** Run `$V report crash`. Expected verdict: `PASS`. - **Cleanup.** Run `$V cleanup crash`. diff --git a/.claude/skills/verify-tickwright/features/ghost-reconcile.md b/.claude/skills/verify-tickwright/features/ghost-reconcile.md index 13ec4f70..379ff522 100644 --- a/.claude/skills/verify-tickwright/features/ghost-reconcile.md +++ b/.claude/skills/verify-tickwright/features/ghost-reconcile.md @@ -1,22 +1,26 @@ # Ghost reconcile -A limit order rests `LIVE` in life one. The paper venue's book dies with the process, so in life -two the venue no longer holds it. The barrier does not conclude on one empty read. It arms the -90 second grace window, the open-order cadence re-reads every 30 seconds, and once the order has -been continuously absent across the window it is rejected as a ghost. The window is real on the -wall clock and already spent on the replay clock. Both are worth proving. +A ghost is a resting order the store holds but the venue does not. The barrier never concludes on +one empty read. It arms a 90 second grace window, the open-order cadence re-reads every 30 +seconds, and an order absent for the whole window ends `rejected` as a ghost. Since #484, paper +rebuilds its book from the store at boot, so a plain restart no longer makes a ghost. This file +proves that a restart keeps the order, on both clocks. The helper cannot make a real ghost today. ## Sub-features -- `ghost-armed` life two clears the barrier with the order still `LIVE` in the store. -- `ghost-wall-clock` on the Hyperliquid feed, `ghost.reconciled` lands 90 seconds after - `engine.barrier_cleared`, not before. -- `ghost-replay` on the replay feed, the first tick with a real timestamp ghosts the order. -- `ghost-reject` the row reads `rejected`, reason `reconciliation: ghost: vanished from the venue`. +- `ghost-keep-replay` on the replay feed, a restart keeps the resting order `LIVE` and ghosts + nothing. +- `ghost-keep-wall-clock` on the Hyperliquid feed, the order is still `LIVE` two minutes after + `engine.barrier_cleared`. That is past the 90 second window. +- `ghost-reject` verified-unreachable. A ghost needs an order the venue has no record of. Paper + now keeps every resting order, and no helper verb removes an order from a venue. + `tests/engine/test_reconcile.py` proves the window, the reject, and the reason + `reconciliation: ghost: vanished from the venue` in process. ## How to get to it (user POV) -- Rest a limit, stop, start again, and wait a minute and a half on a live feed. +- Rest a limit, stop, and start again. The order is still `LIVE`, and the venue can still fill + it. ## Driving it with verify @@ -30,39 +34,34 @@ Preconditions: `$V start ghostlive first --preset paper-livefeed --env "TICKWRIGHT_STRATEGIES=$S"`, `$V await ghostlive first --order $($V cloid rester:BTC:1)=LIVE --timeout 90`, `$V signal ghostlive first TERM`, `$V await ghostlive first --exit`. -- **Wall clock, ghost.** Run the same `start` with life `second`, - `$V await ghostlive second --event engine.barrier_cleared --timeout 60`, then - `$V await ghostlive second --event ghost.reconciled --timeout 200`. Compare the two - `timestamp` fields: the gap is 90 seconds plus at most one 30 second cadence - (observed 90.0 s). Then `TERM`, `--exit`, `dump`. Check: - `$V check ghostlive second ghost-armed --event engine.barrier_cleared --expect 1`, - `$V check ghostlive second ghost-wall-clock --event ghost.reconciled --expect 1`, - `$V check ghostlive second ghost-reject --sql "select state from orders" --expect rejected`, - `$V check ghostlive second ghost-reject --event order.placed --expect 0`. - Then `$V report ghostlive`, expected `PASS (0 of 4 checks failed)`, and `$V cleanup ghostlive`. +- **Wall clock, keep.** Run the same `start` with life `second`, + `$V await ghostlive second --event engine.barrier_cleared --timeout 60`, then wait two + minutes. Then `TERM`, `--exit`, `dump`. Check: + `$V check ghostlive second ghost-keep-wall-clock --sql "select state from orders" --expect live`, + `$V check ghostlive second ghost-keep-wall-clock --event ghost.reconciled --expect 0`, + `$V check ghostlive second ghost-keep-wall-clock --event order.placed --expect 0`. + Then `$V report ghostlive`, expected `PASS (0 of 3 checks failed)`, and `$V cleanup ghostlive`. - **Replay, rest.** Run `$V init ghost --feature ghost-reconcile`, `$V ticks ghost first --row ETH:2500@0`, - `$V ticks ghost second --base 2024-01-01T00:00:05+00:00 --row ETH:2502@0`, + `$V ticks ghost second --base 2024-01-01T00:00:05+00:00 --row ETH:2502@0 --row ETH:2501@2m`, `$V start ghost first --preset paper-replay --env TICKWRIGHT_REPLAY__PATH=first.jsonl --env "TICKWRIGHT_STRATEGIES=$S"`, `$V await ghost first --order $($V cloid rester:ETH:1)=LIVE`, `TERM`, `--exit`. -- **Replay, ghost.** Same `start` with life `second` and `TICKWRIGHT_REPLAY__PATH=second.jsonl`, - then `$V await ghost second --event ghost.reconciled --timeout 10`. It lands on the first - tick. Then `TERM`, `--exit`, `dump`. Check: - `$V check ghost second ghost-replay --event ghost.reconciled --expect 1`, - `$V check ghost second ghost-reject --sql "select state from orders" --expect rejected`, - `$V check ghost second ghost-reject --sql "select reason from orders" --expect "reconciliation: ghost: vanished from the venue"`. -- **Report.** Run `$V report ghost`. Expected verdict: `PASS (0 of 3 checks failed)`. The - `ghost.reconciled` and `order.rejected` alarm lines are the feature, not a fault. +- **Replay, keep.** Same `start` with life `second` and `TICKWRIGHT_REPLAY__PATH=second.jsonl`, + then `$V await ghost second --event engine.feed_started` and wait three seconds. The replay + clock starts at 0, and the first row jumps it to 2024, so the 90 second window is spent at + once. The second row gives reconcile two more minutes. Then `TERM`, `--exit`, `dump`. Check: + `$V check ghost second ghost-keep-replay --sql "select state from orders" --expect live`, + `$V check ghost second ghost-keep-replay --event ghost.reconciled --expect 0`. +- **Report.** Run `$V report ghost`. Expected verdict: `PASS (0 of 2 checks failed)`. - **Cleanup.** Run `$V cleanup ghost`. ## Gotchas -- The replay clock starts at 0 in every life. The barrier arms the window at 0, and the first - tick jumps to 2024, so the window is spent at once. This is why the replay case cannot measure - the 90 seconds. Only the wall-clock case can. The in-process measurement on virtual time is - `tests/engine/test_reconcile.py`. -- A recovered order has no "last event" in the new life, so the 30 second recent-order - protection does not apply to it. Only the grace window guards it. +- Paper puts back only `LIVE` or partly filled GTC limit rows that hold a price (`_resting_orders` + in `app/build.py`). A row from before the store kept order terms has no price. Paper cannot + rest it, so reconcile still ghosts it. The helper cannot make such a row. +- Before #484 this file proved a ghost on paper. Older runs in `$V runs` that read `PASS` on + `ghost-replay` or `ghost-wall-clock` proved that old behavior. - The resting price must stay far from the market so the order is never filled instead. Under the live feed use 20000 for BTC. - The `--base` of the second replay file must be later than the last row of the first. diff --git a/.claude/skills/verify-tickwright/features/hyperliquid-testnet.md b/.claude/skills/verify-tickwright/features/hyperliquid-testnet.md index fee9d944..efd0e92a 100644 --- a/.claude/skills/verify-tickwright/features/hyperliquid-testnet.md +++ b/.claude/skills/verify-tickwright/features/hyperliquid-testnet.md @@ -31,7 +31,7 @@ Preconditions: - Run id `testnet` is unused. Budget about eight minutes. - **Run.** Run `$V init testnet --feature hyperliquid-testnet` and - `$V start testnet rt --preset testnet --forward-key --strategy round_trip_then_flat --param QUANTITY=0.001 --param HOLD_TICKS=3`. + `$V start testnet rt --preset testnet --forward-key --strategy round_trip_then_flat --param QUANTITY=0.001 --param HOLD_TICKS=3 --param WAIT_SECONDS=10`. - **Materialise.** Run `$V await testnet rt --event account.materialised --timeout 60`. The line carries the qualified `account_id` (`hyperliquid-testnet-0x...`) and the venue's cash as `genesis_collateral`. @@ -50,7 +50,9 @@ Preconditions: `$V check testnet rt tn-reconcile --event account.reconciled --expect 1`, `$V check testnet rt tn-cash --event account.healed --expect 0`, `$V check testnet rt tn-redaction --event verify.flat --expect 1` (the run reached the end), - `$V check testnet rt tn-divergence --event valuation.divergence --expect 0` + `$V check testnet rt tn-divergence --event valuation.divergence --expect 0`, + `$V check testnet rt tn-round-trip --sql "select isolated_collateral from positions" --expect 0` + (a closed position holds a known 0, not NULL, #524) and read the `secrets-check` line: `no signing key in evidence`. - **Report.** Run `$V report testnet`. Expected verdict: `PASS`. A FAIL that matches a Gotcha below is known. Any other FAIL is new, and `$V issue-draft testnet` drafts it. @@ -60,7 +62,11 @@ Preconditions: - A buy sent before the first mark is never sent. It ends `failed` and the engine keeps running ([#509](https://github.com/MarcosACH/tickwright/issues/509)). The round trip needs the buy to - fill, so add `--param WAIT_SECONDS=10` to the `rt` life. + fill, so the `rt` life waits 10 seconds with `--param WAIT_SECONDS=10`. +- On live, `margin_used` and `liquidation_price` read `null` in the `verify.fill` and + `verify.tick` lines until a reconcile posts the venue's numbers (#517, #519). That is the + "unknown, not zero" rule, not a failure. Mid-hold, `positions.isolated_collateral` can be NULL + for the same reason. - Every run places the same cloids on the venue, because the cloid is derived from `round_trip:BTC:2`. The engine reads and cancels by oid once it has one, and before the ack it diff --git a/.claude/skills/verify-tickwright/features/paper-round-trip.md b/.claude/skills/verify-tickwright/features/paper-round-trip.md index aaffeb93..c481b596 100644 --- a/.claude/skills/verify-tickwright/features/paper-round-trip.md +++ b/.claude/skills/verify-tickwright/features/paper-round-trip.md @@ -2,8 +2,8 @@ The quickstart path. An operator points the engine at a tick file, a market order fills on the first tick, a low limit order rests, `Ctrl-C` stops the process with exit 0, and a second start -recovers the fill, places nothing new, and ghosts the resting order because the paper book died -with the first process. +recovers the fill and places nothing new. Paper rebuilds its book from the store, so the resting +order is still `LIVE` in the second life (#484). ## Sub-features @@ -11,11 +11,12 @@ with the first process. - `paper-rest` a `single_shot_limit` below the market rests `LIVE`. - `paper-stop` `TERM` exits 0, snapshots strategies, leaves the resting order alone. - `paper-restart` the second life places nothing and exits 0. -- `paper-ghost` the second life rejects the resting order as a ghost on the first tick. +- `paper-keep` the second life keeps the resting order `LIVE` and ghosts nothing. ## How to get to it (user POV) - `cp .env.example .env && uv run tickwright`, then `Ctrl-C`, then `uv run tickwright` again. + `.env.example` runs one market strategy. Add a `single_shot_limit` entry to see a resting order. ## Driving it with verify @@ -37,25 +38,23 @@ Preconditions: `$V check rt first paper-rest --sql "select state from orders where cloid='$REST'" --expect live`, `$V check rt first paper-stop --exit --expect 0`. - **Life two.** Run `$V start rt second --preset paper-replay --env "TICKWRIGHT_STRATEGIES=$S"`, - `$V await rt second --event engine.feed_started`, `$V await rt second --event ghost.reconciled --timeout 10`, + `$V await rt second --event engine.feed_started`, wait two seconds, `$V signal rt second TERM`, `$V await rt second --exit`, `$V dump rt second`. - **Check life two.** Run `$V check rt second paper-restart --event order.placed --expect 0`, `$V check rt second paper-restart --exit --expect 0`, - `$V check rt second paper-ghost --sql "select state from orders where cloid='$REST'" --expect rejected`. -- **Report.** Run `$V report rt`. Expected verdict: `PASS (0 of 7 checks failed)`. + `$V check rt second paper-keep --sql "select state from orders where cloid='$REST'" --expect live`, + `$V check rt second paper-keep --event ghost.reconciled --expect 0`. +- **Report.** Run `$V report rt`. Expected verdict: `PASS (0 of 8 checks failed)`. - **Cleanup.** Run `$V cleanup rt`. Evidence stays in `.agents/verify/rt/evidence/`. ## Gotchas - The engine does not exit when the tick file ends. Waiting on `--exit` without a signal hangs until the timeout. -- The resting order is ghosted in life two, not kept. The barrier leaves it `LIVE` and arms the - grace window at virtual time 0. The first tick jumps the clock to 2024, so the 90 second - window is already spent and the first open-order cycle rejects it. This is correct for the - paper venue, whose book died with life one. See `ghost-reconcile.md` for the real-time window. -- `tests/app/test_cli_e2e.py` asserts no ghost in life two. It passes only because its ticks - sit at `ts_event` 1000 ns, so the clock never moves past the window. Do not copy that - expectation here. +- The resting order is kept in life two, not ghosted. Before the barrier, paper puts back every + `LIVE` or partly filled GTC limit that the store holds (#484, `app/build.py`). A live venue + keeps resting orders across a restart, and paper copies it. Before #484 this order was ghosted + on the first tick. See `ghost-reconcile.md`. - `isolated_collateral` in `positions` equals the notional at the default 1x isolated leverage. That is expected, not a bug. diff --git a/.claude/skills/verify-tickwright/features/portfolio-strategies.md b/.claude/skills/verify-tickwright/features/portfolio-strategies.md index c2a071e7..5103def1 100644 --- a/.claude/skills/verify-tickwright/features/portfolio-strategies.md +++ b/.claude/skills/verify-tickwright/features/portfolio-strategies.md @@ -1,9 +1,10 @@ # Portfolio strategies -Four throwaway strategies in `scripts/strategies.py` read the `Portfolio` seam the way a real -strategy would and log what they read as `verify.*` events. They prove the accounting surface -from the strategy's side of the seam: positions return to flat, unrealized PnL drives a decision, -margin math holds at leverage, and two strategies see their own partitions but one account. +Four of the five throwaway strategies in `scripts/strategies.py` read the `Portfolio` seam the +way a real strategy would and log what they read as `verify.*` events. They prove the accounting +surface from the strategy's side of the seam: positions return to flat, unrealized PnL drives a +decision, margin math holds at leverage, and two strategies see their own partitions but one +account. ## Sub-features @@ -18,7 +19,8 @@ margin math holds at leverage, and two strategies see their own partitions but o - `$V start --preset paper-replay --strategy --param KEY=VALUE`. Params: `SYMBOL` (BTC), `SYMBOL2` (ETH), `QUANTITY` (0.5), `HOLD_TICKS` (2), `TAKE_PROFIT` (100), - `LEVERAGE` (JSON, `{}`). + `LEVERAGE` (JSON, `{}`), `REST_BELOW` (0.8), `WAIT_SECONDS` (0). The last two are for the fifth + strategy, `rest_then_hold`, and for live feeds. See `safe-exit.md`. ## Driving it with verify diff --git a/.claude/skills/verify-tickwright/features/postgres-store.md b/.claude/skills/verify-tickwright/features/postgres-store.md index 85c05a46..93c9dd8d 100644 --- a/.claude/skills/verify-tickwright/features/postgres-store.md +++ b/.claude/skills/verify-tickwright/features/postgres-store.md @@ -34,9 +34,10 @@ Preconditions: `$V await pg second --exit`, `$V dump pg second`. - **Check life two.** Run `$V check pg second pg-restart --event order.placed --expect 0`, `$V check pg second pg-restart --sql "select count(*) from orders" --expect 1`, - `$V check pg second pg-restart --exit --expect 0`. Also `ls .agents/verify/pg/scratch` shows - no `store.db`. -- **Report.** Run `$V report pg`. Expected verdict: `PASS (0 of 6 checks failed)`. + `$V check pg second pg-restart --exit --expect 0`, + `$V check pg second pg-restart --sql "select seq from seq_records where strategy_id='shooter'" --expect 1`. + Also `ls .agents/verify/pg/scratch` shows no `store.db`. +- **Report.** Run `$V report pg`. Expected verdict: `PASS (0 of 7 checks failed)`. - **Cleanup.** Run `$V cleanup pg`. It removes the container only when this run started it. A Postgres you already had up is left running, and the `verify_pg` database stays in it. diff --git a/.claude/skills/verify-tickwright/features/safe-exit.md b/.claude/skills/verify-tickwright/features/safe-exit.md index fb1557c7..1032e716 100644 --- a/.claude/skills/verify-tickwright/features/safe-exit.md +++ b/.claude/skills/verify-tickwright/features/safe-exit.md @@ -4,7 +4,8 @@ An operator gets out of the market in two one-shot commands. `tickwright cancel- resting order in the account. `tickwright flatten` pulls them too, closes every position, and trips the kill switch. Each exits 0 only when its last venue read finds nothing left (ADR-0060). This recipe proves both on Hyperliquid testnet, and that the engine trades nothing after a -flatten until the operator resets the kill switch. +flatten until the operator resets the kill switch. A short paper run proves the store lock +refusal and a flatten with no key. ## Sub-features @@ -14,6 +15,9 @@ flatten until the operator resets the kill switch. - `se-halted` a restarted engine has its first order denied. The venue gets nothing. - `se-reset` after `USR2`, the next life trades a round trip and ends flat. - `se-redaction` the signing key appears nowhere in the evidence. +- `se-lock` on paper, `cancel-all` while the engine runs exits 2 with `exit.refused`. +- `se-paper-flatten` on paper replay, `flatten` exits 0, cancels the resting buy, closes the long, + and trips the kill switch. ## How to get to it (user POV) @@ -68,6 +72,26 @@ Preconditions: - **Report.** Run `$V report safe-exit`. Expected verdict: `PASS`. A FAIL that is not a Gotcha below is new, and `$V issue-draft safe-exit` drafts it. - **Cleanup.** Run `$V cleanup safe-exit`. +- **Paper, set up.** In a separate run, run `$V init se-paper --feature safe-exit`, + `$V ticks se-paper ticks --row BTC:42000@0 --row BTC:42100@1s --row BTC:42200@2s`, + `$V start se-paper held --preset paper-replay --strategy rest_then_hold --param QUANTITY=0.5`, + `$V await se-paper held --event verify.rested`, and + `$V await se-paper held --order $($V cloid rest_then_hold:BTC:2)=LIVE`. Leave it running. +- **Paper, lock.** While `held` runs, run + `$V start se-paper lock --preset paper-replay --command cancel-all` and + `$V await se-paper lock --exit`. Check + `$V check se-paper lock se-lock --exit --expect 2`, + `$V check se-paper lock se-lock --event exit.refused --expect 1`. The line reads + `reason: lock_held` and names the holder's pid. Then `$V signal se-paper held TERM`, + `$V await se-paper held --exit`, `$V dump se-paper held`. +- **Paper, flatten.** Run `$V start se-paper flatten --preset paper-replay --command flatten`, + `$V await se-paper flatten --exit --timeout 60`, `$V dump se-paper flatten`. Check + `$V check se-paper flatten se-paper-flatten --exit --expect 0`, + `$V check se-paper flatten se-paper-flatten --sql "select state from orders where cloid='$($V cloid rest_then_hold:BTC:2)'" --expect cancelled`, + `$V check se-paper flatten se-paper-flatten --sql "select count(*) from positions where cast(signed_size as real) != 0" --expect 0`, + `$V check se-paper flatten se-paper-flatten --sql "select tripped from kill_switch" --expect 1`. + The store holds one `__operator__` market sell of 0.5. Then `$V report se-paper`, expected + `PASS (0 of 6 checks failed)`, and `$V cleanup se-paper`. ## Gotchas @@ -88,3 +112,7 @@ Preconditions: - `halted` uses `margin_watch` on purpose. A denied strategy keeps its `buying` phase in its snapshot, so the `reset` life uses a different strategy id to start clean. - `feed.lagged` lines are normal on testnet. The stream conflates. +- The paper `cancel-all` and `flatten` see the resting buy because paper rebuilds its book from + the store at boot (#484). A replay flatten plays the whole tick file and fills at the last row + (ADR-0059). +- `start` cannot pass `--new-store`, so the `new_store` refusal is not driven here. From ba263ec442495698c1626c9d091fec0da770628f Mon Sep 17 00:00:00 2001 From: Marcos Achaval Date: Sat, 17 Oct 2026 12:20:11 -0300 Subject: [PATCH 2/2] docs(verify): point the paper exit run at its own preconditions The safe exit preconditions asked for a testnet key, so a reader with no key skipped the paper part. Also cite the test that checks the ghost reason, and split one long sentence. Co-Authored-By: Claude Opus 5.5 --- .../skills/verify-tickwright/features/ghost-reconcile.md | 7 ++++--- .claude/skills/verify-tickwright/features/safe-exit.md | 5 ++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.claude/skills/verify-tickwright/features/ghost-reconcile.md b/.claude/skills/verify-tickwright/features/ghost-reconcile.md index 379ff522..0e2826bf 100644 --- a/.claude/skills/verify-tickwright/features/ghost-reconcile.md +++ b/.claude/skills/verify-tickwright/features/ghost-reconcile.md @@ -1,7 +1,7 @@ # Ghost reconcile A ghost is a resting order the store holds but the venue does not. The barrier never concludes on -one empty read. It arms a 90 second grace window, the open-order cadence re-reads every 30 +one empty read. It arms a 90 second grace window. The open-order cadence re-reads every 30 seconds, and an order absent for the whole window ends `rejected` as a ghost. Since #484, paper rebuilds its book from the store at boot, so a plain restart no longer makes a ghost. This file proves that a restart keeps the order, on both clocks. The helper cannot make a real ghost today. @@ -14,8 +14,9 @@ proves that a restart keeps the order, on both clocks. The helper cannot make a `engine.barrier_cleared`. That is past the 90 second window. - `ghost-reject` verified-unreachable. A ghost needs an order the venue has no record of. Paper now keeps every resting order, and no helper verb removes an order from a venue. - `tests/engine/test_reconcile.py` proves the window, the reject, and the reason - `reconciliation: ghost: vanished from the venue` in process. + `tests/engine/test_reconcile.py` proves the window and the reject in process. + `tests/engine/test_execution_manager.py` proves the reason + `reconciliation: ghost: vanished from the venue`. ## How to get to it (user POV) diff --git a/.claude/skills/verify-tickwright/features/safe-exit.md b/.claude/skills/verify-tickwright/features/safe-exit.md index 1032e716..1f8eab0a 100644 --- a/.claude/skills/verify-tickwright/features/safe-exit.md +++ b/.claude/skills/verify-tickwright/features/safe-exit.md @@ -35,6 +35,8 @@ Preconditions: - `flatten` closes everything in the testnet account, not only this run's orders. Use an account that holds nothing else you want to keep. - Run id `safe-exit` is unused. Budget about ten minutes. +- The paper section at the end needs no key and no network. It runs on its own. Run id + `se-paper` is unused. - **Setup.** Run `$V init safe-exit --feature safe-exit` and `$V start safe-exit open --preset testnet --forward-key --strategy rest_then_hold --param QUANTITY=0.001 --param WAIT_SECONDS=10`. @@ -72,7 +74,8 @@ Preconditions: - **Report.** Run `$V report safe-exit`. Expected verdict: `PASS`. A FAIL that is not a Gotcha below is new, and `$V issue-draft safe-exit` drafts it. - **Cleanup.** Run `$V cleanup safe-exit`. -- **Paper, set up.** In a separate run, run `$V init se-paper --feature safe-exit`, +- **Paper, set up.** This section is a separate run. It works with or without the testnet steps + above. Run `$V init se-paper --feature safe-exit`, `$V ticks se-paper ticks --row BTC:42000@0 --row BTC:42100@1s --row BTC:42200@2s`, `$V start se-paper held --preset paper-replay --strategy rest_then_hold --param QUANTITY=0.5`, `$V await se-paper held --event verify.rested`, and