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
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,60 @@ backtests = [store.open(row["bundle_path"]) for row in top]
BacktestReport(backtests=backtests).save("top20.html")
```

#### Weighted multi-metric ranking with `BacktestEvaluationFocus`

Instead of sorting by a single column, use a **focus preset** to score every bundle across multiple metrics at once — profit, risk, consistency, win rate — weighted by what matters most to your workflow:

```python
from investing_algorithm_framework import (
BacktestReport, BacktestEvaluationFocus,
)
from investing_algorithm_framework.cli.index_command import (
build_index, rank_index,
)
from investing_algorithm_framework.services.backtest_store import (
LocalDirStore,
)

# 1. Build (or refresh) the Tier-1 SQLite index.
build_index("./my-backtests/")

# 2. Rank with a built-in focus preset (BALANCED, PROFIT, FREQUENCY, RISK_ADJUSTED).
top = rank_index(
"./my-backtests/",
focus=BacktestEvaluationFocus.RISK_ADJUSTED,
where="summary_number_of_trades > 50",
limit=25,
)

# 3. Or supply fully custom weights — positive favours higher, negative penalises.
top = rank_index(
"./my-backtests/",
weights={
"sharpe_ratio": 3.0,
"sortino_ratio": 2.5,
"max_drawdown": -3.0,
"win_rate": 2.0,
"consistency_score": 1.5,
},
limit=25,
)

# 4. Materialise only the winners and render a focused dashboard.
store = LocalDirStore("./my-backtests/")
winners = [store.open(row["bundle_path"]) for row in top]
BacktestReport(backtests=winners).save("top25_risk_adjusted.html")
```

**Built-in focus presets:**

| Preset | Prioritises |
|--------|------------|
| `BALANCED` | Equal mix of profit, risk-adjusted returns, drawdown penalties, and consistency |
| `PROFIT` | Absolute and relative gains (CAGR, net gain, win rate, profit factor) |
| `FREQUENCY` | High trade count, short durations, and per-trade efficiency |
| `RISK_ADJUSTED` | Sharpe, Sortino, Calmar with strong drawdown and volatility penalties |

Or from the shell:

```bash
Expand Down
31 changes: 31 additions & 0 deletions docusaurus/docs/Advanced Concepts/blotter.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,37 @@ app.set_blotter(SimulationBlotter(
))
```

### VolumeShareSlippage

Volume-aware model with quadratic price impact. Limits fills to a fraction of bar volume, producing partial fills for large orders relative to liquidity.

```python
from investing_algorithm_framework import SimulationBlotter, VolumeShareSlippage

app.set_blotter(SimulationBlotter(
slippage_model=VolumeShareSlippage(
volume_limit=0.025, # max 2.5% of bar volume
price_impact=0.1, # quadratic impact coefficient
)
))
```

### FixedBasisPointsSlippage

Slippage expressed in basis points (1 bp = 0.01 % of price).

```python
from investing_algorithm_framework import SimulationBlotter, FixedBasisPointsSlippage

app.set_blotter(SimulationBlotter(
slippage_model=FixedBasisPointsSlippage(basis_points=5) # 5 bps = 0.05%
))
```

:::tip TradingCost Integration
Slippage models can also be attached directly to a `TradingCost` via the `slippage_model` parameter, without needing a custom blotter. See [TradingCost — Slippage Models](../Risk%20Rules/trading-cost.md#slippage-models) for details.
:::

### Custom Slippage Model

Create your own by extending `SlippageModel`:
Expand Down
2 changes: 1 addition & 1 deletion docusaurus/docs/Risk Rules/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class MyStrategy(TradingStrategy):
| `take_profits` | [`TakeProfitRule`](./take-profit-rule.md) | Bar-end exit when price rises a fixed or trailing percentage from entry / peak. |
| `scaling_rules` | [`ScalingRule`](./scaling-rule.md) | Pyramid into winners and partially close — `max_entries`, `scale_in_percentage`, `scale_out_percentage`, optional `max_position_percentage` cap. |
| `cooldowns` | [`CooldownRule`](./cooldown-rule.md) | Side-aware, per-symbol or portfolio-wide signal throttling after fills. |
| `trading_costs` | [`TradingCost`](./trading-cost.md) | Per-symbol fees and slippage applied during fill simulation. |
| `trading_costs` | [`TradingCost`](./trading-cost.md) | Per-symbol fees and slippage applied during fill simulation. Supports [pluggable slippage models](./trading-cost.md#slippage-models) (volume-based, fixed spread, basis points). |

## Where Rules Are Enforced

Expand Down
135 changes: 134 additions & 1 deletion docusaurus/docs/Risk Rules/trading-cost.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@ TradingCost(
fee_percentage: float = 0.0,
slippage_percentage: float = 0.0,
fee_fixed: float = 0.0,
slippage_model: SlippageModel | None = None,
)
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `symbol` | `str \| None` | `None` | Target symbol (e.g. `"BTC"`). `None` means "market default" when used at the portfolio level. Symbol matching is case-insensitive. |
| `fee_percentage` | `float` | `0.0` | Variable fee in **percent of trade value** (e.g. `0.1` = 0.1 %). |
| `slippage_percentage` | `float` | `0.0` | Slippage in **percent of price**. Buys fill higher, sells fill lower. |
| `slippage_percentage` | `float` | `0.0` | Slippage in **percent of price**. Buys fill higher, sells fill lower. Ignored when `slippage_model` is set. |
| `fee_fixed` | `float` | `0.0` | Flat fee per trade in the trading currency, added on top of `fee_percentage`. |
| `slippage_model` | `SlippageModel \| None` | `None` | Pluggable slippage model. When set, **overrides** `slippage_percentage`. See [Slippage Models](#slippage-models) below. |

## How Costs Are Applied

Expand All @@ -39,6 +41,8 @@ sell_fill_price = price * (1 - slippage_percentage / 100)
fee = trade_value * fee_percentage / 100 + fee_fixed
```

When a `slippage_model` is set, the model's `calculate_slippage()` method replaces the percentage formula above. The fee calculation stays the same.

`trade_value` is computed at the slippage-adjusted price, so fees compound on top of slippage — matching how real exchanges quote post-trade cost.

## Resolution Order
Expand Down Expand Up @@ -118,8 +122,137 @@ PortfolioConfiguration(
- **`StopLossRule` / `TakeProfitRule`** — slippage is applied to the exit fill (`sell` direction), so reported PnL already reflects realistic exits.
- **`ScalingRule`** — every scale-in and scale-out is a separate fill and pays fees independently.

## Slippage Models

The `slippage_model` parameter lets you plug in sophisticated slippage behavior that goes beyond a flat percentage. When set, it **overrides** the `slippage_percentage` field.

```python
from investing_algorithm_framework import (
TradingCost,
VolumeShareSlippage,
FixedSlippage,
FixedBasisPointsSlippage,
)
```

### VolumeShareSlippage

Models slippage as a function of the order's share of bar volume with a **quadratic** price impact. Also enforces a volume limit — at most `volume_limit` fraction of a bar's volume can be filled per bar. Orders exceeding this limit are partially filled and re-evaluated on subsequent bars.

```python
TradingCost(
symbol="BTC",
fee_percentage=0.1,
slippage_model=VolumeShareSlippage(
volume_limit=0.025, # max 2.5% of bar volume
price_impact=0.1, # price impact coefficient
),
)
```

| Parameter | Default | Description |
|---|---|---|
| `volume_limit` | `0.025` | Max fraction of bar volume that can fill per bar (0.025 = 2.5 %). |
| `price_impact` | `0.1` | Coefficient for quadratic impact: `impact = price_impact × (amount / volume)²`. |

**Impact formula:**

```text
participation = amount / volume
impact = price_impact * participation²

buy_fill_price = price * (1 + impact)
sell_fill_price = price * (1 - impact)
```

This is the most realistic built-in model — strategies that trade illiquid assets or large positions relative to volume will see significant market impact, and orders larger than the volume limit will be partially filled.

### FixedSlippage

Adds or subtracts a fixed amount from the order price. Useful for markets with a known, relatively stable spread.

```python
TradingCost(
symbol="ETH",
fee_percentage=0.1,
slippage_model=FixedSlippage(amount=0.50), # fixed $0.50 spread
)
```

| Parameter | Default | Description |
|---|---|---|
| `amount` | `0.01` | Fixed slippage in price units. |

### FixedBasisPointsSlippage

Slippage expressed in basis points (1 bp = 0.01 % of price). Convenient when you want a proportional slippage without thinking in decimals.

```python
TradingCost(
symbol="BTC",
fee_percentage=0.1,
slippage_model=FixedBasisPointsSlippage(basis_points=5), # 5 bps = 0.05%
)
```

| Parameter | Default | Description |
|---|---|---|
| `basis_points` | `5` | Slippage in basis points. |

### Custom Slippage Model

Create your own by extending `SlippageModel`:

```python
from investing_algorithm_framework import SlippageModel

class MySlippageModel(SlippageModel):
def __init__(self, price_impact=0.1, volume_limit=0.025):
self.price_impact = price_impact
self.volume_limit = volume_limit

def calculate_slippage(self, price, order_side, amount=None, volume=None):
"""Return adjusted fill price."""
if amount and volume and volume > 0:
impact = self.price_impact * (amount / volume) ** 2
else:
impact = 0.0

if order_side == "BUY":
return price * (1 + impact)
return price * (1 - impact)

def max_fill_amount(self, order_amount, volume=None):
"""Return maximum fillable amount for this bar."""
if volume and volume > 0:
return min(order_amount, volume * self.volume_limit)
return order_amount
```

The two methods you can override:

| Method | Required | Description |
|---|---|---|
| `calculate_slippage(price, order_side, amount, volume)` | Yes | Return the adjusted fill price after slippage. |
| `max_fill_amount(order_amount, volume)` | No | Return the maximum fillable amount per bar. Default returns the full `order_amount` (no volume limit). |

### Choosing a Slippage Approach

| Approach | When to use |
|---|---|
| `slippage_percentage` | Quick approximation, don't need volume awareness. |
| `FixedSlippage` | Known fixed spread (e.g. a specific venue). |
| `FixedBasisPointsSlippage` | Proportional slippage in familiar units (basis points). |
| `VolumeShareSlippage` | Realistic simulation — large orders impact price, fills are volume-limited. |
| Custom `SlippageModel` | Any other behavior (e.g. time-of-day effects, asymmetric slippage). |

:::tip Backward Compatibility
Setting `slippage_model` is fully optional. Existing strategies using `slippage_percentage` continue to work unchanged.
:::

## See Also

- [Execution Logic](../Advanced%20Concepts/execution-logic.md)
- [Blotter](../Advanced%20Concepts/blotter.md)
- [Orders](../Getting%20Started/orders.md)
- [Risk Rules Overview](./overview.md)
21 changes: 16 additions & 5 deletions examples/tutorial/notebooks/03_param_sweep.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,14 @@
"id": "9",
"metadata": {},
"source": [
"## Quick filter via SQLite index & open report"
"## Pruning & Ranking Backtests\n",
"\n",
"After the parameter sweep completes, we often have dozens (or hundreds) of backtest bundles on disk. Rather than decoding every `.iafbt` file to compare them, we use the **Tier-1 SQLite index** to rank and prune in milliseconds:\n",
"\n",
"1. **`build_index()`** — scans the results directory and promotes every scalar metric from each bundle into a single SQLite table (one row per backtest). This only touches the lightweight Parquet header, so it scales to 10k+ bundles.\n",
"2. **`rank_index()`** — runs a pure-SQL sort/filter on that index. Here we rank by Sharpe ratio and require at least 5 closed trades, returning the top 20 instantly — no full bundle decode needed.\n",
"3. **`prune_backtests()`** — moves every bundle *not* in the top-N list to an archive folder (`_pruned/`). With `flatten=True` the archive is a flat directory for easy browsing. Bundles are never deleted, so you can always recover them.\n",
"4. **`LocalDirStore.open()`** — materialises only the winning bundles into full `Backtest` objects for the HTML dashboard."
]
},
{
Expand All @@ -494,7 +501,7 @@
"metadata": {},
"outputs": [],
"source": [
"from investing_algorithm_framework import BacktestReport\n",
"from investing_algorithm_framework import BacktestReport, BacktestEvaluationFocus\n",
"from investing_algorithm_framework.cli.index_command import (\n",
" build_index, rank_index, format_table, prune_backtests,\n",
")\n",
Expand All @@ -510,14 +517,17 @@
")\n",
"print(f\"Index written to: {index_path}\")\n",
"\n",
"# 2. Rank by Sharpe and keep the top 20 — pure SQL, instant.\n",
"# 2. Rank using weighted multi-metric scoring with a BALANCED focus.\n",
"# This scores every bundle across profit, risk-adjusted returns,\n",
"# drawdown penalties, and cross-window consistency — all from SQLite.\n",
"# Available presets: BALANCED, PROFIT, FREQUENCY, RISK_ADJUSTED.\n",
"top = rank_index(\n",
" str(backtest_results_dir),\n",
" by=\"sharpe_ratio\",\n",
" focus=BacktestEvaluationFocus.BALANCED,\n",
" where=\"summary_number_of_trades_closed > 5\",\n",
" limit=20,\n",
")\n",
"print(f\"\\nTop {len(top)} strategies by Sharpe ratio:\\n\")\n",
"print(f\"\\nTop {len(top)} strategies (BALANCED focus):\\n\")\n",
"print(format_table(top))\n",
"\n",
"# 3. Prune: move everything outside the top 20 to an archive folder.\n",
Expand All @@ -528,6 +538,7 @@
" keep=top,\n",
" archive_dir=str(archive_path),\n",
" show_progress=True,\n",
" flatten=True, # flat archive for easy browsing — no nested folders\n",
")\n",
"print(f\"\\nKept {result['kept']} bundles, pruned {result['pruned']} → {result['archive_dir']}\")\n",
"\n",
Expand Down
4 changes: 3 additions & 1 deletion investing_algorithm_framework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
retag_backtests, migrate_backtests, \
Blotter, DefaultBlotter, SimulationBlotter, Transaction, \
SlippageModel, NoSlippage, PercentageSlippage, FixedSlippage, \
VolumeImpactSlippage, \
VolumeImpactSlippage, VolumeShareSlippage, FixedBasisPointsSlippage, \
CommissionModel, NoCommission, PercentageCommission, FixedCommission, \
FillModel, FullFill, VolumeBasedFill, \
FXRateProvider, StaticFXRateProvider, \
Expand Down Expand Up @@ -258,6 +258,8 @@
"PercentageSlippage",
"FixedSlippage",
"VolumeImpactSlippage",
"VolumeShareSlippage",
"FixedBasisPointsSlippage",
"CommissionModel",
"NoCommission",
"PercentageCommission",
Expand Down
Loading
Loading