bagelquant-bt 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bagelquant_bt-0.1.0/PKG-INFO +71 -0
- bagelquant_bt-0.1.0/README.md +59 -0
- bagelquant_bt-0.1.0/pyproject.toml +34 -0
- bagelquant_bt-0.1.0/src/bagelquant_bt/__init__.py +32 -0
- bagelquant_bt-0.1.0/src/bagelquant_bt/config.py +47 -0
- bagelquant_bt-0.1.0/src/bagelquant_bt/costs.py +31 -0
- bagelquant_bt-0.1.0/src/bagelquant_bt/engine.py +207 -0
- bagelquant_bt-0.1.0/src/bagelquant_bt/exceptions.py +13 -0
- bagelquant_bt-0.1.0/src/bagelquant_bt/factor.py +662 -0
- bagelquant_bt-0.1.0/src/bagelquant_bt/inputs.py +81 -0
- bagelquant_bt-0.1.0/src/bagelquant_bt/performance.py +216 -0
- bagelquant_bt-0.1.0/src/bagelquant_bt/reporting.py +371 -0
- bagelquant_bt-0.1.0/src/bagelquant_bt/results.py +76 -0
- bagelquant_bt-0.1.0/src/bagelquant_bt/returns.py +230 -0
- bagelquant_bt-0.1.0/src/bagelquant_bt/visualization.py +410 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: bagelquant-bt
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Backtesting and factor evaluation for the BagelQuant ecosystem
|
|
5
|
+
Author: BagelQuant
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Requires-Dist: numpy>=2.4.0
|
|
8
|
+
Requires-Dist: plotly>=6.0.0
|
|
9
|
+
Requires-Dist: polars>=1.35.0
|
|
10
|
+
Requires-Python: >=3.13
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# bagelquant-bt
|
|
14
|
+
|
|
15
|
+
`bagelquant-bt` provides backtesting, factor evaluation, performance metrics, and
|
|
16
|
+
Plotly visualization for long-form Polars panels.
|
|
17
|
+
|
|
18
|
+
The public input shape is always explicit:
|
|
19
|
+
|
|
20
|
+
- prices: `time`, `asset_id`, `price`
|
|
21
|
+
- weights: `time`, `asset_id`, `weight`
|
|
22
|
+
- factors: `time`, `asset_id`, `factor`
|
|
23
|
+
|
|
24
|
+
Weights at `time=t` earn each asset's close-to-close return from `t` to the next
|
|
25
|
+
available observation.
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import polars as pl
|
|
29
|
+
|
|
30
|
+
from bagelquant_bt import BacktestConfig, run_backtest
|
|
31
|
+
|
|
32
|
+
prices = pl.DataFrame(
|
|
33
|
+
{
|
|
34
|
+
"time": ["2024-01-02", "2024-01-03"],
|
|
35
|
+
"asset_id": ["AAA", "AAA"],
|
|
36
|
+
"price": [100.0, 102.0],
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
weights = pl.DataFrame(
|
|
40
|
+
{"time": ["2024-01-02"], "asset_id": ["AAA"], "weight": [1.0]}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
result = run_backtest(
|
|
44
|
+
weights,
|
|
45
|
+
prices,
|
|
46
|
+
kind="weights",
|
|
47
|
+
config=BacktestConfig(initial_capital=1_000_000),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
print(result.returns)
|
|
51
|
+
print(result.summary)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Factor evaluation computes daily cross-sectional IC, quantile returns, a
|
|
55
|
+
top-minus-bottom spread, and a TOP N equal-weight backtest.
|
|
56
|
+
|
|
57
|
+
Visualization helpers return Plotly figures:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from bagelquant_bt.visualization import plot_cumulative_returns
|
|
61
|
+
|
|
62
|
+
fig = plot_cumulative_returns(result)
|
|
63
|
+
fig.write_html("cumulative_returns.html")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Development
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv run ruff check .
|
|
70
|
+
uv run pytest
|
|
71
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# bagelquant-bt
|
|
2
|
+
|
|
3
|
+
`bagelquant-bt` provides backtesting, factor evaluation, performance metrics, and
|
|
4
|
+
Plotly visualization for long-form Polars panels.
|
|
5
|
+
|
|
6
|
+
The public input shape is always explicit:
|
|
7
|
+
|
|
8
|
+
- prices: `time`, `asset_id`, `price`
|
|
9
|
+
- weights: `time`, `asset_id`, `weight`
|
|
10
|
+
- factors: `time`, `asset_id`, `factor`
|
|
11
|
+
|
|
12
|
+
Weights at `time=t` earn each asset's close-to-close return from `t` to the next
|
|
13
|
+
available observation.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import polars as pl
|
|
17
|
+
|
|
18
|
+
from bagelquant_bt import BacktestConfig, run_backtest
|
|
19
|
+
|
|
20
|
+
prices = pl.DataFrame(
|
|
21
|
+
{
|
|
22
|
+
"time": ["2024-01-02", "2024-01-03"],
|
|
23
|
+
"asset_id": ["AAA", "AAA"],
|
|
24
|
+
"price": [100.0, 102.0],
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
weights = pl.DataFrame(
|
|
28
|
+
{"time": ["2024-01-02"], "asset_id": ["AAA"], "weight": [1.0]}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
result = run_backtest(
|
|
32
|
+
weights,
|
|
33
|
+
prices,
|
|
34
|
+
kind="weights",
|
|
35
|
+
config=BacktestConfig(initial_capital=1_000_000),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
print(result.returns)
|
|
39
|
+
print(result.summary)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Factor evaluation computes daily cross-sectional IC, quantile returns, a
|
|
43
|
+
top-minus-bottom spread, and a TOP N equal-weight backtest.
|
|
44
|
+
|
|
45
|
+
Visualization helpers return Plotly figures:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from bagelquant_bt.visualization import plot_cumulative_returns
|
|
49
|
+
|
|
50
|
+
fig = plot_cumulative_returns(result)
|
|
51
|
+
fig.write_html("cumulative_returns.html")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Development
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
uv run ruff check .
|
|
58
|
+
uv run pytest
|
|
59
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "bagelquant-bt"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Backtesting and factor evaluation for the BagelQuant ecosystem"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
license = { text = "Apache-2.0" }
|
|
8
|
+
authors = [{ name = "BagelQuant" }]
|
|
9
|
+
dependencies = [
|
|
10
|
+
"numpy>=2.4.0",
|
|
11
|
+
"plotly>=6.0.0",
|
|
12
|
+
"polars>=1.35.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[dependency-groups]
|
|
16
|
+
dev = [
|
|
17
|
+
"pytest>=9.0.0",
|
|
18
|
+
"ruff>=0.14.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["uv_build>=0.8.0,<0.9.0"]
|
|
23
|
+
build-backend = "uv_build"
|
|
24
|
+
|
|
25
|
+
[tool.pytest.ini_options]
|
|
26
|
+
pythonpath = ["src"]
|
|
27
|
+
testpaths = ["tests"]
|
|
28
|
+
|
|
29
|
+
[tool.ruff]
|
|
30
|
+
line-length = 88
|
|
31
|
+
src = ["src", "tests"]
|
|
32
|
+
|
|
33
|
+
[tool.ruff.lint]
|
|
34
|
+
select = ["E", "F", "I", "B", "UP", "RUF"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Backtesting and factor evaluation for the BagelQuant ecosystem."""
|
|
2
|
+
|
|
3
|
+
from .config import BacktestConfig, TransactionCostConfig
|
|
4
|
+
from .engine import run_weight_backtest
|
|
5
|
+
from .exceptions import (
|
|
6
|
+
BacktestConfigError,
|
|
7
|
+
BagelQuantBacktestError,
|
|
8
|
+
InputValidationError,
|
|
9
|
+
)
|
|
10
|
+
from .factor import run_factor_evaluation
|
|
11
|
+
from .reporting import summary_report
|
|
12
|
+
from .results import (
|
|
13
|
+
BacktestResult,
|
|
14
|
+
FactorEvaluationResult,
|
|
15
|
+
PerformanceSummary,
|
|
16
|
+
TransactionCostBreakdown,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"BacktestConfig",
|
|
21
|
+
"BacktestConfigError",
|
|
22
|
+
"BacktestResult",
|
|
23
|
+
"BagelQuantBacktestError",
|
|
24
|
+
"FactorEvaluationResult",
|
|
25
|
+
"InputValidationError",
|
|
26
|
+
"PerformanceSummary",
|
|
27
|
+
"TransactionCostBreakdown",
|
|
28
|
+
"TransactionCostConfig",
|
|
29
|
+
"run_factor_evaluation",
|
|
30
|
+
"run_weight_backtest",
|
|
31
|
+
"summary_report",
|
|
32
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Configuration objects for backtesting and factor evaluation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from .exceptions import BacktestConfigError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class TransactionCostConfig:
|
|
12
|
+
"""Per-trade transaction cost settings."""
|
|
13
|
+
|
|
14
|
+
rate: float = 0.00015
|
|
15
|
+
min_fee: float = 5.0
|
|
16
|
+
|
|
17
|
+
def __post_init__(self) -> None:
|
|
18
|
+
if self.rate < 0:
|
|
19
|
+
raise BacktestConfigError("transaction cost rate must be nonnegative")
|
|
20
|
+
if self.min_fee < 0:
|
|
21
|
+
raise BacktestConfigError("transaction cost min_fee must be nonnegative")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class BacktestConfig:
|
|
26
|
+
"""Shared backtest and factor evaluation configuration."""
|
|
27
|
+
|
|
28
|
+
initial_capital: float
|
|
29
|
+
transaction_cost: TransactionCostConfig = field(
|
|
30
|
+
default_factory=TransactionCostConfig
|
|
31
|
+
)
|
|
32
|
+
annualization: int = 252
|
|
33
|
+
ic_method: str = "spearman"
|
|
34
|
+
quantiles: int = 5
|
|
35
|
+
top_n: int = 50
|
|
36
|
+
|
|
37
|
+
def __post_init__(self) -> None:
|
|
38
|
+
if self.initial_capital <= 0:
|
|
39
|
+
raise BacktestConfigError("initial_capital must be positive")
|
|
40
|
+
if self.annualization <= 0:
|
|
41
|
+
raise BacktestConfigError("annualization must be positive")
|
|
42
|
+
if self.ic_method not in {"spearman", "pearson"}:
|
|
43
|
+
raise BacktestConfigError("ic_method must be 'spearman' or 'pearson'")
|
|
44
|
+
if self.quantiles < 2:
|
|
45
|
+
raise BacktestConfigError("quantiles must be at least 2")
|
|
46
|
+
if self.top_n <= 0:
|
|
47
|
+
raise BacktestConfigError("top_n must be positive")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Transaction cost and turnover calculations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import polars as pl
|
|
6
|
+
|
|
7
|
+
from .inputs import ASSET_ID, TIME
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def turnover(weights: pl.DataFrame) -> pl.DataFrame:
|
|
11
|
+
"""Compute daily absolute weight turnover."""
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
weights.sort([ASSET_ID, TIME])
|
|
15
|
+
.with_columns(
|
|
16
|
+
pl.col("weight")
|
|
17
|
+
.fill_null(0.0)
|
|
18
|
+
.shift(1)
|
|
19
|
+
.over(ASSET_ID)
|
|
20
|
+
.fill_null(0.0)
|
|
21
|
+
.alias("previous_weight")
|
|
22
|
+
)
|
|
23
|
+
.with_columns(
|
|
24
|
+
(pl.col("weight").fill_null(0.0) - pl.col("previous_weight"))
|
|
25
|
+
.abs()
|
|
26
|
+
.alias("weight_delta")
|
|
27
|
+
)
|
|
28
|
+
.group_by(TIME)
|
|
29
|
+
.agg(pl.col("weight_delta").sum().alias("turnover"))
|
|
30
|
+
.sort(TIME)
|
|
31
|
+
)
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Backtest orchestration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import polars as pl
|
|
6
|
+
|
|
7
|
+
from .config import BacktestConfig
|
|
8
|
+
from .costs import turnover
|
|
9
|
+
from .exceptions import BacktestConfigError, InputValidationError
|
|
10
|
+
from .inputs import ASSET_ID, TIME, validate_prices, validate_weights
|
|
11
|
+
from .performance import summarize_performance
|
|
12
|
+
from .results import BacktestResult, TransactionCostBreakdown
|
|
13
|
+
from .returns import (
|
|
14
|
+
_expand_portfolio_weights,
|
|
15
|
+
asset_close_to_close_returns,
|
|
16
|
+
cumulative_returns,
|
|
17
|
+
portfolio_returns,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def run_weight_backtest(
|
|
22
|
+
weights: pl.DataFrame,
|
|
23
|
+
prices: pl.DataFrame,
|
|
24
|
+
*,
|
|
25
|
+
config: BacktestConfig | None = None,
|
|
26
|
+
) -> BacktestResult:
|
|
27
|
+
"""Backtest a long-form portfolio weight frame."""
|
|
28
|
+
|
|
29
|
+
resolved_config = _require_config(config)
|
|
30
|
+
aligned_weights = validate_weights(weights)
|
|
31
|
+
aligned_prices = validate_prices(prices)
|
|
32
|
+
return backtest_weight_frame(
|
|
33
|
+
aligned_weights,
|
|
34
|
+
aligned_prices,
|
|
35
|
+
config=resolved_config,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def backtest_weight_frame(
|
|
40
|
+
weights: pl.DataFrame,
|
|
41
|
+
prices: pl.DataFrame,
|
|
42
|
+
*,
|
|
43
|
+
config: BacktestConfig,
|
|
44
|
+
) -> BacktestResult:
|
|
45
|
+
"""Backtest an already materialized long-form weight frame."""
|
|
46
|
+
|
|
47
|
+
aligned_weights = validate_weights(weights)
|
|
48
|
+
aligned_prices = validate_prices(prices)
|
|
49
|
+
forward_returns = asset_close_to_close_returns(aligned_prices)
|
|
50
|
+
return _backtest_weight_frame_with_forward_returns(
|
|
51
|
+
aligned_weights,
|
|
52
|
+
aligned_prices,
|
|
53
|
+
forward_returns,
|
|
54
|
+
config=config,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _backtest_weight_frame_with_forward_returns(
|
|
59
|
+
weights: pl.DataFrame,
|
|
60
|
+
prices: pl.DataFrame,
|
|
61
|
+
forward_returns: pl.DataFrame,
|
|
62
|
+
*,
|
|
63
|
+
config: BacktestConfig,
|
|
64
|
+
) -> BacktestResult:
|
|
65
|
+
"""Backtest a weight frame with a precomputed forward-return panel."""
|
|
66
|
+
|
|
67
|
+
executable_weights = _expand_portfolio_weights(weights, prices, forward_returns)
|
|
68
|
+
if executable_weights.is_empty():
|
|
69
|
+
raise InputValidationError("at least two overlapping price times are required")
|
|
70
|
+
|
|
71
|
+
gross_returns = portfolio_returns(executable_weights, forward_returns)
|
|
72
|
+
turn = turnover(executable_weights)
|
|
73
|
+
costs, returns, value = _simulate_cost_adjusted_returns(
|
|
74
|
+
weights=executable_weights,
|
|
75
|
+
gross_returns=gross_returns,
|
|
76
|
+
config=config,
|
|
77
|
+
)
|
|
78
|
+
summary, performance = summarize_performance(
|
|
79
|
+
returns=returns,
|
|
80
|
+
turnover=turn,
|
|
81
|
+
costs=costs,
|
|
82
|
+
initial_capital=config.initial_capital,
|
|
83
|
+
annualization=config.annualization,
|
|
84
|
+
)
|
|
85
|
+
return BacktestResult(
|
|
86
|
+
weights=executable_weights,
|
|
87
|
+
asset_returns=forward_returns,
|
|
88
|
+
returns=returns,
|
|
89
|
+
value=value,
|
|
90
|
+
turnover=turn,
|
|
91
|
+
transaction_costs=costs,
|
|
92
|
+
summary=summary,
|
|
93
|
+
performance=performance,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _simulate_cost_adjusted_returns(
|
|
98
|
+
*,
|
|
99
|
+
weights: pl.DataFrame,
|
|
100
|
+
gross_returns: pl.DataFrame,
|
|
101
|
+
config: BacktestConfig,
|
|
102
|
+
) -> tuple[TransactionCostBreakdown, pl.DataFrame, pl.DataFrame]:
|
|
103
|
+
trade_summary = _trade_summary(weights)
|
|
104
|
+
gross_by_time = {
|
|
105
|
+
row[TIME]: float(row["gross_return"] or 0.0)
|
|
106
|
+
for row in gross_returns.iter_rows(named=True)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
current_value = float(config.initial_capital)
|
|
110
|
+
cost_rows: list[dict[str, object]] = []
|
|
111
|
+
return_rows: list[dict[str, object]] = []
|
|
112
|
+
value_rows: list[dict[str, object]] = []
|
|
113
|
+
|
|
114
|
+
for row in trade_summary.iter_rows(named=True):
|
|
115
|
+
time = row[TIME]
|
|
116
|
+
weight_deltas = [float(delta) for delta in row["weight_deltas"]]
|
|
117
|
+
traded_asset_count = len(weight_deltas)
|
|
118
|
+
weight_delta = sum(weight_deltas)
|
|
119
|
+
traded_notional = weight_delta * current_value
|
|
120
|
+
raw_fee = traded_notional * config.transaction_cost.rate
|
|
121
|
+
total_fee = sum(
|
|
122
|
+
max(
|
|
123
|
+
delta * current_value * config.transaction_cost.rate,
|
|
124
|
+
config.transaction_cost.min_fee,
|
|
125
|
+
)
|
|
126
|
+
for delta in weight_deltas
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
cost_return = total_fee / current_value if current_value else 0.0
|
|
130
|
+
gross_return = gross_by_time.get(time, 0.0)
|
|
131
|
+
net_return = gross_return - cost_return
|
|
132
|
+
gross_value = current_value * (1.0 + gross_return)
|
|
133
|
+
current_value *= 1.0 + net_return
|
|
134
|
+
cost_rows.append(
|
|
135
|
+
{
|
|
136
|
+
TIME: time,
|
|
137
|
+
"traded_asset_count": traded_asset_count,
|
|
138
|
+
"traded_notional": traded_notional,
|
|
139
|
+
"raw_fee": raw_fee,
|
|
140
|
+
"min_fee_adjustment": total_fee - raw_fee,
|
|
141
|
+
"total_fee": total_fee,
|
|
142
|
+
"cost_return": cost_return,
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
return_rows.append(
|
|
146
|
+
{TIME: time, "gross_return": gross_return, "net_return": net_return}
|
|
147
|
+
)
|
|
148
|
+
value_rows.append(
|
|
149
|
+
{TIME: time, "gross_value": gross_value, "net_value": current_value}
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
returns = pl.DataFrame(return_rows).sort(TIME)
|
|
153
|
+
value = (
|
|
154
|
+
pl.DataFrame(value_rows)
|
|
155
|
+
.sort(TIME)
|
|
156
|
+
.join(
|
|
157
|
+
cumulative_returns(returns, "gross_return"),
|
|
158
|
+
on=TIME,
|
|
159
|
+
)
|
|
160
|
+
.join(
|
|
161
|
+
cumulative_returns(returns, "net_return"),
|
|
162
|
+
on=TIME,
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
return TransactionCostBreakdown(pl.DataFrame(cost_rows).sort(TIME)), returns, value
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _trade_summary(weights: pl.DataFrame) -> pl.DataFrame:
|
|
169
|
+
return (
|
|
170
|
+
weights.sort([ASSET_ID, TIME])
|
|
171
|
+
.with_columns(
|
|
172
|
+
pl.col("weight")
|
|
173
|
+
.fill_null(0.0)
|
|
174
|
+
.shift(1)
|
|
175
|
+
.over(ASSET_ID)
|
|
176
|
+
.fill_null(0.0)
|
|
177
|
+
.alias("previous_weight")
|
|
178
|
+
)
|
|
179
|
+
.with_columns(
|
|
180
|
+
(pl.col("weight").fill_null(0.0) - pl.col("previous_weight"))
|
|
181
|
+
.abs()
|
|
182
|
+
.alias("weight_delta")
|
|
183
|
+
)
|
|
184
|
+
.group_by(TIME)
|
|
185
|
+
.agg(
|
|
186
|
+
pl.col("weight_delta")
|
|
187
|
+
.filter(pl.col("weight_delta") > 0.0)
|
|
188
|
+
.alias("weight_deltas"),
|
|
189
|
+
)
|
|
190
|
+
.sort(TIME)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _require_config(config: BacktestConfig | None) -> BacktestConfig:
|
|
195
|
+
if config is None:
|
|
196
|
+
raise BacktestConfigError(
|
|
197
|
+
"config is required because initial_capital is needed for minimum fees"
|
|
198
|
+
)
|
|
199
|
+
return config
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _partition_key(key: object) -> object:
|
|
203
|
+
if isinstance(key, tuple):
|
|
204
|
+
return key[0]
|
|
205
|
+
if isinstance(key, list):
|
|
206
|
+
return key[0]
|
|
207
|
+
return key
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Package-specific exceptions for bagelquant-bt."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BagelQuantBacktestError(Exception):
|
|
5
|
+
"""Base exception for bagelquant-bt."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class InputValidationError(BagelQuantBacktestError):
|
|
9
|
+
"""Raised when user-provided tabular inputs are invalid."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BacktestConfigError(BagelQuantBacktestError):
|
|
13
|
+
"""Raised when backtest configuration values are invalid."""
|