pwb-toolbox 0.1.6__py3-none-any.whl → 0.1.8__py3-none-any.whl

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.
@@ -0,0 +1,415 @@
1
+ import matplotlib.pyplot as plt
2
+ from statistics import NormalDist
3
+
4
+ try:
5
+ import pandas as pd # type: ignore
6
+ except ModuleNotFoundError: # pragma: no cover - optional dependency
7
+ pd = None # type: ignore
8
+
9
+ from .metrics import (
10
+ _to_list,
11
+ returns_table,
12
+ annualized_volatility,
13
+ parametric_var,
14
+ sharpe_ratio,
15
+ sortino_ratio,
16
+ skewness,
17
+ kurtosis,
18
+ cumulative_excess_return,
19
+ fama_french_3factor,
20
+ )
21
+
22
+
23
+ def plot_equity_curve(prices, logy: bool = True, ax=None):
24
+ """Plot cumulative return equity curve."""
25
+ if ax is None:
26
+ fig, ax = plt.subplots()
27
+ p = _to_list(prices)
28
+ cum = [v / p[0] for v in p]
29
+ ax.plot(getattr(prices, 'index', range(len(p))), cum)
30
+ if logy:
31
+ ax.set_yscale('log')
32
+ ax.set_xlabel('Date')
33
+ ax.set_ylabel('Cumulative Return')
34
+ return ax
35
+
36
+
37
+ def plot_return_heatmap(prices, ax=None):
38
+ """Plot calendar heatmap of returns from price series."""
39
+ if pd is None:
40
+ raise ImportError("pandas is required for plot_return_heatmap")
41
+ tbl = returns_table(prices)
42
+ if ax is None:
43
+ fig, ax = plt.subplots()
44
+ data = [tbl[m].values for m in tbl.columns if m != 'Year']
45
+ im = ax.imshow(data, aspect='auto', interpolation='none',
46
+ cmap='RdYlGn',
47
+ vmin=min((min(filter(None, row)) for row in data if any(row))),
48
+ vmax=max((max(filter(None, row)) for row in data if any(row))))
49
+ ax.set_yticks(range(len(tbl.index)))
50
+ ax.set_yticklabels(tbl.index)
51
+ ax.set_xticks(range(len(tbl.columns)-1))
52
+ ax.set_xticklabels([c for c in tbl.columns if c != 'Year'])
53
+ plt.colorbar(im, ax=ax)
54
+ return ax
55
+
56
+
57
+ def plot_underwater(prices, ax=None):
58
+ """Plot drawdown (underwater) chart."""
59
+ if ax is None:
60
+ fig, ax = plt.subplots()
61
+ p = _to_list(prices)
62
+ peak = p[0] if p else 0
63
+ dd = []
64
+ for price in p:
65
+ if price > peak:
66
+ peak = price
67
+ dd.append(price / peak - 1)
68
+ ax.plot(getattr(prices, 'index', range(len(p))), dd)
69
+ ax.set_ylabel('Drawdown')
70
+ ax.set_xlabel('Date')
71
+ return ax
72
+
73
+
74
+ def plot_rolling_volatility(prices, window: int = 63, periods_per_year: int = 252, ax=None):
75
+ """Plot rolling annualized volatility."""
76
+ if pd is None:
77
+ raise ImportError("pandas is required for plot_rolling_volatility")
78
+ p = _to_list(prices)
79
+ index = list(getattr(prices, 'index', range(len(p))))
80
+ vols = []
81
+ for i in range(len(p)):
82
+ if i < window:
83
+ vols.append(None)
84
+ else:
85
+ vols.append(annualized_volatility(p[i - window:i + 1], periods_per_year))
86
+ s = pd.Series(vols)
87
+ s.index = index
88
+ if ax is None:
89
+ fig, ax = plt.subplots()
90
+ ax.plot(s.index, s)
91
+ ax.set_ylabel('Volatility')
92
+ ax.set_xlabel('Date')
93
+ return ax
94
+
95
+
96
+ def plot_rolling_var(prices, window: int = 63, level: float = 0.05, ax=None):
97
+ """Plot rolling parametric VaR."""
98
+ if pd is None:
99
+ raise ImportError("pandas is required for plot_rolling_var")
100
+ p = _to_list(prices)
101
+ index = list(getattr(prices, 'index', range(len(p))))
102
+ vars_ = []
103
+ for i in range(len(p)):
104
+ if i < window:
105
+ vars_.append(None)
106
+ else:
107
+ vars_.append(parametric_var(p[i - window:i + 1], level))
108
+ s = pd.Series(vars_)
109
+ s.index = index
110
+ if ax is None:
111
+ fig, ax = plt.subplots()
112
+ ax.plot(s.index, s)
113
+ ax.set_ylabel('VaR')
114
+ ax.set_xlabel('Date')
115
+ return ax
116
+
117
+
118
+ def plot_rolling_sharpe(
119
+ prices,
120
+ window: int = 63,
121
+ risk_free_rate: float = 0.0,
122
+ periods_per_year: int = 252,
123
+ ax=None,
124
+ ):
125
+ """Plot rolling Sharpe ratio."""
126
+ if pd is None:
127
+ raise ImportError("pandas is required for plot_rolling_sharpe")
128
+ p = _to_list(prices)
129
+ index = list(getattr(prices, 'index', range(len(p))))
130
+ vals = []
131
+ for i in range(len(p)):
132
+ if i < window:
133
+ vals.append(None)
134
+ else:
135
+ vals.append(
136
+ sharpe_ratio(p[i - window : i + 1], risk_free_rate, periods_per_year)
137
+ )
138
+ s = pd.Series(vals)
139
+ s.index = index
140
+ if ax is None:
141
+ fig, ax = plt.subplots()
142
+ ax.plot(s.index, s)
143
+ ax.set_ylabel('Sharpe')
144
+ ax.set_xlabel('Date')
145
+ return ax
146
+
147
+
148
+ def plot_rolling_sortino(
149
+ prices,
150
+ window: int = 63,
151
+ risk_free_rate: float = 0.0,
152
+ periods_per_year: int = 252,
153
+ ax=None,
154
+ ):
155
+ """Plot rolling Sortino ratio."""
156
+ if pd is None:
157
+ raise ImportError("pandas is required for plot_rolling_sortino")
158
+ p = _to_list(prices)
159
+ index = list(getattr(prices, 'index', range(len(p))))
160
+ vals = []
161
+ for i in range(len(p)):
162
+ if i < window:
163
+ vals.append(None)
164
+ else:
165
+ vals.append(
166
+ sortino_ratio(p[i - window : i + 1], risk_free_rate, periods_per_year)
167
+ )
168
+ s = pd.Series(vals)
169
+ s.index = index
170
+ if ax is None:
171
+ fig, ax = plt.subplots()
172
+ ax.plot(s.index, s)
173
+ ax.set_ylabel('Sortino')
174
+ ax.set_xlabel('Date')
175
+ return ax
176
+
177
+
178
+ def plot_return_scatter(prices, benchmark_prices, ax=None):
179
+ """Scatter of strategy vs benchmark returns with regression line."""
180
+ if pd is None:
181
+ raise ImportError("pandas is required for plot_return_scatter")
182
+ p = _to_list(prices)
183
+ b = _to_list(benchmark_prices)
184
+ n = min(len(p), len(b))
185
+ if n < 2:
186
+ raise ValueError("insufficient data")
187
+ strat = [p[i] / p[i - 1] - 1 for i in range(1, n)]
188
+ bench = [b[i] / b[i - 1] - 1 for i in range(1, n)]
189
+ mean_x = sum(bench) / len(bench)
190
+ mean_y = sum(strat) / len(strat)
191
+ cov = sum((x - mean_x) * (y - mean_y) for x, y in zip(bench, strat)) / len(bench)
192
+ var_x = sum((x - mean_x) ** 2 for x in bench) / len(bench)
193
+ beta = cov / var_x if var_x else 0.0
194
+ alpha = mean_y - beta * mean_x
195
+ if ax is None:
196
+ fig, ax = plt.subplots()
197
+ ax.scatter(bench, strat, s=10)
198
+ xs = [min(bench), max(bench)]
199
+ ys = [alpha + beta * x for x in xs]
200
+ ax.plot(xs, ys, color='red', label=f"alpha={alpha:.2f}, beta={beta:.2f}")
201
+ ax.set_xlabel('Benchmark Return')
202
+ ax.set_ylabel('Strategy Return')
203
+ ax.legend()
204
+ return ax
205
+
206
+
207
+ def plot_cumulative_excess_return(prices, benchmark_prices, ax=None):
208
+ """Plot cumulative excess return versus benchmark."""
209
+ if pd is None:
210
+ raise ImportError("pandas is required for plot_cumulative_excess_return")
211
+ ser = cumulative_excess_return(prices, benchmark_prices)
212
+ if ax is None:
213
+ fig, ax = plt.subplots()
214
+ ax.plot(ser.index, ser)
215
+ ax.set_ylabel("Cumulative Excess Return")
216
+ ax.set_xlabel("Date")
217
+ return ax
218
+
219
+
220
+ def plot_factor_exposures(prices, factors, ax=None):
221
+ """Bar chart of Fama-French 3 factor exposures."""
222
+ if pd is None:
223
+ raise ImportError("pandas is required for plot_factor_exposures")
224
+ exp = fama_french_3factor(prices, factors)
225
+ names = [n for n in exp.index if n != "alpha"]
226
+ vals = [exp[n] for n in names]
227
+ if ax is None:
228
+ fig, ax = plt.subplots()
229
+ ax.bar(range(len(vals)), vals)
230
+ ax.set_xticks(range(len(names)))
231
+ ax.set_xticklabels(names, rotation=45)
232
+ ax.set_ylabel("Exposure")
233
+ return ax
234
+
235
+
236
+ def plot_trade_return_hist(trades, ax=None, bins=20):
237
+ """Histogram of trade returns for long and short trades."""
238
+ if ax is None:
239
+ fig, ax = plt.subplots()
240
+ longs = [t.get("return", 0) for t in trades if t.get("direction") == "long"]
241
+ shorts = [t.get("return", 0) for t in trades if t.get("direction") == "short"]
242
+ if longs:
243
+ ax.hist(longs, bins=bins, alpha=0.5, label="Long")
244
+ if shorts:
245
+ ax.hist(shorts, bins=bins, alpha=0.5, label="Short")
246
+ ax.set_xlabel("Trade Return")
247
+ ax.set_ylabel("Frequency")
248
+ if longs or shorts:
249
+ ax.legend()
250
+ return ax
251
+
252
+
253
+ def plot_return_by_holding_period(trades, ax=None):
254
+ """Box plot of trade return grouped by holding period."""
255
+ if ax is None:
256
+ fig, ax = plt.subplots()
257
+ groups = {}
258
+ for t in trades:
259
+ entry = t.get("entry")
260
+ exit_ = t.get("exit")
261
+ if entry is None or exit_ is None:
262
+ continue
263
+ dur = (exit_ - entry).days if hasattr(exit_ - entry, "days") else int(exit_ - entry)
264
+ groups.setdefault(dur, []).append(t.get("return", 0))
265
+ if not groups:
266
+ return ax
267
+ durations = sorted(groups)
268
+ data = [groups[d] for d in durations]
269
+ ax.boxplot(data, positions=range(len(data)))
270
+ ax.set_xticks(range(len(data)))
271
+ ax.set_xticklabels([str(d) for d in durations])
272
+ ax.set_xlabel("Holding Period (days)")
273
+ ax.set_ylabel("Return")
274
+ return ax
275
+
276
+
277
+ def plot_exposure_ts(trades, ax=None):
278
+ """Time series of gross and net exposure based on open trades."""
279
+ if pd is None:
280
+ raise ImportError("pandas is required for plot_exposure_ts")
281
+ entries = [t.get("entry") for t in trades if t.get("entry") is not None]
282
+ exits = [t.get("exit") for t in trades if t.get("exit") is not None]
283
+ if not entries or not exits:
284
+ if ax is None:
285
+ fig, ax = plt.subplots()
286
+ return ax
287
+ start = min(entries)
288
+ end = max(exits)
289
+ idx = pd.date_range(start, end)
290
+ gross = [0.0 for _ in idx]
291
+ net = [0.0 for _ in idx]
292
+ for t in trades:
293
+ entry = t.get("entry")
294
+ exit_ = t.get("exit")
295
+ size = t.get("size", 0.0)
296
+ if entry is None or exit_ is None:
297
+ continue
298
+ for i, date in enumerate(idx):
299
+ if entry <= date <= exit_:
300
+ gross[i] += abs(size)
301
+ net[i] += size
302
+ if ax is None:
303
+ fig, ax = plt.subplots()
304
+ ax.plot(idx, gross, label="Gross")
305
+ ax.plot(idx, net, label="Net")
306
+ ax.set_ylabel("Exposure")
307
+ ax.set_xlabel("Date")
308
+ ax.legend()
309
+ return ax
310
+
311
+
312
+ def plot_cumulative_shortfall(trades, ax=None):
313
+ """Plot cumulative implementation shortfall over time."""
314
+ if pd is None:
315
+ raise ImportError("pandas is required for plot_cumulative_shortfall")
316
+
317
+ from .trade_stats import trade_implementation_shortfall
318
+
319
+ dates = []
320
+ cum = []
321
+ total = 0.0
322
+ for t in trades:
323
+ date = t.get("exit") or t.get("entry")
324
+ total += trade_implementation_shortfall(t)
325
+ dates.append(date)
326
+ cum.append(total)
327
+
328
+ ser = pd.Series(cum, index=dates)
329
+ if ax is None:
330
+ fig, ax = plt.subplots()
331
+ ax.plot(ser.index, ser)
332
+ ax.set_ylabel("Cumulative Shortfall")
333
+ ax.set_xlabel("Date")
334
+ return ax
335
+
336
+
337
+ def plot_alpha_vs_return(trades, ax=None):
338
+ """Scatter plot of forecasted alpha versus realised trade return."""
339
+ if pd is None:
340
+ raise ImportError("pandas is required for plot_alpha_vs_return")
341
+
342
+ alphas = [t.get("forecast_alpha") for t in trades if t.get("forecast_alpha") is not None]
343
+ rets = [t.get("return") for t in trades if t.get("forecast_alpha") is not None]
344
+
345
+ if ax is None:
346
+ fig, ax = plt.subplots()
347
+ ax.scatter(alphas, rets, s=10)
348
+ ax.set_xlabel("Forecast Alpha")
349
+ ax.set_ylabel("Realized Return")
350
+ return ax
351
+
352
+
353
+ def plot_qq_returns(prices, ax=None):
354
+ """QQ-plot of returns versus normal distribution."""
355
+ if ax is None:
356
+ fig, ax = plt.subplots()
357
+ p = _to_list(prices)
358
+ if len(p) < 2:
359
+ return ax
360
+ rets = sorted(p[i] / p[i - 1] - 1 for i in range(1, len(p)))
361
+ n = len(rets)
362
+ mean = sum(rets) / n
363
+ var = sum((r - mean) ** 2 for r in rets) / n
364
+ std = var ** 0.5
365
+ dist = NormalDist(mean, std)
366
+ qs = [(i + 0.5) / n for i in range(n)]
367
+ theo = [dist.inv_cdf(q) for q in qs]
368
+ ax.scatter(theo, rets, s=10)
369
+ ax.set_xlabel("Theoretical Quantiles")
370
+ ax.set_ylabel("Empirical Quantiles")
371
+ return ax
372
+
373
+
374
+ def plot_rolling_skewness(prices, window: int = 63, ax=None):
375
+ """Plot rolling skewness of returns."""
376
+ if pd is None:
377
+ raise ImportError("pandas is required for plot_rolling_skewness")
378
+ p = _to_list(prices)
379
+ index = list(getattr(prices, 'index', range(len(p))))
380
+ vals = []
381
+ for i in range(len(p)):
382
+ if i < window:
383
+ vals.append(None)
384
+ else:
385
+ vals.append(skewness(p[i - window : i + 1]))
386
+ s = pd.Series(vals)
387
+ s.index = index
388
+ if ax is None:
389
+ fig, ax = plt.subplots()
390
+ ax.plot(s.index, s)
391
+ ax.set_ylabel("Skewness")
392
+ ax.set_xlabel("Date")
393
+ return ax
394
+
395
+
396
+ def plot_rolling_kurtosis(prices, window: int = 63, ax=None):
397
+ """Plot rolling kurtosis of returns."""
398
+ if pd is None:
399
+ raise ImportError("pandas is required for plot_rolling_kurtosis")
400
+ p = _to_list(prices)
401
+ index = list(getattr(prices, 'index', range(len(p))))
402
+ vals = []
403
+ for i in range(len(p)):
404
+ if i < window:
405
+ vals.append(None)
406
+ else:
407
+ vals.append(kurtosis(p[i - window : i + 1]))
408
+ s = pd.Series(vals)
409
+ s.index = index
410
+ if ax is None:
411
+ fig, ax = plt.subplots()
412
+ ax.plot(s.index, s)
413
+ ax.set_ylabel("Kurtosis")
414
+ ax.set_xlabel("Date")
415
+ return ax
@@ -0,0 +1,138 @@
1
+ from collections import Counter
2
+ from datetime import datetime
3
+ from typing import Mapping, Sequence, Tuple, Any, Dict, List
4
+
5
+
6
+ def hit_rate(trades: Sequence[Mapping[str, Any]]) -> float:
7
+ """Proportion of trades with positive return."""
8
+ if not trades:
9
+ return 0.0
10
+ wins = sum(1 for t in trades if t.get("return", 0) > 0)
11
+ return wins / len(trades)
12
+
13
+
14
+ def average_win_loss(trades: Sequence[Mapping[str, Any]]) -> Tuple[float, float]:
15
+ """Average winning and losing trade returns."""
16
+ wins = [t.get("return", 0) for t in trades if t.get("return", 0) > 0]
17
+ losses = [t.get("return", 0) for t in trades if t.get("return", 0) < 0]
18
+ avg_win = sum(wins) / len(wins) if wins else 0.0
19
+ avg_loss = sum(losses) / len(losses) if losses else 0.0
20
+ return avg_win, avg_loss
21
+
22
+
23
+ def expectancy(trades: Sequence[Mapping[str, Any]]) -> float:
24
+ """Expected return per trade."""
25
+ hr = hit_rate(trades)
26
+ avg_win, avg_loss = average_win_loss(trades)
27
+ return hr * avg_win + (1 - hr) * avg_loss
28
+
29
+
30
+ def profit_factor(trades: Sequence[Mapping[str, Any]]) -> float:
31
+ """Ratio of gross profits to gross losses."""
32
+ gains = sum(t.get("return", 0) for t in trades if t.get("return", 0) > 0)
33
+ losses = -sum(t.get("return", 0) for t in trades if t.get("return", 0) < 0)
34
+ if losses == 0:
35
+ return float("inf") if gains > 0 else 0.0
36
+ return gains / losses
37
+
38
+
39
+ def trade_duration_distribution(trades: Sequence[Mapping[str, Any]]) -> Dict[int, int]:
40
+ """Distribution of trade holding periods in days."""
41
+ durations = []
42
+ for t in trades:
43
+ entry = t.get("entry")
44
+ exit_ = t.get("exit")
45
+ if entry is None or exit_ is None:
46
+ continue
47
+ delta = exit_ - entry
48
+ days = delta.days if hasattr(delta, "days") else int(delta)
49
+ durations.append(days)
50
+ return dict(Counter(durations))
51
+
52
+
53
+ def turnover(trades: Sequence[Mapping[str, Any]]) -> float:
54
+ """Average number of trades per day."""
55
+ if not trades:
56
+ return 0.0
57
+ entries = [t.get("entry") for t in trades if t.get("entry") is not None]
58
+ exits = [t.get("exit") for t in trades if t.get("exit") is not None]
59
+ if not entries or not exits:
60
+ return 0.0
61
+ start = min(entries)
62
+ end = max(exits)
63
+ period = (end - start).days
64
+ if period <= 0:
65
+ return float(len(trades))
66
+ return len(trades) / period
67
+
68
+
69
+ def trade_implementation_shortfall(trade: Mapping[str, Any]) -> float:
70
+ """Implementation shortfall for a single trade.
71
+
72
+ Calculated as the difference between the modelled return and the
73
+ realised return of the trade. If either value is missing the result
74
+ is ``0.0``.
75
+ """
76
+
77
+ model_ret = trade.get("model_return")
78
+ actual_ret = trade.get("return")
79
+ if model_ret is None or actual_ret is None:
80
+ return 0.0
81
+ return model_ret - actual_ret
82
+
83
+
84
+ def cumulative_implementation_shortfall(trades: Sequence[Mapping[str, Any]]) -> float:
85
+ """Total implementation shortfall over a collection of trades."""
86
+
87
+ return sum(trade_implementation_shortfall(t) for t in trades)
88
+
89
+
90
+ def slippage_stats(trades: Sequence[Mapping[str, Any]]) -> Dict[str, float]:
91
+ """Average entry and exit slippage for a set of trades.
92
+
93
+ Slippage is measured relative to the model prices. Positive values
94
+ indicate worse execution than the modelled price.
95
+ """
96
+
97
+ entry_slip: List[float] = []
98
+ exit_slip: List[float] = []
99
+
100
+ for t in trades:
101
+ direction = 1 if t.get("direction") == "long" else -1
102
+
103
+ if "entry_price" in t and "model_entry_price" in t and t["model_entry_price"]:
104
+ entry_slip.append(
105
+ direction
106
+ * (t["entry_price"] - t["model_entry_price"]) / t["model_entry_price"]
107
+ )
108
+
109
+ if "exit_price" in t and "model_exit_price" in t and t["model_exit_price"]:
110
+ exit_slip.append(
111
+ direction
112
+ * (t["model_exit_price"] - t["exit_price"]) / t["model_exit_price"]
113
+ )
114
+
115
+ avg_entry = sum(entry_slip) / len(entry_slip) if entry_slip else 0.0
116
+ avg_exit = sum(exit_slip) / len(exit_slip) if exit_slip else 0.0
117
+ return {"avg_entry_slippage": avg_entry, "avg_exit_slippage": avg_exit}
118
+
119
+
120
+ def latency_stats(trades: Sequence[Mapping[str, Any]]) -> Dict[str, float]:
121
+ """Basic latency metrics in seconds between signal and execution."""
122
+
123
+ latencies = []
124
+ for t in trades:
125
+ signal_time = t.get("signal_time")
126
+ entry_time = t.get("entry")
127
+ if signal_time is None or entry_time is None:
128
+ continue
129
+ delta = entry_time - signal_time
130
+ secs = delta.total_seconds() if hasattr(delta, "total_seconds") else float(delta)
131
+ latencies.append(secs)
132
+
133
+ if not latencies:
134
+ return {"avg_latency_sec": 0.0, "max_latency_sec": 0.0}
135
+
136
+ avg_lat = sum(latencies) / len(latencies)
137
+ max_lat = max(latencies)
138
+ return {"avg_latency_sec": avg_lat, "max_latency_sec": max_lat}
@@ -1,19 +1,23 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pwb-toolbox
3
- Version: 0.1.6
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.7
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!
@@ -0,0 +1,19 @@
1
+ pwb_toolbox/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ pwb_toolbox/backtest/__init__.py,sha256=uXP0toenQFhIOn8uwyoKNRjH1bEyVfO0-ryFZAMb1xE,2066
3
+ pwb_toolbox/backtest/base_strategy.py,sha256=PQTO9vytnxeDplmaDUC8ORYwo9dTUbwhNrrmHlpDAAU,994
4
+ pwb_toolbox/backtest/ib_connector.py,sha256=5T-pgT_MrDOxqdvXgT_hceIeewPs-rN3j4n-Wr-6JGU,2120
5
+ pwb_toolbox/backtest/insight.py,sha256=NPrNr7ToNUpqHvgOjgtsP1g8p1Pn8yXuD6YSO-zYePg,394
6
+ pwb_toolbox/backtest/execution_models/__init__.py,sha256=kMa-C7DPeCwB81pyOp3gjIUSYpI3EuCn1uO9vLTJK4Q,5996
7
+ pwb_toolbox/backtest/portfolio_models/__init__.py,sha256=VDDDOUhu4kPxYJsOb9dH-qHTfM-Hj8O7hmzLXGuSxs8,9353
8
+ pwb_toolbox/backtest/risk_models/__init__.py,sha256=Sbd4CeGGhxRFQfdsiMoL7ws-1NJq6IkhxQhXAnGacpY,6319
9
+ pwb_toolbox/backtest/universe_models/__init__.py,sha256=-NXd_dhPKHgfBpynWjKJ4YxHLvagNhNPfU_JUreK7fc,5715
10
+ pwb_toolbox/datasets/__init__.py,sha256=o2Q6nw8HmV_gTFfovhPJkoGdFsADBunFC4KqBl9Tpaw,22259
11
+ pwb_toolbox/performance/__init__.py,sha256=ds47RiOSL3iIwRE0S8dnGINcVPlZw_I9D21ueTSVP-I,2925
12
+ pwb_toolbox/performance/metrics.py,sha256=szY8m45dZeJHciF4NxPxXlDyc78_5cLyIweRQJ_8lCE,15255
13
+ pwb_toolbox/performance/plots.py,sha256=R6OV-SxJaJnBuJGh8XmsF58a7ERwn2Irf4zEqzGMRz4,12886
14
+ pwb_toolbox/performance/trade_stats.py,sha256=I-iboKMwVLij6pc2r-KfNDnyF3LZV_LzzpgjIcJtgFw,4940
15
+ pwb_toolbox-0.1.8.dist-info/licenses/LICENSE.txt,sha256=_Wjz7o7St3iVSPBRzE0keS8XSqSJ03A3NZ6cMlTaSK8,1079
16
+ pwb_toolbox-0.1.8.dist-info/METADATA,sha256=tVOCTxHNoDRAXG1mzp2NVzUn92OMQmwtwx2RUv7mWJU,7130
17
+ pwb_toolbox-0.1.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
18
+ pwb_toolbox-0.1.8.dist-info/top_level.txt,sha256=TZcXcF2AMkKkibZOuq6AYsHjajPgddHAGjQUT64OYGY,12
19
+ pwb_toolbox-0.1.8.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- pwb_toolbox/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- pwb_toolbox/datasets/__init__.py,sha256=8ruFquxyz5_6D9zImecPmTXruHClkoV0vNX5H0eR4Fw,22249
3
- pwb_toolbox-0.1.6.dist-info/licenses/LICENSE.txt,sha256=_Wjz7o7St3iVSPBRzE0keS8XSqSJ03A3NZ6cMlTaSK8,1079
4
- pwb_toolbox-0.1.6.dist-info/METADATA,sha256=nao3Zw_tNUmsNxm9tql9HfYc3NAOWc_wyJIaYcuuHBA,4617
5
- pwb_toolbox-0.1.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
- pwb_toolbox-0.1.6.dist-info/top_level.txt,sha256=TZcXcF2AMkKkibZOuq6AYsHjajPgddHAGjQUT64OYGY,12
7
- pwb_toolbox-0.1.6.dist-info/RECORD,,