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.
Files changed (133) hide show
  1. quantbt/__init__.py +862 -0
  2. quantbt/adapters/__init__.py +5 -0
  3. quantbt/adapters/nautilus/__init__.py +42 -0
  4. quantbt/adapters/nautilus/_dependency.py +53 -0
  5. quantbt/adapters/nautilus/backend.py +639 -0
  6. quantbt/adapters/nautilus/instruments.py +275 -0
  7. quantbt/adapters/nautilus/options.py +465 -0
  8. quantbt/adapters/nautilus/reports.py +231 -0
  9. quantbt/backends/__init__.py +29 -0
  10. quantbt/backends/_native_event_rust.py +2168 -0
  11. quantbt/backends/native_event.py +5048 -0
  12. quantbt/backends/native_option.py +796 -0
  13. quantbt/backends/native_portfolio.py +928 -0
  14. quantbt/backends/native_vectorized.py +1299 -0
  15. quantbt/backtester.py +485 -0
  16. quantbt/benchmarks/__init__.py +1 -0
  17. quantbt/benchmarks/compare_phase9_parity.py +302 -0
  18. quantbt/benchmarks/gamma_scalping_backtestsample.py +802 -0
  19. quantbt/benchmarks/profile_phase7.py +415 -0
  20. quantbt/benchmarks/run_arbitrage_phase_e.py +160 -0
  21. quantbt/benchmarks/run_optimization_overhead.py +195 -0
  22. quantbt/benchmarks/run_options_engine.py +272 -0
  23. quantbt/benchmarks/run_pct_equity_nautilus_smoke.py +227 -0
  24. quantbt/benchmarks/run_phase12_arbitrage_cert.py +402 -0
  25. quantbt/benchmarks/run_phase12_benchmark_nautilus_cert.py +430 -0
  26. quantbt/benchmarks/run_phase13_portfolio_report.py +97 -0
  27. quantbt/benchmarks/run_phase13_wfo_cache.py +162 -0
  28. quantbt/benchmarks/run_phase14_service_loop.py +694 -0
  29. quantbt/benchmarks/run_phase15a_nautilus_certification.py +514 -0
  30. quantbt/benchmarks/run_phase15b_synthetic_depth.py +175 -0
  31. quantbt/benchmarks/run_phase16_performance_debt.py +389 -0
  32. quantbt/benchmarks/run_phase30e_reactive_runner.py +161 -0
  33. quantbt/benchmarks/run_phase31_intrabar.py +441 -0
  34. quantbt/benchmarks/run_phase34a_native_event_memory.py +179 -0
  35. quantbt/benchmarks/run_phase34b_native_event_prepared_score.py +173 -0
  36. quantbt/benchmarks/run_phase34c_native_event_single_pass.py +176 -0
  37. quantbt/benchmarks/run_phase7.py +654 -0
  38. quantbt/benchmarks/run_portfolio_real_parity.py +537 -0
  39. quantbt/core/__init__.py +323 -0
  40. quantbt/core/arbitrage.py +767 -0
  41. quantbt/core/basket.py +234 -0
  42. quantbt/core/certification.py +274 -0
  43. quantbt/core/constraints.py +155 -0
  44. quantbt/core/engine.py +1144 -0
  45. quantbt/core/event.py +869 -0
  46. quantbt/core/execution_contract.py +197 -0
  47. quantbt/core/execution_depth.py +613 -0
  48. quantbt/core/intrabar_kernel.py +1906 -0
  49. quantbt/core/intrabar_reference.py +907 -0
  50. quantbt/core/intrabar_session.py +156 -0
  51. quantbt/core/market_tape.py +480 -0
  52. quantbt/core/native_event_capabilities.py +121 -0
  53. quantbt/core/native_event_parity.py +336 -0
  54. quantbt/core/order_compiler.py +422 -0
  55. quantbt/core/orders.py +319 -0
  56. quantbt/core/portfolio.py +392 -0
  57. quantbt/core/preprocessor.py +256 -0
  58. quantbt/core/reactive.py +173 -0
  59. quantbt/core/results.py +341 -0
  60. quantbt/core/schema.py +211 -0
  61. quantbt/core/scopes.py +96 -0
  62. quantbt/core/structured_orders.py +378 -0
  63. quantbt/core/types.py +87 -0
  64. quantbt/core/vectorized.py +203 -0
  65. quantbt/endpoint.py +4465 -0
  66. quantbt/engines.py +1014 -0
  67. quantbt/metrics/__init__.py +45 -0
  68. quantbt/metrics/options_analytics.py +46 -0
  69. quantbt/metrics/performance.py +541 -0
  70. quantbt/optimization/__init__.py +94 -0
  71. quantbt/optimization/callbacks.py +116 -0
  72. quantbt/optimization/candidate_selection.py +397 -0
  73. quantbt/optimization/config.py +84 -0
  74. quantbt/optimization/constraints.py +22 -0
  75. quantbt/optimization/evaluator.py +21 -0
  76. quantbt/optimization/evaluators/__init__.py +32 -0
  77. quantbt/optimization/evaluators/arbitrage.py +22 -0
  78. quantbt/optimization/evaluators/generic.py +34 -0
  79. quantbt/optimization/evaluators/grid_dca.py +22 -0
  80. quantbt/optimization/evaluators/intrabar.py +54 -0
  81. quantbt/optimization/evaluators/native_event.py +51 -0
  82. quantbt/optimization/evaluators/options.py +22 -0
  83. quantbt/optimization/evaluators/portfolio.py +42 -0
  84. quantbt/optimization/evaluators/signal.py +43 -0
  85. quantbt/optimization/multiseed.py +176 -0
  86. quantbt/optimization/objectives.py +221 -0
  87. quantbt/optimization/optimizer.py +500 -0
  88. quantbt/optimization/result.py +80 -0
  89. quantbt/optimization/samplers.py +84 -0
  90. quantbt/optimization/space.py +223 -0
  91. quantbt/options/__init__.py +215 -0
  92. quantbt/options/cache.py +116 -0
  93. quantbt/options/conventions.py +158 -0
  94. quantbt/options/data.py +177 -0
  95. quantbt/options/execution.py +600 -0
  96. quantbt/options/fees.py +135 -0
  97. quantbt/options/greeks.py +173 -0
  98. quantbt/options/hedging.py +232 -0
  99. quantbt/options/iv.py +188 -0
  100. quantbt/options/ledger.py +263 -0
  101. quantbt/options/lifecycle.py +94 -0
  102. quantbt/options/margin.py +311 -0
  103. quantbt/options/packages.py +147 -0
  104. quantbt/options/pricing.py +191 -0
  105. quantbt/options/schema.py +227 -0
  106. quantbt/options/selectors.py +281 -0
  107. quantbt/options/strategy.py +249 -0
  108. quantbt/options/surface.py +142 -0
  109. quantbt/options/tape.py +226 -0
  110. quantbt/options/templates/__init__.py +33 -0
  111. quantbt/options/templates/packages.py +354 -0
  112. quantbt/portfolio.py +606 -0
  113. quantbt/py.typed +1 -0
  114. quantbt/reporting/__init__.py +38 -0
  115. quantbt/reporting/arbitrage_audit.py +211 -0
  116. quantbt/reporting/nautilus_bundle.py +758 -0
  117. quantbt/reporting/nautilus_certification.py +226 -0
  118. quantbt/reporting/nautilus_diagnostics.py +225 -0
  119. quantbt/reporting/parity.py +496 -0
  120. quantbt/reporting/portfolio_audit.py +206 -0
  121. quantbt/reporting/portfolio_nautilus.py +140 -0
  122. quantbt/sizing/__init__.py +3 -0
  123. quantbt/sizing/fast.py +67 -0
  124. quantbt/sizing/modes.py +162 -0
  125. quantbt/viz/__init__.py +4 -0
  126. quantbt/viz/plots.py +310 -0
  127. quantbt/viz/themes.py +102 -0
  128. quantbt/walkforward.py +3136 -0
  129. quantbt_engine-1.0.7.dist-info/METADATA +891 -0
  130. quantbt_engine-1.0.7.dist-info/RECORD +133 -0
  131. quantbt_engine-1.0.7.dist-info/WHEEL +5 -0
  132. quantbt_engine-1.0.7.dist-info/licenses/LICENSE +21 -0
  133. 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