docs: jev-trader exec-flow study + FLIGHT13 DITAv3.00/ASEx comparison
This commit is contained in:
238
prod/docs/JEV_EXEC_FLOW_STUDY.md
Normal file
238
prod/docs/JEV_EXEC_FLOW_STUDY.md
Normal file
@@ -0,0 +1,238 @@
|
||||
# JEV‑Trader Execution Flow Study
|
||||
|
||||
**Subject:** `https://github.com/jarrodwatts/jev-trader` (tree at commit `b587759e`, `main`).
|
||||
**Scope:** this document studies the **execution logic outside Jev itself** — i.e. the order‑placement / book‑interaction state machine in `src/trader.ts`, `src/market.ts`, `src/trades.ts`, `src/chain.ts`, `src/server.ts`. Jev (the LLM decision model) is only the **decision point**; it is treated as an opaque oracle below. **Read‑only study: no files in the working tree were altered.**
|
||||
|
||||
**TL;DR:** Jev‑trader is a **single‑in‑flight‑order, post‑only, replace‑every‑block maker‑capture bot**. One order rests on the book at a time; every ~300 ms block it cancels the resting order and posts a fresh one **one tick inside the touch on the model’s side, never crossing**. It earns the spread by being the counterparty taker flow hits; it does **not** chase, does **not** do TP/SL, and has **no venue‑truth reconciliation** — it relies on `eth_getTransactionReceipt` + a 10‑block `lost` timeout. The parts that are portable to Hyperliquid are the *book‑placement policy* and the *serialized one‑send‑per‑block cadence*, **not** the Kuru/Monad‑specific plumbing.
|
||||
|
||||
---
|
||||
|
||||
## 0. Repository surface (what exists, what is dead‑simple)
|
||||
|
||||
```
|
||||
jev-trader/
|
||||
├── package.json # bun runtime; deps: ethers@5, @kuru-labs/kuru-sdk, ai + @ai-sdk/typesafe-ai
|
||||
├── tsconfig.json # strict, verbatimModuleSyntax, noUncheckedIndexedAccess
|
||||
├── index.ts # 1) market.init(); 2) createModel(); 3) startServer(...); 4) new Trader(...);
|
||||
├── # trader.attachTradeFeed(log10(sizePrecision)); startBlockFeed(onBlock)
|
||||
├── src/
|
||||
│ ├── config.ts # env→typed config (23 knobs; see §1)
|
||||
│ ├── chain.ts # JSON‑RPC eth_call/sendRaw/BlockNumber; WS newHeads + poll backstop
|
||||
│ ├── book.ts # ONE eth_call getL2Book decoder (mirrors SDK bit‑for‑bit)
|
||||
│ ├── market.ts # Kuru Market: book read, order build/encode (batchUpdate), send, receipt, gas
|
||||
│ └── model.ts # Model interface: {buy,sell,hold} + upIn10; JevModel vs MockModel
|
||||
├── src/trader.ts # ★ the FSM: onBlock, send→receipt, fill, position, totals
|
||||
├── src/trades.ts # Trade‑log feed (eth_getLogs Trade event, chunked ≤100 blocks)
|
||||
└── src/server.ts # Bun.serve HTTP + SSE (snapshot/history/events, ping 15s)
|
||||
```
|
||||
`src/index.ts` is the **only** program entry. There are **no tests, no services, no worker pool** — one process, one loop, one order. The `scripts/` dir (`probe‑*.ts`, `dry‑encode.ts`, `bench‑read.ts`, `trace‑rpc.ts`) are offline probes/dry‑run signers, **not** on the live path.
|
||||
|
||||
---
|
||||
|
||||
## 1. Configuration (the 23 levers that bound the exec)
|
||||
|
||||
From `config.ts` (the only place behavior is tunated at runtime):
|
||||
|
||||
| knob | value | exec meaning |
|
||||
|---|---|---|
|
||||
| `tradeSizeMon` | 200 (min Kuru order) | **fixed** order size every block — not notional‑scaled |
|
||||
| `maxPositionMon` | 1000 | position cap = **5× trade size**; hard stop‑gap |
|
||||
| `bankrollUsd` | 100 | PnL% denominator only (cosmetic) |
|
||||
| `quoteInsideTicks` | 1 | quote **1 tick inside the touch** |
|
||||
| `marginMon` / `marginUsdc` | 600 / 20 | Kuru margin account top‑up targets |
|
||||
| `gasLimit` / fallback | 350,000 | **hardcoded** per block (Monad charges the limit) |
|
||||
| `maxFeeGwei` / priority | 400 / 2 | static type‑2 fees (EIP‑1559) |
|
||||
| `pendingBlocks` | 10 | receipt‑wait before `lost` |
|
||||
| `refreshBlocks` | 200 | fee estimate + margin + vault refresh cadence |
|
||||
| `horizonBlocks` | 100 | the model’s prediction window (~30 s) |
|
||||
| `model` | `mock`\|`jev` | Jev is swap‑in only here |
|
||||
| `jevUsdPerMTok` | 0.042 | inference cost accounting |
|
||||
|
||||
Key structural facts: **size is constant** (200 MON), **quote distance is fixed at 1 tick**, and **gas is a static ceiling** — nothing adapts to book depth or volatility at runtime. The exec is deliberately stateless across blocks except for `orders`/`inflight`/`position`.
|
||||
|
||||
---
|
||||
|
||||
## 2. The 300‑ms hot path (why it is “exactly two round trips”)
|
||||
|
||||
Per README §“The 300ms budget” and `trader.onBlock`:
|
||||
|
||||
1. `confirmPending(block)` → off‑hot‑path `pollPending` (receipts for prior sends) — overlaps the read.
|
||||
2. `market.readBook()` → **one `eth_call` `getL2Book`** on `READ_RPC_URL` (~18 ms p50 on the public RPC). Optional vault read batched into the same HTTP request.
|
||||
3. `model.decide(state)` → the Jev call (≈ real inference; `MockModel` sleeps 80 ms to emulate).
|
||||
4. `market.send(...)` → `signTransaction` + **`eth_sendRawTransaction`** on `RPC_URL` (returns the hash as soon as the pool accepts it).
|
||||
|
||||
That is it on the hot path: **one read + one write per block**. Fees, margin, vault, estimateGas, receipts, and fills are **never** on the hot path (they run on later blocks or once at `init()` — see `market.init` / `refresh` / `pollPending`). `gasLimit` is estimated **once at startup** (`initGasLimit`: 1 `estimateGas` + 90k headroom × 1.15) and then **hardcoded** — no per‑block `eth_estimateGas`. Effective gas price = `MAX_FEE_GWEI` cap + `PRIORITY_FEE_GWEI`=2gwei; the cap is “free” so the real cost is base+priority. `gasMon = gasLimit × effectiveFeeWei`.
|
||||
|
||||
---
|
||||
|
||||
## 3. The decision point (how Jev is *asked*, not what it answers)
|
||||
|
||||
`model.ts` `QUESTIONS.direction.type = "choice"` with `instructions`:
|
||||
|
||||
> **“Will MON be higher or lower than the current mid after `horizonBlocks` more blocks?”**
|
||||
> goal: trade MON‑USDC on Kuru. Blocks ~300 ms; horizon ≈30 s is the move window. A decision is made every few blocks and held until the next one. The trade crosses the spread (`spreadBps`), so the move must beat that cost.
|
||||
> criteria: buy = “mid more likely HIGHER after horizon, by > spread”; sell = “LOWER”.
|
||||
> timing: “immediate‑or‑cancel market order in the next block” (note: this is the mock’s doc; the live path is post‑only maker rest).
|
||||
|
||||
The model returns `Action ∈ {buy,sell}` (hold only when late) + `probabilities:{buy,sell,hold}` + `upIn10` (= buy prob, the “will price go up” number). `trader.ts:105` maps it: `wanted = action==="sell" ? "sell" : "buy"` (buy wins ties / default). **Jev never says “how big” or “when to exit”** — size is fixed at 200 MON and there is no TP/SL; the model is asked the same binary every block and a fresh order is placed. This is the crux of how Jev is used: a **stream of independent per‑block binary direction calls**, each consumed by one post‑only maker order.
|
||||
|
||||
---
|
||||
|
||||
## 4. HOW IT EXECs AGAINST THE BOOK (the portable core)
|
||||
|
||||
### 4.1 Book reading — one eth_call, exact‑match decode (`book.ts`)
|
||||
`readBook()` issues a **single `eth_call getL2Book`** (`0x46fdfbb1`) tagged `latest`; if the Kuru AMM vault is active (`readVaultParams` `getVaultParams` `0x88bb4f60`), **both calls go in one HTTP batch** — no second round trip. This exists only to beat the SDK path (SDK does 2 sequential eth_calls; jev does 1, or 1 batched). Decoding (`decodeL2Book`, `buildBook`) replicates the Kuru SDK `getFormattedL2OrderBook` **bit‑for‑bit** (parseFloat, floor bids / ceil asks to tick, group by price, same ordering) so `bid/ask/mid/imbalance` match exactly. `buildBook` then emits `levels` (top 5 each side), `depthBps` (10/25/50 bps), `spreadBps`, `imbalance` — the full state fed to `buildState`. A book with an empty side **throws** (`empty book side at block …`), i.e. jev would rather skip a block than guess.
|
||||
|
||||
### 4.2 The placement policy — post‑only, 1 tick inside, never crosses (`market.quotePrice`)
|
||||
```ts
|
||||
quotePrice(side, book):
|
||||
bidU = round(book.bid * scale); askU = round(book.ask * scale) // tick units
|
||||
step = QUOTE_INSIDE_TICKS * tickSize
|
||||
p = side==="buy" ? bidU + step : askU - step // buy rests BELOW touch, sell ABOVE
|
||||
if (buy && p >= askU) p = bidU // clamp to touch (spread too tight)
|
||||
if (sell && p <= bidU) p = askU // never cross
|
||||
return p / scale
|
||||
```
|
||||
Two independent guarantees the order **cannot cross the spread**: (a) the price math clamps to the touch when the spread is < the tick step, and (b) the on‑chain call is encoded with `postOnly=true` (the 5th arg of `batchUpdate` — see `encode`). The quote sits **one tick inside the touch** whenever the book is wide enough — this is the “capture” edge: a taker printing at the touch fills our resting order one tick early and we earn the tick.
|
||||
|
||||
### 4.3 The replace‑every‑block mechanic — atomic cancel‑all + one place (`trader.onBlock` + `market.send`)
|
||||
Every block (when not busy and when `side` is allowed):
|
||||
```ts
|
||||
cancel = [...this.orders.keys()].filter(id => id > 0) // confirmed RESTING order ids only
|
||||
// (inflight txs awaiting receipt are NOT cancelled — they live in this.inflight, keyed by txHash)
|
||||
quote = await market.send(block, side, TRADE_SIZE_MON, book, cancel, side !== wanted)
|
||||
```
|
||||
`market.send` builds **one type‑2 tx `batchUpdate(buyPrices,buySizes,sellPrices,sellSizes, orderIdsToCancel, postOnly=true)`** — the cancel list + the single new post‑only order in **one Kuru call** (atomic from jev’s view: the book never sees a gap where we have no quote). `value: 0` (funded from the margin account). On send: `nonce++`, `inflight.set(hash, quote)`, status=`sent` (`gasMon = gasLimit × feeWei` charged upfront — see §4.5). **Dry run**: `status:"sim"`, no signature; `orders.clear(); orders.set(--simId, {…})` (negative id marks sim).
|
||||
|
||||
This is the replace‑every‑block loop: each block cancels the **last confirmed resting order** and posts a fresh one on the (possibly new) model side. Because cancellations ride in the same tx as the new place (same cloid family on Kuru? — no: it’s cancel‑by‑oid + place, but the cancel list is the prior receipt’s `orderId`), the resting leg moves atomically relative to the book. (Compare FLIGHT §6.4, which uses a same‑cloid atomic **modify** to the touch — fee‑free and truly stateless; jev re‑uses an order‑id cancel list instead.)
|
||||
|
||||
### 4.4 The one‑in‑flight gate (the “never fire two” rule) — `trader.onBlock` busy flag
|
||||
```ts
|
||||
if (this.busy) {
|
||||
this.totals.lateBlocks++
|
||||
if (this.lastBook) this.emit(block, this.lastBook, null, null, true) // decision=null → emit "hold"
|
||||
return
|
||||
}
|
||||
this.busy = true
|
||||
...
|
||||
} finally { this.busy = false }
|
||||
```
|
||||
A block arriving while the prior `readBook→decide→send` is still running is **not** a new opportunity: it is emitted as a **late block** (`decision.hold {buy:0,sell:0,hold:1}`, no quote, `late:true`), and **no order is placed**. This is jev’s serialization primitive: at most **one send per block cadence**. It also means jev never races itself — there is exactly one resting order (the newest confirmed), and the cancel list always names the one prior confirmed resting order.
|
||||
|
||||
### 4.5 Receipt handling — placed / reverted / lost (`market.pollPending` + `parseReceipt`)
|
||||
Receipts are polled **off the hot path**: `confirmPending` runs at the top of the next `onBlock`, `Promise.all` over `pending`, one `eth_getTransactionReceipt` per in‑flight hash.
|
||||
```ts
|
||||
parseReceipt(r, p):
|
||||
if (r.effectiveGasPrice) this.feeWei = BN.from(r.effectiveGasPrice) // refresh fee estimate for real
|
||||
gasMon = this.gasMon(p.gasLimit, effectiveGasPrice ?? feeWei) // charged on reverts too
|
||||
status = (r.status === "0x0") ? "reverted" : "placed" // 0x0 = reverted
|
||||
if (status === "placed"):
|
||||
scan logs for OrderCreated(owner=me) → orderId
|
||||
scan logs for OrdersCanceled(owner=me) → canceled[]
|
||||
// status: sent | placed | reverted | lost | sim
|
||||
```
|
||||
Lifecycle of a quote: `sent` → (receipt) `placed` (orderId recorded, **only then** inserted into `this.orders` for next block’s cancel list) **|** `reverted` (book moved through the price or a cancelled id had already filled; `reverted` counter++; gas still charged) **|** `lost` (no receipt within `pendingBlocks=10` blocks; `status:"lost"`, `gasMon:0`, nonce resynced). `applyQuoteResult` then mutates `orders` (delete canceled, set placed orderId), deletes the inflight entry, adds `gasMon` to totals, and **patches the historical BlockEvent’s `quote`** and fires `onQuote` (SSE `quote` event).
|
||||
|
||||
Critical property: **jev never assumes** — an order is only “resting” once its receipt proves `OrderCreated`. Until then it is `sent` (counts toward the position cap via `restingMon`/`inflight` but is not in the cancel list).
|
||||
|
||||
### 4.6 Fill observation — taker hits our resting order (`trades.ts` + `harvest`)
|
||||
Fills are **not** our own transactions. They arrive two ways:
|
||||
- **Live:** `TradeFeed.poll()` issues `eth_getLogs` for the Kuru `Trade` event (topic `0xf169…21581`) over `[lastBlock+1..block]`, chunked at `MAX_RANGE=100` blocks, windowed to `MAX_CATCHUP=1000`. Each `Trade(uint40 orderId, makerAddress, isBuy, price[1e18], updatedSize, …, filledSize)` is decoded; if `makerAddress === our wallet`, it is a **maker fill** (our side = opposite of taker `isBuy`) with `updatedSize` so a fully‑filled order is dropped. `liveFills` reduces `orders` size by the fill; a fill for a resting order **closes that order out** (`orders.delete`).
|
||||
- **Dry run:** `simFills(prints)` — a simulated order placed at block N is on the book from N+1; a taker print (sell at/below our bid, or buy at/above our ask) for `min(o.size, print.size)` fills it. (No real tx — `txHash:null`, `simulated:true`.)
|
||||
|
||||
`harvest()` aggregates same‑block fills (`aggregate`: total size, size‑weighted price, side with more size) and calls `applyFill`.
|
||||
|
||||
### 4.7 Position + PnL — signed inventory, FIFO cost basis, realized on close (`trader.applyFill` / `emit`)
|
||||
```ts
|
||||
position = { mon: signed_inventory, costUsd: FIFO_basis }
|
||||
applyFill(f):
|
||||
signed = (f.side==="buy") ? +size : -size
|
||||
if flat || same sign: costUsd += signed * price // add to / average position
|
||||
else: closing = min(|signed|,|mon|) * sign(signed)
|
||||
realizedUsd += -closing*(price - entry) ; costUsd += closing*entry
|
||||
remainder = signed-closing ; costUsd += remainder*price // flip opens other way
|
||||
mon += signed ; if |mon|<1e-9 {mon=0; costUsd=0}
|
||||
```
|
||||
Unrealized = `mon * (mid - entryPrice)`; `gasUsd = gasMon * mid`; `pnlUsd = realizedUsd + unrealized - gasUsd`; `pnlPct = pnlUsd / bankrollUsd * 100`. Note the **bankroll is $100** — so `pnlPct` is intentionally dramatic/small‑base; the real P&L is `pnlUsd`/`pnlMon`.
|
||||
|
||||
---
|
||||
|
||||
## 8. State‑machine map (single slot)
|
||||
|
||||
```
|
||||
[IDLE] -- busy=true, onBlock --> [READING_BOOK] -- model.decide(modelSide)
|
||||
| |
|
||||
| busy=false but (cap blocks both sides) |
|
||||
v v
|
||||
[REJECTED_NO_SIDE: emit no quote, model call still recorded, capped?]
|
||||
|
|
||||
allowed(wanted) ? wanted : allowed(other) ? other
|
||||
| (cap = maxPositionMon; live: margin)
|
||||
v
|
||||
[SEND: cancel confirmed resting + post post-only 1-tick-in]
|
||||
| gasMon charged up front, status=SENT, inflight[hash]=quote
|
||||
v
|
||||
[RESTING? NO — emitted this block, receipt lands LATER]
|
||||
|
|
||||
receipts (off-path, pollPending) resolve:
|
||||
PLACED -> orderId in `orders` (becomes next block's cancel list)
|
||||
REVERTED -> reverted++ , gasMon kept, slot freed, nonce resync
|
||||
LOST (>10 blocks) -> status=lost, gasMon=0 [~]
|
||||
fills (off-path, Trade feed -> harvest -> applyFill):
|
||||
live: order size shrinks / order drops [FILL]
|
||||
dry : cross print fills sim order [FILL]
|
||||
|
|
||||
v
|
||||
[position.mon += signed ; realized/cost-basis fold ; totals update]
|
||||
|
|
||||
v
|
||||
emit(BlockEvent) --> next block IDLE again
|
||||
```
|
||||
There is **no explicit EXIT state**. When the model flips side (e.g. was long, now `sell`), the existing bid is cancelled and a new post‑only **ask** is placed one tick inside — the old position is closed by the new flow naturally (a taker lifts the new ask). There is no stop‑loss, no TP, no time‑based exit; a position is only ever reduced by the model deciding the opposite side and posting the opposite quote. This is the single biggest structural difference from any directional strategy and the reason jev is a **spread‑capture / flow‑reversal machine**, not a directional holder.
|
||||
|
||||
---
|
||||
|
||||
## 9. Guardrails and failure modes (the safety surface)
|
||||
|
||||
| mechanism | code | effect |
|
||||
|---|---|---|
|
||||
| one send / block cadence | `Trader.onBlock` `busy` | prevents over‑submission; late blocks become `hold` |
|
||||
| replace‑every‑block (cancel all on each send) | `cancel = orders.keys` in `send` | no stale resting orders; fresh quote every block at the touch |
|
||||
| post‑only never crosses | `quotePrice` clamp + `batchUpdate(...,postOnly=true)` | can never pay the spread / cross the book |
|
||||
| position cap | `allowed()`: `|position.mon + resting ± size| > maxPositionMon` | hard 5× cap; caps the **gross** exposure |
|
||||
| margin check (live) | `allowed()`: usdc ≥ size·ask / mon ≥ size | dry run skips (no wallet) |
|
||||
| receipt lost timeout | `block - p.block >= pendingBlocks(10)` | frees stuck slot, resyncs nonce |
|
||||
| reorg/empty book | `readBook` throws on empty side | block skipped, bot does not guess |
|
||||
| book depth | `readBook` reads full depthBps(10/25/50) — **but exec is size‑blind** | sees depth, but `tradeSizeMon` is fixed regardless |
|
||||
| gas ceiling | static `MAX_FEE_GWEI=400` + `gasLimitFallback` | no per‑block estimation; pays limit, not gasUsed |
|
||||
|
||||
Notable **absences** (vs a hardened shop‑till‑stops engine): no **venue‑truth reconciliation** (no poll of open orders vs the venue; relies on receipt + Trade log), no **network‑partition handling** beyond the WS+poll backstop for block notifications, no **partial‑fill remainder re‑quote** (a partially‑filled resting order is simply replaced next block), no **rate‑limit backoff** (Kuru/Monad don’t seem to surface 429s in this path), and **no TP/SL/MAX_HOLD** exit authority — jev just keeps replacing quotes every block.
|
||||
|
||||
---
|
||||
|
||||
## 10. What is actually attractive / portable from Jev’s exec
|
||||
|
||||
1. **The replace‑every‑block post‑only‑inside‑the‑touch policy** — a fresh maker order at the touch every cadence, capturing taker flow, with zero stale‑order drag. This is the “be the one whose order flow gets picked up” mechanic.
|
||||
2. **The busy‑gate serialization** — at most one send per cadence; no self‑racing; clean ack/fill ordering.
|
||||
3. **The book‑reader exactness** (`book.ts`) — 1 eth_call, SDK‑identical decode, vault batched.
|
||||
4. **Model‑agnosticism of the exec** — the model is a black‑box oracle returning `{buy,sell}` + probs; the exec never imports Jev.
|
||||
|
||||
What is **NOT** portable as‑is: the Kuru `batchUpdate` (cancel+place atomic), the `eth_getLogs` Trade‑feed maker matching, the 1e18 price / tick math, the Ethereum nonce, the Monad gas‑on‑limit pricing, and the 300‑ms on‑chain block assumption (HL is off‑chain orderbook + REST/WS, ~50‑300 ms depending on venue).
|
||||
|
||||
---
|
||||
|
||||
## 11. File‑to‑file code map (for the reader)
|
||||
|
||||
| concern | file:line |
|
||||
|---|---|
|
||||
| state machine loop, busy/late gate, position cap, emit | `src/trader.ts:85` `onBlock`, `:89` busy, `:108` `allowed`, `:125` emit |
|
||||
| single atomic cancel‑all + post‑only place, fire‑and‑forget, inflight | `src/trader.ts:116` `market.send(block, side, size, book, cancel, capped)`, `market.ts:135` `send` |
|
||||
| post‑only price, never cross | `src/market.ts:121` `quotePrice` |
|
||||
| batchUpdate encoding (cancel + place, postOnly flag) | `src/market.ts:185` `encode` |
|
||||
| receipt → placed/reverted/lost, orderId from OrderCreated | `src/market.ts:155` `pollPending`, `:193` `parseReceipt` |
|
||||
| Trade‑log feed + maker‑fill matching + sim‑fills | `src/trades.ts:80` `poll`, `:113` `decode`, `:171` `liveFills`, `:186` `simFills` |
|
||||
| FIFO cost basis + realized PnL | `src/trader.ts:245` `applyFill` |
|
||||
| one‑eth_call book reader (exact SDK match) | `src/book.ts:95` `readBook`, `:121` `decodeL2Book`, `:224` `buildBook` |
|
||||
| block feed (WS newHeads + poll coalesce) | `src/chain.ts:30` `startBlockFeed` |
|
||||
| server (snapshot/history/SSE) | `src/server.ts:11` `startServer` |
|
||||
| the question actually asked of Jev | `src/model.ts:42` `QUESTIONS.direction` |
|
||||
241
prod/docs/JEV_EXEC_FLOW_STUDY_FLIGHT13_COMPARISON.md
Normal file
241
prod/docs/JEV_EXEC_FLOW_STUDY_FLIGHT13_COMPARISON.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# JEV‑Trader vs FLIGHT13 SOA — Execution‑Flow Comparison
|
||||
|
||||
**Read‑only study. No files in the production trees, CH, or HZ were altered. New doc only
|
||||
(`/mnt/dolphinng5_predict/prod/docs/`).**
|
||||
|
||||
**Sources (all read‑only):** jev‑trader `src/{trader,market,trades,book,chain,model,server}.ts`
|
||||
(`b587759e` `main`); FLIGHT13 r29 formal spec
|
||||
`/root/uv-wt/f13-r29/prod/docs/formal/F13_ExecLifecycle_SOA_v5.tla` (`Next` + all transitions);
|
||||
`/root/uv-wt/f13-r29/prod/docs/F13_EXECUTION_FLOW_DIAGRAM.md`;
|
||||
`/root/uv-wt/f13-r29/prod/docs/VIOLET_PASS2.4_LIVE_EXEC_WIRING.md`;
|
||||
`/root/uv-wt/f13-r29/prod/docs/EXEC_SOA_SUPERSESSION_AUDIT_20260808.md`;
|
||||
`/root/uv-wt/f13-r29/prod/clean_arch/dita_v2/{hl_venue,exec_router}.py`; and
|
||||
`/mnt/dolphinng5_predict/prod/docs/DITA_V2_KERNEL_REFERENCE.md`.
|
||||
|
||||
**Operator premise (accepted):** “Our state machine is tad more complete… the `.ts` execs
|
||||
against the book is worth taking in/studying.” This doc agrees the FLIGHT13 FSM is strictly
|
||||
more complete (it must be — it is built for Hyperliquid, where ack‑or‑not / partial / network
|
||||
hell is the baseline), and isolates **exactly what jev‑trader does *against the book* that is
|
||||
worth porting**: the post‑only‑inside‑the‑touch, replace‑every‑block maker‑capture policy and its
|
||||
serialized one‑send‑per‑cadence gate.
|
||||
|
||||
---
|
||||
|
||||
## 0. TL;DR
|
||||
|
||||
| dimension | jev‑trader | FLIGHT13 DITAv3.00 / ASEx | verdict for an HL port |
|
||||
|---|---|---|---|
|
||||
| **order model** | 1 resting order / asset; serialized replaces | multi‑slot (`MaxSlots`); maker chase ladder + standing TP | jev’s *policy* is a special case FLIGHT already hosts |
|
||||
| **exec against the book** | post‑only, 1 tick inside the touch, **cancel‑prev + place‑new every block** (atomic from the bot’s view) | post‑only at the touch (`TOUCH_ALO`); **atomic modify to touch** (same cloid, fee‑free, ≤1/bar) | jev ≈ FLIGHT’s maker chase w/ `offset_ticks=1`; drop‑in not viable — needs the hl_venue adapter |
|
||||
| **ack model** | tx‑receipt = placed/reverted; `lost` after 10 blocks; assumes receipt == truth | `VenueAccept / VenueReject / VenueIndeterminate`; **submit exception → INDETERMINATE unless provenance proves NOT_ATTEMPTED; venue‑truth polls the OBJECT**; `UNKNOWN is never FLAT` | FLIGHT strictly stronger — jev’s 10‑block timeout is the only recovery |
|
||||
| **partial fills** | taker hits resting order; size shrinks; **re‑quoted next block** (no remainder re‑quote within a bar) | `PartialFill` → `pendingFill`; `IocExpire` residual handled by Add.13 `TakeResidualAbandon`; IOC residual inherits the live TAKE obligation | FLIGHT keeps the residual; jev just re‑quotes |
|
||||
| **exit** | none — side flip → new opposite quote naturally closes | `TP_FLOOR → TP → SL → MAX_HOLD → V7(retract ½) → ADVSL(shadow)`; reduce‑only, side‑flipped, MUST_FILL; MAX_HOLD monotonic latch | jev has *no* exit authority — the port must add FLIGHT’s exit policy layer |
|
||||
| **venue** | Kuru AMM on Monad (eth_call book, eth_sendRaw batchUpdate, Trade‑log fills) | Hyperliquid VST (off‑chain OB, REST/WS, batchOrders, postOnly/reduceOnly, cloid nonce) | plumbing differs; policy transfers |
|
||||
| **hot cadence** | ~300 ms block; 2 RPC round trips (1 eth_call book + 1 send) | scan bar ~5–6 s decision; BLUE bar 11 s; chase 33 s; rest ceiling 132 s | jev’s 300 ms cadence is **not** portable to HL‑VST timing — map the *policy*, not the clock |
|
||||
|
||||
---
|
||||
|
||||
## 1. Two state machines, side by side
|
||||
|
||||
### 1.1 jev‑trader — single‑slot, replace‑every‑block (one in‑flight)
|
||||
|
||||
```
|
||||
IDLE(busy=false, block t)
|
||||
│ confirmPending(t): poll receipts for prior inflight → placed|reverted|lost
|
||||
│ (placed → this.orders[orderId] = {side,price,size,block} ← next cancel list)
|
||||
│ (fills: trades.poll(t).then(harvest) → applyFill → position FIFO realized)
|
||||
│
|
||||
├─ IF busy (prev read+decide+send still running) ──► emit(lateness, decision.hold) ──► IDLE (no order)
|
||||
│
|
||||
│ readBook() — 1 eth_call getL2Book (+vault batched); throws on empty side
|
||||
│ decision = model.decide(state) // the question: ↑/↓ after horizonBlocks; {buy,sell,hold}+upIn10
|
||||
│ wanted = (decision.action==sell)? sell : buy // buy wins ties
|
||||
│ side = allowed(wanted)? wanted : allowed(other)? other : null // cap(5×) + margin(live)
|
||||
│
|
||||
├─ IF side==null ──► emit(block, book, decision, null, false, capped=true) ──► IDLE (no order)
|
||||
│
|
||||
│ cancel = [...this.orders.keys].filter(id>0) // confirmed resting ids ONLY
|
||||
│ market.send(t, side, 200 MON, book, cancel, capped) // ONE batchUpdate: cancel[] + place 1 post‑only
|
||||
│ → sign → eth_sendRawTransaction → hash (status=SENT, inflight[hash]=quote, gasMon charged)
|
||||
│ → DARK : status=sim, orders.clear()+orders.set(-simId)
|
||||
└─ emit(BlockEvent{snapshot,history/events,block,quote}) ──► IDLE(t+1)
|
||||
```
|
||||
**There is no EXIT state.** A model side‑flip (long→sell) cancels the bid and posts a one‑tick‑inside **ask**; the next taker print against that ask closes the position. The FSM’s only “memory” across blocks is `orders` (confirmed resting), `inflight` (unacked txs), and `position` (FIFO cost basis).
|
||||
|
||||
State inventory (`trader.ts`): `busy`, `orders:Map(id,Resting)`, `inflight:Map(hash,Quote)`,
|
||||
`position:{mon,costUsd}`, `totals`, `lastBook`, plus per‑block RPC `feeWei`/`gasLimit`.
|
||||
|
||||
### 1.2 FLIGHT13 DITAv3.00/ASEx — multi‑slot FSM with reconciliation (formal TLA+)
|
||||
|
||||
**Order‑lifecycle FSM (TLA `OrderStates`):**
|
||||
`NONE → RESERVED → QUEUED → {INDETERMINATE | LIVE} → {PARTIAL | CANCEL_PENDING | FILLED | CANCELLED | REJECTED}`
|
||||
(plus `pendingFill`, `linkedOwner`, `venueOwner` per object).
|
||||
|
||||
**Per‑trade FSM (`entryPhase`):**
|
||||
`NONE → SELECT_ENTRY_ARM(REST|CHASE) → ReserveInitialMaker(ENTRY_MAKER, TOUCH_ALO, RESERVED)`
|
||||
→ `QueueSubmit` (`QUEUED`, `parityHeld`) → `VenueAccept` (`LIVE`, `venueOwner`) / `VenueReject` (`REJECTED`) / `VenueIndeterminate` (`INDETERMINATE`)
|
||||
→ on `LIVE`: `EntryHold | EntryReprice` (atomic modify‑to‑touch, CHASE only) | `EntryTerminalLive/Dark`
|
||||
→ `EntryEconomicAbandon` (explicit policy verdict, book usable) OR `ReserveEntryTake` (BOUNDED IOC, MUST_FILL) → Add.13 `TakeResidualAbandon`
|
||||
→ `EntryFillOne/DeliverOne` (`kernelPos++`, `undeliveredEntry++`, entry `COMPLETE` or `ABORT_REQUIRED`)
|
||||
→ `POSITION_OPEN` → [`ArmProtection` → `STANDING_TP`] / [exit]
|
||||
→ `RaiseExitSignal` → `SelectExitIntent` (same‑tick `KILL>TP_FLOOR>FIXED_TP>STOP_LOSS>ADVSL>MAX_HOLD`; urgency `KILL/SL>TP_FLOOR/ADVSL>FIXED_TP>MAX_HOLD`) → `ReserveExitTake` (`EXIT_TAKE`, MUST_FILL, reduce‑only, side‑flipped) → `ExitFillOne` (`kernelPos--`, realized) → `ClearExitWhenFlat` → `FLAT`.
|
||||
→ recovery at any tick: `STALE_STATE_RECONCILING` blocks progression until venue‑truth reconciled;
|
||||
`VenueSnapshot/VenueTruthStale/BookTruthStale` + `VenueFillOne/DeliverOne` + `RetireOrder`.
|
||||
|
||||
**Exit is terminal & monotonic:** `MAX_HOLD` deadline latches (`maxHoldFired`, never cleared); `ExitCostRefuse` (cost‑ceiling veto) is legal **only** before the deadline — at terminal it is forced (`TerminalCeilingVetoBug` is a defect): cross with full telemetry. **Invariant `LIV_ExitEventuallyFlat`** (with the cost‑veto + MAX_HOLD latch) is the liveness tooth.
|
||||
|
||||
**Foreign‑exposure path (the orphan class jev cannot express):** `ForeignFill(s,side)` → `foreignExposure++` (a fill for *our* intent‑id that the kernel did not originate — i.e. a stale cloid collision or a foreign place). This arms `ReserveFlatten` (`FLATTEN`, MUST_FILL) whose `quantity = foreignExposure`, side‑flipped, reduce‑only — a **forced flatten whose obligation persists through reject/partial/unknown until flat**. (`NoFlattenerBug` would drop this to `unownedSeen` — a defect.)
|
||||
|
||||
**Invariant teeth (TLA+ `Invs`):** `UNKNOWN is never FLAT`; `cancel REQUEST is never cancel TRUTH`;
|
||||
`one trade may own several simultaneous physical orders`; `pending/self acquisition consumes parity
|
||||
before venue truth catches up`; the `Bug`-prefixed switches are **mutation‑litmus defect flags**
|
||||
(`FillBeforeLinkDropsBug`, `CancelRequestTerminalBug`, `ExitNotReduceOnlyBug`, `WrongSideExitBug`,
|
||||
`OptimisticResizeBug`, …) — TLC proves each fires only under its bug gate.
|
||||
|
||||
### 1.3 The structural gap in one line
|
||||
jev: **`one block ≈ one send ≈ one resting order ≈ book is replaced, not reconciled.** FLIGHT:
|
||||
**`the venue owns the truth; the kernel owns the book; the gap between them is where every
|
||||
defect lives and is exhaustively modeled.** jev’s `lost` (no receipt in 10 blocks) is the *only*
|
||||
reconciliation jev has. FLIGHT has `STALE_STATE_RECONCILING`, venue‑truth polls, foreign‑recovery,
|
||||
standing TPs, MAX_HOLD latches, and a 30‑item defect‑flag lattice over exactly that gap.
|
||||
|
||||
---
|
||||
|
||||
## 2. How each execs against the book (the user’s focus)
|
||||
|
||||
### 2.1 jev‑trader — "replace‑every‑block, one tick inside, post‑only by construction"
|
||||
|
||||
**Against the book, per block (`trader.onBlock`):**
|
||||
1. `readBook()` → one `eth_call getL2Book`; the entire resting‑order price is recomputed from the
|
||||
**current** book each block. There is no "keep the old level"; the old level is cancelled and a
|
||||
new one is placed.
|
||||
2. `quotePrice` (`market.ts:121`) → **`bid + 1 tick` for buy, `ask − 1 tick` for sell**, clamped to the
|
||||
touch when the spread is thinner than a tick. The clamp is the **never‑cross** guarantee: the
|
||||
order always rests at or inside the touch on the model’s side.
|
||||
3. `batchUpdate(buyPrices,buySizes,sellPrices,sellSizes,cancelIds, **postOnly=true**)`
|
||||
(`market.ts:185`) — the cancel list (last block’s confirmed resting `orderId`s) **and** the new
|
||||
post‑only quote are sent **in one Kuru tx**. From the book’s perspective the quote hops every
|
||||
block with no gap: the moment the new order is placed, the old one is cancelled in the same
|
||||
atomic batch. (It is not a true on‑chain modify; it is cancel‑prev + place‑new. FLIGHT does
|
||||
a same‑cloid atomic modify instead — see §3.)
|
||||
4. The resting order therefore **sits one tick inside the touch** and waits for the next taker
|
||||
print that crosses the spread. A taker `sell` fills our `buy` at `bid+1tick`; a taker `buy`
|
||||
fills our `sell` at `ask−1tick`. We are, by construction, **the counterparty the taker hits**,
|
||||
earning the tick plus the spread‑capture edge — exactly “be the one whose order flow gets
|
||||
picked up.”
|
||||
5. Because `tradeSizeMon` is **fixed at 200 MON** and `quoteInsideTicks=1` is **fixed**, the exec is
|
||||
size‑ and distance‑blind to depth. The model sees `depthBps{10/25/50}` and `imbalance` but the
|
||||
placement ignores them — jev earns edge only so long as it (a) is first to the touch each block
|
||||
and (b) the book is wide enough that one tick is inside it.
|
||||
|
||||
**Net:** jev’s book‑exec is a **deterministic, stateless, post‑only hopping** of a single order one
|
||||
tick inside the touch, every ~300 ms. It wins or loses the spread every block; there is no
|
||||
reprice‑to‑touch ladder, no venue‑truth, no partial remainder. The policy is attractive because
|
||||
it is *obviously correct* about not crossing.
|
||||
|
||||
### 2.2 FLIGHT13 — "maker chase ladder + atomic modify + venue‑truth"
|
||||
|
||||
**Against the book, per the timeline + TLA (`F13_EXECUTION_FLOW_DIAGRAM.md`):**
|
||||
1. **t=0 ExecCtl urgency stamp** → `high(do‑or‑die)` = `MARKET Ioc` (taker, 4.5 bp measured);
|
||||
`low(maker‑first)` = `LIMIT Alo + TTL` (maker, 1.5 bp). Order type is chosen **once**, at
|
||||
submit, by the control plane — not by a per‑bar re‑quote.
|
||||
2. `QueueSubmit` → `VenueAccept` (`LIVE`, `venueOwner=o`, `cloid=sha256(intent_id)` journaled as a
|
||||
`(cloid, intent_id)` pair — see `hl_venue.py` `_cloid_for` / `_remember_cloid`). The kernel
|
||||
**does not** re‑quote every bar by default.
|
||||
3. **Chase ladder** (`EntryReprice`, bars = `wall/11s`): bar 0–1 → **atomic modify to the touch**
|
||||
(same cloid, ≤1 bar, **fee‑free**, owner‑side sizing remainder, `join bid — can never cross`);
|
||||
measured edge starts ~6 bp, concave decays; bar 2, edge ≤ 2 bp floor → **`ABANDON` →
|
||||
`expire_now` → same‑sweep `CANCEL`**. So FLIGHT *does* re‑price toward the touch — but only
|
||||
for the CHASE arm, and it does it with a **same‑cloid atomic modify** (fee‑free), not a
|
||||
cancel‑+‑place.
|
||||
4. **Modify‑error taxonomy** (`hl_venue._events_from_modify`→`_modify_error_class`): a failed
|
||||
reprice is **resolved, never guessed**:
|
||||
- `old_order_intact` → quote untouched, retry the reprice;
|
||||
- `old_order_consumed` → cancel half succeeded / place failed → quote gone unfilled, slot freed
|
||||
(no double‑fill: atomicity holds; presence does not);
|
||||
- `selector_gone` → order already gone before the modify (filled/cancelled) → **decide via
|
||||
venue‑truth**, never by assumption.
|
||||
5. **Venue‑truth on stuck** (`VENUE‑TRUTH read`, 60 s cadence, query the *object* not the action):
|
||||
`VenueSnapshot` — GONE → slot freed; FILLED → feed‑gap alarm + `fill path owns position`;
|
||||
`RESTING < ceiling` → re‑cancel; `RESTING ≥ 132 s` → **HALT new ENTERs** (loud alarm);
|
||||
`UNKNOWN` → ask again. Submit exception → `_submit_exc_proves_no_order` →
|
||||
`NOT_ATTEMPTED` (ConnectError/NameResolution/SSLError/timeout‑before‑send) ⇒ safe REJECTED;
|
||||
**anything else ⇒ `VenueIndeterminateError` (INDETERMINATE)** ⇒ the order MAY exist and is
|
||||
**never rolled back to flat at the book layer** (`UNKNOWN is never FLAT`).
|
||||
6. **Fills** (`FillOne`/`DeliverOne`, `VenueFillOne`): taker lifts the maker → `actualPos++` +
|
||||
`pendingFill++`; **self‑acquisition consumes parity before venue truth catches up**; the
|
||||
fill is delivered into the book (`delivered < filled` → pending). `STANDING_TP` is registered
|
||||
when `protection == ARMED`.
|
||||
7. **Exit** (`ReserveExitTake`, MUST_FILL, reduce‑only, side‑flipped; `SMART_EXIT` maker LIMIT
|
||||
+ TTL → `sweep CANCEL → confirm/retry → MARKET cross EXACTLY ONCE`), arbitrated
|
||||
`KILL>TP_FLOOR>FIXED_TP>STOP_LOSS>ADVSL>MAX_HOLD` same‑tick, MAX_HOLD a monotonic latch.
|
||||
8. **Single‑writer** (VIOLET_PASS2.4): `ShadowDecision → ExecIntent → ExecDeadlineDriver@100ms → VST
|
||||
adapter`; fills fold back into the **ASEx‑guarded working‑order + position/capital ledger
|
||||
(single writer — no race with the decision loop’s reads)**. The race test (“decision‑loop reads
|
||||
vs fill‑apply writes through ASEx”) is a named self‑test.
|
||||
|
||||
**Net:** FLIGHT’s book‑exec is a **chase ladder that modifies‑to‑the‑touch for cheap**, keeps the
|
||||
quote alive across bars, resolves every failure by *asking the venue about the object*, and never
|
||||
rolls back a possibly‑live order. jev does the opposite: it *re‑places* the quote every bar and
|
||||
only ever recovers via a 10‑block receipt timeout.
|
||||
|
||||
---
|
||||
|
||||
## 3. Comparison matrix (exec‑against‑book focus)
|
||||
|
||||
| axis | jev‑trader | FLIGHT13 DITAv3.00 / ASEx (VST/HL) | delta |
|
||||
|---|---|---|---|
|
||||
| **quote distance from touch** | fixed 1 tick inside (`QUOTE_INSIDE_TICKS`) | TOUCH / touch + chase‑modify; maker offset configurable (`offset_ticks`) | jev’s “1 tick in” = FLIGHT maker offset=1 |
|
||||
| **how the quote moves** | **cancel‑prev + place‑new every block** (fresh oid each block) | **atomic modify to touch**, same cloid, fee‑free (`EntryReprice`) | jev pays 2 txs/round (cancel+place); FLIGHT 1 modify — FLIGHT cheaper |
|
||||
| **post‑only guarantee** | `quotePrice` clamp **×** `batchUpdate(postOnly=true)` | `postOnly` flag + TOUCH_ALO (Kuru/HL) | FLIGHT also rejects‑and‑reprice on cross |
|
||||
| **order‑state knowledge** | receipt `OrderCreated`/`OrdersCanceled` only | `VenueStatus{NEW/ACKED/PARTIAL_FILLED/FILLED/CANCELED/REJECTED/RATE_LIMITED/CANCELED_REJECTED}` + `VenueIndeterminate` | FLIGHT has partial + rate‑limit states jev lacks |
|
||||
| **ack‑or‑not** | receipt mined ⇒ placed; timeout 10 blocks ⇒ `lost`; else assumed | submit exception ⇒ `VenueIndeterminate` unless `NOT_ATTEMPTED` proven; **venue‑truth polls the OBJECT** | FLIGHT strictly stronger; jev’s `lost` is the only backstop |
|
||||
| **partial fills** | taker fills resting → size shrinks → **re‑quoted next block** | `PartialFill` → held as `pendingFill`; `IocExpire` residual handled by Add.13 (`TakeResidualAbandon`) | FLIGHT preserves the residual obligation; jev just re‑quotes |
|
||||
| **orphan / foreign fill** | none modelable | `ForeignFill` (our cloid, not our intent) → `FLATTEN` MUST_FILL forced‑close | jev has no defense; this is the HL “shared‑key contamination” risk |
|
||||
| **reconciliation cadence** | receipts every block off‑path; Trade‑log every block | venue‑truth 60 s; max 8 cancel retries; rest ceiling 132 s; feed‑gap alarm | FLIGHT survives minutes‑of‑darkness; jev survives ~3 s |
|
||||
| **stuck‑order policy** | timeout 10 blocks ⇒ `lost`, resync nonce | venue‑truth GONE ⇒ free; RESTING ≥132 s ⇒ HALT ENTERs; UNKNOWN ⇒ ask again | FLIGHT escalates to HALT; jev silently gives up |
|
||||
| **exit (close)** | none — side flip posts opposite‑touch quote | `TP_FLOOR>TP>SL>MAX_HOLD>V7½>ADVSL` arbitrated same‑tick; MAX_HOLD monotonic; cost‑ceiling advisory at terminal | jev **has no exit authority at all** |
|
||||
| **concurrency** | 1 asset (MON), 1 order | multi‑asset, `MaxSlots`, one trade may own several simultaneous physical orders | jev is the degenerate single‑slot case |
|
||||
| **single‑writer** | single event‑loop process | ASEx single‑writer lane (P0‑P4 queue); fill‑apply vs read race‑free by design | FLIGHT is concurrency‑safe by construction |
|
||||
|
||||
---
|
||||
|
||||
## 4. Mapping jev’s exec onto FLIGHT/HL (concrete port‑under‑HL recommendation)
|
||||
|
||||
jev‑trader is **not a drop‑in** under `nautilius` (the exec driver) — it is Kuru/Monad‑coupled.
|
||||
But its **book‑placement policy** maps onto an existing FLIGHT surface:
|
||||
|
||||
| jev concept | FLIGHT/HL realization | action |
|
||||
|---|---|---|
|
||||
| post‑only 1‑tick‑inside the touch | `exec_router.plan_entry` maker mode + `maker_price(asset,order_side,ref)` + `post_only=True` + `offset_ticks=1` + `max_spread_bps` gate | **already exists** — set `DOLPHIN_PINK_EXEC_STYLE=maker_entry`, `DOLPHIN_PINK_POST_ONLY=1`, `DOLPHIN_PINK_MAKER_OFFSET_TICKS=1` |
|
||||
| cancel‑prev + place‑new every block | `EntryReprice` atomic modify to touch (CHASE arm) | **replace jev’s cancel+place with FLIGHT’s atomic modify** — cheaper, fee‑free; jev’s replace‑every‑block cadence must be expressed as the chase ladder’s reprice ticks |
|
||||
| one send / block cadence (busy gate) | ASEx single‑writer lane P0 + scan‑bar cadence | do **not** drive 300 ms; map to scan‑bar (5–6 s) so the kernel’s book truth and the HL rate governor stay coherent |
|
||||
| model.side ∈ {buy,sell} + upIn10 | `Intent.action` + `ExecControl.urgency` stamp + FLIGHT brain `upIn10` field | wire jev‑as‑oracle behind the existing `Intent` producer; **do not** let jev touch the book |
|
||||
| position cap (maxPositionMon 1000) | ExecCtl governor / `MaxSlots` + capital‑fresh gate | reuse the governor; jev’s 5× cap is a special case of FLIGHT’s slot + capital gate |
|
||||
| fills = taker hits our resting order | HL `userFills` WS (maker) + `HlFillAdmission` dedup on `tid`; venue size wins (L10) | **hl_venue already owns this** — the fill path is the part that does NOT transfer from jev |
|
||||
| no TP/SL/exit | FLIGHT `EXIT_*` policy (`ReserveExitTake`, MUST_FILL, reduce‑only, side‑flipped) + MAX_HOLD latch | **add FLIGHT’s exit authority** — this is jev’s single biggest missing piece for an HL port. jev’s “side flip closes naturally” is **unsafe on VST** (no forced flatten; orphan/foreign fills exist). |
|
||||
| gas = static ceiling | HL: gas free (off‑chain); HL nonce per‑order, USD fee | drop the whole jev gas model — HL is gasless, but **HL nonce + budget (1 req/$1 USDC traded)** must be honored by the adapter |
|
||||
|
||||
### Recommendation (ranked)
|
||||
|
||||
1. **Port the policy, not the plumbing.** Jev’s attractive thing is *“post one‑tick‑inside, replace toward the touch, post‑only always, one order per cadence.”* That is **FLIGHT maker_entry maker_exit with chase** — already present in `exec_router.py` + `hl_venue.py`. Do not ship jev’s `batchUpdate`/Trade‑log/gasLimit/nonce code.
|
||||
|
||||
2. **Graft jev‑as‑oracle onto the Intent producer.** `model.ts` exposes a clean oracle contract (`{buy,sell} + upIn10`). Wire it behind the existing `ExecIntent → ExecDeadlineDriver@100ms → VST adapter` (VIOLET_PASS2.4 wiring). The oracle must **not** know it posts orders.
|
||||
|
||||
3. **Replace jev’s “cancel+place every block” with FLIGHT’s atomic modify‑to‑touch.** jev pays a cancel gas‑ish on Kuru every 300 ms; on HL the modify‑to‑touch is fee‑free and is the whole point of the chase ladder. Map jev’s cadence to scan‑bar so the book truth / rate governor / `repriceSafe` leash stay coherent.
|
||||
|
||||
4. **Add FLIGHT’s exit authority — jev has none.** A pure side‑flip exit is **not safe on HL/VST** (no orphan/foreign recovery, no MAX_HOLD latch, no reduce‑only enforce). Use `ReserveExitTake` (MUST_FILL, reduce‑only, side‑flipped) + the same‑tick arbitration
|
||||
`KILL>TP_FLOOR>FIXED_TP>STOP_LOSS>ADVSL>MAX_HOLD`. This is the non‑negotiable delta.
|
||||
|
||||
5. **Inherit the ack/partial/venue‑truth stack from hl_venue.** jev’s 10‑block receipt timeout is the only recovery and it assumes “receipt == truth.” Under HL, `_submit_exc_proves_no_order` ⇒ INDETERMINATE‑unless‑NOT‑ATTEMPTED, and venue‑truth polls the OBJECT — port the *policy* but keep FLIGHT’s ack/partial/foreign/FLATTEN recovery verbatim. This is the “tad more complete” part the operator flagged.
|
||||
|
||||
6. **Stay gasless, honor the HL nonce + budget.** jev’s `MAX_FEE_GWEI`/`gasLimitFallback` are pure noise for HL. HL charges per signed action against a **1‑request/$1 USDC‑traded bucket** — `HlVenueAdapter.__init__` already maintains the `_refill_seen`/`_refill_order` ledger with the #59 fix (dedup on venue `tid`, refill via `record_trade_volume`). jev’s fixed‑size 200‑MON orders map cleanly onto that bucket.
|
||||
|
||||
**Bottom line:** jev‑trader’s book‑exec *policy* (post‑only 1‑tick‑inside, replace‑every‑cadence,
|
||||
single‑in‑flight gate) is a **special case** of the FLIGHT13 maker chase ladder already wired through
|
||||
`hl_venue.py` + `exec_router.py`. The port is an **adapter‑layer concern** (map jev’s two RPC
|
||||
round trips onto HL REST/WS batchOrders + userOrders/fills + cloid), **not** a drop‑in, and it
|
||||
**must** import FLIGHT’s exit authority (jev has none) and ack/partial/venue‑truth recovery
|
||||
(jev’s 10‑block timeout is insufficient on VST). The `.ts` is attractive because it is *obvious*;
|
||||
FLIGHT is more complete precisely because the obvious version does not survive HL’s
|
||||
ack‑or‑not / partial / orphan / network‑partition regime.
|
||||
Reference in New Issue
Block a user