pwb-toolbox 0.1.6__tar.gz → 0.1.8__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.
- {pwb_toolbox-0.1.6 → pwb_toolbox-0.1.8}/PKG-INFO +78 -3
- {pwb_toolbox-0.1.6 → pwb_toolbox-0.1.8}/README.md +73 -2
- pwb_toolbox-0.1.8/pwb_toolbox/backtest/__init__.py +57 -0
- pwb_toolbox-0.1.8/pwb_toolbox/backtest/base_strategy.py +33 -0
- pwb_toolbox-0.1.8/pwb_toolbox/backtest/execution_models/__init__.py +153 -0
- pwb_toolbox-0.1.8/pwb_toolbox/backtest/ib_connector.py +69 -0
- pwb_toolbox-0.1.8/pwb_toolbox/backtest/insight.py +21 -0
- pwb_toolbox-0.1.8/pwb_toolbox/backtest/portfolio_models/__init__.py +290 -0
- pwb_toolbox-0.1.8/pwb_toolbox/backtest/risk_models/__init__.py +175 -0
- pwb_toolbox-0.1.8/pwb_toolbox/backtest/universe_models/__init__.py +183 -0
- {pwb_toolbox-0.1.6 → pwb_toolbox-0.1.8}/pwb_toolbox/datasets/__init__.py +8 -5
- pwb_toolbox-0.1.8/pwb_toolbox/performance/__init__.py +123 -0
- pwb_toolbox-0.1.8/pwb_toolbox/performance/metrics.py +465 -0
- pwb_toolbox-0.1.8/pwb_toolbox/performance/plots.py +415 -0
- pwb_toolbox-0.1.8/pwb_toolbox/performance/trade_stats.py +138 -0
- {pwb_toolbox-0.1.6 → pwb_toolbox-0.1.8}/pwb_toolbox.egg-info/PKG-INFO +78 -3
- pwb_toolbox-0.1.8/pwb_toolbox.egg-info/SOURCES.txt +30 -0
- {pwb_toolbox-0.1.6 → pwb_toolbox-0.1.8}/pwb_toolbox.egg-info/requires.txt +2 -0
- {pwb_toolbox-0.1.6 → pwb_toolbox-0.1.8}/setup.cfg +6 -2
- pwb_toolbox-0.1.8/tests/test_backtest.py +29 -0
- pwb_toolbox-0.1.8/tests/test_execution_models.py +114 -0
- pwb_toolbox-0.1.8/tests/test_hf_token.py +24 -0
- pwb_toolbox-0.1.8/tests/test_ib_connector.py +31 -0
- pwb_toolbox-0.1.8/tests/test_portfolio_models.py +111 -0
- pwb_toolbox-0.1.8/tests/test_risk_models.py +77 -0
- pwb_toolbox-0.1.8/tests/test_universe_models.py +54 -0
- pwb_toolbox-0.1.6/pwb_toolbox.egg-info/SOURCES.txt +0 -11
- {pwb_toolbox-0.1.6 → pwb_toolbox-0.1.8}/LICENSE.txt +0 -0
- {pwb_toolbox-0.1.6 → pwb_toolbox-0.1.8}/pwb_toolbox/__init__.py +0 -0
- {pwb_toolbox-0.1.6 → pwb_toolbox-0.1.8}/pwb_toolbox.egg-info/dependency_links.txt +0 -0
- {pwb_toolbox-0.1.6 → pwb_toolbox-0.1.8}/pwb_toolbox.egg-info/top_level.txt +0 -0
- {pwb_toolbox-0.1.6 → pwb_toolbox-0.1.8}/pyproject.toml +0 -0
@@ -1,19 +1,23 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pwb-toolbox
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.8
|
4
4
|
Summary: A toolbox library for quant traders
|
5
5
|
Home-page: https://github.com/paperswithbacktest/pwb-toolbox
|
6
6
|
Author: Your Name
|
7
7
|
Author-email: hello@paperswithbacktest.com
|
8
8
|
License: MIT
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
10
12
|
Classifier: License :: OSI Approved :: MIT License
|
11
13
|
Classifier: Operating System :: OS Independent
|
12
|
-
Requires-Python: >=3.
|
14
|
+
Requires-Python: >=3.10
|
13
15
|
Description-Content-Type: text/markdown
|
14
16
|
License-File: LICENSE.txt
|
15
17
|
Requires-Dist: datasets
|
16
18
|
Requires-Dist: pandas
|
19
|
+
Requires-Dist: ibapi
|
20
|
+
Requires-Dist: ib_insync
|
17
21
|
Dynamic: license-file
|
18
22
|
|
19
23
|
<div align="center">
|
@@ -31,6 +35,7 @@ To install the pwb-toolbox package:
|
|
31
35
|
```bash
|
32
36
|
pip install pwb-toolbox
|
33
37
|
```
|
38
|
+
This package requires Python 3.10 or higher.
|
34
39
|
|
35
40
|
To login to Huggingface Hub with Access Token
|
36
41
|
|
@@ -116,6 +121,77 @@ df = pwb_ds.load_dataset(
|
|
116
121
|
)
|
117
122
|
```
|
118
123
|
|
124
|
+
## Backtest engine
|
125
|
+
|
126
|
+
The `pwb_toolbox.backtest` module offers simple building blocks for running
|
127
|
+
Backtrader simulations. Alpha models generate `Insight` objects which are turned
|
128
|
+
into portfolio weights and executed via Backtrader orders.
|
129
|
+
|
130
|
+
```python
|
131
|
+
from pwb_toolbox.backtest.examples import GoldenCrossAlpha, EqualWeightPortfolio
|
132
|
+
from pwb_toolbox.backtest import run_backtest
|
133
|
+
from pwb_toolbox.backtest.execution_models import ImmediateExecutionModel
|
134
|
+
from pwb_toolbox.backtest.risk_models import MaximumTotalPortfolioExposure
|
135
|
+
from pwb_toolbox.backtest.universe_models import ManualUniverseSelectionModel
|
136
|
+
|
137
|
+
run_backtest(
|
138
|
+
ManualUniverseSelectionModel(["SPY", "QQQ"]),
|
139
|
+
GoldenCrossAlpha(),
|
140
|
+
EqualWeightPortfolio(),
|
141
|
+
execution=ImmediateExecutionModel(),
|
142
|
+
risk=MaximumTotalPortfolioExposure(max_exposure=1.0),
|
143
|
+
start="2015-01-01",
|
144
|
+
)
|
145
|
+
```
|
146
|
+
|
147
|
+
## Performance Analysis
|
148
|
+
|
149
|
+
After running a backtest you can analyze the returned equity series using the
|
150
|
+
`pwb_toolbox.performance` module.
|
151
|
+
|
152
|
+
```python
|
153
|
+
from pwb_toolbox.backtest.examples import GoldenCrossAlpha, EqualWeightPortfolio
|
154
|
+
from pwb_toolbox.backtest import run_backtest
|
155
|
+
from pwb_toolbox.backtest.execution_models import ImmediateExecutionModel
|
156
|
+
from pwb_toolbox.performance import total_return, cagr
|
157
|
+
from pwb_toolbox.performance.plots import plot_equity_curve
|
158
|
+
|
159
|
+
result, equity = run_backtest(
|
160
|
+
ManualUniverseSelectionModel(["SPY", "QQQ"]),
|
161
|
+
GoldenCrossAlpha(),
|
162
|
+
EqualWeightPortfolio(),
|
163
|
+
execution=ImmediateExecutionModel(),
|
164
|
+
start="2015-01-01",
|
165
|
+
)
|
166
|
+
|
167
|
+
print("Total return:", total_return(equity))
|
168
|
+
print("CAGR:", cagr(equity))
|
169
|
+
|
170
|
+
plot_equity_curve(equity)
|
171
|
+
```
|
172
|
+
|
173
|
+
Plotting utilities require `matplotlib`; some metrics also need `pandas`.
|
174
|
+
|
175
|
+
## Live trading with Interactive Brokers
|
176
|
+
|
177
|
+
`run_ib_strategy` streams Interactive Brokers data and orders. Install `ibapi` and either `atreyu-backtrader-api` or `ib_insync`.
|
178
|
+
|
179
|
+
```python
|
180
|
+
from pwb_toolbox.backtest import IBConnector, run_ib_strategy
|
181
|
+
from pwb_toolbox.backtest.example.engine import SimpleIBStrategy
|
182
|
+
|
183
|
+
data_cfg = [{"dataname": "AAPL", "name": "AAPL"}]
|
184
|
+
run_ib_strategy(
|
185
|
+
SimpleIBStrategy,
|
186
|
+
data_cfg,
|
187
|
+
host="127.0.0.1",
|
188
|
+
port=7497,
|
189
|
+
client_id=1,
|
190
|
+
)
|
191
|
+
```
|
192
|
+
|
193
|
+
Configure `host`, `port`, and `client_id` to match your TWS or Gateway settings. Test with an Interactive Brokers paper account before trading live.
|
194
|
+
|
119
195
|
## Contributing
|
120
196
|
|
121
197
|
Contributions to the `pwb-toolbox` package are welcome! If you have any improvements, new datasets, or strategy ideas to share, please follow these guidelines:
|
@@ -150,5 +226,4 @@ The `pwb-toolbox` package is released under the MIT license. See the LICENSE fil
|
|
150
226
|
## Contact
|
151
227
|
|
152
228
|
For any questions, issues, or suggestions regarding the `pwb-toolbox` package, please contact the maintainers or create an issue on the repository. We appreciate your feedback and involvement in improving the package.
|
153
|
-
|
154
229
|
Happy trading!
|
@@ -13,6 +13,7 @@ To install the pwb-toolbox package:
|
|
13
13
|
```bash
|
14
14
|
pip install pwb-toolbox
|
15
15
|
```
|
16
|
+
This package requires Python 3.10 or higher.
|
16
17
|
|
17
18
|
To login to Huggingface Hub with Access Token
|
18
19
|
|
@@ -98,6 +99,77 @@ df = pwb_ds.load_dataset(
|
|
98
99
|
)
|
99
100
|
```
|
100
101
|
|
102
|
+
## Backtest engine
|
103
|
+
|
104
|
+
The `pwb_toolbox.backtest` module offers simple building blocks for running
|
105
|
+
Backtrader simulations. Alpha models generate `Insight` objects which are turned
|
106
|
+
into portfolio weights and executed via Backtrader orders.
|
107
|
+
|
108
|
+
```python
|
109
|
+
from pwb_toolbox.backtest.examples import GoldenCrossAlpha, EqualWeightPortfolio
|
110
|
+
from pwb_toolbox.backtest import run_backtest
|
111
|
+
from pwb_toolbox.backtest.execution_models import ImmediateExecutionModel
|
112
|
+
from pwb_toolbox.backtest.risk_models import MaximumTotalPortfolioExposure
|
113
|
+
from pwb_toolbox.backtest.universe_models import ManualUniverseSelectionModel
|
114
|
+
|
115
|
+
run_backtest(
|
116
|
+
ManualUniverseSelectionModel(["SPY", "QQQ"]),
|
117
|
+
GoldenCrossAlpha(),
|
118
|
+
EqualWeightPortfolio(),
|
119
|
+
execution=ImmediateExecutionModel(),
|
120
|
+
risk=MaximumTotalPortfolioExposure(max_exposure=1.0),
|
121
|
+
start="2015-01-01",
|
122
|
+
)
|
123
|
+
```
|
124
|
+
|
125
|
+
## Performance Analysis
|
126
|
+
|
127
|
+
After running a backtest you can analyze the returned equity series using the
|
128
|
+
`pwb_toolbox.performance` module.
|
129
|
+
|
130
|
+
```python
|
131
|
+
from pwb_toolbox.backtest.examples import GoldenCrossAlpha, EqualWeightPortfolio
|
132
|
+
from pwb_toolbox.backtest import run_backtest
|
133
|
+
from pwb_toolbox.backtest.execution_models import ImmediateExecutionModel
|
134
|
+
from pwb_toolbox.performance import total_return, cagr
|
135
|
+
from pwb_toolbox.performance.plots import plot_equity_curve
|
136
|
+
|
137
|
+
result, equity = run_backtest(
|
138
|
+
ManualUniverseSelectionModel(["SPY", "QQQ"]),
|
139
|
+
GoldenCrossAlpha(),
|
140
|
+
EqualWeightPortfolio(),
|
141
|
+
execution=ImmediateExecutionModel(),
|
142
|
+
start="2015-01-01",
|
143
|
+
)
|
144
|
+
|
145
|
+
print("Total return:", total_return(equity))
|
146
|
+
print("CAGR:", cagr(equity))
|
147
|
+
|
148
|
+
plot_equity_curve(equity)
|
149
|
+
```
|
150
|
+
|
151
|
+
Plotting utilities require `matplotlib`; some metrics also need `pandas`.
|
152
|
+
|
153
|
+
## Live trading with Interactive Brokers
|
154
|
+
|
155
|
+
`run_ib_strategy` streams Interactive Brokers data and orders. Install `ibapi` and either `atreyu-backtrader-api` or `ib_insync`.
|
156
|
+
|
157
|
+
```python
|
158
|
+
from pwb_toolbox.backtest import IBConnector, run_ib_strategy
|
159
|
+
from pwb_toolbox.backtest.example.engine import SimpleIBStrategy
|
160
|
+
|
161
|
+
data_cfg = [{"dataname": "AAPL", "name": "AAPL"}]
|
162
|
+
run_ib_strategy(
|
163
|
+
SimpleIBStrategy,
|
164
|
+
data_cfg,
|
165
|
+
host="127.0.0.1",
|
166
|
+
port=7497,
|
167
|
+
client_id=1,
|
168
|
+
)
|
169
|
+
```
|
170
|
+
|
171
|
+
Configure `host`, `port`, and `client_id` to match your TWS or Gateway settings. Test with an Interactive Brokers paper account before trading live.
|
172
|
+
|
101
173
|
## Contributing
|
102
174
|
|
103
175
|
Contributions to the `pwb-toolbox` package are welcome! If you have any improvements, new datasets, or strategy ideas to share, please follow these guidelines:
|
@@ -132,5 +204,4 @@ The `pwb-toolbox` package is released under the MIT license. See the LICENSE fil
|
|
132
204
|
## Contact
|
133
205
|
|
134
206
|
For any questions, issues, or suggestions regarding the `pwb-toolbox` package, please contact the maintainers or create an issue on the repository. We appreciate your feedback and involvement in improving the package.
|
135
|
-
|
136
|
-
Happy trading!
|
207
|
+
Happy trading!
|
@@ -0,0 +1,57 @@
|
|
1
|
+
from .base_strategy import BaseStrategy
|
2
|
+
|
3
|
+
from .insight import Direction, Insight
|
4
|
+
|
5
|
+
from .portfolio_models import (
|
6
|
+
PortfolioConstructionModel,
|
7
|
+
EqualWeightingPortfolioConstructionModel,
|
8
|
+
InsightWeightingPortfolioConstructionModel,
|
9
|
+
MeanVarianceOptimizationPortfolioConstructionModel,
|
10
|
+
BlackLittermanOptimizationPortfolioConstructionModel,
|
11
|
+
RiskParityPortfolioConstructionModel,
|
12
|
+
UnconstrainedMeanVariancePortfolioConstructionModel,
|
13
|
+
TargetPercentagePortfolioConstructionModel,
|
14
|
+
DollarCostAveragingPortfolioConstructionModel,
|
15
|
+
InsightRatioPortfolioConstructionModel,
|
16
|
+
)
|
17
|
+
from .ib_connector import IBConnector, run_ib_strategy
|
18
|
+
from .example.engine import run_backtest, run_ib_backtest
|
19
|
+
|
20
|
+
__all__ = [
|
21
|
+
"Direction",
|
22
|
+
"Insight",
|
23
|
+
"PortfolioConstructionModel",
|
24
|
+
"EqualWeightingPortfolioConstructionModel",
|
25
|
+
"InsightWeightingPortfolioConstructionModel",
|
26
|
+
"MeanVarianceOptimizationPortfolioConstructionModel",
|
27
|
+
"BlackLittermanOptimizationPortfolioConstructionModel",
|
28
|
+
"RiskParityPortfolioConstructionModel",
|
29
|
+
"UnconstrainedMeanVariancePortfolioConstructionModel",
|
30
|
+
"TargetPercentagePortfolioConstructionModel",
|
31
|
+
"DollarCostAveragingPortfolioConstructionModel",
|
32
|
+
"InsightRatioPortfolioConstructionModel",
|
33
|
+
"RiskManagementModel",
|
34
|
+
"TrailingStopRiskManagementModel",
|
35
|
+
"MaximumDrawdownPercentPerSecurity",
|
36
|
+
"MaximumDrawdownPercentPortfolio",
|
37
|
+
"MaximumUnrealizedProfitPercentPerSecurity",
|
38
|
+
"MaximumTotalPortfolioExposure",
|
39
|
+
"SectorExposureRiskManagementModel",
|
40
|
+
"MaximumOrderQuantityPercentPerSecurity",
|
41
|
+
"CompositeRiskManagementModel",
|
42
|
+
"IBConnector",
|
43
|
+
"run_ib_strategy",
|
44
|
+
"run_backtest",
|
45
|
+
"run_ib_backtest",
|
46
|
+
]
|
47
|
+
from .risk_models import (
|
48
|
+
RiskManagementModel,
|
49
|
+
TrailingStopRiskManagementModel,
|
50
|
+
MaximumDrawdownPercentPerSecurity,
|
51
|
+
MaximumDrawdownPercentPortfolio,
|
52
|
+
MaximumUnrealizedProfitPercentPerSecurity,
|
53
|
+
MaximumTotalPortfolioExposure,
|
54
|
+
SectorExposureRiskManagementModel,
|
55
|
+
MaximumOrderQuantityPercentPerSecurity,
|
56
|
+
CompositeRiskManagementModel,
|
57
|
+
)
|
@@ -0,0 +1,33 @@
|
|
1
|
+
import backtrader as bt
|
2
|
+
from tqdm import tqdm
|
3
|
+
|
4
|
+
|
5
|
+
class BaseStrategy(bt.Strategy):
|
6
|
+
"""Base strategy providing progress logging utilities."""
|
7
|
+
|
8
|
+
params = (("total_days", 0),)
|
9
|
+
|
10
|
+
def __init__(self):
|
11
|
+
super().__init__()
|
12
|
+
self.pbar = tqdm(total=self.params.total_days)
|
13
|
+
self.log_data = []
|
14
|
+
|
15
|
+
def is_tradable(self, data):
|
16
|
+
"""Return True if the instrument's price is not constant."""
|
17
|
+
if len(data.close) < 3:
|
18
|
+
return False
|
19
|
+
return data.close[0] != data.close[-2]
|
20
|
+
|
21
|
+
def __next__(self):
|
22
|
+
"""Update progress bar and log current value."""
|
23
|
+
self.pbar.update(1)
|
24
|
+
self.log_data.append(
|
25
|
+
{
|
26
|
+
"date": self.datas[0].datetime.date(0).isoformat(),
|
27
|
+
"value": self.broker.getvalue(),
|
28
|
+
}
|
29
|
+
)
|
30
|
+
|
31
|
+
def get_latest_positions(self):
|
32
|
+
"""Get a dictionary of the latest positions."""
|
33
|
+
return {data._name: self.broker.getposition(data).size for data in self.datas}
|
@@ -0,0 +1,153 @@
|
|
1
|
+
"""Execution models for order placement using Backtrader."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import Dict
|
6
|
+
import backtrader as bt
|
7
|
+
|
8
|
+
|
9
|
+
class ExecutionModel:
|
10
|
+
"""Base execution model class."""
|
11
|
+
|
12
|
+
def execute(self, strategy: bt.Strategy, weights: Dict[str, float]):
|
13
|
+
"""Place orders on the given strategy."""
|
14
|
+
raise NotImplementedError
|
15
|
+
|
16
|
+
|
17
|
+
class ImmediateExecutionModel(ExecutionModel):
|
18
|
+
"""Immediately send market orders using ``order_target_percent``."""
|
19
|
+
|
20
|
+
def execute(self, strategy: bt.Strategy, weights: Dict[str, float]):
|
21
|
+
for data in strategy.datas:
|
22
|
+
target = weights.get(data._name, 0.0)
|
23
|
+
strategy.order_target_percent(data=data, target=target)
|
24
|
+
|
25
|
+
|
26
|
+
class StandardDeviationExecutionModel(ExecutionModel):
|
27
|
+
"""Only trade when recent volatility exceeds a threshold."""
|
28
|
+
|
29
|
+
def __init__(self, lookback: int = 20, threshold: float = 0.01):
|
30
|
+
self.lookback = lookback
|
31
|
+
self.threshold = threshold
|
32
|
+
|
33
|
+
def execute(self, strategy: bt.Strategy, weights: Dict[str, float]):
|
34
|
+
prices = strategy.prices
|
35
|
+
for data in strategy.datas:
|
36
|
+
symbol = data._name
|
37
|
+
series = prices[symbol]["close"] if (symbol, "close") in prices.columns else prices[symbol]
|
38
|
+
if len(series) < self.lookback:
|
39
|
+
continue
|
40
|
+
vol = series.pct_change().rolling(self.lookback).std().iloc[-1]
|
41
|
+
if vol is not None and vol > self.threshold:
|
42
|
+
target = weights.get(symbol, 0.0)
|
43
|
+
strategy.order_target_percent(data=data, target=target)
|
44
|
+
|
45
|
+
|
46
|
+
class VolumeWeightedAveragePriceExecutionModel(ExecutionModel):
|
47
|
+
"""Split orders evenly over a number of steps to approximate VWAP."""
|
48
|
+
|
49
|
+
def __init__(self, steps: int = 3):
|
50
|
+
self.steps = steps
|
51
|
+
self._progress: Dict[str, int] = {}
|
52
|
+
|
53
|
+
def execute(self, strategy: bt.Strategy, weights: Dict[str, float]):
|
54
|
+
for data in strategy.datas:
|
55
|
+
symbol = data._name
|
56
|
+
step = self._progress.get(symbol, 0)
|
57
|
+
if step >= self.steps:
|
58
|
+
continue
|
59
|
+
target = weights.get(symbol, 0.0) * (step + 1) / self.steps
|
60
|
+
strategy.order_target_percent(data=data, target=target)
|
61
|
+
self._progress[symbol] = step + 1
|
62
|
+
|
63
|
+
|
64
|
+
class VolumePercentageExecutionModel(ExecutionModel):
|
65
|
+
"""Execute only a percentage of the target each call."""
|
66
|
+
|
67
|
+
def __init__(self, percentage: float = 0.25):
|
68
|
+
self.percentage = percentage
|
69
|
+
self._filled: Dict[str, float] = {}
|
70
|
+
|
71
|
+
def execute(self, strategy: bt.Strategy, weights: Dict[str, float]):
|
72
|
+
for data in strategy.datas:
|
73
|
+
symbol = data._name
|
74
|
+
current = self._filled.get(symbol, 0.0)
|
75
|
+
target = weights.get(symbol, 0.0)
|
76
|
+
remaining = target - current
|
77
|
+
if abs(remaining) < 1e-6:
|
78
|
+
continue
|
79
|
+
step_target = current + remaining * self.percentage
|
80
|
+
self._filled[symbol] = step_target
|
81
|
+
strategy.order_target_percent(data=data, target=step_target)
|
82
|
+
|
83
|
+
|
84
|
+
class TimeProfileExecutionModel(ExecutionModel):
|
85
|
+
"""Execute orders based on a predefined time profile (e.g. TWAP)."""
|
86
|
+
|
87
|
+
def __init__(self, profile: Dict[int, float] | None = None):
|
88
|
+
self.profile = profile or {0: 1.0}
|
89
|
+
self._called = 0
|
90
|
+
|
91
|
+
def execute(self, strategy: bt.Strategy, weights: Dict[str, float]):
|
92
|
+
factor = self.profile.get(self._called, 0.0)
|
93
|
+
for data in strategy.datas:
|
94
|
+
target = weights.get(data._name, 0.0) * factor
|
95
|
+
strategy.order_target_percent(data=data, target=target)
|
96
|
+
self._called += 1
|
97
|
+
|
98
|
+
|
99
|
+
class TrailingLimitExecutionModel(ExecutionModel):
|
100
|
+
"""Use trailing limit orders for execution."""
|
101
|
+
|
102
|
+
def __init__(self, trail: float = 0.01):
|
103
|
+
self.trail = trail
|
104
|
+
|
105
|
+
def execute(self, strategy: bt.Strategy, weights: Dict[str, float]):
|
106
|
+
prices = strategy.prices
|
107
|
+
for data in strategy.datas:
|
108
|
+
symbol = data._name
|
109
|
+
price = prices[symbol]["close"].iloc[-1] if (symbol, "close") in prices.columns else prices[symbol].iloc[-1]
|
110
|
+
target = weights.get(symbol, 0.0)
|
111
|
+
if target > 0:
|
112
|
+
strategy.buy(data=data, exectype=bt.Order.Limit, price=price * (1 - self.trail))
|
113
|
+
elif target < 0:
|
114
|
+
strategy.sell(data=data, exectype=bt.Order.Limit, price=price * (1 + self.trail))
|
115
|
+
|
116
|
+
|
117
|
+
class AdaptiveExecutionModel(ExecutionModel):
|
118
|
+
"""Switch between immediate and VWAP execution based on volatility."""
|
119
|
+
|
120
|
+
def __init__(self, threshold: float = 0.02, steps: int = 3):
|
121
|
+
self.threshold = threshold
|
122
|
+
self.vwap = VolumeWeightedAveragePriceExecutionModel(steps=steps)
|
123
|
+
self.immediate = ImmediateExecutionModel()
|
124
|
+
|
125
|
+
def execute(self, strategy: bt.Strategy, weights: Dict[str, float]):
|
126
|
+
prices = strategy.prices
|
127
|
+
for data in strategy.datas:
|
128
|
+
symbol = data._name
|
129
|
+
series = prices[symbol]["close"] if (symbol, "close") in prices.columns else prices[symbol]
|
130
|
+
if len(series) < 2:
|
131
|
+
continue
|
132
|
+
vol = series.pct_change().iloc[-1]
|
133
|
+
if abs(vol) > self.threshold:
|
134
|
+
self.immediate.execute(strategy, {symbol: weights.get(symbol, 0.0)})
|
135
|
+
else:
|
136
|
+
self.vwap.execute(strategy, {symbol: weights.get(symbol, 0.0)})
|
137
|
+
|
138
|
+
|
139
|
+
class BufferedExecutionModel(ExecutionModel):
|
140
|
+
"""Only execute when target differs sufficiently from last order."""
|
141
|
+
|
142
|
+
def __init__(self, buffer: float = 0.05):
|
143
|
+
self.buffer = buffer
|
144
|
+
self._last: Dict[str, float] = {}
|
145
|
+
|
146
|
+
def execute(self, strategy: bt.Strategy, weights: Dict[str, float]):
|
147
|
+
for data in strategy.datas:
|
148
|
+
symbol = data._name
|
149
|
+
target = weights.get(symbol, 0.0)
|
150
|
+
last = self._last.get(symbol)
|
151
|
+
if last is None or abs(target - last) > self.buffer:
|
152
|
+
strategy.order_target_percent(data=data, target=target)
|
153
|
+
self._last[symbol] = target
|
@@ -0,0 +1,69 @@
|
|
1
|
+
"""Lightweight helpers for running Interactive Brokers backtests."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import Iterable, Mapping, Type
|
6
|
+
|
7
|
+
import backtrader as bt
|
8
|
+
|
9
|
+
|
10
|
+
class IBConnector:
|
11
|
+
"""Utility for creating Backtrader IB stores and data feeds."""
|
12
|
+
|
13
|
+
def __init__(
|
14
|
+
self,
|
15
|
+
host: str = "127.0.0.1",
|
16
|
+
port: int = 7497,
|
17
|
+
client_id: int = 1,
|
18
|
+
store_class: Type[bt.stores.IBStore] | None = None,
|
19
|
+
feed_class: Type[bt.feeds.IBData] | None = None,
|
20
|
+
) -> None:
|
21
|
+
self.host = host
|
22
|
+
self.port = port
|
23
|
+
self.client_id = client_id
|
24
|
+
self.store_class = store_class or bt.stores.IBStore
|
25
|
+
self.feed_class = feed_class or bt.feeds.IBData
|
26
|
+
|
27
|
+
def get_store(self) -> bt.stores.IBStore:
|
28
|
+
"""Instantiate and return an ``IBStore``."""
|
29
|
+
return self.store_class(host=self.host, port=self.port, clientId=self.client_id)
|
30
|
+
|
31
|
+
def create_feed(self, **kwargs) -> bt.feeds.IBData:
|
32
|
+
"""Create an ``IBData`` feed bound to the connector's store."""
|
33
|
+
store = kwargs.pop("store", None) or self.get_store()
|
34
|
+
return self.feed_class(store=store, **kwargs)
|
35
|
+
|
36
|
+
|
37
|
+
def run_ib_strategy(
|
38
|
+
strategy: type[bt.Strategy],
|
39
|
+
data_config: Iterable[Mapping[str, object]],
|
40
|
+
**ib_kwargs,
|
41
|
+
):
|
42
|
+
"""Run ``strategy`` with Interactive Brokers data feeds.
|
43
|
+
|
44
|
+
Parameters
|
45
|
+
----------
|
46
|
+
strategy:
|
47
|
+
The ``bt.Strategy`` subclass to execute.
|
48
|
+
data_config:
|
49
|
+
Iterable of dictionaries passed to ``IBData`` for each feed.
|
50
|
+
ib_kwargs:
|
51
|
+
Arguments forwarded to :class:`IBConnector`.
|
52
|
+
Examples
|
53
|
+
--------
|
54
|
+
>>> data_cfg = [{"dataname": "AAPL", "name": "AAPL", "what": "MIDPOINT"}]
|
55
|
+
>>> run_ib_strategy(MyStrategy, data_cfg, host="127.0.0.1")
|
56
|
+
|
57
|
+
"""
|
58
|
+
connector = IBConnector(**ib_kwargs)
|
59
|
+
cerebro = bt.Cerebro()
|
60
|
+
store = connector.get_store()
|
61
|
+
cerebro.broker = store.getbroker()
|
62
|
+
|
63
|
+
for cfg in data_config:
|
64
|
+
data = connector.create_feed(store=store, **cfg)
|
65
|
+
name = cfg.get("name")
|
66
|
+
cerebro.adddata(data, name=name)
|
67
|
+
|
68
|
+
cerebro.addstrategy(strategy)
|
69
|
+
return cerebro.run()
|
@@ -0,0 +1,21 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from enum import Enum, auto
|
3
|
+
from datetime import datetime
|
4
|
+
|
5
|
+
|
6
|
+
class Direction(Enum):
|
7
|
+
"""Possible directions for an Insight."""
|
8
|
+
|
9
|
+
UP = auto()
|
10
|
+
DOWN = auto()
|
11
|
+
FLAT = auto()
|
12
|
+
|
13
|
+
|
14
|
+
@dataclass
|
15
|
+
class Insight:
|
16
|
+
"""Simple trading signal produced by an Alpha model."""
|
17
|
+
|
18
|
+
symbol: str
|
19
|
+
direction: Direction
|
20
|
+
timestamp: datetime
|
21
|
+
weight: float = 1.0
|