Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8b5254f
fix: cast equity values to float in BacktestResult.to_equity_dataframe()
stefan-jansen Feb 26, 2026
43ae551
fix(result): use daily returns in to_tearsheet(), add calendar param
stefan-jansen Feb 26, 2026
e76f0bb
chore: delete all deprecated code and backward compat shims
stefan-jansen Feb 27, 2026
742322e
chore: apply pre-commit normalization updates
stefan-jansen Feb 27, 2026
b1503f4
feat: simplify config/types and remove stale presets
stefan-jansen Feb 27, 2026
9d90e02
refactor: extract broker orchestration into core components
stefan-jansen Feb 27, 2026
12df7b5
feat: centralize engine profiles and wire validation harness
stefan-jansen Feb 27, 2026
f519d3b
refactor: shrink root API and isolate validation imports
stefan-jansen Feb 27, 2026
45241e1
test: add deterministic core contract tests (phase C1)
stefan-jansen Feb 27, 2026
d467671
test: add property-based accounting and ordering invariants (phase C2)
stefan-jansen Feb 27, 2026
60473ad
test: enforce slim root API surface (phase C3)
stefan-jansen Feb 27, 2026
74932e3
chore(validation): improve correctness runner failure diagnostics
stefan-jansen Feb 27, 2026
35315d9
refactor(validation): use profiles in benchmark suite ml4t runner
stefan-jansen Feb 27, 2026
ecc616b
refactor(broker): remove dead order-processing branches after core split
stefan-jansen Feb 27, 2026
4a8a4c6
refactor(risk): move position-state helpers into risk engine
stefan-jansen Feb 27, 2026
3a48104
refactor(core): extract fill engine from broker
stefan-jansen Feb 27, 2026
e316d41
test/docs: add cross-engine contracts and align strategy-order guides
stefan-jansen Feb 27, 2026
3de7424
refactor(ci): finalize E4 broker cleanup and contract gate
stefan-jansen Feb 27, 2026
93b39bd
test(contracts): add book-parity behavior contracts
stefan-jansen Feb 27, 2026
09b545f
harden parity config wiring and rebalance behavior
stefan-jansen Feb 27, 2026
dbf80b5
test: harden coverage measurement and result edge cases
stefan-jansen Feb 27, 2026
0d61a38
test: cover result config/yaml and tearsheet error branches
stefan-jansen Feb 27, 2026
18b436c
test: normalize property test formatting for CI
stefan-jansen Feb 27, 2026
41af582
test: skip cross-engine contract when framework deps missing
stefan-jansen Feb 27, 2026
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
24 changes: 23 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,32 @@ jobs:
- name: Run tests
run: uv run pytest tests/ -v --tb=short -x --no-cov

contracts:
name: Cross-Engine Contracts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"

- name: Set up Python
run: uv python install ${{ env.PYTHON_VERSION }}

- name: Install dependencies (with comparison frameworks)
run: uv sync --dev --extra comparison

- name: Run cross-engine contract test (scenario 01)
env:
ML4T_COMPARISON_INPROC: "1"
run: uv run pytest tests/contracts/test_cross_engine_contracts.py -v --tb=short --no-cov

build:
name: Build Package
runs-on: ubuntu-latest
needs: [lint, typecheck, test]
needs: [lint, typecheck, test, contracts]
steps:
- uses: actions/checkout@v4
with:
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ ENV/

# Testing
.pytest_cache/
.hypothesis/
.coverage
htmlcov/
.tox/
Expand All @@ -39,3 +40,7 @@ src/ml4t/backtest/_version.py
# Claude Code (local development only)
CLAUDE.md
.claude/

# Validation-generated local artifacts
validation/.zipline/
validation/CORRECTNESS_RESULTS.md
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,18 @@ The library is validated against VectorBT Pro, Backtrader, and Zipline:

See [validation/README.md](validation/README.md) for test methodology.

Release-gate commands:

```bash
# Fast parity contract gate (scenario 01 across vectorbt/backtrader/zipline)
ML4T_COMPARISON_INPROC=1 uv run pytest tests/contracts/test_cross_engine_contracts.py -q

# Full correctness runner (selected scenarios)
python validation/run_all_correctness.py --framework vectorbt_oss --scenarios 01,03,05,09
python validation/run_all_correctness.py --framework backtrader --scenarios 01,03,05,09
python validation/run_all_correctness.py --framework zipline --scenarios 01,03,05,09
```

## Technical Characteristics

- **Event-driven**: Each bar processes sequentially with exit-first logic
Expand Down
6 changes: 3 additions & 3 deletions api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ modules:
methods:
- name: run
returns: BacktestResult

- name: Strategy
description: Base class for trading strategies
methods:
Expand All @@ -40,7 +40,7 @@ modules:
type: int
- name: equity
type: float

- name: BacktestConfig
params:
- name: initial_cash
Expand All @@ -55,7 +55,7 @@ modules:
- name: margin_ratio
type: float
default: 1.0

- name: BacktestResult
properties:
- name: equity_curve
Expand Down
58 changes: 36 additions & 22 deletions docs/user-guide/orders.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,32 @@

ML4T Backtest supports multiple order types for realistic simulation.

Orders are submitted from a strategy using `broker.submit_order(...)`.

## Market Orders

Execute at the next bar's open price:
```python
from ml4t.backtest.types import OrderType
```

Execute at market according to the configured execution mode/profile:

```python
self.buy(size=100) # Market buy
self.sell(size=100) # Market sell
broker.submit_order("AAPL", 100, order_type=OrderType.MARKET) # buy
broker.submit_order("AAPL", -100, order_type=OrderType.MARKET) # sell
```

## Limit Orders

Execute only if price reaches the limit:

```python
from ml4t.backtest import OrderType

self.buy(size=100, price=99.50, order_type=OrderType.LIMIT)
broker.submit_order(
"AAPL",
100,
order_type=OrderType.LIMIT,
limit_price=99.50,
)
```

- **Buy limit**: Fills if price drops to or below limit
Expand All @@ -29,22 +38,27 @@ self.buy(size=100, price=99.50, order_type=OrderType.LIMIT)
Trigger a market order when stop price is reached:

```python
self.buy(size=100, stop=101.00, order_type=OrderType.STOP)
broker.submit_order(
"AAPL",
-100,
order_type=OrderType.STOP,
stop_price=95.00,
)
```

- **Buy stop**: Triggers when price rises to stop (breakout entry)
- **Sell stop**: Triggers when price falls to stop (stop loss)

## Stop-Limit Orders
## Trailing Stop Orders

Trigger a limit order when stop is reached:
Trailing stops dynamically update the stop level as price moves favorably.

```python
self.buy(
size=100,
stop=101.00,
price=101.50,
order_type=OrderType.STOP_LIMIT
broker.submit_order(
"AAPL",
-100,
order_type=OrderType.TRAILING_STOP,
trail_amount=2.50,
)
```

Expand All @@ -60,12 +74,12 @@ This matches real broker behavior and prevents unrealistic fills.
## Order Management

```python
# Set stop loss after entry
self.set_stop(price=95.00)

# Set profit target
self.set_target(price=110.00)

# Cancel all open orders
self.cancel_all()
broker.get_order(order_id)
broker.get_pending_orders()
broker.update_order(order_id, limit_price=100.0)
broker.cancel_order(order_id)
broker.close_position("AAPL")

# Bracket order helper: entry + take profit + stop loss
broker.submit_bracket("AAPL", quantity=100, take_profit=110, stop_loss=95)
```
107 changes: 42 additions & 65 deletions docs/user-guide/strategies.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,98 +4,75 @@ Learn how to build effective trading strategies with ML4T Backtest.

## Strategy Base Class

All strategies inherit from `Strategy`:
All strategies inherit from `Strategy` and implement `on_data(...)`:

```python
from ml4t.backtest import Strategy
from ml4t.backtest.strategy import Strategy

class MyStrategy(Strategy):
def on_bar(self, bar):
# Called for each price bar
pass

def on_fill(self, fill):
# Called when an order is filled
def on_data(self, timestamp, data, context, broker):
# Called once per bar/timestamp.
# data: {asset: {"open","high","low","close","volume",...}}
pass
```

## Available Methods

### Order Methods
Optional lifecycle hooks:

```python
self.buy(size, price=None) # Submit buy order
self.sell(size, price=None) # Submit sell order
self.close() # Close current position
self.set_stop(price) # Set stop loss
self.set_target(price) # Set profit target
def on_start(self, broker): ...
def on_end(self, broker): ...
```

### Position Info
## Trading Through The Broker

```python
self.position # Current position size (int)
self.equity # Current equity (float)
self.cash # Available cash (float)
```

## Strategy Patterns
Strategies submit orders via the provided `broker` object.

### Momentum Strategy
### Core broker methods

```python
class MomentumStrategy(Strategy):
def __init__(self, fast=10, slow=30):
self.fast = fast
self.slow = slow

def on_bar(self, bar):
fast_ma = bar.close_ma(self.fast)
slow_ma = bar.close_ma(self.slow)

if fast_ma > slow_ma and self.position == 0:
self.buy(size=100)
elif fast_ma < slow_ma and self.position > 0:
self.close()
broker.submit_order("AAPL", 100) # buy market
broker.submit_order("AAPL", -100) # sell market
broker.submit_order("AAPL", 100, order_type=...) # limit/stop/trailing stop
broker.close_position("AAPL")
broker.order_target_percent("AAPL", 0.25)
broker.order_target_value("AAPL", 50_000)
broker.get_position("AAPL")
broker.get_account_value()
broker.get_cash()
```

### Risk-Managed Position Sizing
## Strategy Patterns

### Signal threshold (single asset)

```python
class RiskManagedStrategy(Strategy):
def __init__(self, risk_per_trade=0.02):
self.risk_per_trade = risk_per_trade

def on_bar(self, bar):
if self.position == 0:
# Size based on 2% risk
stop_distance = bar.atr(14) * 2
size = int(self.equity * self.risk_per_trade / stop_distance)
self.buy(size=size)
self.set_stop(bar.close - stop_distance)
class SignalStrategy(Strategy):
def on_data(self, timestamp, data, context, broker):
bar = data.get("AAPL")
if not bar:
return
signal = context.get("signal", 0.0)
pos = broker.get_position("AAPL")
if signal > 0.5 and pos is None:
broker.submit_order("AAPL", 100)
elif signal < -0.5 and pos is not None:
broker.close_position("AAPL")
```

### Signal-Based Strategy

Use ML model predictions as signals:
### Multi-asset rebalance

```python
class SignalStrategy(Strategy):
def __init__(self, signals):
self.signals = signals

def on_bar(self, bar):
signal = self.signals.get(bar.datetime)
class RebalanceStrategy(Strategy):
def __init__(self, target_weights):
self.target_weights = target_weights

if signal > 0.5 and self.position == 0:
self.buy(size=100)
elif signal < -0.5 and self.position > 0:
self.close()
def on_data(self, timestamp, data, context, broker):
broker.rebalance_to_weights(self.target_weights)
```

## Best Practices

1. **Avoid look-ahead bias**: Only use data available at `bar.datetime`
1. **Avoid look-ahead bias**: use only data from the current callback arguments
2. **Account for costs**: Test with realistic commission and slippage
3. **Validate with VectorBT**: Ensure results match expected behavior
3. **Validate engine profile behavior**: check `vectorbt`, `backtrader`, and `zipline` profiles
4. **Size positions appropriately**: Don't risk more than 1-2% per trade
Loading