Regime-aware Binance spot trading bot (BTC/ETH/SOL vs USDC), built for 24/7 unattended operation on a Raspberry Pi 4.
Primary goals, in order:
- Generate a high-quality dataset of market states and bot decisions.
- Survive (capital preservation, robust unattended operation).
- Profit — a byproduct of 1 and 2, never a reason to relax them.
See kurfurst_claude_code_prompt.md for the full spec.
Four decision layers, each pure (no I/O), so the SAME code runs in backtest, paper and live — only the executor differs:
- Regime Engine (4h): TREND / RANGE / CHAOS. CHAOS = no trading.
- Signal Engine (1h): regime selects the playbook (trend pullback / range edge).
- Veto Layer: abnormal candle, spread, BTC correlation, min RR 1.5 after fees.
- Pressure System: LIVE → REDUCED → SHADOW, kill-switch at −20%.
The event loop (kurfurst/core/event_loop.py) wires these behind the live
data feed, executor, state store and observer. The backtester
(kurfurst/backtest/engine.py) drives the identical decision functions over
history.
python -m venv .venv
.venv\Scripts\pip install -e .[dev]
.venv\Scripts\ruff check .
.venv\Scripts\pytest
# Backtest + Monte Carlo over a parquet of 1h candles
kurfurst backtest --config config/v0.1.yaml --parquet BTC/USDC=tests/fixtures/btc_1h.parquet
# Analytics report (P&L, per-regime, veto counterfactuals, feature importance)
kurfurst report --config config/v0.1.yaml --parquet BTC/USDC=tests/fixtures/btc_1h.parquet
# Run the live loop — PAPER is the default
kurfurst run --config config/v0.1.yaml --mode paper
# Watch it: local read-only dashboard at http://127.0.0.1:8000
kurfurst dashboard --config config/v0.1.yaml
Set KURFURST_DB_PATH in .env so run and dashboard point at the same
SQLite file. The dashboard opens the DB read-only and never disturbs the bot.
- paper (default): real Binance mainnet market data (read-only keys), fills simulated internally with the same fill model as the backtester. No orders.
- testnet: places real orders on the Binance sandbox to verify order mechanics only. Never use testnet prices for paper data — they are not real.
- live: real money. Refuses to start unless started with
--mode live(or--live) ANDKURFURST_LIVE_CONFIRMin.envequals exactlyI_UNDERSTAND_THIS_TRADES_REAL_MONEY, AND the clock is verified synced.
DB and logs MUST live on an external USB SSD (SD-card corruption is a known failure mode). chrony (NTP) is mandatory — the bot refuses to go live with an unverified clock.
sudo ./deploy/deploy.sh # installs to /opt/kurfurst, paper mode, systemd
# edit /opt/kurfurst/.env (KURFURST_DB_PATH -> /mnt/ssd/kurfurst/kurfurst.db)
sudo systemctl start kurfurst
journalctl -u kurfurst -f
systemd restarts on crash (Restart=always); on restart the bot reconciles
its state against the exchange (it never trusts local state alone).
systemctl stop kurfurst (SIGTERM) — the loop finishes its cycle, persists
state, and exits. Restart re-reconciles.
On startup the bot loads its persisted position/pending order, then (live/ testnet) queries the exchange and reconciles:
- holding confirmed → resume (stops, breakeven/trailing state preserved);
- no holding → position treated as closed externally;
- unexpected holding with no record → HALT for manual review;
- orphan orders → cancelled.
Stops are software-managed (no resting exchange stop, to keep backtest == live). A crash leaves the position briefly unprotected until restart — acceptable on a small account; the loop persists a moved stop BEFORE acting on it so a restart never resumes a looser stop than intended.
At −20% of the configured initial equity the bot liquidates, halts, and requires a manual restart. Investigate before restarting.
The bot is deterministic and never self-tunes. Learning is offline and versioned:
- Collect — let paper trading run (target ≥ 100 closed trades; at this frequency, several months).
- Export & review —
kurfurst report --config config/vX.Y.yaml .... Read: P&L per pair/regime, veto frequency and counterfactuals (is a veto saving or costing R?), pressure timeline, and the DESCRIPTIVE-ONLY feature-importance report (ignored when n < 100 — noise). - Decide — a human proposes parameter changes. Strategy-level parameters (playbook entries, RR, SL/TP) only after ≥ 100 closed trades on the current version; veto/regime thresholds may be reviewed earlier.
- Version — copy
config/vX.Y.yaml→config/vX.(Y+1).yaml, change the parameter, add aCHANGELOG.mdentry explaining WHICH report motivated it. Every DB row is stamped with the config version, so results stay attributable and versions are comparable (config_version_comparisonin the report). - Re-run — restart the bot on the new version. Never edit a released version in place.
Only consider live after: ≥ 100 closed paper trades (ideally 200+), max
drawdown within Monte Carlo expectations, no unexplained backtest-vs-paper
divergence, and zero unhandled crash states. Then set the .env confirmation
phrase and start with --mode live.
- Kill-switch and daily limit use the loop's REALIZED equity (tracked internally, += pnl on each close — like the backtester). This avoids the live-cash-collapse bug, but a deep UNREALIZED drawdown still halts only when the position closes; there is no intra-trade mark-to-market (auditor E3).
- Order size is not rounded to the exchange lot/precision step; the loop skips
sub-
min_notional_quoteorders but does not snap to the lot size, so a live order could still be rejected for LOT_SIZE (auditor E1). - A partial market-sell on exit keeps the residual position and retries next cycle; it does not aggressively re-sell within the same cycle.