This repository now has a clearer recorder → research/replay → executor flow so live trading decisions can be validated offline before they are trusted in production.
recorder— connects to Deriv, stores raw ticks plus selected metadata, and never trades.research— replays recorded ticks deterministically through the decision pipeline with no live network requirement.executor— runs the live trading loop and current trader FSM.deriv-bot— compatibility wrapper that delegates toexecutor.
Use the recorder to collect raw historical market data and audit metadata.
DERIV_API_TOKEN=... DERIV_APP_ID=... DERIV_RECORDER_SYMBOLS=R_100,R_50 cargo run --bin recorderRecorder responsibilities:
- subscribe to live market data,
- write append-only ticks into
raw_ticks, - persist selected non-price metadata into
recorder_metadata, - maintain an offline dataset suitable for replay.
Use the research binary to replay recorded ticks through the decision engine without a live WebSocket.
cargo run --bin research -- summarize
cargo run --bin research -- replay R_100 500
cargo run --bin research -- replay R_100 500 --with-execution
cargo run --bin research -- report
cargo run --bin research -- report <run_id>Replay responsibilities in this PR:
- load historical ticks deterministically from SQLite,
- run the same shared
DecisionEngineprobability / prior / regime / stake proposal flow used by live execution, - separate decision from execution by allowing signal-only mode or simulated execution mode,
- persist experiment metadata plus proposed / entered / settled replay lifecycle telemetry for later comparison,
- print baseline offline summaries for quick validation.
Use the executor for live runs after replay validation.
DERIV_API_TOKEN=... DERIV_APP_ID=... cargo run --bin executorExecutor responsibilities:
- connect to Deriv live services,
- evaluate decisions through the shared
DecisionEngine, - place trades through the existing trader FSM,
- persist decision, intent, and executed-trade lifecycle telemetry alongside execution outcomes.
Required for executor and recorder:
DERIV_API_TOKENDERIV_APP_IDDERIV_ENDPOINT— defaults towss://ws.binaryws.com/websockets/v3
DERIV_RECORDER_SYMBOLSDERIV_RECORDER_DB_PATHDERIV_RECORDER_BALANCEDERIV_RECORDER_TIMEDERIV_RECORDER_RETENTION_DAYS
DERIV_RESEARCH_DB_PATHDERIV_CONTRACT_DURATIONDERIV_MIN_STAKEDERIV_INITIAL_BALANCEDERIV_MAX_POSITIONSDERIV_MAX_DAILY_LOSSDERIV_COOLDOWN_MSDERIV_MAX_CONSEC_LOSSESDERIV_MODEL_PATHDERIV_ALLOW_MODEL_FALLBACKDERIV_RESEARCH_STRATEGY_VERSIONDERIV_RESEARCH_PRIOR_VERSION
DERIV_SYMBOLDERIV_ACCOUNT_TYPEDRY_RUNDERIV_INITIAL_BALANCEDERIV_STRATEGYDERIV_CONTRACT_DURATIONDERIV_DURATION_UNITDERIV_STAKEDERIV_MIN_STAKEDERIV_MODEL_PATHDERIV_ALLOW_MODEL_FALLBACKDERIV_MAX_POSITIONSDERIV_MAX_DAILY_LOSSDERIV_COOLDOWN_MSDERIV_MAX_CONSEC_LOSSESDERIV_STOP_LOSS_PCTDERIV_TELEMETRY_DB_PATHDERIV_TELEMETRY_BIND
The SQLite layer now keeps experiment-oriented data in a more normalized layout.
raw_ticks— append-only recorded tick history.recorder_metadata— balance/time or other recorder-side payloads.experiment_runs— run metadata includingrun_id, binary type, model version, strategy version, prior version, config fingerprint, and run timestamp.decision_events— one row per decision snapshot with regime, model metadata, probabilities, proposed/executed stake, and rejection reason.trade_intents— one execution-attempt record derived from a decision, including signal-only, rejection, submission, failure, or execution state.executed_trades— realized or simulated execution records for trades that actually opened, plus exit reason and PnL.
alpha_signalsview — exposes decision probabilities in the old shape.decision_snapshotsview — exposes decision records in the old shape.
This design keeps raw ticks and experiment metadata normalized instead of forcing every concept into one sparse table.
The project now treats decision generation and execution telemetry as separate layers, with a single lifecycle per logical opportunity:
-
one evaluated opportunity should produce one primary
decision_eventsrow, -
that decision may produce one
trade_intentsrow describing the execution attempt semantics, -
and only an actual open should create an
executed_tradesrow. -
decision_events.decisionhold— no actionable entry was produced or the entry was blocked.signal— the shared decision engine produced an actionable entry intent.
-
trade_intents.intent_statussignal_only— replay or audit-only signal with no execution attempt.rejected— an intent existed, but risk / timing / lifecycle checks blocked it.submitted— live execution attempted to route the intent through the trader FSM.execution_failed— a live execution attempt was made but no trade opened.executed— a trade was actually opened.dry_run_executed— executor-owned dry-run opened a synthetic simulated trade lifecycle without routing to Deriv.
-
executed_trades.statusopen— trade is currently open.settled— live trade settled naturally.closed_early— live trade was sold before expiry.aborted— live execution attempt was interrupted before a clean close.simulated_settled— replay execution completed through the simulated lifecycle.dry_run_open— executor-owned dry-run opened a simulated trade row.dry_run_settled— executor-owned dry-run closed that simulated trade row.
- Replay, signal-only: one
decision_eventsrow withdecision=signal, onetrade_intentsrow withintent_status=signal_only, and noexecuted_tradesrow. - Live, execution failed: one
decision_eventsrow withdecision=signal, onetrade_intentsrow withintent_status=execution_failed, and noexecuted_tradesrow because nothing opened. - Live, trade opened successfully: one
decision_eventsrow withdecision=signal, onetrade_intentsrow that movessubmitted -> executed, and oneexecuted_tradesrow that movesopen -> settled|closed_early|aborted. - Replay with simulated execution: one
decision_eventsrow withdecision=signal, onetrade_intentsrow withintent_status=executed, and oneexecuted_tradesrow that finishes assimulated_settled. - Executor dry-run: one
decision_eventsrow withdecision=signal, onetrade_intentsrow withintent_status=dry_run_executed, and oneexecuted_tradesrow that movesdry_run_open -> dry_run_settledusing a syntheticcontract_idprefixed withdry_run:.
benchmark_signal is now a normalized comparator derived from the same shared decision contract in both replay and live paths:
CALLwhen the shared decision logic points long,PUTwhen it points short,HOLDwhen the shared decision logic rejects entry.
It is no longer allowed to drift between a legacy live-only strategy output and a replay-only placeholder.
Reports distinguish between:
- decisions — rows in
decision_events, - signal intents —
trade_intentsrows withintent_status = signal_only, - trades — rows in
executed_tradesonly.
So a signal does not count as a trade in reports.
Win/loss reporting is intentionally conservative:
- only rows with realized non-
NULLpnlcount toward wins or losses, opentrades are reported separately,- non-open rows with
NULL pnlare reported as unresolved, abortedrows withNULL pnlare also broken out explicitly asaborted_without_pnl.
- Replay uses the shared
DecisionEnginein replay-owned lifecycle mode. In execution-enabled replay, the engine itself opens simulated trades, closes them at contract expiry, updates its internalRiskGate, and recordssimulated_settledoutcomes. - Live execution uses the same decision generation path in live-synchronized mode. In this mode the executor and trader FSM remain the source of truth for order lifecycle, while the engine only keeps a synchronized risk view.
DRY_RUN=1now also uses live-synchronized mode, but the executor owns the simulation lifecycle instead of Deriv. The trader still opens a realistic in-memory contract shape, the executor persists anexecuted_tradesrow withdry_run_open, then immediately settles it asdry_run_settledwithexit_reason=dry_run_simulated_expiry.- The executor now calls explicit synchronization hooks on the engine:
notify_live_balance(balance)whenever Deriv sends an updated account balance,notify_live_trade_opened(...)after the trader FSM has a real open contract,notify_live_trade_closed(...)after settlement or early close with realized PnL,notify_live_trade_aborted(...)when a disconnect or interrupted execution invalidates the open trade.
- The same open/close synchronization hooks are also used for executor dry-run, so Kelly sizing and open-position gating stay coherent across consecutive dry-run trades.
- This prevents live drift: Kelly sizing reads the latest synchronized live balance, open-position gating matches the trader FSM, and realized PnL is only applied once when the live trade truly closes.
- On reconnect, the executor clears aborted live state through the same synchronization path so the engine does not keep a phantom open position.
- Expect one
decision_eventsrow per actionable dry-run opportunity. - Expect one
trade_intentsrow withintent_status = dry_run_executed. - Expect one
executed_tradesrow whose lifecycle ends atstatus = dry_run_settled. - Dry-run rows are distinct from replay rows because replay uses
status = simulated_settled. - Dry-run rows are distinct from real executor rows because they use
dry_run_*statuses and syntheticcontract_idvalues prefixed withdry_run:.
research report prints practical baseline metrics including:
- decision count,
- signal-intent count,
- executed trade count,
- average edge,
- PnL summary,
- win/loss summary,
- regime distribution,
- rejection-reason counts.
- schema round-trip tests cover run metadata, decisions, intents, and executions,
- replay/report tests cover command parsing and report aggregation,
- lifecycle tests reject impossible combinations such as
signal_onlyintents withexecuted_trades, - replay fixtures can be created by recording a short
raw_tickssequence and replaying it throughresearch.
- replay currently uses a practical simulated execution outcome rather than a full contract lifecycle model,
- the live executor still uses the existing trader FSM and is not redesigned in this PR,
- no dashboard or notebook ecosystem is added,
- ONNX-backed builds can still be blocked in restricted environments if
ortcannot download runtime binaries.
cargo fmtcargo check --bins --tests