quantbt-engine 1.0.7__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.
- quantbt/__init__.py +862 -0
- quantbt/adapters/__init__.py +5 -0
- quantbt/adapters/nautilus/__init__.py +42 -0
- quantbt/adapters/nautilus/_dependency.py +53 -0
- quantbt/adapters/nautilus/backend.py +639 -0
- quantbt/adapters/nautilus/instruments.py +275 -0
- quantbt/adapters/nautilus/options.py +465 -0
- quantbt/adapters/nautilus/reports.py +231 -0
- quantbt/backends/__init__.py +29 -0
- quantbt/backends/_native_event_rust.py +2168 -0
- quantbt/backends/native_event.py +5048 -0
- quantbt/backends/native_option.py +796 -0
- quantbt/backends/native_portfolio.py +928 -0
- quantbt/backends/native_vectorized.py +1299 -0
- quantbt/backtester.py +485 -0
- quantbt/benchmarks/__init__.py +1 -0
- quantbt/benchmarks/compare_phase9_parity.py +302 -0
- quantbt/benchmarks/gamma_scalping_backtestsample.py +802 -0
- quantbt/benchmarks/profile_phase7.py +415 -0
- quantbt/benchmarks/run_arbitrage_phase_e.py +160 -0
- quantbt/benchmarks/run_optimization_overhead.py +195 -0
- quantbt/benchmarks/run_options_engine.py +272 -0
- quantbt/benchmarks/run_pct_equity_nautilus_smoke.py +227 -0
- quantbt/benchmarks/run_phase12_arbitrage_cert.py +402 -0
- quantbt/benchmarks/run_phase12_benchmark_nautilus_cert.py +430 -0
- quantbt/benchmarks/run_phase13_portfolio_report.py +97 -0
- quantbt/benchmarks/run_phase13_wfo_cache.py +162 -0
- quantbt/benchmarks/run_phase14_service_loop.py +694 -0
- quantbt/benchmarks/run_phase15a_nautilus_certification.py +514 -0
- quantbt/benchmarks/run_phase15b_synthetic_depth.py +175 -0
- quantbt/benchmarks/run_phase16_performance_debt.py +389 -0
- quantbt/benchmarks/run_phase30e_reactive_runner.py +161 -0
- quantbt/benchmarks/run_phase31_intrabar.py +441 -0
- quantbt/benchmarks/run_phase34a_native_event_memory.py +179 -0
- quantbt/benchmarks/run_phase34b_native_event_prepared_score.py +173 -0
- quantbt/benchmarks/run_phase34c_native_event_single_pass.py +176 -0
- quantbt/benchmarks/run_phase7.py +654 -0
- quantbt/benchmarks/run_portfolio_real_parity.py +537 -0
- quantbt/core/__init__.py +323 -0
- quantbt/core/arbitrage.py +767 -0
- quantbt/core/basket.py +234 -0
- quantbt/core/certification.py +274 -0
- quantbt/core/constraints.py +155 -0
- quantbt/core/engine.py +1144 -0
- quantbt/core/event.py +869 -0
- quantbt/core/execution_contract.py +197 -0
- quantbt/core/execution_depth.py +613 -0
- quantbt/core/intrabar_kernel.py +1906 -0
- quantbt/core/intrabar_reference.py +907 -0
- quantbt/core/intrabar_session.py +156 -0
- quantbt/core/market_tape.py +480 -0
- quantbt/core/native_event_capabilities.py +121 -0
- quantbt/core/native_event_parity.py +336 -0
- quantbt/core/order_compiler.py +422 -0
- quantbt/core/orders.py +319 -0
- quantbt/core/portfolio.py +392 -0
- quantbt/core/preprocessor.py +256 -0
- quantbt/core/reactive.py +173 -0
- quantbt/core/results.py +341 -0
- quantbt/core/schema.py +211 -0
- quantbt/core/scopes.py +96 -0
- quantbt/core/structured_orders.py +378 -0
- quantbt/core/types.py +87 -0
- quantbt/core/vectorized.py +203 -0
- quantbt/endpoint.py +4465 -0
- quantbt/engines.py +1014 -0
- quantbt/metrics/__init__.py +45 -0
- quantbt/metrics/options_analytics.py +46 -0
- quantbt/metrics/performance.py +541 -0
- quantbt/optimization/__init__.py +94 -0
- quantbt/optimization/callbacks.py +116 -0
- quantbt/optimization/candidate_selection.py +397 -0
- quantbt/optimization/config.py +84 -0
- quantbt/optimization/constraints.py +22 -0
- quantbt/optimization/evaluator.py +21 -0
- quantbt/optimization/evaluators/__init__.py +32 -0
- quantbt/optimization/evaluators/arbitrage.py +22 -0
- quantbt/optimization/evaluators/generic.py +34 -0
- quantbt/optimization/evaluators/grid_dca.py +22 -0
- quantbt/optimization/evaluators/intrabar.py +54 -0
- quantbt/optimization/evaluators/native_event.py +51 -0
- quantbt/optimization/evaluators/options.py +22 -0
- quantbt/optimization/evaluators/portfolio.py +42 -0
- quantbt/optimization/evaluators/signal.py +43 -0
- quantbt/optimization/multiseed.py +176 -0
- quantbt/optimization/objectives.py +221 -0
- quantbt/optimization/optimizer.py +500 -0
- quantbt/optimization/result.py +80 -0
- quantbt/optimization/samplers.py +84 -0
- quantbt/optimization/space.py +223 -0
- quantbt/options/__init__.py +215 -0
- quantbt/options/cache.py +116 -0
- quantbt/options/conventions.py +158 -0
- quantbt/options/data.py +177 -0
- quantbt/options/execution.py +600 -0
- quantbt/options/fees.py +135 -0
- quantbt/options/greeks.py +173 -0
- quantbt/options/hedging.py +232 -0
- quantbt/options/iv.py +188 -0
- quantbt/options/ledger.py +263 -0
- quantbt/options/lifecycle.py +94 -0
- quantbt/options/margin.py +311 -0
- quantbt/options/packages.py +147 -0
- quantbt/options/pricing.py +191 -0
- quantbt/options/schema.py +227 -0
- quantbt/options/selectors.py +281 -0
- quantbt/options/strategy.py +249 -0
- quantbt/options/surface.py +142 -0
- quantbt/options/tape.py +226 -0
- quantbt/options/templates/__init__.py +33 -0
- quantbt/options/templates/packages.py +354 -0
- quantbt/portfolio.py +606 -0
- quantbt/py.typed +1 -0
- quantbt/reporting/__init__.py +38 -0
- quantbt/reporting/arbitrage_audit.py +211 -0
- quantbt/reporting/nautilus_bundle.py +758 -0
- quantbt/reporting/nautilus_certification.py +226 -0
- quantbt/reporting/nautilus_diagnostics.py +225 -0
- quantbt/reporting/parity.py +496 -0
- quantbt/reporting/portfolio_audit.py +206 -0
- quantbt/reporting/portfolio_nautilus.py +140 -0
- quantbt/sizing/__init__.py +3 -0
- quantbt/sizing/fast.py +67 -0
- quantbt/sizing/modes.py +162 -0
- quantbt/viz/__init__.py +4 -0
- quantbt/viz/plots.py +310 -0
- quantbt/viz/themes.py +102 -0
- quantbt/walkforward.py +3136 -0
- quantbt_engine-1.0.7.dist-info/METADATA +891 -0
- quantbt_engine-1.0.7.dist-info/RECORD +133 -0
- quantbt_engine-1.0.7.dist-info/WHEEL +5 -0
- quantbt_engine-1.0.7.dist-info/licenses/LICENSE +21 -0
- quantbt_engine-1.0.7.dist-info/top_level.txt +1 -0
quantbt/viz/plots.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""
|
|
2
|
+
quantbt.viz.plots
|
|
3
|
+
-----------------
|
|
4
|
+
Two standalone plot functions that accept a BacktestResult.
|
|
5
|
+
|
|
6
|
+
quick_plot(result) Cumulative return + drawdown. Used by analyze().
|
|
7
|
+
tearsheet(result) Full dashboard: return, drawdown, rolling metrics,
|
|
8
|
+
monthly heatmap, PnL attribution, position exposure.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import matplotlib.pyplot as plt
|
|
16
|
+
import matplotlib.dates as mdates
|
|
17
|
+
import matplotlib.gridspec as gridspec
|
|
18
|
+
import matplotlib.ticker as ticker
|
|
19
|
+
import numpy as np
|
|
20
|
+
import pandas as pd
|
|
21
|
+
import seaborn as sns
|
|
22
|
+
|
|
23
|
+
from ..core.types import BacktestResult
|
|
24
|
+
from ..metrics.performance import (
|
|
25
|
+
full_report,
|
|
26
|
+
rolling_sharpe,
|
|
27
|
+
rolling_drawdown,
|
|
28
|
+
)
|
|
29
|
+
from .themes import apply_theme, PALETTE
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ── shared helpers ────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _fmt_date(ax, interval_months: int = 3):
|
|
36
|
+
ax.xaxis.set_major_locator(mdates.MonthLocator(interval=interval_months))
|
|
37
|
+
ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m"))
|
|
38
|
+
plt.setp(ax.xaxis.get_majorticklabels(), rotation=30, ha="right", fontsize=7)
|
|
39
|
+
ax.tick_params(axis='x', pad=2)
|
|
40
|
+
|
|
41
|
+
def _annotate_liq(ax, result: BacktestResult, c: dict):
|
|
42
|
+
if result.liquidated and result.liquidation_bar > 0:
|
|
43
|
+
liq_dt = result.equity.index[result.liquidation_bar]
|
|
44
|
+
ax.axvline(liq_dt, color=c["drawdown"], linewidth=1.2, linestyle="--", alpha=0.8)
|
|
45
|
+
ax.text(liq_dt, ax.get_ylim()[1] * 0.95, " liquidation",
|
|
46
|
+
color=c["drawdown"], fontsize=7, va="top")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ── quick_plot ────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
def quick_plot(
|
|
52
|
+
result: BacktestResult,
|
|
53
|
+
theme: str = "dark",
|
|
54
|
+
figsize: tuple = (14, 6),
|
|
55
|
+
title: Optional[str] = None,
|
|
56
|
+
scope: str = "auto",
|
|
57
|
+
) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Two-panel figure: cumulative return (top) and drawdown (bottom).
|
|
60
|
+
Suitable as a fast sanity-check or inline notebook output.
|
|
61
|
+
"""
|
|
62
|
+
from ..core.scopes import scoped_result
|
|
63
|
+
|
|
64
|
+
result = scoped_result(result, scope=scope)
|
|
65
|
+
c = apply_theme(theme)
|
|
66
|
+
|
|
67
|
+
eq = result.daily_equity
|
|
68
|
+
if len(eq) < 2:
|
|
69
|
+
eq = result.equity.dropna()
|
|
70
|
+
ret = (eq / eq.iloc[0] - 1) * 100
|
|
71
|
+
dd = rolling_drawdown(result) * 100 # already daily
|
|
72
|
+
if len(dd) < 2:
|
|
73
|
+
peak = eq.cummax()
|
|
74
|
+
dd = (peak - eq) / peak.replace(0, np.nan) * 100
|
|
75
|
+
|
|
76
|
+
rpt = full_report(result)
|
|
77
|
+
|
|
78
|
+
fig, axes = plt.subplots(
|
|
79
|
+
2, 1, figsize=figsize,
|
|
80
|
+
gridspec_kw={"height_ratios": [3, 1], "hspace": 0.04},
|
|
81
|
+
sharex=True,
|
|
82
|
+
facecolor=c["bg"],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
ax_ret, ax_dd = axes
|
|
86
|
+
|
|
87
|
+
# ── Return ──
|
|
88
|
+
ax_ret.plot(ret.index, ret.values, color=c["equity"], linewidth=1.8)
|
|
89
|
+
ax_ret.axhline(0, color=c["grid"], linewidth=0.6)
|
|
90
|
+
ax_ret.set_ylabel("Cumulative Return (%)", labelpad=8)
|
|
91
|
+
ax_ret.yaxis.set_major_formatter(ticker.FormatStrFormatter("%.1f%%"))
|
|
92
|
+
|
|
93
|
+
# summary label top-right
|
|
94
|
+
label = (
|
|
95
|
+
f"Return {rpt['total_return_pct']:+.1f}% "
|
|
96
|
+
f"Sharpe {rpt['sharpe']:.2f} "
|
|
97
|
+
f"MDD {rpt['max_drawdown_pct']:.1f}%"
|
|
98
|
+
)
|
|
99
|
+
ax_ret.set_title(
|
|
100
|
+
title or f"quantbt | {result.symbols[0] if len(result.symbols) == 1 else 'Portfolio'}",
|
|
101
|
+
loc="left", fontsize=11, fontweight="normal",
|
|
102
|
+
)
|
|
103
|
+
ax_ret.text(
|
|
104
|
+
0.99, 0.97, label,
|
|
105
|
+
transform=ax_ret.transAxes,
|
|
106
|
+
ha="right", va="top",
|
|
107
|
+
fontsize=8, color=c["text"], alpha=0.85,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
_annotate_liq(ax_ret, result, c)
|
|
111
|
+
|
|
112
|
+
# ── Drawdown ──
|
|
113
|
+
ax_dd.fill_between(dd.index, dd.values, 0,
|
|
114
|
+
color=c["drawdown"], alpha=0.55, linewidth=0)
|
|
115
|
+
ax_dd.plot(dd.index, dd.values, color=c["drawdown"], linewidth=0.8)
|
|
116
|
+
ax_dd.set_ylabel("Drawdown (%)", labelpad=8)
|
|
117
|
+
ax_dd.yaxis.set_major_formatter(ticker.FormatStrFormatter("%.1f%%"))
|
|
118
|
+
ax_dd.invert_yaxis()
|
|
119
|
+
|
|
120
|
+
_fmt_date(ax_dd)
|
|
121
|
+
fig.align_ylabels(axes)
|
|
122
|
+
# Thêm dòng này trước plt.show()
|
|
123
|
+
fig.autofmt_xdate(rotation=30, ha='right')
|
|
124
|
+
plt.tight_layout(pad=1.5)
|
|
125
|
+
plt.show()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ── tearsheet ─────────────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
def tearsheet(
|
|
131
|
+
result: BacktestResult,
|
|
132
|
+
theme: str = "dark",
|
|
133
|
+
figsize: tuple = (16, 20),
|
|
134
|
+
trading_days: int = 365,
|
|
135
|
+
benchmark: Optional[pd.Series] = None,
|
|
136
|
+
title: Optional[str] = None,
|
|
137
|
+
scope: str = "auto",
|
|
138
|
+
) -> None:
|
|
139
|
+
"""
|
|
140
|
+
Full performance tearsheet.
|
|
141
|
+
|
|
142
|
+
Panels
|
|
143
|
+
------
|
|
144
|
+
1 Cumulative return (+ optional benchmark)
|
|
145
|
+
2 Underwater drawdown
|
|
146
|
+
3 Rolling 30-day Sharpe
|
|
147
|
+
4 Monthly returns heatmap
|
|
148
|
+
5 Per-symbol PnL contribution
|
|
149
|
+
6 Daily position exposure
|
|
150
|
+
"""
|
|
151
|
+
from ..core.scopes import scoped_result
|
|
152
|
+
|
|
153
|
+
result = scoped_result(result, scope=scope)
|
|
154
|
+
c = apply_theme(theme)
|
|
155
|
+
rpt = full_report(result, trading_days)
|
|
156
|
+
|
|
157
|
+
eq = result.daily_equity
|
|
158
|
+
ret = (eq / eq.iloc[0] - 1) * 100
|
|
159
|
+
dd = rolling_drawdown(result) * 100
|
|
160
|
+
rs = rolling_sharpe(result, window=30, trading_days=trading_days)
|
|
161
|
+
|
|
162
|
+
# ── layout ──
|
|
163
|
+
fig = plt.figure(figsize=figsize, facecolor=c["bg"])
|
|
164
|
+
gs = gridspec.GridSpec(
|
|
165
|
+
6, 2, figure=fig,
|
|
166
|
+
height_ratios=[2.2, 1.0, 1.0, 1.4, 1.4, 1.4],
|
|
167
|
+
hspace=0.45, wspace=0.35,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# ── 1. cumulative return ──
|
|
171
|
+
ax1 = fig.add_subplot(gs[0, :])
|
|
172
|
+
ax1.plot(ret.index, ret.values, color=c["equity"], linewidth=1.8, label="Strategy")
|
|
173
|
+
if benchmark is not None:
|
|
174
|
+
bm = (benchmark.resample("1D").last().ffill() / benchmark.resample("1D").last().ffill().iloc[0] - 1) * 100
|
|
175
|
+
ax1.plot(bm.index, bm.values, color=c["benchmark"], linewidth=1.2,
|
|
176
|
+
linestyle="--", label="Benchmark")
|
|
177
|
+
ax1.axhline(0, color=c["grid"], linewidth=0.6)
|
|
178
|
+
ax1.set_ylabel("Cumulative Return (%)")
|
|
179
|
+
ax1.yaxis.set_major_formatter(ticker.FormatStrFormatter("%.1f%%"))
|
|
180
|
+
ax1.legend(loc="upper left")
|
|
181
|
+
_annotate_liq(ax1, result, c)
|
|
182
|
+
|
|
183
|
+
# header title
|
|
184
|
+
header = (
|
|
185
|
+
f"Return {rpt['total_return_pct']:+.1f}% "
|
|
186
|
+
f"CAGR {rpt['cagr_pct']:.1f}% "
|
|
187
|
+
f"Sharpe {rpt['sharpe']:.2f} "
|
|
188
|
+
f"Sortino {rpt['sortino']:.2f} "
|
|
189
|
+
f"Calmar {rpt['calmar']:.2f} "
|
|
190
|
+
f"MDD {rpt['max_drawdown_pct']:.1f}%"
|
|
191
|
+
)
|
|
192
|
+
ax1.set_title(
|
|
193
|
+
title or "Performance Tearsheet",
|
|
194
|
+
loc="left", fontsize=13, fontweight="normal", pad=12,
|
|
195
|
+
)
|
|
196
|
+
ax1.text(
|
|
197
|
+
0.99, 0.97, header,
|
|
198
|
+
transform=ax1.transAxes,
|
|
199
|
+
ha="right", va="top",
|
|
200
|
+
fontsize=8, color=c["text"], alpha=0.9,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# ── 2. drawdown ──
|
|
204
|
+
ax2 = fig.add_subplot(gs[1, :], sharex=ax1)
|
|
205
|
+
ax2.fill_between(dd.index, dd.values, 0,
|
|
206
|
+
color=c["drawdown"], alpha=0.55, linewidth=0)
|
|
207
|
+
ax2.plot(dd.index, dd.values, color=c["drawdown"], linewidth=0.8)
|
|
208
|
+
ax2.set_ylabel("Drawdown (%)")
|
|
209
|
+
ax2.invert_yaxis()
|
|
210
|
+
_annotate_liq(ax2, result, c)
|
|
211
|
+
|
|
212
|
+
# ── 3. rolling Sharpe ──
|
|
213
|
+
ax3 = fig.add_subplot(gs[2, :], sharex=ax1)
|
|
214
|
+
ax3.plot(rs.index, rs.values, color=c["neutral"], linewidth=1.4)
|
|
215
|
+
ax3.axhline(0, color=c["grid"], linewidth=0.6)
|
|
216
|
+
ax3.axhline(1, color=c["long"], linewidth=0.6, linestyle="--", alpha=0.6)
|
|
217
|
+
ax3.set_ylabel("Rolling Sharpe (30d)")
|
|
218
|
+
|
|
219
|
+
_fmt_date(ax3)
|
|
220
|
+
|
|
221
|
+
# ── 4. monthly heatmap ──
|
|
222
|
+
ax4 = fig.add_subplot(gs[3, :])
|
|
223
|
+
_monthly_heatmap(result, ax4, c, trading_days)
|
|
224
|
+
|
|
225
|
+
# ── 5. PnL attribution ──
|
|
226
|
+
ax5 = fig.add_subplot(gs[4, :])
|
|
227
|
+
_pnl_attribution(result, ax5, c)
|
|
228
|
+
|
|
229
|
+
# ── 6. position exposure ──
|
|
230
|
+
ax6 = fig.add_subplot(gs[5, :], sharex=ax1)
|
|
231
|
+
_position_exposure(result, ax6, c)
|
|
232
|
+
|
|
233
|
+
for ax in [ax1, ax2, ax3, ax6]:
|
|
234
|
+
ax.set_xlim(eq.index.min(), eq.index.max())
|
|
235
|
+
|
|
236
|
+
fig.align_ylabels([ax1, ax2, ax3, ax6])
|
|
237
|
+
|
|
238
|
+
fig.autofmt_xdate(rotation=30, ha='right')
|
|
239
|
+
plt.tight_layout(pad=1.5, rect=[0, 0.02, 1, 1])
|
|
240
|
+
plt.show()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ── tearsheet sub-panels (private) ───────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
def _monthly_heatmap(result: BacktestResult, ax, c: dict, trading_days: int):
|
|
246
|
+
daily = result.daily_equity
|
|
247
|
+
try:
|
|
248
|
+
monthly = daily.resample("ME").last().pct_change().dropna() * 100
|
|
249
|
+
except Exception:
|
|
250
|
+
monthly = daily.resample("M").last().pct_change().dropna() * 100
|
|
251
|
+
|
|
252
|
+
years = sorted(monthly.index.year.unique())
|
|
253
|
+
heat = pd.DataFrame(0.0, index=years, columns=range(1, 13))
|
|
254
|
+
for idx, val in monthly.items():
|
|
255
|
+
heat.loc[idx.year, idx.month] = val
|
|
256
|
+
|
|
257
|
+
vmax = max(abs(heat.values).max(), 1.0)
|
|
258
|
+
sns.heatmap(
|
|
259
|
+
heat, annot=True, fmt=".1f", ax=ax,
|
|
260
|
+
cmap="RdYlGn", center=0, vmin=-vmax, vmax=vmax,
|
|
261
|
+
linewidths=0.4, linecolor=c["border"],
|
|
262
|
+
cbar_kws={"shrink": 0.6, "label": "%"},
|
|
263
|
+
annot_kws={"size": 7},
|
|
264
|
+
)
|
|
265
|
+
ax.set_title("Monthly Returns (%)", loc="left")
|
|
266
|
+
ax.set_xticklabels(
|
|
267
|
+
["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],
|
|
268
|
+
fontsize=7,
|
|
269
|
+
)
|
|
270
|
+
ax.set_yticklabels(ax.get_yticklabels(), fontsize=7)
|
|
271
|
+
ax.set_xlabel("")
|
|
272
|
+
ax.set_ylabel("")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _pnl_attribution(result: BacktestResult, ax, c: dict):
|
|
276
|
+
colors = list(PALETTE["dark"].values())[4:] # cycle through accent colours
|
|
277
|
+
for i, sym in enumerate(result.symbols):
|
|
278
|
+
price_change = result.closes[f"Close_{sym}"].diff().fillna(0)
|
|
279
|
+
prev_pos = result.positions[f"Position_{sym}"].shift(1).fillna(0)
|
|
280
|
+
contrib = (prev_pos * price_change).resample("1D").sum().cumsum()
|
|
281
|
+
ax.plot(
|
|
282
|
+
contrib.index, contrib.values,
|
|
283
|
+
label=sym,
|
|
284
|
+
color=colors[i % len(colors)],
|
|
285
|
+
linewidth=1.4,
|
|
286
|
+
)
|
|
287
|
+
ax.axhline(0, color=c["grid"], linewidth=0.6)
|
|
288
|
+
ax.set_ylabel("PnL Contribution")
|
|
289
|
+
ax.legend(loc="upper left", ncol=min(len(result.symbols), 6))
|
|
290
|
+
ax.set_title("Cumulative PnL Contribution per Symbol", loc="left")
|
|
291
|
+
_fmt_date(ax)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _position_exposure(result: BacktestResult, ax, c: dict):
|
|
295
|
+
colors = list(PALETTE["dark"].values())[4:]
|
|
296
|
+
for i, sym in enumerate(result.symbols):
|
|
297
|
+
pos = result.positions[f"Position_{sym}"].resample("1D").last()
|
|
298
|
+
ax.fill_between(
|
|
299
|
+
pos.index, pos.values, 0,
|
|
300
|
+
where=(pos.values > 0),
|
|
301
|
+
color=c["long"], alpha=0.5,
|
|
302
|
+
)
|
|
303
|
+
ax.fill_between(
|
|
304
|
+
pos.index, pos.values, 0,
|
|
305
|
+
where=(pos.values < 0),
|
|
306
|
+
color=c["short"], alpha=0.5,
|
|
307
|
+
)
|
|
308
|
+
ax.axhline(0, color=c["grid"], linewidth=0.6)
|
|
309
|
+
ax.set_ylabel("Position (units)")
|
|
310
|
+
ax.set_title("Position Exposure", loc="left")
|
quantbt/viz/themes.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
quantbt.viz.themes
|
|
3
|
+
------------------
|
|
4
|
+
Centralised styling. Two themes: 'dark' (presentation / screen)
|
|
5
|
+
and 'light' (report / print).
|
|
6
|
+
|
|
7
|
+
Usage
|
|
8
|
+
~~~~~
|
|
9
|
+
from quantbt.viz.themes import apply_theme, PALETTE
|
|
10
|
+
apply_theme('dark')
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import matplotlib as mpl
|
|
16
|
+
import matplotlib.pyplot as plt
|
|
17
|
+
|
|
18
|
+
# ── Palettes ─────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
PALETTE = {
|
|
21
|
+
"dark": {
|
|
22
|
+
"bg": "#0d1117",
|
|
23
|
+
"axes_bg": "#161b22",
|
|
24
|
+
"text": "#c9d1d9",
|
|
25
|
+
"grid": "#21262d",
|
|
26
|
+
"border": "#30363d",
|
|
27
|
+
"equity": "#58a6ff",
|
|
28
|
+
"drawdown": "#f85149",
|
|
29
|
+
"benchmark": "#8b949e",
|
|
30
|
+
"long": "#3fb950",
|
|
31
|
+
"short": "#f78166",
|
|
32
|
+
"neutral": "#a371f7",
|
|
33
|
+
"bar_pos": "#3fb950",
|
|
34
|
+
"bar_neg": "#f85149",
|
|
35
|
+
},
|
|
36
|
+
"light": {
|
|
37
|
+
"bg": "#ffffff",
|
|
38
|
+
"axes_bg": "#f6f8fa",
|
|
39
|
+
"text": "#24292f",
|
|
40
|
+
"grid": "#d0d7de",
|
|
41
|
+
"border": "#d0d7de",
|
|
42
|
+
"equity": "#0550ae",
|
|
43
|
+
"drawdown": "#cf222e",
|
|
44
|
+
"benchmark": "#57606a",
|
|
45
|
+
"long": "#1a7f37",
|
|
46
|
+
"short": "#cf222e",
|
|
47
|
+
"neutral": "#8250df",
|
|
48
|
+
"bar_pos": "#1a7f37",
|
|
49
|
+
"bar_neg": "#cf222e",
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def apply_theme(theme: str = "dark") -> dict:
|
|
55
|
+
"""
|
|
56
|
+
Apply matplotlib rcParams for the chosen theme.
|
|
57
|
+
Returns the colour palette dict for downstream use.
|
|
58
|
+
"""
|
|
59
|
+
if theme not in PALETTE:
|
|
60
|
+
raise ValueError(f"theme must be 'dark' or 'light', got '{theme}'")
|
|
61
|
+
|
|
62
|
+
c = PALETTE[theme]
|
|
63
|
+
|
|
64
|
+
mpl.rcParams.update({
|
|
65
|
+
# figure
|
|
66
|
+
"figure.facecolor": c["bg"],
|
|
67
|
+
"figure.dpi": 130,
|
|
68
|
+
# axes
|
|
69
|
+
"axes.facecolor": c["axes_bg"],
|
|
70
|
+
"axes.edgecolor": c["border"],
|
|
71
|
+
"axes.labelcolor": c["text"],
|
|
72
|
+
"axes.spines.top": False,
|
|
73
|
+
"axes.spines.right": False,
|
|
74
|
+
"axes.grid": True,
|
|
75
|
+
"axes.grid.axis": "y",
|
|
76
|
+
"axes.titlepad": 10,
|
|
77
|
+
"axes.titlesize": 11,
|
|
78
|
+
"axes.labelsize": 9,
|
|
79
|
+
# grid
|
|
80
|
+
"grid.color": c["grid"],
|
|
81
|
+
"grid.linewidth": 0.5,
|
|
82
|
+
"grid.alpha": 1.0,
|
|
83
|
+
# ticks
|
|
84
|
+
"xtick.color": c["text"],
|
|
85
|
+
"ytick.color": c["text"],
|
|
86
|
+
"xtick.labelsize": 8,
|
|
87
|
+
"ytick.labelsize": 8,
|
|
88
|
+
# text
|
|
89
|
+
"text.color": c["text"],
|
|
90
|
+
# legend
|
|
91
|
+
"legend.facecolor": c["axes_bg"],
|
|
92
|
+
"legend.edgecolor": c["border"],
|
|
93
|
+
"legend.fontsize": 8,
|
|
94
|
+
"legend.framealpha": 0.85,
|
|
95
|
+
# lines
|
|
96
|
+
"lines.linewidth": 1.6,
|
|
97
|
+
# font
|
|
98
|
+
"font.family": "monospace",
|
|
99
|
+
"font.size": 9,
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
return c
|