Full-stack quantitative crypto trading system: Bollinger-Band-Squeeze strategy on 2h price bars with 1-minute intra-bar exit precision, trading Coinbase Advanced Trade perpetual futures (XRP, ETH, SOL) with real margin math. Multi-sampler Optuna optimization, a six-test statistical validation pipeline, live dashboard hosted on a DigitalOcean VPS, and Telegram alerting.
- Live-enabled assets: XRP, ETH, SOL (BTC and DOGE params exist in
params.pybut are not enabled for live execution). - The live bot has executed zero fills since 2026-06-06. The futures account balance is $40.98 and each contract requires $290+ margin, so the sizing routine rejects every signal with
size=0.0. The strategy math still runs, but no trades occur without a deposit. - BTC live params fail the deep validator's out-of-sample gate (holdout PF 0.80, negative PnL). They are in the file for historical reference and pending replacement — not for deployment.
- BTC re-optimization is running now. 5 concurrent Optuna processes (BTC random, BTC TPE, BTC CMA-ES, DOGE random, DOGE TPE) plus a continuous "deep validator" daemon that scans the DBs for high-scoring candidates and runs the same statistical gate the live params must pass. Of 157 BTC candidates with Optuna score ≥ 0.80 tested to date, zero have passed the baseline validation gate.
Backtest reruns the currently-deployed params.py on 18 months of unseen data using the same signal generator + 1-min exit precision + real perpetual-futures margin math the live bot uses.
| Asset | Trades | Win rate | PnL |
|---|---|---|---|
| XRP | 28 | 67.9% | +$2,425 |
| ETH | 38 | 57.9% | +$2,057 |
| SOL | 64 | 51.6% | +$1,842 |
| Portfolio | 130 | +$6,324 |
Real margin rates (XRP 37.3%/56.6%, ETH 24.6%/33.5%, SOL 36.6%/56.0% for long/short), 10 bps slippage per side, 10 bps commission per side, 60-second signal poll delay, integer contract sizing.
| Metric | XRP | ETH | SOL |
|---|---|---|---|
| Total PnL | +$2,904 | +$4,184 | +$2,694 |
| Annualised return | +18.6% | +26.4% | +17.3% |
| Buy-and-hold same window | −55.1% | −52.7% | −61.9% |
| Alpha vs. underlying (ann) | +17.4% | +24.5% | +17.2% |
| Beta to underlying | ~0.00 | ~0.01 | ~0.00 |
| Profit factor | 2.78 | 1.93 | 1.42 |
| Annualised Sharpe | 2.09 | 1.54 | 1.05 |
| Annualised Sortino | 7.03 | 3.96 | 2.79 |
| Max drawdown | 5.8% | 11.2% | 14.6% |
| Max time underwater | 87 days | 202 days | 306 days |
| Calmar ratio | 3.95 | 2.36 | 1.32 |
| Ulcer Index | 2.25 | 4.43 | 5.90 |
| Monte-Carlo p-value | 0.008 | 0.041 | 0.108 |
| Bootstrap PF 95% CI | [1.28, 8.36] | [0.98, 4.20] | [0.86, 2.54] |
| Deflated Sharpe (selection-bias corrected) | 10.9% | 2.4% | 0.4% |
- Joint Sharpe: 2.45 (higher than any individual asset — inter-asset correlations near zero)
- Joint max drawdown: 10.6%
- Correlation of daily PnLs: XRP↔ETH = −0.02, XRP↔SOL = +0.09, ETH↔SOL = +0.22
- Deflated Sharpe is sobering. After correcting for the ~14,000 parameter trials tested per asset, the probability that the observed edge is real (rather than a lucky pick from many attempts) drops to single-digit percentages for ETH and SOL. The economic case — positive alpha while underlying assets were down 50–60% — is stronger than the statistical case for any single asset. The portfolio-level Sharpe is what carries the credibility.
- The live bot has been effectively paused for weeks. Account funding is the bottleneck, not signal quality. Any "live P&L" figure on the dashboard reflects a stale state file, not current trading activity.
- BTC search has stalled. After ~4 days of continuous optimization across three samplers, the objective best has crept up by 0.04 for TPE and 0.02 for random. Zero of the resulting high-score candidates have cleared even the baseline validation gate that ETH and SOL live params both pass.
Every candidate parameter set must pass six independent tests before being marked as tradeable:
| Test | What it detects |
|---|---|
| Baseline train + holdout | Basic profitability and out-of-sample survival |
| Monte Carlo bootstrap on trade sequence | Whether the equity path is distinguishable from random ordering |
| Random-Entry MC (novel) | Whether the signal has entry edge, or if the ATR-based exit geometry is doing all the work |
| Parameter Perturbation MC (novel) | Whether the parameter set is on a robust hill or a lucky spike (isolated peak → reject) |
| Deflated Sharpe Ratio (Bailey/López de Prado) | Corrects observed Sharpe for the ~14,000 trials tested per asset |
| Bootstrap 95% CIs on PF, Sharpe, expectancy | Replaces point estimates with distributions |
The Random-Entry MC in particular kills many superficially-good candidates: if a monkey with the same trade frequency and same ATR exit rules would have made the same PnL, the signal has no edge. For XRP/ETH/SOL live params, the observed PF sits at the 96th–100th percentile of random-entry runs. For a top-scoring BTC candidate (#3445, Optuna score 0.8252), it sat at the 78th percentile — real strategies clear ≥95th, so it was rejected.
The "expected PnL" curve on the dashboard is not a simplified backtest. It uses:
- The exact contract sizes and margin rates each product would demand on Coinbase (XRP-PERP-INTX, ETP-20DEC30-CDE, etc.)
- Integer-contract sizing that rejects trades if the margin budget can't afford one full contract
- 60-second poll delay matching the live bot's signal check frequency
- 10-bps slippage and 10-bps commission per side
- 1-minute intra-bar TP/SL detection (not 2h bar-close approximation)
The result is what the bot would have realized if it had been running and funded during the holdout.
Three samplers run in parallel per asset:
- Random — 30,000 trials, broad exploration
- TPE (tree-structured Parzen estimator) — 10,000 trials, warm-started from random
- CMA-ES — 100 seeds × 200 trials each, evolutionary refinement around promising regions
All state persists in per-asset SQLite databases (~600 MB total for BTC alone), so runs survive laptop sleep, kernel updates, and cross-day resumption. Combined trial count as of writing: ~29,000 valid BTC trials, ~22,000 DOGE, ~15,000 each for XRP/ETH/SOL.
A background process (dashboard/btc_deep_validator.py) scans the BTC optimization DBs every 30 minutes, picks up unseen trials with Optuna score ≥ 0.80, and runs the full six-test pipeline on each. Results are logged to a JSONL file for later review, and a Telegram alert fires only for candidates that pass every test — a much stricter gate than the previous "high Optuna score" alert.
Hosted on the same VPS as the live bot, gated by nginx basic auth. Shows:
- Real-time account balance, positions, buying power
- Live equity curve overlaid with the expected (backtest) curve
- Daily PnL bars
- Per-asset state (last signal, indicator values, cooldowns)
- Recent trades table
- Event feed (state saves, entries/exits, warnings)
- Bot health (uptime, error count, last heartbeat)
Data pipeline: daily cron regenerates the expected-PnL curve using incremental tail-append cache updates (avoids Coinbase rate-limit truncation that silently destroyed the cache during an earlier deployment).
Notable candidate alerts, live entry/exit notifications, error alerts, daily heartbeats. Uses environment-scoped secrets, no plaintext tokens in the repo.
keltrader/
├── params.py # Single source of truth for per-asset live params
├── run_backtest.py # Backtest runner — spot and leveraged modes
├── run_live_multi_asset.py # Live trading entry point
├── optimizer.py # Optuna optimizer (random / TPE / CMA-ES)
├── validate_params.py # Post-optimization validation (holdout + MC + permutation)
│
├── strategy/
│ ├── technical.py # BB, KC, squeeze state, momentum, RSI, volume, ATR
│ └── signal_generator.py # Indicators → entry direction + ATR SL/TP + sizing
│
├── core/
│ ├── backtester.py # Event-driven backtester (spot + leveraged futures)
│ ├── monte_carlo.py # Bootstrap MC on trade sequence
│ ├── permutation_test.py # Price-shuffle permutation test
│ ├── ensemble.py # Multi-param ensemble signal aggregation
│ └── experiment_tracker.py # SQLite log of backtest/optimization runs
│
├── live/
│ └── coinbase_live_trader.py # Live engine: signal poll loop, order placement,
│ # OCO bracket management, state persistence, Telegram
│
├── dashboard/
│ ├── server.py # FastAPI backend
│ ├── static/{index.html,app.js} # Tailwind + Chart.js frontend
│ ├── regen_backtest.py # Daily live-parity expected-PnL regeneration
│ ├── refresh_caches.py # Incremental tail-append cache updates
│ ├── cron_regen_and_push.sh # Daily cron: refresh caches → regen → scp to VPS
│ ├── btc_deep_validator.py # Continuous BTC candidate validation daemon
│ └── opt_telegram_watcher.py # Optuna-score-based Telegram alerts
│
├── data/
│ └── data_utils.py # Multi-timeframe Coinbase data with SQLite/pickle cache
│
├── db/ # Per-asset Optuna study databases (SQLite)
└── tests/ # Pytest suite
Entry logic (Bollinger-Band Squeeze):
- Bollinger Bands contract inside Keltner Channels (compression = squeeze)
- Squeeze must persist for at least N bars (asset-specific; XRP=5, ETH=3, SOL=3)
- Squeeze release + momentum acceleration triggers entry
- Volume-ratio filter confirms breakout direction
- RSI filter rejects entries at asset-specific overbought/oversold extremes
- ATR-scaled stop loss and take profit are set at entry (asymmetric R:R)
- Trailing-stop activation once price moves N × risk in favor
- Position size scales with signal quality (squeeze duration × volume × momentum)
Exit logic:
- Intra-bar TP/SL detection using 1-minute bars
- OCO bracket orders in live execution (both TP and SL sit as limit/stop orders on the exchange)
- Time-based max hold (asset-specific)
Per-asset parameters are independently optimized against the same objective function and stored in params.py. The live bot reads them at startup; the backtester reads the same file.
python run_backtest.py # All default assets
python run_backtest.py --symbols ETH/USD # Single asset
python run_backtest.py --symbols ETH/USD,XRP/USD,SOL/USD # Multiple
python run_backtest.py --leverage --maintenance-margin 0.666 # Futures modepython optimizer.py --symbol BTC --signal-tf 2h --method random --random-trials 30000
python optimizer.py --symbol BTC --signal-tf 2h --method tpe --tpe-trials 10000
python optimizer.py --symbol BTC --signal-tf 2h --method cmaes --cmaes-trials 200
python optimizer.py --symbol BTC --show-best # Report top candidates
python optimizer.py --symbol BTC --evaluate-holdout # Post-hoc OOS checkObjective function: cross-validated composite score with hard gates (PnL > 0, PF ≥ 1.2, MDD ≤ 25%, Sharpe > 0, min-trades scaled by fold length). Trials persist to SQLite; runs are safely resumable.
export COINBASE_API_KEY="your_key"
export COINBASE_API_SECRET="your_secret_pem"
export TELEGRAM_BOT_TOKEN="..."
export TELEGRAM_CHAT_ID="..."
python run_live_multi_asset.py --symbols XRP,ETH,SOL
python run_live_multi_asset.py --paper # No real orders
python run_live_multi_asset.py --list-futures # Show available contractsThe live engine loads params.py, connects to Coinbase Advanced Trade, and enters a signal poll loop. On each 2h close it checks for BB Squeeze release + volume + RSI + trend filter; if all pass, sizes the position against real futures margin, places an OCO bracket (entry limit + TP + SL), persists state, and posts to Telegram. State is written to disk every hour so restarts pick up correctly.
FastAPI backend under dashboard/server.py, static frontend under dashboard/static/. Deployed on the same VPS as the live bot behind nginx with basic auth. Data files (equity curves, trades, health) live under dashboard/static/data/; the daily cron regenerates them.
dashboard/refresh_caches.py handles incremental tail-append: loads the existing pickle cache, fetches only the bars from the last cached timestamp to now, appends and saves. This avoids the Coinbase rate-limit truncation problem where a full-window refetch returns only a partial batch and destroys the cache. The old naive cron used a full refetch and destroyed 16 months of 1-min history in three days before the bug was identified.
pytest tests/Covers scoring, signal generation, technical indicators, Monte Carlo, ensemble, optimizer internals.
For informational purposes only. Past backtest performance does not guarantee future results. Deflated Sharpe values in single-digit percentages mean the observed edge cannot be statistically distinguished from lucky selection out of thousands of trials at the individual-asset level. Portfolio-level Sharpe (uncorrelated-asset averaging) is the more defensible number.
MIT