Algorithmic trading research & execution workspace for crypto perpetual futures, built on NautilusTrader. It covers the full loop:
download exchange tick data → backtest on real ticks → optimize parameters with Optuna → run live on Binance.
Everything is driven by a small set of strategy modules and three command-line scripts. Backtests and optimizations run against real trade ticks (not synthetic OHLC) downloaded from Binance, Gate.io and Bybit and stored locally as Parquet. The same strategy code that is backtested and tuned can then be deployed live on Binance USDT-margined perpetual futures.
| Piece | What it does |
|---|---|
strategies/ |
Trading strategy modules (signal + order management) |
scripts/download_ticks.py |
Download historical trade ticks from an exchange |
scripts/run_backtest.py |
Backtest a strategy on ticks, print stats & reports |
scripts/optimize_volume_spike.py |
Optuna parameter search (multi-objective / scored) |
scripts/run_live_binance.py |
Run a strategy live on Binance futures |
configs/ |
Pair presets, optimizer ranges, live bot config, API keys |
Long-only momentum entry on a volume + price spike.
- Signal: a bar whose close-to-close rise ≥
price_rise_pctand whose volume ≥volume_multiplier× the average volume over the lastlookback_barsbars. - Entry: market BUY (immediate, taker fee).
- Exit: stop-loss is always placed. Optionally a take-profit, and/or a trailing stop that
arms after price moves
trailing_activation_pctin profit and then follows the high bytrailing_stop_pct. Whichever exit triggers first closes the position. - One position per instrument at a time.
This is the strategy that is parameter-optimized and ships with tuned presets in
configs/volume_spike_long.json.
See strategies/volume_spike_long.py.
Long-only strategy that waits for a pullback instead of chasing the spike.
- Signal: a bullish spike bar (close > open, rise ≥
candle_rise_pct, volume > 0). - Entry: a Limit BUY placed
entry_offset_pctbelow that bar's close, so it fills on the retrace (maker fee). Can also be configured as a market-if-touched entry (taker). - Exit: take-profit + stop-loss, each configurable as maker (limit) or taker (market-if-touched). Supports pre-placed reduce-only exits so they sit on the exchange for live trading, with full partial-fill handling.
This is the strategy wired into the live Binance runner.
See strategies/pullback_long.py.
A minimal MACD zero-cross strategy (long when MACD crosses above zero, short when below),
adapted from the NautilusTrader tutorial. Kept as a compact reference for how a Strategy /
StrategyConfig pair is structured. See strategies/macd_strategy.py.
├── strategies/
│ ├── volume_spike_long.py # primary: volume/price spike, long-only
│ ├── pullback_long.py # limit-entry pullback (used live)
│ └── macd_strategy.py # MACD crossover reference example
├── scripts/
│ ├── download_ticks.py # download trade ticks (Binance / Gate / Bybit)
│ ├── run_backtest.py # backtest on real ticks + HTML reports
│ ├── optimize_volume_spike.py # Optuna parameter optimization
│ ├── run_live_binance.py # live trading on Binance USDT-M futures
│ └── utils/ # tick helpers, fee/exchange comparisons, order tests
├── configs/
│ ├── volume_spike_long.json # tuned pair presets (backtest)
│ ├── optimize_volume_spike.json # parameter search ranges per pair
│ ├── bot/live_binance.json # live bot config (pairs, leverage, sizing)
│ └── secrets.json # Binance API keys (gitignored)
├── catalog/ # Parquet tick catalog (gitignored, per-machine)
├── db/ # Optuna study databases (gitignored)
├── reports/ # generated HTML backtest reports (gitignored)
└── pyproject.toml
Dependencies are managed with uv.
# Install uv (if not already installed)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Install dependencies (creates .venv automatically)
uv syncRequires Python 3.12+. All scripts are run through uv run python ....
Trade ticks are downloaded per symbol/exchange and stored as Parquet under
catalog/data/trade_tick/{SYMBOL}-PERP.{EXCHANGE}/. This directory is not committed
(large files) — each machine downloads its own data.
# Full download from a start date
uv run python scripts/download_ticks.py --exchange binance --symbol SIREN --start 2025-01-01
# Backfill older history behind existing data
uv run python scripts/download_ticks.py --exchange gate --symbol FHE --prepend 2025-04-10
# Append only new data (Binance only)
uv run python scripts/download_ticks.py --exchange binance --symbol SIREN --incrementalSupported exchanges: binance, gate, bybit.
Backtests run on real trade ticks with a 10,000 USDT starting balance, 5× leverage and
a realistic maker/taker fee model. Parameters come either from a named preset in
configs/volume_spike_long.json or from explicit CLI overrides.
# Run a tuned preset by name
uv run python scripts/run_backtest.py \
--strategy volume_spike_long \
--pair beat \
--timeframe 15m \
--start 2025-01-01
# Or specify all parameters manually
uv run python scripts/run_backtest.py \
--strategy volume_spike_long \
--pair siren \
--start 2025-01-01 \
--timeframe 15m \
--lookback 50 --rise 0.4 --sl 12.5 --tp 5.5 --vol-mul 12.1 \
--trailing-activation 0.0 --trailing-stop 0.0Output includes win rate, total / average P&L, profit factor, max drawdown, and P&L broken
down by week and by month (with commissions). Add --generate-report trades for an
interactive HTML report, or --export-trades out.csv to dump every position.
Timeframes: 1m, 5m, 15m, 30m, 1h, 4h, 1d.
optimize_volume_spike.py searches the parameter space with Optuna and stores every trial in a
SQLite study you can browse in the dashboard. Search ranges per pair live in
configs/optimize_volume_spike.json as [low, high, step].
uv run python scripts/optimize_volume_spike.py \
--pair siren \
--start 2025-01-01 \
--end 2026-01-01 \
--trials 5000 \
--workers 5 \
--sampler tpe \
--mode multiparam \
--timeframe 15m \
--db db/optuna_volspike_siren_15m.db \
--reset
# Inspect the study
uv run optuna-dashboard sqlite:///db/optuna_volspike_siren_15m.db --port 8081Samplers: tpe (default) or nsga2.
Modes:
pareto— multi-objective: maximize Sharpe, total P&L, and −|max drawdown| simultaneously.multiparam— single weighted score (sigmoid-normalized Calmar / Sharpe / profit factor).zero-dd— prioritizes strategies with (near-)zero drawdown.
Metrics computed per trial: Sharpe, Sortino, Calmar, profit factor, win rate, total P&L, max drawdown, and trade count.
run_live_binance.py runs a strategy live on Binance USDT-margined perpetual futures through a
NautilusTrader TradingNode (with position reconciliation on startup). The live bot config lives
in configs/bot/live_binance.json.
# Live (real account)
PYTHONPATH=. uv run python scripts/run_live_binance.py \
--config configs/bot/live_binance.json
# Testnet
PYTHONPATH=. uv run python scripts/run_live_binance.py \
--config configs/bot/live_binance.json \
--testnetAPI keys are read from the BINANCE_API_KEY / BINANCE_API_SECRET environment variables, or
from configs/secrets.json (binance_api_key / binance_api_secret). For testnet, set
BINANCE_TESTNET_API_KEY / BINANCE_TESTNET_API_SECRET. configs/secrets.json is gitignored —
never commit real keys.
| File | Purpose |
|---|---|
configs/volume_spike_long.json |
Tuned VolumeSpikeLong presets per pair (used by the backtester). Each preset is a full parameter set with a note on its historical behavior. |
configs/optimize_volume_spike.json |
Optuna search ranges [low, high, step] per pair, plus a trailing on/off flag. |
configs/bot/live_binance.json |
Live bot: trader id, testnet flag, leverage, timeframe, and per-pair strategy settings. |
configs/secrets.json |
Binance API keys (gitignored). |
Note: preset values in these configs are the result of long optimization runs — treat them as data. When adding parameters, only add new fields; don't overwrite existing tuned values.
Tick data is stored as Parquet in catalog/data/trade_tick/{SYMBOL}-PERP.{EXCHANGE}/, with
instrument metadata under catalog/data/crypto_perpetual/. The catalog/, db/ and reports/
directories are gitignored (large / machine-local), so a fresh clone starts empty — run
download_ticks.py to populate ticks before backtesting.
Quick per-month tick count for a symbol:
uv run python -c "
import pyarrow.parquet as pq, pandas as pd, os
from collections import defaultdict
d = 'catalog/data/trade_tick/SIREN-PERP.BINANCE'
monthly = defaultdict(int)
for f in sorted(os.listdir(d)):
if not f.endswith('.parquet'): continue
ts = pd.to_datetime(pq.read_table(os.path.join(d, f), columns=['ts_init'])['ts_init'].to_pandas(), unit='ns')
for m, c in ts.dt.to_period('M').value_counts().items(): monthly[str(m)] += c
for m in sorted(monthly): print(f'{m}: {monthly[m]:>12,}')
print(f'TOTAL: {sum(monthly.values()):>12,}')
"