This is a systematic crypto paper trading system that evaluates 10 independent strategies against two perpetual futures contracts (BTC-PERP and ETH-PERP) on Hyperliquid. The system ingests real-time market data from four venues, normalizes it through a feed aggregation layer, evaluates strategy logic on every candle close, and executes paper trades through a simulated order pipeline that mirrors the live execution path.
The architecture is a three-tier monolith: data ingestion, strategy evaluation, and execution. Everything runs as a single async Python process inside Docker. Persistence is SQLite in WAL mode -- a deliberate choice for a single-writer workload where operational simplicity outweighs the concurrency advantages of PostgreSQL. The dashboard is a separate read-only container that queries the same database file.
Four feed adapters share a common DataFeed protocol with push-based candle callbacks.
HyperliquidFeed is the primary source: persistent WebSocket for live candles and
trade events, REST for historical backfill at startup. BinanceFeed provides
secondary candle data, open interest snapshots (5-minute polling), and funding rates.
CoinbaseFeed polls spot prices every 30 seconds for perp/spot basis calculations.
DeribitFeed is implemented but not currently registered -- it would supply options
chain data (IV, Greeks, GEX) for QuantFlow.
The FeedAggregator deduplicates candles by (symbol, timeframe, open_time_ms)
using an LRU cache of 10,000 entries. When the same candle arrives from multiple feeds, Hyperliquid's
version is canonical because it is the execution venue. Deduplicated candles flow into the
CandleManager, which buffers them and triggers close callbacks to the strategy registry.
All 10 strategies implement a shared StrategyProtocol and are registered in a
StrategyRegistry that fans out candle close events without arbitration. Each strategy
evaluates independently and emits decisions (pass/no-trade) or signals (entry/stop/target with
confidence). The registry does not resolve conflicts between strategies -- if two strategies signal
opposing directions on the same symbol, both are processed.
Price Action Strategies:
Flow-Based Strategies:
Statistical Arbitrage:
Market Microstructure:
Volatility / Quantitative:
Two executor implementations share the same interface. PaperExecutor simulates fills
with configurable slippage (5 bps entry, 10 bps stop). HyperliquidLiveExecutor is
fully implemented with IOC orders, SDK factory injection for testability, and a 60-second position
refresh cadence. The live executor defaults to testnet mode and requires explicit configuration to
target mainnet.
Risk management: 1% of portfolio per trade, minimum 2:1 reward-to-risk ratio enforced, maximum 3 concurrent positions (configurable), 10% daily loss limit, 5% session drawdown pause, and a persistent kill switch file that halts all new order submissions. Circuit breakers use 5-minute temporary pauses for transient issues (latency spikes, order failures) and escalate to the persistent kill switch only for balance collapse or repeated reconciliation failures.
Reconciliation is alert-only in v1: it detects phantom positions, orphans, side mismatches, and size mismatches between local state and exchange-reported positions, but never auto-corrects. Alerts are batched to avoid spam.
The trading container exposes /health, /status, and /metrics
on port 8081. Health state is written to data/health.json every 30 seconds by the
HealthMonitor. The DataHealthChecker evaluates per-strategy data
availability and reports healthy/warming/degraded/starved status. Telegram health alerting fires
after 5 continuous unhealthy minutes when TELEGRAM_BOT_TOKEN and
TELEGRAM_CHAT_ID are configured. The dashboard reads all state from SQLite and the
health file in read-only mode.
Pre-live validation: scripts/pre_live_check.py runs 20 checks covering uptime, signal
volume, strategy registration, risk parameter bounds, feed freshness, circuit breaker presence,
error rates, and data health. All 20 must pass before live trading is discussed.
Every significant decision made during construction is recorded here with full rationale. Decisions
are pulled from DECISION-LOG.md. Click any decision to expand its reasoning,
alternatives considered, and current status.
Decision: Use SQLite (WAL mode) as the sole database.
Rationale: Single-writer workload (one trading process), all reads are local, no concurrent access. SQLite WAL provides good read concurrency for the dashboard. Zero operational overhead -- no separate database service, no connection pooling, no schema migrations tooling. The entire DB is a single file that can be copied for backup.
Alternatives rejected: PostgreSQL (requires separate service, connection management, deployment complexity for a single-user system). DuckDB (good for analytics but overkill for this OLTP workload).
Status: Still stands. Revisit if adding a second writer process, or if DB size exceeds ~10GB.
Decision: DataFeed Protocol uses register_candle_callback(cb) pattern (push-based)
rather than async generators or queues.
Rationale: Matches existing HyperliquidFeed's callback pattern, minimizing refactoring. Push-based is natural for WebSocket feeds where data arrives asynchronously.
Alternatives rejected: Async generator (cleaner consumer code but requires buffering), asyncio.Queue (explicit backpressure but adds complexity and potential deadlocks).
Status: Still stands. Revisit if backpressure becomes an issue with many feeds.
Decision: FeedAggregator deduplicates by (symbol, tf, open_time_ms). When same candle arrives from multiple feeds, Hyperliquid's version is canonical.
Rationale: Hyperliquid is the execution venue -- its prices determine fills, stops, and P&L. Using HL candle data ensures strategy signals are generated against the same price series that orders execute on, avoiding basis risk between signal and execution.
Alternatives rejected: VWAP across all sources (introduces basis risk with execution venue), latest-arrived wins (non-deterministic), configurable per-symbol (over-engineered for 2 symbols).
Status: Still stands. Revisit if HL has significantly more downtime than Binance.
Decision: Use an LRU-ish dedup cache sized for 10,000 candle keys.
Rationale: Enough to hold weeks of candle identity for the current symbol/timeframe set without unbounded growth.
Alternatives rejected: Unlimited cache (memory risk), much smaller rolling window.
Status: Still stands. Revisit if symbol coverage expands materially.
Decision: Poll Coinbase spot every 30 seconds and synthesize candles.
Rationale: Basis calculations do not need sub-second spot updates; REST is simpler and more failure-tolerant.
Status: Still stands. Revisit if spot-latency-sensitive logic appears.
Decision: Implement Monday Range as the first (and only MVP) Trader Mayne setup.
Rationale: Most algorithmically precise setup in the spec -- clear rules for range marking (Monday H/L), clear entry trigger (15m wick sweep + close back inside), clear stop (wick extreme), clear target (Monday midpoint). All other setups (OB, Breaker, SFP) require additional zone detection infrastructure that didn't exist yet.
Alternatives rejected: OB Retest (most common setup, but OB identification algorithm has ambiguities requiring founder clarification), SFP (simpler signal but relies on swing detection quality being validated first).
Status: Still stands. OB and Breaker retest have since been implemented. Revisit if Monday Range consistently produces zero signals after 2+ weeks.
Decision: Use different swing lookback values per timeframe: Weekly=3, 3-Day=3, Daily=5.
Rationale: The spec says "lookback=5" but Weekly candles with lookback=5 require 10+ weeks of surrounding candles to identify a swing. Lookback=3 for W/3D captures structural turns while remaining practical. Daily with lookback=5 provides better noise filtering on the most granular HTF.
Status: Still stands. Both symbols currently show NEUTRAL bias -- could be market conditions or parameters too strict. Needs time to assess.
Decision: Use M=1.0 for position sizing instead of the performance-adaptive session multiplier.
Rationale: The session multiplier formula M = (1 + R_s) ^ (1 + g(t)) amplifies
winners and dampens losers within a session. Without backtesting data to calibrate g(t), implementing
it risks outsized positions during favorable streaks that haven't been validated.
Status: Still stands. Revisit after 30+ days of paper data show consistent positive sessions where larger sizing would have improved returns.
D006 -- Order Block: Last opposing candle before a displacement candle (body >= 2x average body of last 10 candles). Zone = candle body (open to close). Standard ICT definition. 2x threshold filters normal candles.
D007 -- Breaker: OB becomes a Breaker when any single candle closes through the zone. Polarity flips. Single close-through is the strictest definition of failure. Binary, no tunable percentage parameter.
D008 -- SFP: Wick extends beyond swing level AND candle closes back inside, both on the same candle. Next candle must confirm by closing in the SFP direction. Single-candle requirement ensures genuine rejection.
Status: All still stand. Needs signal review against source material before live trading.
Decision: When SFP occurs at an OB/Breaker zone, confidence is multiplied by 1.5 (capped at 1.0).
Rationale: Confluence setups are higher conviction because multiple independent signals align. A 1.5x multiplier increases priority without saturating confidence at 1.0 too easily.
Alternatives rejected: Additive boost +0.2 (less scalable), 2x multiplier (too aggressive, most confluence setups would hit 1.0 cap), no boost (fails to distinguish single-signal from multi-signal).
Status: Still stands. No confluence setups have fired yet -- TraderMayne bias is NEUTRAL.
D016: Large reversal wick defined as at least twice the candle body. Common discretionary threshold, deterministic enough for MVP.
D017: Cascade exhaustion approximated with large wick plus elevated volume instead of direct exchange liquidation feed. Hyperliquid does not expose liquidation events in the current pipeline.
Status: Both still stand. WickFlow is generating signals -- review whether signal quality matches the intended setup logic.
D020: Three pairs: BTC/ETH, BTC/SOL, ETH/SOL. High-liquidity pairs for cleaner relative-value behavior.
D021: Entry at +/-2 standard deviations. Standard, conservative threshold that limits overtrading.
Compromise: SOL-PERP is not in the configured symbol list. SOL pairs may be data-starved. This is a known gap -- PairFlow may only reliably operate on BTC/ETH unless SOL is added to the feed config.
Status: Still stands with the SOL data caveat.
Decision: Simplify footprint analysis to candle-level delta and related heuristics instead of full price-level tick ladder.
Rationale: Captures most of the intended signal value without full tick ladder complexity.
Status: Moot currently -- strategy is starved because 1m/5m candles are not configured. Needs decision on whether to add microstructure timeframes.
Decision: Registered strategies evaluate independently with no priority/conflict resolver.
Rationale: Keeps the architecture simple and faithful to per-strategy autonomy. If two strategies signal opposing directions on the same symbol, both are processed.
Status: Still stands. Needs revisiting before live -- conflicting live signals on the same symbol could create net-zero positions or excessive exposure.
Decision: Single Docker service for the trading engine. No microservice decomposition.
Rationale: 2 symbols x 5 timeframes x 10 strategies = ~100 evaluations per candle close, completing in <100ms on a single core. CPU is not the bottleneck. Splitting into microservices would require inter-service communication (gRPC, Redis pub/sub) adding latency and operational complexity for zero benefit at this scale.
Status: Still stands. Revisit if a single strategy or data feed consumes >50% CPU consistently.
Decision: Enforce a minimum 30-day validation window before go-live discussion.
Rationale: Need exposure to multiple market conditions, not a lucky streak. 30 days provides weekday/weekend variation, likely at least one volatility event, and enough signals across strategies to assess whether the system behaves as designed.
Status: Still stands. Currently at day 1 of 30.
D033: Live executor treats connections as testnet-first unless explicitly overridden. Safe default.
D037: Dry-run mode is opt-in via config, not implicit on every live execute. Making every live call dry-run by default would silently flatten legitimate live positions.
Status: Still stands. The live-mode bootstrap flow may later get its own state machine where dry-run becomes a mandatory preflight stage.
D035: Pause trading after 5% session drawdown (measured from session-start equity).
D036: Cap simultaneous live exposure at 3 positions. Conservative early-live posture.
D046: Tiered escalation: 5-minute temporary pauses for transient issues (latency, order failures), persistent kill switch only for balance collapse or repeated reconciliation failures.
Compromise: Session-start drawdown measurement is simpler than rolling-peak measurement but more permissive after intraday gains. Accepted for MVP.
Status: Still stands. Needs founder review -- are 5%/3-position/50% kill switch the right parameters for live?
Decision: First live cut trades BTC-PERP only with reduced position limits.
Rationale: Simplest, most liquid starting condition for operational validation.
Status: Still stands. Revisit once early live performance is stable.
D042: Keep HyperliquidLiveExecutor.execute() synchronous like PaperExecutor, but
inject exchange/info client factories for testability.
D043: get_open_positions() returns local cached positions with 60-second remote poll.
Prevents freshly opened local position from being overwritten by immediate empty remote response.
D044: hyperliquid-python-sdk and eth-account are pinned in
requirements.txt but only imported when no injected factories are supplied.
Status: All still stand. D042/D043 may need revisiting if partial fills or websocket-driven execution updates are needed.
Decision: V1 reconciler compares only OPEN positions, groups by symbol, classifies drift as phantom/orphan/side-mismatch/size-mismatch. Emits batched alerts but never mutates state.
Rationale: Silent auto-repair can make a bad situation worse. Alert-only lets operators decide.
Status: Still stands. Revisit when reconciliation logic is deeply trusted and observable.
D012: Binance is an unauthenticated market-data feed only. No credential sprawl since we never execute on Binance.
D014: Binance was opt-in behind config initially. Now enabled by default after multi-feed path matured.
Status: D012 still stands. D014 superseded -- Binance is now enabled by default.
Decision: Use Deribit only for options data, never for execution.
Rationale: Options inputs enrich QuantFlow's regime logic without introducing another execution venue.
Status: Still stands. But DeribitFeed is not registered. Code is complete, needs wiring in main.py. Decision needed on whether to enable it.
Decision: Serve health/status from the main process on a secondary port (8081).
Rationale: Fits the MVP monolith. Avoids another container.
Status: Still stands. Revisit if health checks affect latency or deployment isolation.
Decision: Keep the trading repo private. Contains sensitive strategy and operational logic.
Status: Still stands.
D048: scripts/init_github.sh always creates local git history even when remote
creation is unavailable. Local version control improves recoverability immediately.
D049: Cold-start test (scripts/cold_start_test.sh) uses an exit trap to restore
the original database and WAL/SHM files after validation. Running against the live paper-trading environment
means the test must be non-destructive.
Status: Both still stand.
D041: Health alerting activates only when bot token and chat ID are present in environment. Avoids hard runtime dependency for paper-mode setups.
D050: Telegram health monitor waits for 5 continuous unhealthy minutes before alerting, then rate-limits further alerts. Prevents spam on transient startup/feed blips.
Status: Still stands. Revisit for live mode -- alerting should be a required guardrail, not optional.
Decision: Keep /status limited to uptime, strategy count, open positions, last signal timestamp, per-feed connectivity, and execution mode. /health already exposes the full machine-readable health document.
Status: Still stands.
Decision: Full deployment architecture, DB/table inventory, runbooks, and troubleshooting live
in docs/INFRASTRUCTURE.md. README is a concise repo entrypoint.
Status: Still stands. 1,187-line infra document is the authoritative operational reference.
The core data pipeline is operational. HyperliquidFeed WebSocket is connected, candles are flowing for all configured timeframes (W, D, 4H, 15m), and the trade stream is active with 6,000 trades ingested. Binance is providing OI snapshots (40 received, 12-second freshness) and funding rates. Spot prices from Coinbase and Binance's periodic fetch are flowing (192 rows, 16-second freshness). The database holds 2,168 candles across 11 tables, 4.15 MB, no corruption.
Three strategies are actively producing signals: ValentiniFlow (6 signals today, orderflow momentum), WickFlow (4 signals, wick reversals and liquidity pool sweeps), and SkewFlow (4 signals, perp premium detection). All three have healthy data feeds and are evaluating on every candle close. TraderMayne, OIFlow, and PairFlow are evaluating but have not produced signals yet -- TraderMayne because HTF bias is NEUTRAL, OIFlow and PairFlow because their specific conditions (OI divergence, spread z-score extremes) have not appeared.
QuantFlow has candle and RV data but is missing OI snapshots in its own pipeline
(the periodic OI fetch feeds oi_flow.ingest_snapshot() but not
quant_flow.ingest_oi_snapshot() -- a wiring bug, not a design issue). Options data is
completely absent because DeribitFeed is not registered. QuantFlow can only fire RV-regime signals,
not full confluence signals.
TraderMayne HTF bias is NEUTRAL for both symbols. This is not a bug -- the market simply hasn't produced clear swing structure (HH/HL or LH/LL) on the Weekly/3-Day/Daily timeframes yet. Monday Range signals are gated behind directional bias. This may resolve within days or weeks depending on market conditions. OB retest and Breaker retest setups can still fire independently of bias if conditions align.
Four strategies cannot produce signals with the current configuration. Each requires a specific decision about whether to expand the system's data surface:
main.py. Enabling it requires confirming
that Deribit's public API provides sufficient options data without an account, or setting up a
Deribit account for authenticated access.
There is also a wiring bug: quant_flow.ingest_oi_snapshot() is never called from
main.py's periodic OI fetch loop, even though OI snapshots are flowing to
oi_flow. This is a code fix, not a design decision.
What needs to happen before TASK-D6 (go-live) can be unblocked, in order: