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,348 @@
1
+ """Abstract base strategy and concrete strategy implementations."""
2
+ from __future__ import annotations
3
+
4
+ from abc import ABC, abstractmethod
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+ import numpy as np
9
+
10
+
11
+ @dataclass
12
+ class Trade:
13
+ """Record of a single trade."""
14
+
15
+ step: int
16
+ quantity: float # positive = buy, negative = sell
17
+ price: float
18
+ label: str = ""
19
+ slippage: float = 0.0 # slippage cost paid on this trade
20
+ commission: float = 0.0 # commission paid on this trade
21
+
22
+ @property
23
+ def notional(self) -> float:
24
+ """Gross notional value of the trade."""
25
+ return abs(self.quantity) * self.price
26
+
27
+ @property
28
+ def total_cost(self) -> float:
29
+ """Total execution cost (slippage + commission)."""
30
+ return self.slippage + self.commission
31
+
32
+
33
+ class Strategy(ABC):
34
+ """Base class for backtesting strategies.
35
+
36
+ Sub-classes must implement :meth:`on_data`.
37
+ """
38
+
39
+ def __init__(self):
40
+ self.position: float = 0.0
41
+ self.trades: list[Trade] = []
42
+ self.cash: float = 0.0
43
+ self._greeks_history: dict[str, list] = {}
44
+
45
+ @abstractmethod
46
+ def on_data(self, step: int, price: float, **kwargs) -> float:
47
+ """Called each time step with current price.
48
+
49
+ Parameters
50
+ ----------
51
+ step : int
52
+ Current time step index.
53
+ price : float
54
+ Current asset price.
55
+
56
+ Returns
57
+ -------
58
+ float
59
+ Desired position size (signed). The engine will trade the
60
+ difference between current and desired position.
61
+ """
62
+
63
+ def on_fill(self, trade: Trade) -> None:
64
+ """Called after a trade is executed. Override for bookkeeping."""
65
+
66
+
67
+ class DeltaHedgeStrategy(Strategy):
68
+ """Delta-hedge a short option position using Black-Scholes delta.
69
+
70
+ At each step compute BS delta and rebalance the hedge portfolio.
71
+ Also tracks per-step Delta and Gamma in ``_greeks_history`` for
72
+ P&L attribution via :func:`~tensorquantlib.backtest.metrics.hedge_pnl_attribution`.
73
+
74
+ Parameters
75
+ ----------
76
+ K : float
77
+ Option strike.
78
+ T_total : float
79
+ Total time to expiry at inception (years).
80
+ r : float
81
+ Risk-free rate.
82
+ sigma : float
83
+ Implied volatility.
84
+ option_type : str
85
+ ``'call'`` or ``'put'``.
86
+ n_steps : int
87
+ Total number of rebalancing steps.
88
+ """
89
+
90
+ def __init__(self, K: float, T_total: float, r: float, sigma: float,
91
+ option_type: str = "call", n_steps: int = 252):
92
+ super().__init__()
93
+ self.K = K
94
+ self.T_total = T_total
95
+ self.r = r
96
+ self.sigma = sigma
97
+ self.option_type = option_type
98
+ self.n_steps = n_steps
99
+ self._greeks_history: dict[str, list] = {"delta": [], "gamma": [], "T_remain": []}
100
+
101
+ def _bs_delta(self, S: float, T_remain: float) -> float:
102
+ from scipy.stats import norm
103
+ if T_remain <= 0:
104
+ if self.option_type == "call":
105
+ return 1.0 if S > self.K else 0.0
106
+ else:
107
+ return -1.0 if S < self.K else 0.0
108
+ d1 = (np.log(S / self.K) + (self.r + 0.5 * self.sigma**2) * T_remain) / \
109
+ (self.sigma * np.sqrt(T_remain))
110
+ if self.option_type == "call":
111
+ return float(norm.cdf(d1))
112
+ return float(norm.cdf(d1) - 1.0)
113
+
114
+ def _bs_gamma(self, S: float, T_remain: float) -> float:
115
+ from scipy.stats import norm
116
+ if T_remain <= 0:
117
+ return 0.0
118
+ d1 = (np.log(S / self.K) + (self.r + 0.5 * self.sigma**2) * T_remain) / \
119
+ (self.sigma * np.sqrt(T_remain))
120
+ return float(norm.pdf(d1) / (S * self.sigma * np.sqrt(T_remain)))
121
+
122
+ def on_data(self, step: int, price: float, **kwargs) -> float:
123
+ T_remain = max(self.T_total * (1 - step / self.n_steps), 0.0)
124
+ delta = self._bs_delta(price, T_remain)
125
+ gamma = self._bs_gamma(price, T_remain)
126
+ self._greeks_history["delta"].append(delta)
127
+ self._greeks_history["gamma"].append(gamma)
128
+ self._greeks_history["T_remain"].append(T_remain)
129
+ return delta
130
+
131
+
132
+ class GammaScalpingStrategy(Strategy):
133
+ """Gamma scalping: delta-hedge a long straddle and harvest realized volatility.
134
+
135
+ The strategy holds a synthetic long straddle (long gamma) and
136
+ continuously delta-hedges to remain directionally neutral. It
137
+ profits when *realized* volatility exceeds the *implied* volatility
138
+ embedded in the initial option price.
139
+
140
+ Per-step P&L attribution (stored in ``_greeks_history``):
141
+
142
+ .. code-block:: text
143
+
144
+ Daily P&L ≈ ½Γ(ΔS)² (gamma P&L — profits from large moves)
145
+ + Θ · Δt (theta P&L — loses time value daily)
146
+ + residual (higher-order / hedging error)
147
+
148
+ When realized_vol > implied_vol the gamma P&L dominates and the
149
+ strategy is profitable over the full holding period.
150
+
151
+ Parameters
152
+ ----------
153
+ K : float
154
+ Straddle strike (ideally near-ATM).
155
+ T_total : float
156
+ Time to expiry at inception (years).
157
+ r : float
158
+ Risk-free rate.
159
+ sigma_implied : float
160
+ Implied volatility at inception.
161
+ n_steps : int
162
+ Total rebalancing steps.
163
+ """
164
+
165
+ def __init__(self, K: float, T_total: float, r: float,
166
+ sigma_implied: float, n_steps: int = 252):
167
+ super().__init__()
168
+ self.K = K
169
+ self.T_total = T_total
170
+ self.r = r
171
+ self.sigma = sigma_implied
172
+ self.n_steps = n_steps
173
+ self._greeks_history: dict[str, list] = {
174
+ "delta": [], "gamma": [], "theta": [],
175
+ "theoretical_gamma_pnl": [], "theoretical_theta_pnl": [],
176
+ }
177
+ self._prev_price: float | None = None
178
+
179
+ def _straddle_delta(self, S: float, T_remain: float) -> float:
180
+ """Straddle delta = N(d1) − N(−d1) = 2N(d1) − 1."""
181
+ from scipy.stats import norm
182
+ if T_remain <= 0:
183
+ return 0.0
184
+ d1 = (np.log(S / self.K) + (self.r + 0.5 * self.sigma ** 2) * T_remain) / \
185
+ (self.sigma * np.sqrt(T_remain))
186
+ return float(2.0 * norm.cdf(d1) - 1.0)
187
+
188
+ def _straddle_gamma(self, S: float, T_remain: float) -> float:
189
+ """Straddle Gamma = 2 × call Gamma (call and put share the same Gamma)."""
190
+ from scipy.stats import norm
191
+ if T_remain <= 0:
192
+ return 0.0
193
+ d1 = (np.log(S / self.K) + (self.r + 0.5 * self.sigma ** 2) * T_remain) / \
194
+ (self.sigma * np.sqrt(T_remain))
195
+ return float(2.0 * norm.pdf(d1) / (S * self.sigma * np.sqrt(T_remain)))
196
+
197
+ def _straddle_theta(self, S: float, T_remain: float) -> float:
198
+ """Straddle Theta per year (negative = time decay)."""
199
+ from scipy.stats import norm
200
+ if T_remain <= 0:
201
+ return 0.0
202
+ d1 = (np.log(S / self.K) + (self.r + 0.5 * self.sigma ** 2) * T_remain) / \
203
+ (self.sigma * np.sqrt(T_remain))
204
+ d2 = d1 - self.sigma * np.sqrt(T_remain)
205
+ theta_call = (-S * norm.pdf(d1) * self.sigma / (2.0 * np.sqrt(T_remain))
206
+ - self.r * self.K * np.exp(-self.r * T_remain) * norm.cdf(d2))
207
+ theta_put = (-S * norm.pdf(d1) * self.sigma / (2.0 * np.sqrt(T_remain))
208
+ + self.r * self.K * np.exp(-self.r * T_remain) * norm.cdf(-d2))
209
+ return float(theta_call + theta_put)
210
+
211
+ def on_data(self, step: int, price: float, **kwargs) -> float:
212
+ T_remain = max(self.T_total * (1 - step / self.n_steps), 0.0)
213
+ dt = self.T_total / self.n_steps
214
+ delta = self._straddle_delta(price, T_remain)
215
+ gamma = self._straddle_gamma(price, T_remain)
216
+ theta = self._straddle_theta(price, T_remain)
217
+
218
+ if self._prev_price is not None:
219
+ dS = price - self._prev_price
220
+ theoretical_gamma_pnl = 0.5 * gamma * dS ** 2
221
+ theoretical_theta_pnl = theta * dt # negative (time decay)
222
+ else:
223
+ theoretical_gamma_pnl = 0.0
224
+ theoretical_theta_pnl = 0.0
225
+
226
+ self._prev_price = price
227
+ self._greeks_history["delta"].append(delta)
228
+ self._greeks_history["gamma"].append(gamma)
229
+ self._greeks_history["theta"].append(theta)
230
+ self._greeks_history["theoretical_gamma_pnl"].append(theoretical_gamma_pnl)
231
+ self._greeks_history["theoretical_theta_pnl"].append(theoretical_theta_pnl)
232
+ return delta
233
+
234
+
235
+ class DeltaGammaHedgeStrategy(Strategy):
236
+ """Delta-Gamma neutral hedge using two options and the underlying.
237
+
238
+ Holds a primary long option (strike ``K1``) and hedges **Gamma**
239
+ using a second option (strike ``K2``). Any residual Delta is
240
+ neutralised using the underlying asset.
241
+
242
+ At each rebalancing step the hedge ratios are:
243
+
244
+ .. math::
245
+
246
+ Q_{\\text{hedge}} = \\frac{\\Gamma_1(S,T)}{\\Gamma_2(S,T)}
247
+ \\qquad \\text{(makes net Gamma = 0)}
248
+
249
+ N_{\\text{stock}} = -(\\Delta_1 - Q_{\\text{hedge}} \\cdot \\Delta_2)
250
+ \\qquad \\text{(makes net Delta = 0)}
251
+
252
+ The ``_greeks_history`` dict records ``net_delta``, ``net_gamma``,
253
+ ``hedge_ratio``, and ``stock_position`` at every step.
254
+
255
+ Parameters
256
+ ----------
257
+ K1 : float
258
+ Strike of the *primary* (long) option.
259
+ K2 : float
260
+ Strike of the *hedge* option (must differ from K1 for the
261
+ gamma hedge to be non-trivial).
262
+ T_total : float
263
+ Time to expiry at inception (years).
264
+ r : float
265
+ Risk-free rate.
266
+ sigma : float
267
+ Implied volatility (flat smile assumed).
268
+ n_steps : int
269
+ Total rebalancing steps.
270
+ option_type : str
271
+ ``'call'`` or ``'put'`` for both legs.
272
+ """
273
+
274
+ def __init__(self, K1: float, K2: float, T_total: float, r: float,
275
+ sigma: float, n_steps: int = 252, option_type: str = "call"):
276
+ super().__init__()
277
+ self.K1 = K1
278
+ self.K2 = K2
279
+ self.T_total = T_total
280
+ self.r = r
281
+ self.sigma = sigma
282
+ self.n_steps = n_steps
283
+ self.option_type = option_type
284
+ self._greeks_history: dict[str, list] = {
285
+ "net_delta": [], "net_gamma": [], "hedge_ratio": [], "stock_position": []
286
+ }
287
+
288
+ def _delta(self, S: float, K: float, T_remain: float) -> float:
289
+ from scipy.stats import norm
290
+ if T_remain <= 0:
291
+ return 0.0
292
+ d1 = (np.log(S / K) + (self.r + 0.5 * self.sigma ** 2) * T_remain) / \
293
+ (self.sigma * np.sqrt(T_remain))
294
+ if self.option_type == "call":
295
+ return float(norm.cdf(d1))
296
+ return float(norm.cdf(d1) - 1.0)
297
+
298
+ def _gamma(self, S: float, K: float, T_remain: float) -> float:
299
+ from scipy.stats import norm
300
+ if T_remain <= 0:
301
+ return 0.0
302
+ d1 = (np.log(S / K) + (self.r + 0.5 * self.sigma ** 2) * T_remain) / \
303
+ (self.sigma * np.sqrt(T_remain))
304
+ return float(norm.pdf(d1) / (S * self.sigma * np.sqrt(T_remain)))
305
+
306
+ def on_data(self, step: int, price: float, **kwargs) -> float:
307
+ T_remain = max(self.T_total * (1 - step / self.n_steps), 0.0)
308
+ delta1 = self._delta(price, self.K1, T_remain)
309
+ gamma1 = self._gamma(price, self.K1, T_remain)
310
+ delta2 = self._delta(price, self.K2, T_remain)
311
+ gamma2 = self._gamma(price, self.K2, T_remain)
312
+
313
+ # Hedge ratio neutralises gamma
314
+ Q_hedge = gamma1 / gamma2 if abs(gamma2) > 1e-12 else 0.0
315
+ # Net delta after gamma hedge; we hold -net_delta in underlying
316
+ net_delta = delta1 - Q_hedge * delta2
317
+ stock_position = -net_delta
318
+
319
+ self._greeks_history["net_delta"].append(net_delta + stock_position) # ≈ 0
320
+ self._greeks_history["net_gamma"].append(gamma1 - Q_hedge * gamma2) # ≈ 0
321
+ self._greeks_history["hedge_ratio"].append(Q_hedge)
322
+ self._greeks_history["stock_position"].append(stock_position)
323
+ return stock_position
324
+
325
+
326
+ class StraddleStrategy(Strategy):
327
+ """Buy a straddle at regular intervals.
328
+
329
+ This is a simple strategy that opens a new straddle position every
330
+ ``interval`` steps by buying one unit of the asset and recording
331
+ the entry price.
332
+
333
+ Parameters
334
+ ----------
335
+ interval : int
336
+ Number of steps between new straddle entries.
337
+ """
338
+
339
+ def __init__(self, interval: int = 21):
340
+ super().__init__()
341
+ self.interval = interval
342
+ self._entries: list[float] = []
343
+
344
+ def on_data(self, step: int, price: float, **kwargs) -> float:
345
+ if step % self.interval == 0:
346
+ self._entries.append(price)
347
+ return self.position + 1.0 # add one unit
348
+ return self.position
@@ -0,0 +1,6 @@
1
+ """Core tensor engine with reverse-mode autodiff."""
2
+
3
+ from tensorquantlib.core import ops
4
+ from tensorquantlib.core.tensor import Tensor
5
+
6
+ __all__ = ["Tensor", "ops"]
@@ -0,0 +1,70 @@
1
+ """
2
+ Public API for differentiable tensor operations.
3
+
4
+ All operations are implemented in tensor.py alongside the Tensor class
5
+ (they share the _unbroadcast helper and Tensor internals). This module
6
+ re-exports them with a clean namespace for external use.
7
+
8
+ Usage:
9
+ from tensorquantlib.core import ops
10
+ z = ops.exp(x)
11
+ z = ops.norm_cdf(d1)
12
+
13
+ Note: ``tsum`` and ``tpow`` are used instead of ``sum`` / ``pow`` to avoid
14
+ shadowing Python built-ins when doing ``from ops import *``.
15
+ """
16
+
17
+ from tensorquantlib.core.tensor import tensor_abs as abs
18
+ from tensorquantlib.core.tensor import tensor_add as add
19
+ from tensorquantlib.core.tensor import tensor_clip as clip
20
+ from tensorquantlib.core.tensor import tensor_cos as cos
21
+ from tensorquantlib.core.tensor import tensor_div as div
22
+ from tensorquantlib.core.tensor import tensor_exp as exp
23
+ from tensorquantlib.core.tensor import tensor_log as log
24
+ from tensorquantlib.core.tensor import tensor_matmul as matmul
25
+ from tensorquantlib.core.tensor import tensor_maximum as maximum
26
+ from tensorquantlib.core.tensor import tensor_mean as mean
27
+ from tensorquantlib.core.tensor import tensor_mul as mul
28
+ from tensorquantlib.core.tensor import tensor_neg as neg
29
+ from tensorquantlib.core.tensor import tensor_norm_cdf as norm_cdf
30
+ from tensorquantlib.core.tensor import tensor_pow as tpow
31
+ from tensorquantlib.core.tensor import tensor_reshape as reshape
32
+ from tensorquantlib.core.tensor import tensor_sin as sin
33
+ from tensorquantlib.core.tensor import tensor_softmax as softmax
34
+ from tensorquantlib.core.tensor import tensor_sqrt as sqrt
35
+ from tensorquantlib.core.tensor import tensor_sub as sub
36
+ from tensorquantlib.core.tensor import tensor_sum as tsum
37
+ from tensorquantlib.core.tensor import tensor_tanh as tanh
38
+ from tensorquantlib.core.tensor import tensor_transpose as transpose
39
+ from tensorquantlib.core.tensor import tensor_where as where
40
+
41
+ # Legacy aliases kept for backward compatibility (do NOT use in new code —
42
+ # they shadow Python built-ins when imported with ``from ops import *``).
43
+ pow = tpow # noqa: A001
44
+ sum = tsum # noqa: A001
45
+
46
+ __all__ = [
47
+ "abs",
48
+ "add",
49
+ "clip",
50
+ "cos",
51
+ "div",
52
+ "exp",
53
+ "log",
54
+ "matmul",
55
+ "maximum",
56
+ "mean",
57
+ "mul",
58
+ "neg",
59
+ "norm_cdf",
60
+ "reshape",
61
+ "sin",
62
+ "softmax",
63
+ "sqrt",
64
+ "sub",
65
+ "tanh",
66
+ "tpow",
67
+ "tsum",
68
+ "transpose",
69
+ "where",
70
+ ]