Skip to content

Commit 1bd2af1

Browse files
committed
feat(archive): durable Maker strategy acceptance
1 parent d20daf8 commit 1bd2af1

95 files changed

Lines changed: 9885 additions & 605 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@ jobs:
1212
test:
1313
runs-on: ubuntu-latest
1414

15+
services:
16+
clickhouse:
17+
image: clickhouse/clickhouse-server:24.8
18+
env:
19+
CLICKHOUSE_USER: default
20+
CLICKHOUSE_PASSWORD: integration-test
21+
ports:
22+
- 8123:8123
23+
options: >-
24+
--health-cmd "wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1"
25+
--health-interval 5s
26+
--health-timeout 3s
27+
--health-retries 20
28+
1529
steps:
1630
- name: Checkout repo
1731
uses: actions/checkout@v3
@@ -32,6 +46,11 @@ jobs:
3246

3347
- name: Run tests
3448
run: bun test
49+
env:
50+
CLICKHOUSE_REQUIRED: "1"
51+
CLICKHOUSE_TEST_URL: http://localhost:8123
52+
CLICKHOUSE_USER: default
53+
CLICKHOUSE_PASSWORD: integration-test
3554

3655
# Gate PRs on the same strict build the publish workflow runs on tag push
3756
# (dts-bundle-generator + strict tsc). Without this, develop can merge code

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
2626
.eslintcache
2727
.cache
2828
*.tsbuildinfo
29+
archive-forwarder-spool.sqlite*
2930

3031
# research outputs
3132
research/python/examples/output/

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ A high-performance gRPC-based cryptocurrency exchange broker service that provid
2424
- API keys for supported exchanges (e.g., Binance, Bybit, etc.)
2525
- Optional: Verity prover URL for zero-knowledge proof integration
2626

27+
## Service architecture
28+
29+
See [SERVICES_ARCHITECTURE.md](SERVICES_ARCHITECTURE.md) for the authoritative boundaries between the full broker, operator market-data collector, archive-forwarder, research viewer, offline tools, and externally owned systems.
30+
2731
## 🛠️ Installation
2832

2933
1. **Clone the repository:**
@@ -97,8 +101,15 @@ CLICKHOUSE_PORT=8123 bun run start-candle-viewer # http://localhost:8091
97101

98102
Dev watchers: `dev:candle-viewer`, `dev:archive-forwarder`, `dev:archive-watch` (see [research/README.md](research/README.md)).
99103

104+
Maker `hb_runtime` delivery uses the forwarder's durable strategy-only SQLite
105+
acceptance boundary. See [docs/archive-forwarder-durable-acceptance.md](docs/archive-forwarder-durable-acceptance.md)
106+
for the wire versions, fixed quota/retention, retry ownership, health semantics,
107+
and required production volume.
108+
100109
Key env vars: `CEX_BROKER_ARCHIVE_ENABLED=true`, `CEX_BROKER_ARCHIVE_FORWARDER_URL`, `CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH`, and `CEX_BROKER_DEPLOYMENT_ID`. The archive is disabled for every enable value except the exact string `true`. Production durability requires the dead-letter file to reside on persistent writable storage or a mounted volume; a container-local ephemeral path is not durable.
101110

111+
Canonical replay capture for FIET-901/FIET-903 adds deployment-owned archive roles, capture bundles, four-feed collector configuration, deterministic checksums, conflict-blocking replay views, and mandatory legacy-to-canonical table migration before canonical-only deployment. Credential resolution remains environment-loaded broker first, request metadata second, and supported public fallback last. See [docs/canonical-market-data-replay.md](docs/canonical-market-data-replay.md) for the complete deployment and cutover contract.
112+
102113
#### Wallet-authenticated exchanges
103114

104115
Some exchanges (for example Hyperliquid, Vertex, Paradex, and Derive) authenticate with an on-chain wallet instead of exchange-issued API keys. The broker keeps the same `API_KEY` / `API_SECRET` interface and maps credentials internally based on each exchange's CCXT `requiredCredentials`:

docker/clickhouse-research.compose.yml

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ services:
55
ports:
66
- "8123:8123"
77
- "9000:9000"
8+
environment:
9+
- CLICKHOUSE_USER=default
10+
- CLICKHOUSE_PASSWORD
811
volumes:
912
- clickhouse_data:/var/lib/clickhouse
1013
- ../schema/clickhouse/market_data.sql:/docker-entrypoint-initdb.d/01_market_data.sql:ro
@@ -24,19 +27,22 @@ services:
2427
ports:
2528
- "8090:8090"
2629
environment:
27-
ARCHIVE_FORWARDER_PORT: "8090"
28-
CLICKHOUSE_HOST: clickhouse
29-
CLICKHOUSE_PORT: "8123"
30-
CLICKHOUSE_USER: default
31-
CLICKHOUSE_PASSWORD: ""
32-
CLICKHOUSE_DATABASE: market_data
30+
- ARCHIVE_FORWARDER_PORT=8090
31+
- CLICKHOUSE_HOST=clickhouse
32+
- CLICKHOUSE_PORT=8123
33+
- CLICKHOUSE_USER=default
34+
- CLICKHOUSE_PASSWORD
35+
- CLICKHOUSE_DATABASE=market_data
36+
- ARCHIVE_FORWARDER_SPOOL_PATH=/var/lib/archive-forwarder/spool.sqlite
3337
volumes:
3438
- ..:/app
39+
- archive_forwarder_spool:/var/lib/archive-forwarder
3540
depends_on:
3641
- clickhouse
3742

3843
volumes:
3944
clickhouse_data:
45+
archive_forwarder_spool:
4046

4147
networks:
4248
default:
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Maker archive-forwarder durable acceptance
2+
3+
## Purpose and ownership boundary
4+
5+
Maker's runtime archive bridge makes one non-blocking HTTP attempt. The archive
6+
forwarder therefore owns a conforming request only after it commits the request
7+
to its SQLite spool. It then returns HTTP 202 and drains each represented table
8+
to ClickHouse independently. Maker does not retry an acknowledged batch.
9+
10+
This path is deliberately narrow: `source=hb_runtime` and exclusively these five
11+
tables:
12+
13+
- `strategy_data.policy_evaluation_events`
14+
- `strategy_data.strategy_policy_snapshots`
15+
- `strategy_data.market_identity`
16+
- `strategy_data.symbol_mapping`
17+
- `strategy_data.inventory_settlement_events`
18+
19+
Mixed requests are rejected atomically. Strategy tables under another source
20+
are also rejected. `broker_read`, `broker_write`, account, execution, market-data,
21+
and Maker replay requests keep their existing direct synchronous ClickHouse path
22+
and do not consume spool capacity.
23+
24+
## Wire versions
25+
26+
The envelope requires non-empty `source`, non-empty `deployment_id`, and 1–1000
27+
rows. Missing/empty `schema_version` and version `1` use the legacy contract.
28+
Version `2` additionally requires non-empty `producer_id`, `producer_run_id`,
29+
`stream_name`, and `archive_event_id`, plus positive integer `stream_seq` and
30+
`seq`. Unknown versions and row/envelope provenance mismatches return HTTP 400.
31+
32+
`test/fixtures/archive_forwarder_envelope.json` is byte-identical to the fixture
33+
at Maker commit `563594435853c88cca5b187b8c999f845e31136b`; its pinned SHA-256 is
34+
`784f647e048052a6c3382309b1a86abfbe08bc162363ead9fc88eaa1ba3d50c9`.
35+
36+
## Spool storage and fixed service limits
37+
38+
The forwarder uses Bun SQLite with WAL, foreign keys, a 5-second busy timeout,
39+
and `synchronous=FULL`. Local runs default to:
40+
41+
```text
42+
./archive-forwarder-spool.sqlite
43+
```
44+
45+
Production must configure and persist:
46+
47+
```text
48+
ARCHIVE_FORWARDER_SPOOL_PATH=/var/lib/archive-forwarder/spool.sqlite
49+
```
50+
51+
Mount `/var/lib/archive-forwarder` on durable storage. The supplied Dockerfile
52+
and compose service do so. This is an archive-forwarder service option, not a
53+
core broker `CEX_BROKER_*` option.
54+
55+
Quota is fixed at 1 GiB of deterministic payload/metadata accounting. Retention
56+
is fixed at 72 hours from admission. Neither has an environment override. Quota
57+
reservation, expiry cleanup, batch insertion, and per-table work insertion are
58+
transactional.
59+
60+
## Responses and retry behavior
61+
62+
- `202`: the complete Maker batch is durably owned; ClickHouse completion may be pending.
63+
- `400`: invalid envelope, table/source mix, provenance, or schema version/identity.
64+
- `429`: accepting the batch would exceed the fixed spool quota; ownership was not accepted.
65+
- `503`: the spool is missing, corrupt, unwritable, or cannot commit; ownership was not accepted.
66+
67+
Transient ClickHouse failures retry after 1, 2, 4, 8, 16, 32, then 60 seconds,
68+
with ±20% jitter, through retention expiry. Completion is tracked per table, so
69+
a failed table never replays successful siblings. Schema and authentication
70+
failures become terminal and remain visible until expiry instead of hot-looping.
71+
72+
Each batch/table work item persists one stable SHA-256 deduplication token. The
73+
worker passes it as `insert_deduplication_token`, while every strategy table has
74+
`non_replicated_deduplication_window=1000000`. A crash after a ClickHouse commit
75+
but before SQLite completion therefore retries with the same token.
76+
77+
## Health and operations
78+
79+
`GET /health` reports ClickHouse/schema readiness separately from spool
80+
writability and exposes queued batches/work, terminal/expired work, accounted
81+
bytes, oldest age, and the last bounded error class.
82+
83+
- ClickHouse down + spool healthy: HTTP 200, `status=degraded`, durable admission available.
84+
- ClickHouse ready + spool healthy: HTTP 200, `status=ok`.
85+
- Spool unhealthy: HTTP 503, `status=unavailable`, even when ClickHouse is reachable.
86+
87+
On restart, the service attempts to open/migrate the spool before listening and
88+
resumes incomplete work after ClickHouse schema readiness. If the persisted file
89+
is unavailable or corrupt, the service remains up only to expose unavailable
90+
health and the existing direct path; strategy admission fails with 503 until an
91+
operator preserves/repairs the file and restarts. Never delete or replace a spool
92+
file with pending work during rollback. Stop admission, preserve the volume, and
93+
retain the additive ClickHouse columns.
94+
95+
The production feed/archive observation window is a separate FIET-937 cutover
96+
gate. Repository conformance checks do not claim that observation has run.
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Canonical CEX market-data replay archive
2+
3+
This is the deployment and operations contract for the CEX-broker portions of FIET-901 and FIET-903. FIET-903's broker RPC/capability/current-snapshot/live-stream sub-scope was completed by `cex-broker-order-book-depth-sourcing`; this change adds capture, storage, migration, and replay integrity. It does not add or gate RPCs.
4+
5+
## Deployment identity and credential precedence
6+
7+
TEE and non-TEE deployments run the same broker binary and register the full `ExecuteAction` and `Subscribe` service. Archive role does not reduce that service surface.
8+
9+
Set the deployment-owned archive identity:
10+
11+
```env
12+
CEX_BROKER_ARCHIVE_ENABLED=true
13+
CEX_BROKER_ARCHIVE_SOURCE=broker_read
14+
CEX_BROKER_DEPLOYMENT_ID=market-reader-eu-1
15+
CEX_BROKER_CAPTURE_BUNDLE_ID=cex-2026-08-03-eu-1
16+
CEX_BROKER_MARKET_CAPTURE_ENVIRONMENT=production
17+
CEX_BROKER_ARCHIVE_FORWARDER_URL=http://archive-forwarder:8090/archive
18+
CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH=/var/lib/cex-broker/archive-loss.jsonl
19+
```
20+
21+
`CEX_BROKER_ARCHIVE_SOURCE` is closed to `broker_read|broker_write` and defaults to `broker_write` for existing deployments. The writer stamps this immutable value into envelopes, rows, and loss records. It is never inferred from API-key presence. Production FIET-901 collector startup requires `broker_read` and a non-empty deployment-owned capture bundle. Development capture explicitly uses `CEX_BROKER_MARKET_CAPTURE_ENVIRONMENT=development` and generates a `development:<deployment>` bundle when none is supplied.
22+
23+
Credential resolution uses the broker's established fixed precedence and requires no archive-specific credential configuration:
24+
25+
1. Use a matching broker account loaded from `.env`/deployment configuration.
26+
2. If no matching environment broker exists, use a complete request `api-key` and `api-secret` pair.
27+
3. If neither source exists, construct a credentialless exchange only for operations that already support public access.
28+
29+
When environment and request credentials are both present, the environment-loaded broker wins. The broker does not classify keys as public, read-only, or write-capable and does not introduce credential-source or permission-attestation settings. For a non-TEE reader, ensure every credential that deployment or its trusted callers may supply has exchange-side trading and withdrawal permissions disabled. Those exchange permissions—not archive source or credential location—establish effective privilege.
30+
31+
## Four-feed collector
32+
33+
The collector is an independent keep-alive client of a separately deployed full broker. Set its broker target and point its canonical configuration at a JSON document containing only feed intent:
34+
35+
```env
36+
CEX_BROKER_URL=cex-broker.internal:8086
37+
CEX_BROKER_MARKET_DATA_COLLECTOR_CONFIG=/etc/cex-broker/market-data-subscriptions.json
38+
```
39+
40+
```json
41+
{
42+
"subscriptions": [
43+
{ "exchange": "binance", "symbol": "BTC/USDT", "feed": "ORDERBOOK", "depthLimit": 50 },
44+
{ "exchange": "binance", "symbol": "BTC/USDT", "feed": "TICKER" },
45+
{ "exchange": "binance", "symbol": "BTC/USDT", "feed": "TRADES" },
46+
{ "exchange": "binance", "symbol": "BTC/USDT", "feed": "OHLCV", "timeframe": "1m", "bootstrapLimit": 100 }
47+
]
48+
}
49+
```
50+
51+
Run it with `bun run start-market-data-collector`. The collector starts no loopback broker, loads no CEX credentials, sends no API-key metadata, owns no archive writer, and does not connect to ClickHouse. The remote broker resolves its environment-first credentials and attaches the production environment, deployment, capture bundle, source, and integrity provenance configured in the preceding section. Collector JSON containing `environment`, `captureBundleId`, or other archive identity is rejected.
52+
53+
Each entry has an independent reconnect supervisor and health state. OHLCV retains bootstrap/catch-up and stamps `broker_bootstrap_fetch_v1` separately from live capture. ORDERBOOK, TICKER, and TRADES record unrecoverable gaps after reconnect rather than synthesize missing events.
54+
55+
`CEX_BROKER_OHLCV_COLLECTOR_CONFIG` remains supported as an OHLCV-only array. Move those entries into the canonical document and add `feed: "OHLCV"`; archive identity stays on the broker. The `start-ohlcv-collector` command and existing service directory/image remain compatibility wrappers for both formats.
56+
57+
An external CCXT or Hummingbot fallback is an optional out-of-band producer of the shared capture contract, not a broker-collector implementation. It must declare its provider, versioned fallback source mode, reason, configured exchange, and configured pair. Cross-venue or cross-pair substitution is rejected.
58+
59+
## Capture and integrity contract
60+
61+
All canonical rows carry source, deployment, capture bundle, exchange, trading pair/source symbol, provider, feed, source mode, source/received timestamps, raw capture ID/scope, schema version, checksum algorithm, raw checksum, normalized checksum, and provenance completeness. The current versions are:
62+
63+
- schema: `1.0.0`
64+
- checksum: `sha256-canonical-json-v1`
65+
- construction: sampled top-N snapshots for live/current broker order books
66+
67+
Raw payloads are redacted before identity/checksum calculation. Canonical JSON sorts object keys, uses finite plain-decimal numbers, normalizes negative zero, omits undefined object values, and excludes checksum fields from their own projections. The TypeScript fixture is `test/fixtures/canonical-market-capture-v1.json`; `research/hummingbot/canonical_capture_fixture.py` is the matching Maker-side verifier.
68+
69+
Exact L2 is future-facing and non-blocking for this delivery. The broker reports it unsupported and never silently labels sampled evidence exact. A future exact producer must supply complete continuity proof.
70+
71+
## ClickHouse and replay
72+
73+
Canonical storage is:
74+
75+
- `market_data.cex_stream_events` for the redacted raw ledger
76+
- `market_data.cex_ticker_events`, `market_data.cex_trades`, and `market_data.cex_ohlcv`
77+
- append-only `market_data.cex_order_book_levels` and `market_data.cex_order_book_depth_summary`
78+
79+
`cex_ohlcv` uses `ReplacingMergeTree(broker_version)`; `cex_ohlcv_closed` applies `FINAL` and closed-bar semantics. Order-book physical duplicates remain auditable. The `_canonical` views expose one checksum-consistent logical row, while `_conflicts` expose keys with multiple checksums. Same-batch conflicts are rejected by the forwarder; cross-batch conflicts remain stored and must block the affected replay bundle.
80+
81+
Use `schema/clickhouse/canonical_market_data_replay.sql` for bounded bundle/exchange/pair/source-time replay. Its conflict preflights must return no rows before consuming the canonical views.
82+
83+
The retained FIET-907 reference exporter materializes a conflict-free order-book window directly from ClickHouse to Maker-compatible Parquet files:
84+
85+
```bash
86+
CEX_BROKER_REPLAY_EXPORT_DIRECTORY=/tmp/maker-capture \
87+
CEX_BROKER_REPLAY_CAPTURE_BUNDLE_IDS=cex-2026-08-03-eu-1 \
88+
CEX_BROKER_REPLAY_EXCHANGE=binance \
89+
CEX_BROKER_REPLAY_TRADING_PAIR=BTC-USDT \
90+
CEX_BROKER_REPLAY_START_TIME_MS=1785715200000 \
91+
CEX_BROKER_REPLAY_END_TIME_MS=1785801600000 \
92+
bun scripts/export-canonical-orderbook-parquet.ts
93+
94+
uv run --project research/python --extra dev \
95+
python research/hummingbot/order_book_parquet_fixture.py \
96+
/tmp/maker-capture/order_book_levels.parquet \
97+
/tmp/maker-capture/order_book_depth_summary.parquet
98+
```
99+
100+
The exporter refuses to overwrite existing files or export a selected window with an order-book checksum conflict. The Python verifier checks capture-core field presence and recomputes every normalized-row checksum from the Parquet values. Neither tool calls the broker or an exchange. Fixture materialization, coverage reports, and replay-bundle assembly are owned by [FIET-907](https://linear.app/usherlabs/issue/FIET-907/clickhouse-backtest-fixture-materializers-and-coveragereplay-bundles), not by the live capture runtime.
101+
102+
For complete strategy-pair validation, point `CEX_BROKER_REPLAY_VALIDATION_CONFIG` at a JSON document whose `windows` array contains `captureBundleIds`, `exchange`, `tradingPair`, `startTimeMs`, and `endTimeMs`, then run `bun scripts/validate-canonical-market-replay.ts`. Every configured window must contain raw and normalized ORDERBOOK, TICKER, TRADES, and OHLCV evidence; any missing feed or checksum conflict fails validation.
103+
104+
## Migration, cutover, rollback, and deployment observation
105+
106+
The upgraded broker always writes the latest canonical schema. There is no runtime legacy/dual/canonical write setting. Upgrading an existing legacy deployment therefore requires the ClickHouse table migration before the new broker version is deployed.
107+
108+
Follow `schema/clickhouse/migrations/canonical_market_data_replay_cutover.sql` phase by phase: apply canonical DDL, quiesce legacy writers, migrate every retained bounded window, validate parity, switch consumers, and only then deploy the canonical-only broker.
109+
110+
```bash
111+
CEX_BROKER_MIGRATION_START_TIME_MS=1700000000000 \
112+
CEX_BROKER_MIGRATION_END_TIME_MS=1700086400000 \
113+
bun run scripts/migrate-legacy-market-data-to-canonical.ts
114+
115+
# Repeat after reviewing dry-run counts:
116+
CEX_BROKER_CANONICAL_MIGRATION_CONFIRM=true \
117+
CEX_BROKER_MIGRATION_START_TIME_MS=1700000000000 \
118+
CEX_BROKER_MIGRATION_END_TIME_MS=1700086400000 \
119+
bun run scripts/migrate-legacy-market-data-to-canonical.ts
120+
```
121+
122+
The script reads `market_data.orderbook_snapshots` and `market_data.candles` directly from ClickHouse and writes their canonical equivalents. It never reads fixture files and never calls a broker or exchange. Legacy migration stamps `legacy_migration_v1` and `provenance_complete=0`; unavailable bundle/raw ID/raw scope/raw checksum remain `NULL`. Reruns preserve identical logical checksums: order-book canonical views collapse agreeing physical deliveries and OHLCV replacement semantics select the recorded broker version.
123+
124+
Before cutover, run the parity and replay queries for every configured pair/window and complete the FIET-937 production observation window. Record feed health, last-frame age, reconnects, unrecoverable gaps, received/archived/invalid/sampled rows, queue saturation, journaled rows, checksum conflicts, parity mismatches, and Maker replay consumption. Any unaccounted row, conflict, parity mismatch, stale feed, or persistent journal growth blocks deployment cutover. This operational gate is not an implementation-completion requirement for the archived OpenSpec change and repository checks do not claim that it has occurred.
125+
126+
Rollback stops the upgraded broker, restores retained legacy names if necessary, and rolls back to the previous legacy-writing application version. The runbook uses renames rather than drops; canonical and legacy base data remain recoverable throughout the retention period.

0 commit comments

Comments
 (0)