tensorquantlib 0.3.0__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 (44) hide show
  1. tensorquantlib/__init__.py +313 -0
  2. tensorquantlib/__main__.py +315 -0
  3. tensorquantlib/backtest/__init__.py +48 -0
  4. tensorquantlib/backtest/engine.py +240 -0
  5. tensorquantlib/backtest/metrics.py +320 -0
  6. tensorquantlib/backtest/strategy.py +348 -0
  7. tensorquantlib/core/__init__.py +6 -0
  8. tensorquantlib/core/ops.py +70 -0
  9. tensorquantlib/core/second_order.py +465 -0
  10. tensorquantlib/core/tensor.py +928 -0
  11. tensorquantlib/data/__init__.py +16 -0
  12. tensorquantlib/data/market.py +160 -0
  13. tensorquantlib/finance/__init__.py +52 -0
  14. tensorquantlib/finance/american.py +263 -0
  15. tensorquantlib/finance/basket.py +291 -0
  16. tensorquantlib/finance/black_scholes.py +219 -0
  17. tensorquantlib/finance/credit.py +199 -0
  18. tensorquantlib/finance/exotics.py +885 -0
  19. tensorquantlib/finance/fx.py +204 -0
  20. tensorquantlib/finance/greeks.py +133 -0
  21. tensorquantlib/finance/heston.py +543 -0
  22. tensorquantlib/finance/implied_vol.py +277 -0
  23. tensorquantlib/finance/ir_derivatives.py +203 -0
  24. tensorquantlib/finance/jump_diffusion.py +203 -0
  25. tensorquantlib/finance/local_vol.py +146 -0
  26. tensorquantlib/finance/rates.py +381 -0
  27. tensorquantlib/finance/risk.py +344 -0
  28. tensorquantlib/finance/variance_reduction.py +420 -0
  29. tensorquantlib/finance/volatility.py +355 -0
  30. tensorquantlib/py.typed +0 -0
  31. tensorquantlib/tt/__init__.py +43 -0
  32. tensorquantlib/tt/decompose.py +576 -0
  33. tensorquantlib/tt/ops.py +386 -0
  34. tensorquantlib/tt/pricing.py +304 -0
  35. tensorquantlib/tt/surrogate.py +634 -0
  36. tensorquantlib/utils/__init__.py +5 -0
  37. tensorquantlib/utils/validation.py +126 -0
  38. tensorquantlib/viz/__init__.py +27 -0
  39. tensorquantlib/viz/plots.py +331 -0
  40. tensorquantlib-0.3.0.dist-info/METADATA +602 -0
  41. tensorquantlib-0.3.0.dist-info/RECORD +44 -0
  42. tensorquantlib-0.3.0.dist-info/WHEEL +5 -0
  43. tensorquantlib-0.3.0.dist-info/licenses/LICENSE +21 -0
  44. tensorquantlib-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,240 @@
1
+ """Backtesting engine with realistic execution cost models."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+
6
+ import numpy as np
7
+
8
+ from tensorquantlib.backtest.strategy import Strategy, Trade
9
+
10
+
11
+ # ------------------------------------------------------------------ #
12
+ # Execution cost models
13
+ # ------------------------------------------------------------------ #
14
+
15
+ @dataclass
16
+ class SlippageModel:
17
+ """Market-impact and bid-ask spread model.
18
+
19
+ Total slippage = half-spread cost + square-root market-impact cost.
20
+
21
+ Parameters
22
+ ----------
23
+ fixed_spread : float
24
+ One-way half-spread as a fraction of price.
25
+ E.g. ``0.0005`` = 5 bps one-way (10 bps round-trip).
26
+ market_impact : float
27
+ Square-root market-impact coefficient.
28
+ Cost per unit = ``price × market_impact × sqrt(|qty| / adv)``.
29
+ Set to 0 to disable market impact.
30
+ adv : float
31
+ Average daily volume (units). Used for impact scaling only.
32
+ """
33
+
34
+ fixed_spread: float = 0.0
35
+ market_impact: float = 0.0
36
+ adv: float = 1_000_000.0
37
+
38
+ def cost(self, price: float, quantity: float) -> float:
39
+ """Compute slippage cost for one trade (always non-negative)."""
40
+ spread_cost = abs(quantity) * price * self.fixed_spread
41
+ if self.market_impact > 0.0 and self.adv > 0.0:
42
+ impact_cost = (abs(quantity) * price * self.market_impact
43
+ * np.sqrt(abs(quantity) / self.adv))
44
+ else:
45
+ impact_cost = 0.0
46
+ return float(spread_cost + impact_cost)
47
+
48
+
49
+ @dataclass
50
+ class CommissionModel:
51
+ """Commission / brokerage fee model.
52
+
53
+ Total commission = max(per_trade + per_unit×|qty| + percentage×notional,
54
+ minimum).
55
+
56
+ Parameters
57
+ ----------
58
+ per_trade : float
59
+ Fixed fee per order (e.g. ``1.0`` = $1 per trade).
60
+ per_unit : float
61
+ Fee per unit traded (e.g. ``0.005`` = half a cent per share).
62
+ percentage : float
63
+ Fraction of notional (e.g. ``0.001`` = 10 bps of notional).
64
+ minimum : float
65
+ Minimum commission per trade.
66
+ """
67
+
68
+ per_trade: float = 0.0
69
+ per_unit: float = 0.0
70
+ percentage: float = 0.0
71
+ minimum: float = 0.0
72
+
73
+ def cost(self, price: float, quantity: float) -> float:
74
+ """Compute commission for one trade (always non-negative)."""
75
+ notional = abs(quantity) * price
76
+ c = (self.per_trade
77
+ + abs(quantity) * self.per_unit
78
+ + notional * self.percentage)
79
+ return float(max(c, self.minimum))
80
+
81
+
82
+ # Pre-built convenience presets
83
+ #: Zero-cost model (default when no model is supplied).
84
+ ZERO_COST = CommissionModel()
85
+ #: Interactive Brokers-style equity commission ($0.005/share, $1 min).
86
+ EQUITY_COMM = CommissionModel(per_unit=0.005, minimum=1.0)
87
+ #: Typical institutional FX desk (0.2 bps of notional).
88
+ FX_COMM = CommissionModel(percentage=0.00002)
89
+ #: Liquid-equity half-spread slippage (5 bps one-way).
90
+ EQUITY_SLIP = SlippageModel(fixed_spread=0.0005)
91
+ #: Illiquid name: 20 bps spread + square-root market impact.
92
+ ILLIQUID_SLIP = SlippageModel(fixed_spread=0.002, market_impact=0.1, adv=50_000)
93
+
94
+
95
+ # ------------------------------------------------------------------ #
96
+ # Backtest result
97
+ # ------------------------------------------------------------------ #
98
+
99
+ @dataclass
100
+ class BacktestResult:
101
+ """Container for backtest output."""
102
+
103
+ equity_curve: np.ndarray
104
+ """Equity value at each step."""
105
+
106
+ trades: list[Trade]
107
+ """List of all executed trades."""
108
+
109
+ returns: np.ndarray
110
+ """Per-step returns of the equity curve."""
111
+
112
+ final_equity: float
113
+ """Final portfolio value."""
114
+
115
+ total_commission: float = 0.0
116
+ """Total commissions paid during the backtest."""
117
+
118
+ total_slippage: float = 0.0
119
+ """Total slippage cost paid during the backtest."""
120
+
121
+ total_turnover: float = 0.0
122
+ """Total gross notional traded (sum of |qty| * price)."""
123
+
124
+ n_trades: int = 0
125
+ """Number of trades executed."""
126
+
127
+ greeks_history: dict = field(default_factory=dict)
128
+ """Per-step Greeks recorded by the strategy (if any)."""
129
+
130
+
131
+ # ------------------------------------------------------------------ #
132
+ # Engine
133
+ # ------------------------------------------------------------------ #
134
+
135
+ class BacktestEngine:
136
+ """Run a :class:`Strategy` over a price series with realistic execution.
137
+
138
+ Parameters
139
+ ----------
140
+ strategy : Strategy
141
+ Strategy instance to run.
142
+ prices : array-like
143
+ 1-D array of asset prices (one per time step).
144
+ initial_capital : float
145
+ Starting cash.
146
+ slippage : SlippageModel, optional
147
+ Slippage model. Defaults to ``SlippageModel()`` (zero slippage).
148
+ Use :data:`EQUITY_SLIP` for a realistic liquid-equity preset.
149
+ commission : CommissionModel, optional
150
+ Commission model. Defaults to ``CommissionModel()`` (zero fees).
151
+ Use :data:`EQUITY_COMM` for an Interactive Brokers-style preset.
152
+
153
+ Examples
154
+ --------
155
+ Zero-cost (default)::
156
+
157
+ engine = BacktestEngine(strategy, prices)
158
+
159
+ With realistic costs::
160
+
161
+ from tensorquantlib.backtest.engine import EQUITY_SLIP, EQUITY_COMM
162
+ engine = BacktestEngine(
163
+ strategy, prices,
164
+ slippage=EQUITY_SLIP,
165
+ commission=EQUITY_COMM,
166
+ )
167
+ """
168
+
169
+ def __init__(
170
+ self,
171
+ strategy: Strategy,
172
+ prices,
173
+ initial_capital: float = 1_000_000.0,
174
+ slippage: SlippageModel | None = None,
175
+ commission: CommissionModel | None = None,
176
+ ):
177
+ self.strategy = strategy
178
+ self.prices = np.asarray(prices, dtype=float)
179
+ self.initial_capital = initial_capital
180
+ self.slippage = slippage if slippage is not None else SlippageModel()
181
+ self.commission = commission if commission is not None else CommissionModel()
182
+
183
+ def run(self) -> BacktestResult:
184
+ """Execute the backtest and return a :class:`BacktestResult`."""
185
+ strat = self.strategy
186
+ prices = self.prices
187
+ n = len(prices)
188
+
189
+ strat.cash = self.initial_capital
190
+ strat.position = 0.0
191
+ strat.trades = []
192
+
193
+ equity = np.zeros(n)
194
+ total_commission = 0.0
195
+ total_slippage = 0.0
196
+ total_turnover = 0.0
197
+
198
+ for i in range(n):
199
+ desired = strat.on_data(i, prices[i])
200
+ delta_pos = desired - strat.position
201
+
202
+ if abs(delta_pos) > 1e-12:
203
+ slip = self.slippage.cost(prices[i], delta_pos)
204
+ comm = self.commission.cost(prices[i], delta_pos)
205
+ notional = abs(delta_pos) * prices[i]
206
+
207
+ total_slippage += slip
208
+ total_commission += comm
209
+ total_turnover += notional
210
+
211
+ # Cash: pay for shares + execution costs
212
+ strat.cash -= delta_pos * prices[i] + slip + comm
213
+ strat.position = desired
214
+
215
+ trade = Trade(
216
+ step=i,
217
+ quantity=delta_pos,
218
+ price=prices[i],
219
+ slippage=slip,
220
+ commission=comm,
221
+ )
222
+ strat.trades.append(trade)
223
+ strat.on_fill(trade)
224
+
225
+ equity[i] = strat.cash + strat.position * prices[i]
226
+
227
+ returns = np.diff(equity) / np.where(
228
+ np.abs(equity[:-1]) > 1e-12, equity[:-1], 1.0
229
+ )
230
+ return BacktestResult(
231
+ equity_curve=equity,
232
+ trades=strat.trades,
233
+ returns=returns,
234
+ final_equity=float(equity[-1]),
235
+ total_commission=total_commission,
236
+ total_slippage=total_slippage,
237
+ total_turnover=total_turnover,
238
+ n_trades=len(strat.trades),
239
+ greeks_history=dict(getattr(strat, "_greeks_history", {})),
240
+ )
@@ -0,0 +1,320 @@
1
+ """Performance metrics for backtesting."""
2
+ from __future__ import annotations
3
+
4
+ from typing import TYPE_CHECKING
5
+
6
+ import numpy as np
7
+
8
+ if TYPE_CHECKING:
9
+ from tensorquantlib.backtest.strategy import Trade
10
+
11
+
12
+ def sharpe_ratio(returns: np.ndarray, rf: float = 0.0,
13
+ periods_per_year: int = 252) -> float:
14
+ """Annualised Sharpe ratio.
15
+
16
+ Parameters
17
+ ----------
18
+ returns : array-like
19
+ Period returns (e.g. daily).
20
+ rf : float
21
+ Risk-free rate per period.
22
+ periods_per_year : int
23
+ Annualisation factor (252 for daily).
24
+
25
+ Returns
26
+ -------
27
+ float
28
+ Annualised Sharpe ratio.
29
+ """
30
+ r = np.asarray(returns, dtype=float)
31
+ excess = r - rf
32
+ std = np.std(excess, ddof=1)
33
+ if std < 1e-15:
34
+ return 0.0
35
+ return float(np.mean(excess) / std * np.sqrt(periods_per_year))
36
+
37
+
38
+ def max_drawdown(equity: np.ndarray) -> float:
39
+ """Maximum drawdown from peak.
40
+
41
+ Parameters
42
+ ----------
43
+ equity : array-like
44
+ Equity curve (absolute values).
45
+
46
+ Returns
47
+ -------
48
+ float
49
+ Maximum drawdown as a positive fraction (e.g. 0.15 = 15%).
50
+ """
51
+ eq = np.asarray(equity, dtype=float)
52
+ peak = np.maximum.accumulate(eq)
53
+ dd = (peak - eq) / np.where(peak > 0, peak, 1.0)
54
+ return float(np.max(dd))
55
+
56
+
57
+ def sortino_ratio(returns: np.ndarray, rf: float = 0.0,
58
+ periods_per_year: int = 252) -> float:
59
+ """Annualised Sortino ratio (downside deviation only).
60
+
61
+ Parameters
62
+ ----------
63
+ returns : array-like
64
+ Period returns.
65
+ rf : float
66
+ Risk-free rate per period.
67
+ periods_per_year : int
68
+ Annualisation factor.
69
+
70
+ Returns
71
+ -------
72
+ float
73
+ Annualised Sortino ratio.
74
+ """
75
+ r = np.asarray(returns, dtype=float)
76
+ excess = r - rf
77
+ downside = excess[excess < 0]
78
+ if len(downside) == 0:
79
+ return float("inf") if np.mean(excess) > 0 else 0.0
80
+ down_std = np.sqrt(np.mean(downside**2))
81
+ if down_std < 1e-15:
82
+ return 0.0
83
+ return float(np.mean(excess) / down_std * np.sqrt(periods_per_year))
84
+
85
+
86
+ def win_rate(trades: np.ndarray) -> float:
87
+ """Fraction of profitable trades.
88
+
89
+ Parameters
90
+ ----------
91
+ trades : array-like
92
+ P&L per trade (positive = profit).
93
+
94
+ Returns
95
+ -------
96
+ float
97
+ Win rate in [0, 1].
98
+ """
99
+ t = np.asarray(trades, dtype=float)
100
+ if len(t) == 0:
101
+ return 0.0
102
+ return float(np.sum(t > 0) / len(t))
103
+
104
+
105
+ def profit_factor(trades: np.ndarray) -> float:
106
+ """Profit factor = gross profits / gross losses.
107
+
108
+ Parameters
109
+ ----------
110
+ trades : array-like
111
+ P&L per trade.
112
+
113
+ Returns
114
+ -------
115
+ float
116
+ Profit factor. Returns inf if no losses.
117
+ """
118
+ t = np.asarray(trades, dtype=float)
119
+ gross_profit = float(np.sum(t[t > 0]))
120
+ gross_loss = float(np.abs(np.sum(t[t < 0])))
121
+ if gross_loss < 1e-15:
122
+ return float("inf") if gross_profit > 0 else 0.0
123
+ return gross_profit / gross_loss
124
+
125
+
126
+ # ------------------------------------------------------------------ #
127
+ # Additional metrics
128
+ # ------------------------------------------------------------------ #
129
+
130
+ def annualized_return(equity: np.ndarray, periods_per_year: int = 252) -> float:
131
+ """Compound annualized growth rate (CAGR) from an equity curve.
132
+
133
+ Parameters
134
+ ----------
135
+ equity : array-like
136
+ Equity value at each step (must start > 0).
137
+ periods_per_year : int
138
+ Annualisation factor (252 for daily).
139
+
140
+ Returns
141
+ -------
142
+ float
143
+ Annualized return, e.g. 0.12 = 12 %.
144
+ """
145
+ eq = np.asarray(equity, dtype=float)
146
+ if len(eq) < 2 or eq[0] <= 0:
147
+ return 0.0
148
+ total_periods = len(eq) - 1
149
+ years = total_periods / periods_per_year
150
+ if years <= 0:
151
+ return 0.0
152
+ return float((eq[-1] / eq[0]) ** (1.0 / years) - 1.0)
153
+
154
+
155
+ def calmar_ratio(equity: np.ndarray, periods_per_year: int = 252) -> float:
156
+ """Calmar ratio = annualized return / maximum drawdown.
157
+
158
+ Parameters
159
+ ----------
160
+ equity : array-like
161
+ Equity curve.
162
+ periods_per_year : int
163
+ Annualisation factor.
164
+
165
+ Returns
166
+ -------
167
+ float
168
+ Calmar ratio. Returns ``inf`` if max drawdown is zero.
169
+ """
170
+ ann_ret = annualized_return(equity, periods_per_year)
171
+ mdd = max_drawdown(equity)
172
+ if mdd < 1e-15:
173
+ return float("inf") if ann_ret > 0 else 0.0
174
+ return float(ann_ret / mdd)
175
+
176
+
177
+ def information_ratio(
178
+ strategy_returns: np.ndarray,
179
+ benchmark_returns: np.ndarray,
180
+ periods_per_year: int = 252,
181
+ ) -> float:
182
+ """Annualised information ratio (IR).
183
+
184
+ IR = E[active_return] / std(active_return) * sqrt(periods_per_year)
185
+
186
+ Parameters
187
+ ----------
188
+ strategy_returns : array-like
189
+ Period returns of the strategy.
190
+ benchmark_returns : array-like
191
+ Period returns of the benchmark.
192
+ periods_per_year : int
193
+ Annualisation factor.
194
+
195
+ Returns
196
+ -------
197
+ float
198
+ Annualised information ratio.
199
+ """
200
+ active = np.asarray(strategy_returns, dtype=float) - np.asarray(benchmark_returns, dtype=float)
201
+ std = np.std(active, ddof=1)
202
+ if std < 1e-15:
203
+ return 0.0
204
+ return float(np.mean(active) / std * np.sqrt(periods_per_year))
205
+
206
+
207
+ def turnover(trades: list, initial_capital: float = 1_000_000.0) -> float:
208
+ """Annualized one-way turnover as a fraction of initial capital.
209
+
210
+ turnover = total_notional_traded / initial_capital
211
+
212
+ Parameters
213
+ ----------
214
+ trades : list of Trade
215
+ Executed trades (each must have ``.quantity`` and ``.price``).
216
+ initial_capital : float
217
+ Starting capital used as the denominator.
218
+
219
+ Returns
220
+ -------
221
+ float
222
+ Turnover (e.g. 2.5 = 250 % of capital traded).
223
+ """
224
+ if not trades:
225
+ return 0.0
226
+ total_notional = sum(abs(t.quantity) * t.price for t in trades)
227
+ return float(total_notional / initial_capital)
228
+
229
+
230
+ def hedge_pnl_attribution(
231
+ equity_curve: np.ndarray,
232
+ deltas: list,
233
+ gammas: list,
234
+ prices: np.ndarray,
235
+ ) -> dict[str, np.ndarray]:
236
+ """Decompose per-step portfolio P&L into Delta, Gamma, and residual.
237
+
238
+ For a continuously delta-hedged portfolio, the per-step P&L can be
239
+ approximated as:
240
+
241
+ .. math::
242
+
243
+ \\text{Total P\\&L} \\approx
244
+ \\underbrace{\\Delta \\cdot \\Delta S}_{\\text{delta P\\&L}}
245
+ + \\underbrace{\\tfrac{1}{2}\\Gamma (\\Delta S)^2}_{\\text{gamma P\\&L}}
246
+ + \\text{residual}
247
+
248
+ The residual captures higher-order terms (Theta decay, vanna, etc.) and
249
+ hedging error from discrete rebalancing.
250
+
251
+ Parameters
252
+ ----------
253
+ equity_curve : array-like
254
+ Portfolio equity at each step (length N).
255
+ deltas : list of float
256
+ Per-step delta of the overall position (length N).
257
+ gammas : list of float
258
+ Per-step gamma (length N).
259
+ prices : array-like
260
+ Underlying asset price at each step (length N).
261
+
262
+ Returns
263
+ -------
264
+ dict
265
+ Keys ``'delta_pnl'``, ``'gamma_pnl'``, ``'residual_pnl'``.
266
+ Each is an array of length N-1.
267
+ """
268
+ equity = np.asarray(equity_curve, dtype=float)
269
+ prices_arr = np.asarray(prices, dtype=float)
270
+ delta_arr = np.asarray(deltas, dtype=float)
271
+ gamma_arr = np.asarray(gammas, dtype=float)
272
+
273
+ n = len(equity) - 1
274
+ total_pnl = np.diff(equity)
275
+ dS = np.diff(prices_arr[:n + 1])
276
+
277
+ delta_pnl = delta_arr[:n] * dS
278
+ gamma_pnl = 0.5 * gamma_arr[:n] * dS ** 2
279
+ residual_pnl = total_pnl - delta_pnl - gamma_pnl
280
+
281
+ return {
282
+ "delta_pnl": delta_pnl,
283
+ "gamma_pnl": gamma_pnl,
284
+ "residual_pnl": residual_pnl,
285
+ }
286
+
287
+
288
+ def hedge_efficiency(
289
+ hedged_equity: np.ndarray,
290
+ unhedged_equity: np.ndarray,
291
+ ) -> float:
292
+ """Variance reduction achieved by a hedge.
293
+
294
+ .. math::
295
+
296
+ \\text{efficiency} = 1 - \\frac{\\operatorname{Var}(\\text{hedged P\\&L})}{
297
+ \\operatorname{Var}(\\text{unhedged P\\&L})}
298
+
299
+ Returns 1.0 for a perfect hedge, 0.0 if the hedge adds no value,
300
+ and a negative number if the hedge increases variance.
301
+
302
+ Parameters
303
+ ----------
304
+ hedged_equity : array-like
305
+ Equity curve of the hedged portfolio.
306
+ unhedged_equity : array-like
307
+ Equity curve of the same portfolio *without* hedging.
308
+
309
+ Returns
310
+ -------
311
+ float
312
+ Hedge efficiency in (-inf, 1].
313
+ """
314
+ hedged_ret = np.diff(np.asarray(hedged_equity, dtype=float))
315
+ unhedged_ret = np.diff(np.asarray(unhedged_equity, dtype=float))
316
+ var_unhedged = float(np.var(unhedged_ret, ddof=1))
317
+ if var_unhedged < 1e-15:
318
+ return 0.0
319
+ var_hedged = float(np.var(hedged_ret, ddof=1))
320
+ return float(1.0 - var_hedged / var_unhedged)