Skip to content

BrandonKKY/quantbot

Repository files navigation

QuantBot

A C++17 algorithmic trading bot for Alpaca that runs the same engine in live/paper trading and in backtesting — so what you measure offline is exactly what trades.

Language: C++17 | Platform: Windows / MSVC | Broker: Alpaca Paper/Live

Paper-trading research project. It trades Alpaca's paper environment by default; live is a one-flag switch but has never been funded. No claim of live profitability is made anywhere in this repo.


What This Is

QuantBot is an automated trading bot built around a single shared decision pipeline. It connects to Alpaca's v2 REST API, fetches daily bars, and trades a long-only EMA trend overlay (and an optional dual-momentum rotation mode) across a small ETF universe. Positions are sized by ATR-based volatility risk, protected by server-side bracket orders (stop-loss + take-profit that survive a process crash), and governed by three independent circuit breakers. The same TradingEngine that places live orders also drives the offline simulator, so there is no "backtest version" of the logic that can silently drift from production.

What makes it technically serious is the research infrastructure around that engine. Strategies are validated with walk-forward analysis (rolling 5-year in-sample / 1-year out-of-sample windows), parameter sweeps are Bonferroni-corrected for multiple testing, and every refactor must reproduce a bit-exact regression gate (471,874.87 final equity / 328 trades) before it can merge. Five separate alpha extensions were built, measured honestly out-of-sample, and rejected when they failed — the research history is documented in full rather than hidden. A background reconciliation daemon, a partial-trim fill model unit-tested to the penny, and O(1) incremental indicators verified bit-identical against batch references round out the production-grade engineering.


Architecture

                                  config.json
                                       │  (symbols, risk limits, strategy params)
                                       ▼
                                    Config ──────────────┐
                                       │                 │ (validated parameters)
                                       ▼                 ▼
   ┌─────────────┐   bar data   ┌──────────────┐   sizing+breakers   ┌──────────────┐
   │  Strategy   │ ◄─────────── │ TradingEngine │ ◄─────────────────► │ RiskManager  │
   │ (EMA/filter │              │  (one tick =  │   (RiskDecision)    │ 3 safety     │
   │  pipeline)  │ ───────────► │  decision     │                     │ layers       │
   └─────────────┘   signals    │  cycle)       │                     └──────────────┘
                     (Signal)    └──────┬───────┘
                                        │ orders / position queries
                                        ▼
                                ┌────────────────────┐
                                │  ExecutionBroker    │  (abstract interface)
                                └───────┬────────────┘
                       ┌────────────────┴─────────────────┐
                       ▼                                   ▼
              ┌─────────────────┐                 ┌──────────────────┐
              │ AlpacaBroker    │  HTTPS/REST     │ BacktestBroker   │  replayed bars
              │ (Paper / Live)  │ ◄────────────►  │ (simulation)     │  (zero network)
              └────────┬────────┘   orders/fills  └──────────────────┘
                       │ positions + working orders
                       ▼
              ┌─────────────────┐
              │   RiskDaemon    │  background thread: reconciles positions vs.
              │ (reconciler)    │  server-side exit orders every ~30 min
              └─────────────────┘

   Logger ◄──── (timestamped events from every component above: ticks, signals,
                 orders, fills, breaker trips, daemon repairs)

Arrow legend: config.json feeds validated parameters into the engine, strategy, and risk manager. Strategy receives bar data and returns a Signal. RiskManager receives the signal + equity and returns a sized RiskDecision (or a rejection). TradingEngine sends Orders to whichever ExecutionBroker is active and queries Positions back. The RiskDaemon independently reads positions + working orders from the broker and repairs missing protection. The Logger is fed by everything.


Key Technical Features

  • Single engine, dual runtime — the live bot and the backtester share TradingEngine::tick() verbatim. Why it matters: eliminates the most common source of live-vs-backtest divergence; there is no simplified simulation to drift from production.
  • O(1) incremental indicators with parity verification — EMA / ATR / ADX / RSI / SMA maintained in constant time per bar. Why: the indverify harness proves them bit-identical to the batch reference (max abs diff 0.0), so the speedup carries zero behavioral risk.
  • Walk-forward validation — rolling 5-year in-sample / 1-year out-of-sample windows, parameters re-selected per block, OOS segments stitched into one curve. Why: it's the honest test for whether an edge survives on unseen data, not just a flattering full-history fit.
  • Bonferroni-corrected parameter sweep — N randomized variants run in parallel; the significance hurdle rises with N. Why: prevents "the luckiest of 200 variants" from being mistaken for a real edge — multiple-testing done correctly.
  • Bit-exact regression gate471,874.87 / 328 trades on the 4-ETF config (and 490,511,624,146.48 / 1,257 on the 100-name rotation gate). Why: any change that moves these numbers when feature flags are off is a bug, caught immediately.
  • RiskDaemon — background worker that renews GTC bracket legs before Alpaca's 90-day expiry, re-brackets orphaned positions, and enforces stops the market gapped through. Why: a trend system holds winners longer than 90 days; without this, protection silently expires.
  • ATR-based sizing + three-layer circuit breakers — per-trade volatility sizing, daily-loss lockout, and a portfolio-drawdown kill. Why: risk is bounded at three independent scopes, and the manager can only ever reduce what the strategy asked for.
  • Partial-trim fill model, unit-tested to the penny — supports trims, shorts, and short→long flips with sign-correct P&L. Why: parttest and shorttest assert cash equals summed P&L exactly, so the accounting is provably correct.

Research Results (Honest)

Five alpha extensions were built and validated out-of-sample. All five were rejected. The baseline (Variant H) remains the best risk-adjusted configuration tested.

Experiment Out-of-Sample Result Max Drawdown Verdict
Baseline — Variant H (reference) WF per-trade Sharpe 0.60 (bar 0.63) 22.6% Best tested
Volatility targeting WF Sharpe 0.60 (bar) — no lift vs 0.63 20.2% Rejected
Synthetic covered calls (strike-capped) Net option alpha ≈ $116k = 8% of gross; lift is compounding-inflated + pre-cost 19.3% Rejected
Managed-futures ETF proxies Blended Sharpe 0.70 ≈ 0.69 equity-only; correlation 0.33 (> 0.20 bar) ~15–16% Rejected
ML regime classifier Clean-holdout per-trade 0.88 < 0.92 baseline; tuned weights overfit 8% (holdout) Rejected
Universe expansion (20 ETFs) WF per-trade 0.47 < 0.60 baseline 40.2% Rejected

Per-trade Sharpe was introduced during the regime experiment; the three earlier experiments are reported on the metric that actually drove their verdict (full figures in PROGRESS_REPORT.md and RESEARCH_LOG.md).

Rejecting a hypothesis with clean out-of-sample data is the point of the process, not a failure of the system. Each "no" was reached by the same honest gauntlet — walk-forward, multiple-testing correction, held-out data — and the consistency of those rejections is itself the most important result: it maps the real ceiling of a long-only ETF strategy instead of curve-fitting past it.


Variant H — Current Best Configuration

Property Value
Universe SPY, QQQ, IWM, GLD
Entry mode EMA trend (long while EMA20 > EMA50 and price > 200-day EMA)
Walk-forward per-trade Sharpe 0.60
Walk-forward max drawdown 22.6%
Bar-level Sharpe 0.63
Starting capital $100,000 (paper)
Data range tested 1993 – 2026

Honest framing: this is roughly a 6%/year, ~20%-drawdown system. It does not beat buy-and-hold SPY on raw return — its value proposition is comparable returns at a fraction of the drawdown, fully automated and risk-bounded.


Project Structure

quant-bot/
├── include/                      # Header-only modules (the engine lives here)
│   ├── Types.hpp                 # Core data types: Bar, BarView, Order, Position, Signal
│   ├── Config.hpp                # config.json loader + validation; requiredHistoryBars()
│   ├── Indicators.hpp            # Batch indicator math (EMA/ATR/ADX) — parity reference
│   ├── IncrementalIndicators.hpp # O(1) stateful EMA/ATR/ADX/RSI/SMA (bit-identical)
│   ├── Strategy.hpp              # Signal generation: EMA trend + stacked entry filters
│   ├── RiskManager.hpp           # Position sizing + 3 circuit breakers + concentration warn
│   ├── Broker.hpp                # ExecutionBroker abstract interface + Alpaca declarations
│   ├── Backtest.hpp              # BacktestBroker, PreparedData, metrics declarations
│   ├── TradingEngine.hpp         # The shared per-tick pipeline (live == backtest)
│   ├── RiskDaemon.hpp            # Background bracket-reconciliation worker
│   ├── OptionsYield.hpp          # Synthetic covered-call overlay (research, flag-gated)
│   ├── RegimeFeatures.hpp        # O(1) regime feature engine (research, flag-gated)
│   ├── RegimeClassifier.hpp      # Logistic regime classifier (research, flag-gated)
│   └── Logger.hpp                # Thread-safe singleton logger (console + file)
├── src/
│   ├── main.cpp                  # Live/paper bot entry point + timed tick loop
│   ├── Broker.cpp                # All Alpaca REST plumbing (only HTTP/OpenSSL TU)
│   ├── Backtest.cpp              # Fill model, data loading, metrics, walk-forward slicer
│   ├── backtest_main.cpp         # CLI: single run / sweep / walk-forward / date-slice
│   ├── cachegen.cpp              # CSV → QBARS1 binary cache generator + universe selector
│   ├── trade_auditor.cpp         # Offline live-vs-backtest drift auditor
│   ├── indicator_verify.cpp      # Parity + benchmark harness for incremental indicators
│   ├── partial_trim_test.cpp     # Unit proof: partial-trim P&L accounting
│   ├── shorting_test.cpp         # Unit proof: short / cover / flip accounting
│   └── regime_optimizer.cpp      # Offline regime-weight search (holdout-protected)
├── config.json                   # Live config (Variant H) — placeholder credentials only
├── config_expanded_*.json        # Expanded-universe research configs (trend/rotation/sweep)
├── bench/                        # ~38 saved experiment configs (named variants A–H, R*, etc.)
├── data/                         # ~180 dividend-adjusted daily OHLCV CSVs (research dataset)
├── data_cache/                   # QBARS1 binary caches + universe_100.txt (machine-specific)
├── scripts/                      # download_universe.ps1 (Yahoo fetcher), start_bot.bat
├── CMakeLists.txt                # 8 build targets (see below)
├── RUNBOOK.md                    # Operator's manual (setup, monitoring, troubleshooting)
├── RESEARCH_LOG.md               # Narrative of the five experiments
└── PROGRESS_REPORT.md            # Detailed per-session research findings

Build targets: quantbot (live bot), backtest (simulator/sweep/walk-forward), auditor, cachegen, indverify, parttest, shorttest, regime_optimizer.


How to Build and Run

Prerequisites: Visual Studio 2022 Community (or the VS C++ toolchain), CMake, Ninja, and a full OpenSSL dev package (e.g. winget install FireDaemon.OpenSSL — "Light" runtime-only packages lack headers and will not work). nlohmann/json and cpp-httplib are fetched automatically at configure time.

# 1. Clone
git clone <your-repo-url> quant-bot
cd quant-bot

# 2. Set Alpaca paper credentials as environment variables (NEVER put real keys in config.json)
setx APCA_API_KEY_ID     "PK....your paper key...."
setx APCA_API_SECRET_KEY "....your paper secret...."
#    (reopen the terminal afterwards — setx only affects new shells)

# 3. Configure + build.  IMPORTANT: build from PowerShell, not Git Bash.
#    The MSYS Bash layer mangles `cmd /c` (it rewrites /c to a path), so vcvars
#    never loads and the build silently no-ops. _build.bat calls vcvars64.bat
#    then `cmake --build build`.
cmake -S . -B build -G Ninja
cmd /c _build.bat                      # builds all targets
cmd /c _build.bat --target backtest    # or one target

# 4. Run a single backtest (full report + equity/trade CSVs in logs/)
.\build\backtest.exe config.json --data data

# 5. Run walk-forward validation (5y in-sample / 1y out-of-sample, rolling)
.\build\backtest.exe config.json --walkforward 5 1 --data data

# 6. Verify the regression gate — must print 471874.87 / 328 trades
.\build\backtest.exe config.json --data data | Select-String "Final equity|Trades "

# 7. Run the verification suite (all must pass)
.\build\indverify.exe ; .\build\parttest.exe ; .\build\shorttest.exe

# 8. Run the live/paper bot (uses config.json; is_live:false => paper)
.\build\quantbot.exe

To stop the bot gracefully: Ctrl+C (open positions stay protected by their server-side brackets). To liquidate everything immediately: create a file named KILL_SWITCH in the working directory (delete it before restarting). See RUNBOOK.md for the full operating manual.


Safety Systems

Three independent layers, smallest scope to largest:

  1. RiskManager (per-decision). Sizes every entry by ATR-derived risk (a fixed fraction of equity lost if the stop is hit), caps single-position notional, and enforces two breakers: a daily-loss lockout (no new entries once the day is down past the limit) and a portfolio-drawdown kill (flag a full liquidation once equity falls past the drawdown limit from its high-water mark). It can only ever reduce what the strategy requested, never amplify it.

  2. RiskDaemon (background, ~30 min). Cross-references open positions against their working exit orders and repairs divergence: re-brackets orphaned positions, renews GTC legs before Alpaca's 90-day expiry, and closes positions the market gapped through while protection was momentarily absent. This is the safety net that keeps long-held winners from silently becoming unhedged.

  3. KILL_SWITCH file (operator). Dropping an empty file named KILL_SWITCH in the working directory makes the bot cancel all orders and liquidate all positions within one tick — an emergency stop that does not require killing the process or finding the terminal. The bot also refuses to start while the file exists.


Limitations (Honest)

  • Paper trading only. Never funded live. There is no live fill-confirmation loop — placeOrder trusts the HTTP 200 and does not poll order status, so a rejected/partially-filled live order could go unnoticed.
  • No API retry/backoff. A transient Alpaca 5xx or 429 fails the call for that tick (it retries next tick); there is no exponential backoff or rate-limit handling.
  • Approximate state on restart. Trailing-stop anchors and bars-held are reconstructed from broker data after a crash/restart, not persisted — a stop may re-anchor slightly differently than before the restart.
  • No alerting or health endpoint. Errors go to the log file; nothing pages an operator, and there is no external "is it alive?" probe.
  • Sharpe target not met. The stated goal was OOS per-trade Sharpe > 0.70; the honest current figure is 0.60 (Variant H). Five extensions failed to lift it.
  • Windows / MSVC only. Build is wired for the VS toolchain; the documented MSYS cmd /c path-mangling issue means builds must go through PowerShell.

License

MIT.

About

Production C++ algorithmic trading system with live Alpaca integration and walk-forward validated backtesting

Topics

Resources

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors