Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .claude/skills/verify-tickwright/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<name>"` 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
Expand Down Expand Up @@ -230,7 +231,7 @@ All in `.claude/skills/verify-tickwright/scripts/`, all executable:
| `verify` | `.claude/skills/verify-tickwright/scripts/verify <verb> ...` (wrapper on the venv python) |
| `verify.py` | the helper itself, `verify --help` and `verify <verb> --help` |
| `run_strategy.py` | run by `verify start --strategy <name>`, 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

Expand Down
12 changes: 7 additions & 5 deletions .claude/skills/verify-tickwright/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
29 changes: 23 additions & 6 deletions .claude/skills/verify-tickwright/features/config-refusals.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@

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

- `refuse-genesis` no `TICKWRIGHT_PAPER__GENESIS_COLLATERAL` on a paper run.
- `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)

Expand All @@ -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
Expand All @@ -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
Expand Down
8 changes: 5 additions & 3 deletions .claude/skills/verify-tickwright/features/crash-recovery.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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`.

Expand Down
72 changes: 36 additions & 36 deletions .claude/skills/verify-tickwright/features/ghost-reconcile.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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.
Expand All @@ -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
Expand Down
Loading
Loading