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..0e2826bf 100644 --- a/.claude/skills/verify-tickwright/features/ghost-reconcile.md +++ b/.claude/skills/verify-tickwright/features/ghost-reconcile.md @@ -1,22 +1,27 @@ # 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 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) -- 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 +35,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..1f8eab0a 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) @@ -31,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`. @@ -68,6 +74,27 @@ 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.** 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 + `$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 +115,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.