Skip to content

pineforge-feed: OKX, Bybit and Binance USD-M venues (slice 2) - #18

Merged
luisleo526 merged 16 commits into
mainfrom
lv/feed-venues
Oct 3, 2026
Merged

luisleo526 merged 16 commits into
mainfrom
lv/feed-venues

Conversation

@luisleo526

Copy link
Copy Markdown
Contributor

Summary

Adds the remaining approved venues to the native pineforge-feed adapter in native/feed/. Public market data only: no API keys, orders, accounts or fills.

Venue Bars Ticks
OKX spot and swap confirmed candle1m rows trades-all, tradeId strictly +1, healed by history-trades pages within the 3-month window; swap quantities converted exactly from ctVal x ctMult (decimal, never float); inverse swaps refused
Bybit spot and linear confirmed kline.1 refused at startup: Bybit trade IDs are not a contiguous cursor and its REST cannot page back through trades
Binance USD-M routed /market stream, closed kline x=true opt-in agg-ticks: one print per aggregate a (never expanded), healed by aggTrades fromId within the 48-hour retention, stopping beyond it
Coinbase refused as deferred refused as deferred

Completeness: the next-print fence

A time event for minute M is emitted only when all of these hold:

  • a contiguous ID chain from a REST-proven predecessor;
  • the next print of that chain at or after M+60 s;
  • the venue's closed candle for M.

OKX raw prints must also equal the candle exactly. USD-M aggregates are dated by trade time and never split, so a fill can sit in a different minute than Binance's kline. Agg-ticks minutes therefore close on the fence without kline equality. The README states the consequence at the point of choice: over 120 consecutive minutes, 5% differed by one tick in the open and by the moved fill in volume, while close, high and low never differed. --mode bars gives kline-exact bars.

Unchanged from slice 1

Every slice-1 guarantee carries over:

  • exact decimal tokens; no hidden gap; exits 20-23;
  • group commit with persist-before-publish; --resume/--output-from;
  • libcurl >= 8.14.1 with ws/wss; no new dependency.

Changes in this slice

  • A quiet start now waits for the first print instead of stopping.
  • Bybit HTTP 403 (a rate ban) stops 20 (retryable).
  • OKX multipliers are re-checked on every reconnect (a change stops 21).

Verification

  • Unit and mock: 45 unit groups and 76 mock scenarios in Release, ASan+UBSan and TSan, with 0 sanitizer reports. Every venue quirk has a synthetic-fixture unit test and a mock-venue scenario. Two independent reviews' mutants are all killed.
  • Live, public data, 960 s each with a SIGKILL restart: OKX swap bars and ticks, Bybit linear bars, USD-M bars and agg-ticks, OKX spot bars and ticks, and Bybit spot bars all PASS. In every gate, bars and prints equal REST and the runner's actions equal its batch over the same bars.
  • Python: uv run pytest -q and ruff check . pass; the Python package and wheel are unchanged.
  • Committed fixtures are synthetic.

🤖 Generated with Claude Code

luisleo526 and others added 16 commits October 4, 2026 04:21
The mock's client-frame parser fell through from the 16-bit extended
length to the 64-bit branch, so a client frame of exactly 127 bytes was
read with a garbage 64-bit length. Client frames used to be short PONGs;
subscription frames for OKX trades-all plus candles are 127 bytes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Venue gates, checked at startup before any network access:
- OKX spot and linear swap: confirmed candle1m bars (confirm "1"), and
  raw ticks from trades-all only. A print carrying `count` or a push on
  the aggregated trades channel stops with 23. Swap sizes and candle
  volumes convert exactly to base units with ctVal x ctMult from the
  public instrument (decimal multiply, never float); inverse swaps are
  refused. Holes heal by trade ID through history-trades pages of 100,
  within the 3-month window. Text ping keepalive; notice reconnects.
- Bybit spot and linear: confirmed kline.1 bars only. Tick modes are
  refused: trade IDs are not a contiguous cursor and REST cannot page
  back. Two-entry pushes, and newest-first REST rows requested in
  windows no wider than the limit.
- Binance USD-M: confirmed bars on the routed /market stream, and an
  opt-in agg-ticks mode that maps each aggregate `a` to one print with
  quantity `q` (it includes RPI executions, like the kline volume),
  never expanding f..l. Heals through aggTrades fromId within 48 hours;
  -4166 and older gaps stop with 20. The exchangeInfo document exceeds
  the parser bound, so only its rateLimits member is extracted.
- Coinbase is refused as deferred.

OKX and USD-M ticks close a minute on a next-print fence: the contiguous
ID chain from the REST-proven predecessor reaches a print of a later
minute, and the minute's confirmed candle (WebSocket, or REST behind a
later confirmed WebSocket candle) equals its prints exactly in OHLC and
volume. A quiet minute closes on the fence with a zero-volume candle.

The pump now takes a venue connection (path, subscription frames, text
keepalive, frame classifier); keepalive replies never count as data.
REST is allowlisted and paced per venue. Sessions move to session.cpp;
the Binance spot proof is unchanged. Overlap and revision checks compare
exact decimal values rather than lexemes (OKX renders one price as
84850.0 on its WebSocket and 84850 over REST), and a disagreement with
REST must survive spaced re-reads before it stops with 21, because a
just-closed minute's REST row can trail the WebSocket close. The cursor
and warmup manifest bind the quantity multiplier.

The public E2E harness takes --venue/--market/--symbol/--modes and
verifies against each venue's REST in normalized units; R-B2 applies to
every tick mode.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A just-closed minute's REST row can trail the WebSocket close, as
missing, unconfirmed (OKX) or with older values. The feed already
re-read a disagreeing overlap; it now also re-reads when the newest row
is missing or unconfirmed, before stopping with 20. Live, an OKX warmup
can ask for its newest candle a moment after the WebSocket confirm.

The qualification harness compares the tape with REST right after the
last minute closes, so its bar, print and candle checks now pass only
when REST agrees within spaced re-reads (about 25 s); a persistent
disagreement still fails.

The mock suite's CTest timeout rises to 420 s: under TSan it takes about
140 s on the test machine, and hosted runners are slower.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review fixes for the venue slice:
- OKX's time lookup for the last print before --start can answer an
  older print (REST trailing the newest prints, or prints of one
  millisecond out of ID order). The feed now walks forward by trade ID
  until the next print is at or after the start and anchors only then;
  until that print is visible it stops with 20 and saves nothing, so a
  restart cannot loop on a wrong predecessor.
- USD-M looks for the first aggregate at or after the start within the
  next hour, so a quiet start minute has a fence.
- A print at or before the anchored predecessor but at or after the
  start stops with 23 (time regression) or 21 (changed predecessor),
  not with a misleading buffered-cursor 21; an empty predecessor
  overlap is 20, not 21.
- OKX 50001/50004/50013/50026 and Bybit 10000/10016 are retried with
  backoff like HTTP 5xx instead of stopping with 23.
- A WebSocket the venue accepts but closes before any frame now spends
  the reconnect budget; loopback test origins use 50 ms backoff units.
- Healing at the newest prints re-reads an empty REST page for about
  7 seconds, like the reconnect overlap.
- make_session refuses a tick mode that the venue cannot prove.

Tests: resume rebuilds the open fenced minute; a changed contract
multiplier stops 21 on resume; the predecessor walk and its unanchored
20; retried venue codes; USD-M -1121; a quiet USD-M start minute; the
reconnect budget; the warmup REST lag now starts from the WebSocket
script, so sanitizer timing cannot defeat it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
OKX and USD-M close a minute without prints on the next-print fence when
its candle shows zero volume. The harness now accepts such a minute only
with a zero-volume REST candle, and the batch sees that flat candle.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The final live round stopped USD-M agg-ticks with 21: every aggregate of
the minute equalled REST aggTrades, but Binance's own kline was 0.003
BTC (one fill) short, and the next minute's kline was 0.003 long with a
different OHLC. An aggregate is dated by its trade time and never split,
so one can hold a fill that the next kline counts: the kline is not the
exact sum of aggregate prints, and comparing them is not a completeness
check.

USD-M aggregate minutes now close on the next-print fence (the
contiguous aggregate chain reaches a later minute) once the venue has
closed the minute; OKX raw prints still require exact candle equality.
The harness verifies every aggregate against REST and reports, rather
than requires, kline differences for USD-M.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
USD-M aggregate minutes are no longer compared with the kline, so the
reason for `q` over `nq` is that `nq` drops RPI executions from the
print stream. Comment and README wording only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Two unit fixtures and two slice-1 mock action records carried values copied
from live market data. Replace them with hand-written numbers; no behaviour
depends on the values.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lier

A fence session with nothing anchored no longer stops 20 when a later
minute closes before any print after --start exists: forward() asks the
venue for a predecessor only once a print at or after the start is visible,
and otherwise waits. Nothing is anchored or emitted before that proof. An
empty look-ahead page is read once instead of four times over 7 s.

Every connection re-reads OKX instrument metadata; a changed ctVal x ctMult
stops 21, because quantities already emitted used the old multiplier.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Bybit answers 403 for its roughly ten-minute IP rate ban. A per-venue
RestPolicy rejection hook now runs before the generic access-denied check
and maps that 403 to 20, so a supervised restart recovers; other venues
keep 403 as a permanent 23.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Unit and mock cases kill the review's surviving mutants: a forming candle
never closes a fenced minute; a conflicting duplicate print stops 21,
emitted or buffered; a changed closed candle or start predecessor on
reconnect, and a candle revised before its minute closes, stop 21; the
unconfirmed-print buffer is bounded; the reconnect print overlap and a heal
page re-read trailing REST; a non-unit ID inside an OKX or USD-M REST page
stops 20. The Bybit confirm-flag scenario now gives the forming rows other
values and asserts the emitted bars. Also covered: a quiet start waits, a
Bybit 403 stops 20, and a changed OKX multiplier stops 21 on reconnect.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
For agg-ticks the harness also runs the strategy over Binance klines for
the proven minutes and prints, as information next to kline_differs, how
many actions differ from the runner. The gate stays the batch over the bars
built from the feed's own prints.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The README states where agg-ticks is chosen that its bars can differ from
Binance klines at minute edges, with the measured rate and a worked fill,
that --mode bars gives kline-exact bars, and that the matching backtest
runs on prints-built bars. It also documents that output tokens come from
the delivering source, what the tick batch replays, the quiet-start wait,
Bybit's 403, the OKX multiplier re-check and value-based overlaps. --help
carries the agg-ticks note.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The scenario's REST gained its first print after 0.5 s, inside the 1.5 s an
OKX predecessor lookup waits, so it also passed for a feed that anchored
blindly. Three seconds makes that stop 20 and the scenario fail. Also
shortens a comment line that ruff flags.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@luisleo526
luisleo526 merged commit 34e294a into main Oct 3, 2026
17 checks passed
@luisleo526
luisleo526 deleted the lv/feed-venues branch October 3, 2026 23:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant