Files
sentiment-engine/prod/docs/JEV_EXEC_FLOW_STUDY.md
Codex c0445e60ea feat(sentiment): add trade asset aliases & known entities; add Glassnode/WSJ RSS sources
- Added 15 trade assets (ZIL, ONG, ONE, STX, ALGO, DASH, LTC, FET, XTZ, ENJ, XLM, ETC, TRX) to asset_aliases.yaml
- Added same assets with metadata to known_entities.yaml (chain, type, market_cap_rank)
- Added Glassnode Insights (0.85 cred) and WSJ Crypto (0.85 cred) to sources.yaml
- Trade log analysis shows these small-caps had zero news coverage - aliases enable entity extraction
2026-09-23 10:00:37 +02:00

156 lines
17 KiB
Markdown

# Jev Trader: Execution Flow and Decision Semantics
## Scope and source identity
This is a read-only study of the public Jev Trader repository at commit b587759e459ea049590102e54a0b07800864cdc3 (main, cloned 2026-09-22), the full tracked file inventory, and its backend implementation. The Jev clone is at /tmp/jev-exec-study-b587759. No Jev process, wallet, provider call, or order was run. No execution test was added or run.
The relevant upstream files are src/index.ts, src/config.ts, src/chain.ts, src/book.ts, src/market.ts, src/trader.ts, src/trades.ts, src/model.ts, src/server.ts; scripts/*; package.json, SPEC.md, README.md; and web/src/app/*, web/src/components/*, web/src/lib/*. Source citations below link to the pinned commit.
## Executive finding
Jev is a compact asynchronous single-market strategy loop. On each newly observed Monad block, it reads a Kuru book, asks Jev for a buy/sell classification, applies local inventory and margin limits, then submits a signed post-only batchUpdate containing a new limit order and cancellations for previously confirmed order IDs. It later polls transaction receipts and Kuru trade logs.
The critical correction to the preliminary draft is that the busy flag serializes the book-read/decision/send routine, not venue transactions or outstanding orders. Receipt polling runs concurrently; send returns after the RPC returns a hash; a later block can send another transaction while earlier transactions remain pending. Unknown/late receipts, overlapping polls, fills racing receipt handling and slow model responses can therefore produce more than one live or uncertain order. “Exactly one resting order” is not an invariant in this source.
Jev is a decision provider, not an order-management agent here. It returns an action and provider-reported choice distribution from one structured question. The code does not validate that distribution as a calibrated forecast, derive size from it, choose execution style, or decide an exit. The actual prompt describes IOC execution while the code submits post-only orders. Jev's confidence must not be interpreted as fill probability, maker edge or realized expectancy.
## Boot and feed
src/index.ts performs this sequence:
1. Instantiate Market and await market.init().
2. Construct the selected Model.
3. Start the HTTP/SSE server.
4. Construct Trader with event callbacks and attach its trade feed.
5. Start the block feed, whose callback invokes Trader.onBlock.
The package entry is src/index.ts. The repository-root index.ts only prints “Hello World.” MODEL selects Jev only for the exact value “jev”; otherwise createModel returns MockModel. Dry run is enabled when DRY_RUN is exactly “true” or no private key is configured. Numeric environment values are converted without comprehensive range validation. There is no durable trader-state restore or startup reconciliation of open orders.
chain.startBlockFeed combines HTTP eth_blockNumber polling (default 150 ms) with optional WebSocket newHeads and a newest-block coalescer. The callback is invoked without awaiting its promise. Async interval polls can overlap; WS JSON parsing is not guarded; older block numbers are ignored with no reorg rollback. RPC has no explicit timeout, retry policy, HTTP status check or result schema validation. Book and log reads are independent latest/range requests, not guaranteed snapshots of the triggering block.
## Per-block decision and order path
Trader.onBlock increments counters and launches receipt polling for previous sends. Periodic market.refresh is also off-path. If busy is already true, the block is counted late and may be emitted with lastBook; no new model call is started. Otherwise busy is set and the routine:
1. Awaits Market.readBook, stores the latest book and appends its mid to a process-local 400-value history.
2. Starts TradeFeed.poll(block) without awaiting it.
3. Builds TradeState and awaits model.decide.
4. Uses the model's sell action as sell; other values default to buy.
5. Calls allowed() for the desired side. If disallowed, it tries the opposite side as a risk-reducing alternative. If neither is allowed, no order is sent.
6. Overwrites decision.action with the actually selected side, then calls Market.send with cancellations for currently known positive order IDs.
7. Stores the returned hash as in-flight, emits the block, and clears busy in finally.
An exception is logged; it does not cancel existing orders. There is no decision-age deadline before send, so a slow model may submit using a stale captured book.
Book decoding in src/book.ts parses Kuru L2/vault data, applies tick rounding and derives mid, spread, imbalance, depth and top levels. Empty sides throw. buildState passes current book and process-local price/trade histories, returns and allowed-side flags. Coalesced/missed blocks mean its history is not guaranteed to be N consecutive canonical observations.
## Jev's decision and its implications
### What the live adapter asks
src/model.ts constructs a TypeSafe evaluation model using JEV_MODEL_ID (default jev-latest). JevModel.decide calls experimental_evaluate with the current TradeState, one choice question named direction and maxRetries=0.
The instructions ask whether MON will be higher or lower than the current mid after horizonBlocks, say the move should exceed spread, and describe an “immediate-or-cancel market order in the next block.” Inputs named include taker flow, book depth and imbalance, returns, recent mids, and allowed buy/sell flags. Jev is told that a disallowed side will be traded in the other direction.
The response becomes Action plus buy/sell/hold values. If probabilities are absent, the adapter makes a one-hot fallback for the selected choice. It sets hold to zero and upIn10 equal to buy probability. Despite the name, upIn10 is not a separate 10-block forecast in this source. The code does not normalize or range-check probabilities, calibrate them, set an abstain threshold or record a stable model version in each decision.
### What Jev controls
The model's choice is an input to side selection, not the final execution instruction. Quantity is fixed at TRADE_SIZE_MON (default 200); code selects price from the book and post-only execution. Jev does not choose quantity, limit price, IOC/taker use, cancellation timing, stop, take-profit, exit reason or post-entry holding period.
When allowed() rejects Jev's side but permits the other, Trader changes decision.action while leaving probability values untouched. A displayed sell can therefore coexist with a high buy probability because risk constraints selected the reducing side. These are distinct facts, and must not be collapsed into a single calibrated Jev output.
The prompt's “by more than the spread” language does not create a numerical expected-value calculation. A provider choice distribution is not by itself a calibrated probability that a trade profits after fees, fill selection, queue position, adverse selection and inventory costs.
### The prompt/execution mismatch
The prompt describes IOC market execution crossing the spread. The implementation encodes batchUpdate with postOnly=true and chooses a price one tick better than the same-side best quote when the spread permits; if that reaches the opposite touch, it falls back to the same-side touch. Post-only guards against crossing at acceptance, but this is a different economic event from immediate taker execution.
Maker fills are conditional: the counterparty chooses to trade against a resting quote, often while price is moving through it. That creates adverse-selection risk. “One tick better than the same-side touch” is a price concession and queue-priority reset on every replacement, not a guaranteed captured tick or profit. Net expectancy requires actual fill-conditional markouts, fees/gas, queue age, cancellation latency and inventory costs.
The official TypeSafe documentation describes Jev as a structured decision/classification layer and stresses measuring classification quality on the deployer's labelled data. Tool routing is a separate capability; this repository's Jev call has one choice output and no tools. Jev does not send a transaction in this code: Market signs and submits only after Trader applies local constraints. See [Pydantic TypeSafe / Jev documentation](https://pydantic.dev/docs/ai/models/typesafe/), especially decision measurement and tool routing.
## Price and transaction lifecycle
### Quote construction
Market.quotePrice converts best bid and ask to integer tick units. A buy is best bid plus quoteInsideTicks; a sell is best ask minus quoteInsideTicks. If the candidate reaches/crosses the opposite touch, it reverts to the same-side touch. Market.encode builds one batchUpdate with one side's price/size arrays, known IDs to cancel, and postOnly=true.
A single batchUpdate can atomically apply its listed cancellations and new order within one EVM transaction. This does not give the bot a one-order invariant: it cancels only IDs already learned from confirmed receipts, cannot include pending or unknown IDs, and does not wait for earlier transactions. A reverted transaction leaves the book unchanged.
### Send, nonce and uncertainty
Market.send signs locally and calls eth_sendRawTransaction. It increments its local nonce and adds the transaction to Market.pending only after the RPC returns a hash. If the call throws, it attempts latest-nonce resynchronization and rethrows. If a node accepted a transaction but its response is lost, the source does not first preserve the signed hash in its pending map. There is no explicit RPC deadline or transaction replacement strategy.
After the send RPC returns, busy clears. A later block can send another transaction while one or more earlier hashes remain pending. Thus busy is a decision-routine gate, not a transaction mutex.
### Receipt polling
confirmPending launches Market.pollPending without awaiting it. Pending hashes are polled concurrently on each block; calls from successive blocks can overlap. A map-presence check prevents duplicate application after another poll removed the same hash, but one slow RPC within Promise.all can delay delivery of other results from that invocation.
A non-null receipt is removed and parsed. Status 0x0 is reverted; any other value, including an absent or unexpected status, is treated as placed. Parsed OrderCreated and OrdersCanceled logs for the local wallet update known IDs. There is no open-order query to verify the resulting state.
After pendingBlocks (default 10) without a receipt, the pending record is removed and emitted as lost; gas is shown as zero for that event and nonce resync is attempted. “Lost” means local polling gave up, not venue proof the transaction cannot still land. The path has no subsequent reconciliation of that uncertain order.
### Fill logs and accounting races
TradeFeed polls Kuru Trade logs in chunks of up to 100 blocks, maintains a process-local cursor and ring, and suppresses fills on its first catch-up poll. It does not persist its cursor, deduplicate by stable transaction/log identity or roll back removed logs after a reorg.
Live harvest applies fills to position even if Trader.orders does not know the order ID. It deletes/resizes a known local order, but has no lifecycle record for an unknown ID. A fill can precede the order-creation receipt. Later applyQuoteResult can insert the original full order size, even if already filled or partially filled. A late receipt can similarly re-add an order after a subsequent replacement. These are concrete ordering gaps.
The dry-run simulator matches prints against local resting quotes by price and print size. It does not model queue position, latency, cancellation races, conditional maker fills, fees or adverse selection; it cannot establish live maker profitability.
Inventory uses signed position and weighted-average cost basis, not FIFO. Opposite fills realize against the average basis and excess quantity flips the position. Gas estimates are attached to quotes, while totals charge gas when receipt results are applied. Displayed trading PnL includes realized/unrealized USD value minus tracked gas; inference spend is separate and not deducted. Venue maker fees are not included.
### Local exposure checks
allowed() uses signed position plus known resting orders and full-size in-flight quotes on the side being considered, then checks absolute exposure against MAX_POSITION_MON. This is side-specific projected signed exposure, not a universal gross-risk guarantee across uncertain transactions. A funded wallet also uses cached margin balances refreshed periodically; source does not reserve/decrement local margin for each unconfirmed order. These are local guards, not exchange-authoritative reconciliation.
## Source map
| Full local path | Role | Limitation relevant to execution |
|---|---|---|
| /tmp/jev-exec-study-b587759/src/index.ts | Runtime composition | no restore/open-order reconciliation |
| /tmp/jev-exec-study-b587759/src/config.ts | Environment and defaults | limited range/enum validation |
| /tmp/jev-exec-study-b587759/src/chain.ts | RPC, block WS/poll | no timeout/reorg; polls may overlap |
| /tmp/jev-exec-study-b587759/src/book.ts | ABI decode and features | independent latest reads |
| /tmp/jev-exec-study-b587759/src/model.ts | Jev and mock model | IOC prompt, post-only runtime; no calibration |
| /tmp/jev-exec-study-b587759/src/market.ts | quote, signer, receipts, nonce, margin | unknown transaction state not reconciled |
| /tmp/jev-exec-study-b587759/src/trader.ts | decision gate, orders, fills, PnL | receipt/fill races; busy is not tx serialization |
| /tmp/jev-exec-study-b587759/src/trades.ts | trade-log polling | in-memory cursor, no reorg-safe dedupe |
| /tmp/jev-exec-study-b587759/src/server.ts | snapshot/history/SSE | telemetry only |
| /tmp/jev-exec-study-b587759/web/src/lib/useFeed.ts | EventSource client reducer | UI-live does not prove venue freshness |
| /tmp/jev-exec-study-b587759/web/src/components/ | chart, decisions, stats and feed | presentation only |
| /tmp/jev-exec-study-b587759/scripts/ | ABI/RPC probes and smoke scripts | not all scripts are offline/read-only |
The UI consumes in-memory snapshots and SSE updates; it has no order command endpoint. A live SSE connection only proves UI-to-server reachability, not that model calls, book data, orders or venue truth are current.
## Failure and recovery summary
| Condition | Source behavior | Recovery in source |
|---|---|---|
| book/model stalls | block marked late; current routine remains busy | no age deadline or stale-result cancellation |
| read/model/send error | log only; existing order map remains | no automatic cancel/reconcile |
| node accepts tx but response is lost | hash may never enter pending | nonce resync on thrown request only |
| multiple pending transactions | receipt polls run off-path | not serialized by busy |
| no receipt for 10 blocks | removed as lost | no authoritative open-order lookup |
| fill precedes receipt | position updates without local order | receipt can restore stale size |
| unknown order ID in successful receipt | status may be placed without usable ID | no venue-state reconciliation |
| trade log replay/reorg | cursor/ring are in memory | no stable dedupe/rollback |
| process restart | orders, position and cursor reset | no restore/reconcile |
| bad/un-calibrated Jev output | same-size order can still be posted | no quality gate |
## What is portable
Jev's structured directional output and feature prompt are research inputs worth testing. Keep order identity, fill economics, unknown-state handling, cancellation truth, exposure reservation, exit authority and reconciliation under FLIGHT's existing execution owner. Treat Jev output as policy input. A separate bounded execution policy should decide price, post-only versus IOC, size and deadline.
For model evaluation, record input, raw output, model ID and latency. Score the stated directional horizon on every signal, whether or not it filled; separately score fill-conditional markout, fees/gas, queue age, cancel latency and inventory cost. Calibrate out of sample by regime and side before using probabilities for sizing or risk.
## Inventory and limitations
Pinned source: https://github.com/jarrodwatts/jev-trader/tree/b587759e459ea049590102e54a0b07800864cdc3
Local clone: /tmp/jev-exec-study-b587759/
Official Jev documentation: https://pydantic.dev/docs/ai/models/typesafe/
The full tracked inventory is available with git -C /tmp/jev-exec-study-b587759 ls-files. Besides runtime code it includes package/lock/config metadata, docs, authoring instructions, RPC/ABI scripts and the Next.js dashboard. The lockfiles pin dependencies, not execution behavior. This static audit does not establish provider reliability, live Jev accuracy, Kuru economics, profitable fills or a deployed runtime configuration.