Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 68 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,45 +39,43 @@ pip install ml4t-backtest
## Quick Start

```python
import polars as pl
from ml4t.backtest import Engine, Strategy, BacktestConfig, DataFeed
from ml4t.backtest.risk import StopLoss, TakeProfit, RuleChain

class TrendFollowing(Strategy):
def __init__(self, fast=10, slow=30):
self.fast = fast
self.slow = slow

class SignalStrategy(Strategy):
def on_data(self, timestamp, data, context, broker):
close = data["close"]
fast_ma = close.rolling(self.fast).mean().iloc[-1]
slow_ma = close.rolling(self.slow).mean().iloc[-1]

position = broker.get_position("SPY")

if fast_ma > slow_ma and position is None:
broker.submit_order("SPY", quantity=100, side="BUY")
elif fast_ma < slow_ma and position is not None:
broker.close_position("SPY")
for asset, bar in data.items():
signal = bar.get("signals", {}).get("prediction", 0)
price = bar.get("close", 0)
position = broker.get_position(asset)

if position is None and signal > 0.5:
shares = (broker.get_account_value() * 0.10) / price
if shares > 0:
broker.submit_order(asset, shares)
elif position is not None and signal < -0.5:
broker.close_position(asset)

config = BacktestConfig(
initial_cash=100_000,
commission_rate=0.001,
slippage_rate=0.0005,
)

feed = DataFeed(price_data)
engine = Engine(feed, TrendFollowing(), config)
feed = DataFeed(prices_df=prices, signals_df=signals)
engine = Engine(feed, SignalStrategy(), config)
result = engine.run()

print(f"Total Return: {result.total_return:.2%}")
print(f"Sharpe Ratio: {result.metrics['sharpe_ratio']:.2f}")
print(f"Total Return: {result.metrics['total_return_pct']:.2f}%")
print(f"Sharpe Ratio: {result.metrics['sharpe']:.2f}")
```

## Risk Management

Position-level exit rules:

```python
from ml4t.backtest.risk import StopLoss, TakeProfit, TrailingStop, RuleChain
from ml4t.backtest import Strategy, StopLoss, TakeProfit, TrailingStop, RuleChain

class MyStrategy(Strategy):
def on_start(self, broker):
Expand All @@ -91,7 +89,7 @@ class MyStrategy(Strategy):
Portfolio-level controls:

```python
from ml4t.backtest.risk import MaxPositions, MaxDrawdown, DailyLossLimit
from ml4t.backtest.risk.portfolio.limits import MaxDrawdownLimit, DailyLossLimit
```

## Framework Profiles
Expand Down Expand Up @@ -140,26 +138,50 @@ config = BacktestConfig(
## Commission and Slippage

```python
from ml4t.backtest import PercentCommission, PercentSlippage
from ml4t.backtest import BacktestConfig, CommissionType

config = BacktestConfig(
commission_rate=0.001, # 10 bps percentage
slippage_rate=0.0005, # 5 bps slippage
stop_slippage_rate=0.001, # Additional slippage for stop exits
)

# Or per-share (Interactive Brokers style)
config = BacktestConfig(
commission_model=PercentCommission(rate=0.001),
slippage_model=PercentSlippage(rate=0.0005),
commission_type=CommissionType.PER_SHARE,
commission_per_share=0.005,
commission_minimum=1.0,
)
```

## Multi-Asset Support
## Multi-Asset Rebalancing

```python
class RankingStrategy(Strategy):
def on_data(self, timestamp, data, context, broker):
returns = data["close"].pct_change(20)
ranked = returns.iloc[-1].sort_values(ascending=False)
from ml4t.backtest import Strategy, TargetWeightExecutor, RebalanceConfig

class WeightStrategy(Strategy):
def __init__(self):
self.executor = TargetWeightExecutor(RebalanceConfig(
min_trade_value=100,
min_weight_change=0.01,
))
self.bar_count = 0

# Long top 10
for asset in ranked.head(10).index:
if broker.get_position(asset) is None:
broker.submit_order(asset, quantity=100, side="BUY")
def on_data(self, timestamp, data, context, broker):
self.bar_count += 1
if self.bar_count % 21 != 1: # Monthly rebalance
return

# ML predictions → portfolio weights
weights = {}
for asset, bar in data.items():
signal = bar.get("signals", {}).get("prediction", 0)
if signal and signal > 0:
weights[asset] = signal
if weights:
total = sum(weights.values())
weights = {a: w / total for a, w in weights.items()}
self.executor.execute(weights, data, broker)
```

## Cross-Framework Validation
Expand Down Expand Up @@ -209,6 +231,18 @@ Benchmark on 250 assets x 20 years daily data (1.26M bars):
| vs Zipline | 8x faster |
| vs LEAN | 5x faster |

## Documentation

- [Getting Started](docs/getting-started/quickstart.md) — your first backtest
- [Strategies](docs/user-guide/strategies.md) — strategy interface and templates
- [Stateful Strategies](docs/user-guide/stateful-strategies.md) — advanced event-driven patterns (Kelly sizing, pairs trading, circuit breakers)
- [Execution Semantics](docs/user-guide/execution-semantics.md) — fill timing, ordering, stops
- [Configuration](docs/user-guide/configuration.md) — 40+ behavioral knobs
- [Risk Management](docs/user-guide/risk-management.md) — stops, trails, portfolio limits
- [Rebalancing](docs/user-guide/rebalancing.md) — weight-based portfolio management
- [Market Impact](docs/user-guide/market-impact.md) — commission, slippage, and impact models
- [Profiles](docs/user-guide/profiles.md) — framework parity presets

## Technical Characteristics

- **Event-driven**: Each bar processes sequentially with exit-first logic
Expand Down
7 changes: 7 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,14 @@ pip install ml4t-backtest
- [Configuration](user-guide/configuration.md) -- all 40+ knobs explained
- [Profiles](user-guide/profiles.md) -- framework parity and presets
- [Strategies](user-guide/strategies.md) -- writing strategies and templates
- [Stateful Strategies](user-guide/stateful-strategies.md) -- advanced event-driven patterns
- [Risk Management](user-guide/risk-management.md) -- stops, trails, portfolio limits
- [Rebalancing](user-guide/rebalancing.md) -- weight-based portfolio management
- [Data Feed](user-guide/data-feed.md) -- preparing price and signal data
- [Results & Analysis](user-guide/results.md) -- metrics, trades, equity export
- [Market Impact](user-guide/market-impact.md) -- commission, slippage, and impact models
- [Orders](user-guide/orders.md) -- order types and bracket orders
- [Accounts](user-guide/accounts.md) -- cash, crypto, and margin accounts
- [API Reference](api/index.md) -- full API documentation

## Part of the ML4T Ecosystem
Expand Down
10 changes: 10 additions & 0 deletions docs/user-guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,16 @@ config = BacktestConfig(
)
```

## See It in Action

The [Machine Learning for Trading](https://github.com/stefan-jansen/machine-learning-for-trading) book uses BacktestConfig across all case studies:

- **Ch16 case studies** — each case study loads config from `setup.yaml` via `get_backtest_config()`, setting initial_cash, commission_rate, slippage_rate, and execution_mode
- **Ch16 / NB13** (`futures_backtesting`) — ContractSpec with CommissionType.PER_CONTRACT for CME futures
- **Ch19 case studies** — risk management config (stop fill modes, trailing stop timing)

The book pattern: `BacktestConfig()` with 4 overrides (initial_cash, commission_rate, slippage_rate, execution_mode), loaded from YAML. Costs come from `setup.yaml` via a utility function. This covers the vast majority of use cases.

## Next Steps

- [Profiles](profiles.md) -- pre-built configs for each framework
Expand Down
8 changes: 8 additions & 0 deletions docs/user-guide/data-feed.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ result = run_backtest("data/prices.parquet", strategy, signals="data/signals.par

DataFeed pre-partitions data by timestamp at initialization and pre-extracts column indices for O(1) per-bar access. For 1M bars, this uses roughly 100 MB (10x less than converting everything to Python dicts upfront).

## See It in Action

The [Machine Learning for Trading](https://github.com/stefan-jansen/machine-learning-for-trading) book prepares DataFeed inputs in every Engine case study:

- **Ch16 case studies** — each case study loads OHLCV from Parquet, constructs a signals DataFrame from ML predictions, and passes both to DataFeed
- **Ch16 / NB13** (`futures_backtesting`) — multi-contract futures data with session boundaries and overnight gaps
- The common pattern: `prices_df` is a stacked multi-asset OHLCV DataFrame, `signals_df` contains prediction columns aligned by (timestamp, asset)

## Next Steps

- [Quickstart](../getting-started/quickstart.md) -- end-to-end examples
Expand Down
8 changes: 8 additions & 0 deletions docs/user-guide/execution-semantics.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,14 @@ config = BacktestConfig(share_type=ShareType.FRACTIONAL)
config = BacktestConfig(share_type=ShareType.INTEGER)
```

## See It in Action

The [Machine Learning for Trading](https://github.com/stefan-jansen/machine-learning-for-trading) book demonstrates execution semantics across chapters:

- **Ch16 / NB11** (`engine_divergence_anatomy`) — detailed analysis of how SAME_BAR vs NEXT_BAR and fill ordering affect backtest results
- **Ch18** (`portfolio_construction`) — LinearImpact and SquareRootImpact market impact models with VolumeParticipationLimit
- **Ch16 case studies** — each case study uses setup.yaml to configure commission_rate, slippage_rate, and execution_mode

## Next Steps

- [Configuration](configuration.md) -- complete reference for all 40+ parameters
Expand Down
Loading
Loading