A trading backtesting simulator that fetches OHLCV data from the Jup.ag Oracle and replays JSON-defined strategies against historical price action. Supports multi-rule entries, DCA ladders, cascade (linked) rules, and configurable risk management.
Requires Python 3.12+ and uv.
uv syncThis installs the trade-sim CLI and all dependencies.
uv run trade-sim run \
--feed BTCUSD --type 15 --range 1y \
--strategy strategies/champions/02_deep_guarded.jsonuv run trade-sim run \
--feed SOLUSD --type 15 --range 30d \
--entry-drop 5.0 --entry-window 4h \
--take-profit 3.0 --stop-loss 12.0 \
--capital 5000 --fee 0.1 --max-positions 5 --position-size 250| Flag | Description | Default |
|---|---|---|
--feed |
Trading pair: BTCUSD, SOLUSD, ETHUSD |
required |
--type |
Candle width: 1, 5, 15, 1h, 1D |
required |
--range |
Lookback: 1d, 7d, 30d, 6m, 1y |
required |
--strategy |
Path to JSON strategy file | — |
--entry-drop |
Entry drop % (simple mode) | — |
--entry-window |
Lookback window for entry (simple mode) | 30m |
--take-profit |
Take profit % (simple mode) | — |
--stop-loss |
Stop loss % (simple mode) | — |
--capital |
Initial capital in USD | 1000 |
--fee |
Fee per side (%) | 0.1 |
--max-positions |
Max concurrent open positions | 3 |
--position-size |
Position size in USD | 100 |
--position-sizing |
fixed or percentage |
fixed |
--output-dir |
Directory for results | output/ |
-v / --verbose |
Enable debug logging | off |
Each run produces two files in the output directory:
<FEED>_<timestamp>_chart.html— Interactive Plotly chart (candlestick + volume + equity curve with buy/sell markers).<FEED>_<timestamp>_summary.json— Full results: strategy config, performance metrics, per-rule breakdown, and trade log.
Strategies are JSON files with entry/exit rules, position sizing, and risk parameters. The simulator supports three patterns: single-rule, DCA ladder (independent multi-level), and cascade (linked dependent rules).
| Field | Values | Description |
|---|---|---|
entry.type |
price_drop, price_rise |
Direction of price move that triggers entry |
entry.percentage |
> 0 |
Required magnitude of move (%) |
entry.window |
1m–1d |
Lookback period to measure the move |
exit.take_profit_pct |
> 0 |
Close position at this % profit from entry |
exit.stop_loss_pct |
> 0 or omit |
Close position at this % loss from entry |
Valid windows: 1m, 5m, 10m, 15m, 30m, 1h, 2h, 4h, 1d.
fixed: Each position uses a fixed USD amount (e.g.,250USD).percentage: Each position uses a percentage of current capital (e.g.,5for 5%).
Buy on a 5% dip over 4 hours, take profit at +3%, stop loss at -12%:
{
"initial_capital": 5000,
"fee_pct": 0.1,
"max_concurrent_positions": 5,
"position_sizing": { "type": "fixed", "value": 250 },
"rules": [
{
"id": "deep_guarded",
"entry": { "type": "price_drop", "percentage": 5.0, "window": "4h" },
"exit": { "take_profit_pct": 3.0, "stop_loss_pct": 12.0 },
"max_positions": 5
}
]
}Three independent levels that trigger at progressively deeper dips:
{
"initial_capital": 5000,
"fee_pct": 0.1,
"max_concurrent_positions": 15,
"position_sizing": { "type": "fixed", "value": 100 },
"rules": [
{
"id": "dca_level_1",
"entry": { "type": "price_drop", "percentage": 1.5, "window": "30m" },
"exit": { "take_profit_pct": 0.8 },
"max_positions": 5
},
{
"id": "dca_level_2",
"entry": { "type": "price_drop", "percentage": 3.0, "window": "1h" },
"exit": { "take_profit_pct": 1.5 },
"max_positions": 5
},
{
"id": "dca_level_3",
"entry": { "type": "price_drop", "percentage": 5.0, "window": "2h" },
"exit": { "take_profit_pct": 2.5 },
"max_positions": 5
}
]
}The second rule only triggers when the base rule has an open position, measuring the additional drop from the parent's entry price:
{
"initial_capital": 5000,
"fee_pct": 0.1,
"max_concurrent_positions": 10,
"position_sizing": { "type": "fixed", "value": 150 },
"rules": [
{
"id": "cascade_base",
"entry": { "type": "price_drop", "percentage": 3.0, "window": "1h" },
"exit": { "take_profit_pct": 2.0 },
"max_positions": 5
},
{
"id": "cascade_deep",
"entry": { "type": "price_drop", "percentage": 3.0, "window": "2h" },
"exit": { "take_profit_pct": 3.0 },
"max_positions": 5,
"depends_on": {
"rule_id": "cascade_base",
"condition": "has_open_position",
"drop_from": "parent_entry"
}
}
]
}| Field | Values | Description |
|---|---|---|
rule_id |
string | ID of the parent rule |
condition |
has_open_position, has_closed_position |
When the child rule is eligible |
drop_from |
parent_entry, current_price |
Reference price for measuring the child's entry drop |
Analyze historical dip patterns without running any strategy — find average/median dip depth, bounce height, and duration across configurable buckets:
uv run python -B scripts/dip_analyzer.py --feed BTCUSD --type 15
uv run python -B scripts/dip_analyzer.py --feed SOLUSD --type 5 --min-dip 1.0
uv run python -B scripts/dip_analyzer.py --feed ETHUSD --type 15 --save-json| Flag | Description | Default |
|---|---|---|
--feed |
Trading pair | required |
--type |
Candle width (5 or 15) |
required |
--min-dip |
Minimum dip % to include | 0.5 |
--save-json |
Export raw dip data to output/ |
off |
Output includes:
- Dip depth stats (avg, median, std, percentiles)
- Bounce height stats
- Dip duration stats (formatted as Xh Ym)
- Bucketed analysis — Small (0.5–1%), Medium (1–2%), Large (2–3%), Big (3–5%), Crash (5–10%), Extreme (10%+)
- Monthly breakdown table
- Top 10 biggest dips with timestamps and duration
- Strategy parameter suggestions based on the data
Automatically search for the best strategy parameters using Bayesian Optimization (Optuna TPE sampler):
# Single rule, 200 trials
uv run python -B scripts/optimize.py --feed BTCUSD --type 15
# Multi-rule DCA ladder (2 rules, 300 trials)
uv run python -B scripts/optimize.py --feed SOLUSD --type 15 --rules 2 --trials 300
# Custom objective, reproducible seed
uv run python -B scripts/optimize.py --feed ETHUSD --type 15 --objective sharpe --seed 42| Flag | Description | Default |
|---|---|---|
--feed |
Trading pair | required |
--type |
Candle width (5 or 15) |
required |
--range |
Data range | 1y |
--trials |
Number of optimization trials | 200 |
--rules |
Number of rules (1–3) | 1 |
--entry-type |
price_drop or price_rise |
price_drop |
--objective |
composite, pnl, sharpe, profit_factor, expectancy |
composite |
--capital |
Initial capital | 5000 |
--split |
Train/validation ratio | 0.75 |
--seed |
Random seed for reproducibility | random |
Parameters optimized:
- Entry percentage (0.5–15%) and lookback window (1h/2h/4h/1d)
- Take-profit (0.5–15%) and stop-loss (2–25%)
- Max concurrent positions (1–10) and max per rule (1–10)
- Position size ($100–$1000)
Anti-overfitting:
- Walk-forward validation (75/25 train/test split)
- Minimum 10 trades threshold
- Overfitting warning when validation score < 50% of train score
- Train vs validation metrics displayed side by side
The best strategy is auto-exported to output/optimized_<FEED>_<TYPE>m.json.
Evolutionary search using tournament selection, BLX-α crossover, and Gaussian mutation:
# Default: 50 pop × 100 generations
uv run python -B scripts/optimize_ga.py --feed BTCUSD --type 15
# Smaller run for quick exploration
uv run python -B scripts/optimize_ga.py --feed SOLUSD --type 15 \
--pop-size 30 --generations 50 --rules 2
# Tune mutation and crossover rates
uv run python -B scripts/optimize_ga.py --feed ETHUSD --type 15 \
--crossover-rate 0.9 --mutation-rate 0.2 --elite-count 3 --seed 42| Flag | Description | Default |
|---|---|---|
--feed |
Trading pair | required |
--type |
Candle width (5 or 15) |
required |
--range |
Data range | 1y |
--rules |
Number of rules (1–3) | 1 |
--entry-type |
price_drop or price_rise |
price_drop |
--objective |
composite, pnl, sharpe, profit_factor, expectancy |
composite |
--capital |
Initial capital | 5000 |
--pop-size |
Population size | 50 |
--generations |
Number of generations | 100 |
--crossover-rate |
Crossover probability | 0.8 |
--mutation-rate |
Per-gene mutation probability | 0.15 |
--tournament-size |
Tournament selection pool size | 3 |
--elite-count |
Top N individuals kept unchanged | 2 |
--split |
Train/validation split ratio | 0.75 |
--seed |
Random seed | random |
How it works:
- Initializes a random population, evaluates fitness on the train set
- Each generation: elites survive, parents selected via tournament, offspring created via BLX-α blend crossover (floats) and uniform crossover (categorical/integer), then Gaussian mutation
- A
_repair()step enforces constraints (e.g. max_per_rule sum ≤ max_concurrent) - Convergence chart shows best/mean/worst score per generation
- Walk-forward validation and overfitting detection (same as Bayesian)
Multi-fold walk-forward optimization that tests parameter stability across time:
# Rolling mode: 6-month train → 2-month test, sliding forward
uv run python -B scripts/optimize_wfa.py --feed BTCUSD --type 15
# Anchored mode: training window grows from the start
uv run python -B scripts/optimize_wfa.py --feed SOLUSD --type 15 \
--mode anchored --train-months 8 --test-months 2
# More trials per fold for higher quality
uv run python -B scripts/optimize_wfa.py --feed ETHUSD --type 15 \
--trials 300 --train-months 4 --test-months 1 --seed 42| Flag | Description | Default |
|---|---|---|
--feed |
Trading pair | required |
--type |
Candle width (5 or 15) |
required |
--range |
Data range | 1y |
--rules |
Number of rules (1–3) | 1 |
--entry-type |
price_drop or price_rise |
price_drop |
--objective |
composite, pnl, sharpe, profit_factor, expectancy |
composite |
--capital |
Initial capital | 5000 |
--mode |
rolling (fixed-size window slides) or anchored (train grows from start) |
rolling |
--train-months |
Training window in months | 6 |
--test-months |
Test window in months | 2 |
--trials |
Optuna trials per fold | 100 |
--seed |
Random seed | random |
How it works:
- Splits data into sequential train/test folds based on calendar months
- Each fold runs an independent Bayesian optimization (Optuna) on the training window
- The best parameters from each fold are evaluated out-of-sample on the test window
- Aggregates OOS metrics: total trades, win rate, PnL, and per-fold scores
Output includes:
- Per-fold table with train score, OOS trades, OOS PnL, OOS score
- Parameter stability table showing mean/stddev for numeric params and most-common value for categorical
- Consistency assessment: HIGH (≥75% profitable folds), MODERATE (≥50%), LOW (<50%)
- Best OOS fold's strategy auto-exported to
output/optimized_<FEED>_<TYPE>m.json
Run all strategies in a directory against one feed:
uv run python -B scripts/batch_backtest.pyBy default runs the strategies/champions/ folder on BTCUSD 15m 1y. Override with environment variables:
STRATEGY_SET=scalping_v2 uv run python -B scripts/batch_backtest.pyOutputs a comparison table and saves results to output/batch_comparison.json.
Test champion strategies across BTC, SOL, and ETH simultaneously:
uv run python -B scripts/multi_asset_backtest.pyOutputs per-asset tables plus an aggregated cross-asset summary to output/champion_comparison.json.
Each backtest reports:
| Metric | Description |
|---|---|
| Win Rate | % of trades closed at take profit |
| Profit Factor | Gross profits / gross losses |
| Expectancy | Average expected $ per trade |
| Sharpe Ratio | Risk-adjusted return (annualized) |
| Max Drawdown | Largest peak-to-trough equity decline |
| Recovery Factor | Total PnL / max drawdown |
| Avg Duration | Mean trade holding time |
| Max Consecutive Wins/Losses | Longest winning and losing streaks |
The strategies/ folder contains 42 ready-to-use strategy files organized by research iteration:
| Directory | Count | Description |
|---|---|---|
strategies/ |
2 | example.json, dip_buyer.json — basic examples |
strategies/scalping/ |
8 | Round 1 — initial scalping approaches |
strategies/scalping_v2/ |
8 | Round 2 — deep dip mean reversion |
strategies/scalping_v3/ |
8 | Round 3 — optimized DCA + protective SL |
strategies/champions/ |
8 | Finals — best performers tested cross-asset |
strategies/big_dip/ |
8 | Big-dip strategies targeting 3–7% drops |
strategies/dip_data/ |
8 | Data-driven from dip analyzer bucket statistics |
Top performer: champions/02_deep_guarded.json — 5% dip entry, 3% TP, 12% SL. Averaged 82% win rate and PF 1.07 across BTC, SOL, and ETH over 1 year of 15-minute data.
uv run pytest tests/ -qsrc/trade_simulator/
├── api.py # OHLCV data fetching with pagination
├── cli.py # Click CLI interface
├── engine.py # Backtesting simulation loop
├── models.py # Candle, Position, TradeResult dataclasses
├── plotting.py # Interactive Plotly charts
├── report.py # JSON summary and console output
└── strategy.py # Pydantic strategy schema and validation
scripts/
├── batch_backtest.py # Single-asset batch runner
├── multi_asset_backtest.py # Multi-asset batch runner
├── dip_analyzer.py # Dip depth/bounce/duration analysis
├── optim_shared.py # Shared utilities for optimizer scripts
├── optimize.py # Bayesian hyperparameter optimization
├── optimize_ga.py # Genetic Algorithm optimization
└── optimize_wfa.py # Walk-Forward Analysis
strategies/ # Pre-built strategy JSON files
tests/ # pytest test suite
MIT
{ // Optional description (ignored by engine) "_description": "My strategy", // Account settings "initial_capital": 5000, // Starting balance in USD "fee_pct": 0.1, // Fee per side (0.1% = 0.2% round trip) "max_concurrent_positions": 5, // Position sizing "position_sizing": { "type": "fixed", // "fixed" (USD) or "percentage" (of capital) "value": 250 }, // One or more trading rules "rules": [ { "id": "rule_1", // Unique identifier "entry": { "type": "price_drop", // "price_drop" or "price_rise" "percentage": 5.0, // Required price change % "window": "4h" // Lookback window (1m–1d) }, "exit": { "take_profit_pct": 3.0, // TP from entry price (required) "stop_loss_pct": 12.0 // SL from entry price (optional) }, "max_positions": 5, // Per-rule position limit // Optional: link to a parent rule (cascade/DCA) "depends_on": { "rule_id": "parent_rule", // Must match another rule's id "condition": "has_open_position", // or "has_closed_position" "drop_from": "parent_entry" // or "current_price" } } ] }