Skip to content

Architecture

Gordon v7 is a five-service Rust split with six library crates published to an internal kellnr registry, one schema migrator, one Next.js console, and one Python research lab. Services communicate over a single NATS JetStream stream and a shared PostgreSQL instance. No two services share a process, a binary, or a set of credentials.

Runtime stack

Services and crates

Runtime services

ServicePortResponsibility
gordon-data8081Sole writer of market_data.*. WebSocket ingest from Binance/alt.me/Deribit/FRED. REST /klines, /warmup, /healthz.
gordon-risk8082Five circuit breakers at 30 Hz. Halt-latch. Emergency flatten. Pause commands.
gordon-manager8083Control plane + console BFF. Bot lifecycle. Backtests. WS fan-out to browser.
gordon-bot8084Pure strategy engine. No exchange keys. One container per strategy.
gordon-executor8085Sole holder of trading-capable Binance keys. Consumes intents, reconciles, emergency-flattens.
gordon-migrateOne-shot schema migrator. Owns migrations/. Applies then exits 0 or 1.

Library crates (kellnr)

CrateVersionRole
gordon-kernel2.xError taxonomy (GordonError, ErrorKind, ErrorCode) + TraceId. Narrow leaf, frozen-ish.
gordon-domain1.xCross-service business types + ExecutionModel trait. Churns with business changes.
gordon-protocol7.xNATS bus subjects + payload structs (KlineEvent, OrderIntentEvent, RiskCommandEvent, BreakerEvent). Additive-only.
gordon-platform0.4.xRuntime libs: HTTP client + breaker, Axum trace middleware, tracing-subscriber, pg LISTEN/NOTIFY.
gordon-strategy4.xStrategy trait, MarketContext, Signal, 14 entry strategies, overlays, BacktestExecution. Pure math, no I/O.
gordon-bus2.xPublisher/Consumer traits over NATS JetStream + Postgres outbox.
gordon-exchange5.xBinance REST + WS + rate limiter + FeeModel. Live and testnet.
gordon-test-db1.xPer-test fresh Postgres clone (template-based). Optional NATS test helper.

Other repos

RepoRole
gordon-consoleNext.js 16 operator UI. App Router. Auth.js v5 + Kanidm OIDC.
gordon-labPython research lab. Walk-forward ablation, ML experiments. Read-only DB role.
gordon-docsThis VitePress site. LAN-only on srv-apps.

Split rationale

Gordon v6 ran as two monolithic repos (gordon-trading + gordon-ui). All concerns were co-located in a single process. Three concrete problems drove the v7 split:

1. Blast radius. A panicking strategy engine killed data ingest and order reconciliation in the same process. There was no way to restart only the broken component.

2. Key co-mingling. Trading-capable Binance API keys were loaded by the same process that fetched market data. Even read-only ingest carried write-capable credentials.

3. Deployment coupling. A backtest configuration change required a full restart of the live trading loop.

The five-service split is the coarsest decomposition that achieves independent deployment, key isolation, and blast-radius containment. A two-service split (data + trading) would still combine strategy, risk, and execution in one process. A microservices approach (10+ services) adds coordination overhead without solving the stated problems.

Consequences

  • gordon-executor holds the only trading-capable keys. No other service can submit orders even if compromised.
  • gordon-bot crash leaves gordon-executor running; open positions are protected.
  • Backtest runs (gordon-manager) do not compete with live strategy evaluation (gordon-bot) for CPU.
  • gordon-data can be restarted independently without pausing risk monitoring.
  • Cross-service IPC introduces network hops. For the NATS-based hot paths this is acceptable. For browser fan-out, gordon-manager is the aggregation point.

Database

Single PostgreSQL instance (GORDON_DATABASE_URL). search_path = trading, market_data, public.

Least-privilege roles per service (enforced by gordon-migrate migrations):

RoleGrants
gordon_data_writerINSERT on market_data.*
gordon_executorINSERT/UPDATE on trading.orders, trading.trades, trading.order_events, trading.fill_events
gordon_riskINSERT on risk_events, risk_audit_log; column-level UPDATE on bot_configs.desired_state only
gordon_managerINSERT on bot_configs, runs, equity_points; reads via named views
gordon_botINSERT on order_intents; advisory-lock-based lease via bot_leases
gordon_lab_readerSELECT only — INSERT/UPDATE/DELETE revoked

Direct SELECT on underlying market_data.* tables is revoked for runtime services. Readers go through named views (v_klines_reader, v_metrics_reader, v_macro_reader, v_funding_rates_reader, v_open_interest_reader).

Infrastructure

HostRole
srv-appsPi5, 2 TB SSD, ARM64. Runtime host for the full v7 stack + Postgres + nginx + Grafana + Loki + Prometheus + kellnr + gordon-docs.
srv-corePi5. Pi-hole DNS, Bitcoin node. CI runner (ARM64) + restic backup destination for trading.*.
srv-computeX64 CI runners only.

Operator-accessible surface on srv-apps: gordon-console on port 3000 only. All other services are Docker-internal.

Invariants

  • gordon-executor is the sole holder of trading-capable exchange keys.
  • gordon-data is the sole writer of market_data.*.
  • gordon-migrate is the sole owner of schema migrations.
  • gordon-lab never writes to the database (gordon_lab_reader role revokes all writes DB-side).
  • All runtime services gate on gordon-migrate completing (service_completed_successfully) before starting.
  • No service holds a direct SELECT grant on underlying market_data.* tables — named views only.
  • Event Flow — NATS stream topology and outbox pattern.
  • BFF Boundary — which traffic goes through manager and which goes direct.
  • Modules — per-service and per-crate reference pages.

Gordon — keep compounding without blowing up