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
@@ -0,0 +1,135 @@
1
+ """
2
+ Option fee schedules.
3
+
4
+ Phase 5 implements deterministic per-leg capped fees. There is intentionally no
5
+ package-level cap because real venues cap option fees per contract/leg.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Union
12
+
13
+ from ..core.orders import Fill
14
+ from ..core.schema import LiquiditySide
15
+ from .schema import OptionInstrumentSpec, PremiumConvention
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class OptionFeeResult:
20
+ fee: float
21
+ currency: str
22
+ raw_fee: float
23
+ cap: float
24
+ capped: bool
25
+ schedule_id: str
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class OptionFeeSchedule:
30
+ schedule_id: str
31
+ fee_currency: str
32
+ maker_rate: float = 0.0
33
+ taker_rate: float = 0.0
34
+ cap_premium_fraction: float = 0.125
35
+ per_contract_fee: float = 0.0
36
+ premium_convention: Union[PremiumConvention, str] = PremiumConvention.LINEAR_QUOTE
37
+
38
+ def __post_init__(self) -> None:
39
+ object.__setattr__(self, "premium_convention", _coerce_premium(self.premium_convention))
40
+ object.__setattr__(self, "fee_currency", str(self.fee_currency).upper())
41
+ if not self.schedule_id:
42
+ raise ValueError("schedule_id is required")
43
+ if not self.fee_currency:
44
+ raise ValueError("fee_currency is required")
45
+ if self.maker_rate < 0.0 or self.taker_rate < 0.0:
46
+ raise ValueError("maker_rate and taker_rate must be >= 0")
47
+ if self.cap_premium_fraction < 0.0:
48
+ raise ValueError("cap_premium_fraction must be >= 0")
49
+ if self.per_contract_fee < 0.0:
50
+ raise ValueError("per_contract_fee must be >= 0")
51
+
52
+ def rate_for(self, liquidity: LiquiditySide) -> float:
53
+ return self.maker_rate if liquidity is LiquiditySide.MAKER else self.taker_rate
54
+
55
+
56
+ def deribit_inverse_fee_schedule(
57
+ *,
58
+ base_currency: str = "BTC",
59
+ per_contract_fee: float = 0.0003,
60
+ cap_premium_fraction: float = 0.125,
61
+ ) -> OptionFeeSchedule:
62
+ return OptionFeeSchedule(
63
+ schedule_id=f"deribit_{base_currency.lower()}_inverse_options_phase5",
64
+ fee_currency=base_currency,
65
+ per_contract_fee=per_contract_fee,
66
+ cap_premium_fraction=cap_premium_fraction,
67
+ premium_convention=PremiumConvention.INVERSE_BASE,
68
+ )
69
+
70
+
71
+ def deribit_linear_usdc_fee_schedule(
72
+ *,
73
+ taker_rate: float = 0.0003,
74
+ maker_rate: float = 0.0003,
75
+ cap_premium_fraction: float = 0.125,
76
+ ) -> OptionFeeSchedule:
77
+ return OptionFeeSchedule(
78
+ schedule_id="deribit_linear_usdc_options_phase5",
79
+ fee_currency="USDC",
80
+ maker_rate=maker_rate,
81
+ taker_rate=taker_rate,
82
+ cap_premium_fraction=cap_premium_fraction,
83
+ premium_convention=PremiumConvention.LINEAR_QUOTE,
84
+ )
85
+
86
+
87
+ def calculate_option_fee(
88
+ fill: Fill,
89
+ instrument: OptionInstrumentSpec,
90
+ schedule: OptionFeeSchedule,
91
+ *,
92
+ reference_price: float,
93
+ ) -> OptionFeeResult:
94
+ """
95
+ Calculate a per-leg capped option fee.
96
+
97
+ For inverse options the common venue-like form is a base-currency fee per
98
+ contract capped by a fraction of option premium. For linear options the raw
99
+ fee is reference notional times rate, also capped by option premium.
100
+ """
101
+ if schedule.premium_convention != instrument.premium_convention:
102
+ raise ValueError("fee schedule premium convention does not match instrument")
103
+ if schedule.fee_currency != instrument.premium_currency:
104
+ raise ValueError("fee schedule currency must match option premium currency in Phase 5")
105
+ if reference_price <= 0.0:
106
+ raise ValueError("reference_price must be > 0")
107
+ premium_notional = float(fill.qty) * float(fill.price) * float(instrument.multiplier)
108
+ cap = premium_notional * float(schedule.cap_premium_fraction)
109
+ if instrument.premium_convention is PremiumConvention.INVERSE_BASE:
110
+ raw_fee = float(fill.qty) * float(instrument.multiplier) * float(schedule.per_contract_fee)
111
+ else:
112
+ raw_fee = (
113
+ float(fill.qty)
114
+ * float(instrument.multiplier)
115
+ * float(reference_price)
116
+ * float(schedule.rate_for(fill.liquidity))
117
+ )
118
+ fee = min(raw_fee, cap) if schedule.cap_premium_fraction > 0.0 else raw_fee
119
+ return OptionFeeResult(
120
+ fee=float(fee),
121
+ currency=schedule.fee_currency,
122
+ raw_fee=float(raw_fee),
123
+ cap=float(cap),
124
+ capped=bool(fee < raw_fee),
125
+ schedule_id=schedule.schedule_id,
126
+ )
127
+
128
+
129
+ def _coerce_premium(value: Union[PremiumConvention, str]) -> PremiumConvention:
130
+ if isinstance(value, PremiumConvention):
131
+ return value
132
+ try:
133
+ return PremiumConvention(str(value))
134
+ except ValueError as exc:
135
+ raise ValueError("premium_convention is invalid") from exc
@@ -0,0 +1,173 @@
1
+ """
2
+ Option Greeks with explicit units.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+ import math
9
+ from typing import Union
10
+
11
+ from .pricing import (
12
+ black76_d1_d2,
13
+ black76_price,
14
+ normal_cdf,
15
+ normal_pdf,
16
+ _coerce_kind,
17
+ _non_negative_float,
18
+ _positive_float,
19
+ )
20
+ from .schema import OptionKind
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class OptionGreeks:
25
+ price: float
26
+ delta: float
27
+ gamma: float
28
+ vega: float
29
+ theta: float
30
+ currency: str
31
+ unit: str
32
+
33
+ @property
34
+ def vega_per_vol_point(self) -> float:
35
+ """Return vega for a 1 vol-point change, not a 1.0 vol change."""
36
+ return self.vega / 100.0
37
+
38
+
39
+ def linear_black76_greeks(
40
+ forward: float,
41
+ strike: float,
42
+ time_to_expiry: float,
43
+ volatility: float,
44
+ option_kind: Union[OptionKind, str],
45
+ *,
46
+ discount: float = 1.0,
47
+ currency: str = "QUOTE",
48
+ ) -> OptionGreeks:
49
+ """Return Black-76 Greeks in quote currency per 1 underlying."""
50
+ kind = _coerce_kind(option_kind)
51
+ fwd, strike_, tau, vol, df = _validated_greek_inputs(forward, strike, time_to_expiry, volatility, discount)
52
+ price = black76_price(fwd, strike_, tau, vol, kind, discount=df)
53
+ if tau <= 0.0 or vol <= 0.0:
54
+ delta = df if (kind is OptionKind.CALL and fwd > strike_) else 0.0
55
+ if kind is OptionKind.PUT and fwd < strike_:
56
+ delta = -df
57
+ return OptionGreeks(price=price, delta=delta, gamma=0.0, vega=0.0, theta=0.0, currency=currency, unit="quote")
58
+ d1, _ = black76_d1_d2(fwd, strike_, tau, vol)
59
+ pdf = normal_pdf(d1)
60
+ if kind is OptionKind.CALL:
61
+ delta = df * normal_cdf(d1)
62
+ else:
63
+ delta = df * (normal_cdf(d1) - 1.0)
64
+ gamma = df * pdf / (fwd * vol * math.sqrt(tau))
65
+ vega = df * fwd * pdf * math.sqrt(tau)
66
+ theta = -0.5 * df * fwd * pdf * vol / math.sqrt(tau)
67
+ return OptionGreeks(
68
+ price=price,
69
+ delta=delta,
70
+ gamma=gamma,
71
+ vega=vega,
72
+ theta=theta,
73
+ currency=str(currency).upper(),
74
+ unit="quote",
75
+ )
76
+
77
+
78
+ def inverse_black76_greeks_base(
79
+ forward: float,
80
+ strike: float,
81
+ time_to_expiry: float,
82
+ volatility: float,
83
+ option_kind: Union[OptionKind, str],
84
+ *,
85
+ discount: float = 1.0,
86
+ currency: str = "BASE",
87
+ ) -> OptionGreeks:
88
+ """Return inverse option Greeks in native base settlement currency."""
89
+ fwd = _positive_float(forward, "forward")
90
+ linear = linear_black76_greeks(fwd, strike, time_to_expiry, volatility, option_kind, discount=discount)
91
+ price = linear.price / fwd
92
+ delta = linear.delta / fwd - linear.price / (fwd * fwd)
93
+ gamma = linear.gamma / fwd - 2.0 * linear.delta / (fwd * fwd) + 2.0 * linear.price / (fwd * fwd * fwd)
94
+ vega = linear.vega / fwd
95
+ theta = linear.theta / fwd
96
+ return OptionGreeks(
97
+ price=price,
98
+ delta=delta,
99
+ gamma=gamma,
100
+ vega=vega,
101
+ theta=theta,
102
+ currency=str(currency).upper(),
103
+ unit="base",
104
+ )
105
+
106
+
107
+ def inverse_black76_greeks_quote(
108
+ forward: float,
109
+ strike: float,
110
+ time_to_expiry: float,
111
+ volatility: float,
112
+ option_kind: Union[OptionKind, str],
113
+ *,
114
+ discount: float = 1.0,
115
+ currency: str = "QUOTE",
116
+ ) -> OptionGreeks:
117
+ """
118
+ Return inverse option Greeks converted to quote reporting currency.
119
+
120
+ Under the Phase 2 inverse convention, quote-reporting value equals the
121
+ corresponding linear Black-76 value, so Greeks match the linear Greeks.
122
+ """
123
+ return linear_black76_greeks(
124
+ forward,
125
+ strike,
126
+ time_to_expiry,
127
+ volatility,
128
+ option_kind,
129
+ discount=discount,
130
+ currency=currency,
131
+ )
132
+
133
+
134
+ def scale_greeks_to_reporting_currency(
135
+ greeks: OptionGreeks,
136
+ conversion_rate: float,
137
+ *,
138
+ reporting_currency: str,
139
+ vega_per_vol_point: bool = False,
140
+ ) -> OptionGreeks:
141
+ """
142
+ Statically scale Greeks into a reporting currency.
143
+
144
+ This is a pure currency conversion helper. It does not add chain-rule delta
145
+ from a conversion rate that itself depends on the underlying.
146
+ """
147
+ rate = _positive_float(conversion_rate, "conversion_rate")
148
+ vega_scale = 0.01 if vega_per_vol_point else 1.0
149
+ return OptionGreeks(
150
+ price=greeks.price * rate,
151
+ delta=greeks.delta * rate,
152
+ gamma=greeks.gamma * rate,
153
+ vega=greeks.vega * rate * vega_scale,
154
+ theta=greeks.theta * rate,
155
+ currency=str(reporting_currency).upper(),
156
+ unit=f"{greeks.unit}_reported",
157
+ )
158
+
159
+
160
+ def _validated_greek_inputs(
161
+ forward: float,
162
+ strike: float,
163
+ time_to_expiry: float,
164
+ volatility: float,
165
+ discount: float,
166
+ ) -> tuple[float, float, float, float, float]:
167
+ return (
168
+ _positive_float(forward, "forward"),
169
+ _positive_float(strike, "strike"),
170
+ _non_negative_float(time_to_expiry, "time_to_expiry"),
171
+ _non_negative_float(volatility, "volatility"),
172
+ _positive_float(discount, "discount"),
173
+ )
@@ -0,0 +1,232 @@
1
+ """
2
+ Option hedge policy primitives.
3
+
4
+ Hedge accounting is intentionally explicit about ordering: hedge PnL for a
5
+ price move is earned by the hedge quantity held before that move; rebalance
6
+ decisions are evaluated after option package fills and Greek recomputation.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field
12
+ from enum import Enum
13
+ from typing import Dict, Optional, Sequence
14
+
15
+ import numpy as np
16
+ import pandas as pd
17
+
18
+ from .greeks import OptionGreeks
19
+ from .ledger import OptionLedger
20
+ from .schema import OptionInstrumentSpec
21
+
22
+
23
+ class OptionHedgePolicyType(str, Enum):
24
+ FIXED_THRESHOLD = "fixed_threshold"
25
+ HYSTERESIS_BAND = "hysteresis_band"
26
+ TIME_BASED = "time_based"
27
+ REALIZED_VOL_SCALED_BAND = "realized_vol_scaled_band"
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class OptionHedgeConfig:
32
+ policy: OptionHedgePolicyType = OptionHedgePolicyType.FIXED_THRESHOLD
33
+ target_delta: float = 0.0
34
+ threshold: float = 0.05
35
+ enter_band: float = 0.10
36
+ exit_band: float = 0.03
37
+ rebalance_interval_ns: int = 0
38
+ realized_vol_window: int = 20
39
+ realized_vol_multiplier: float = 1.0
40
+ min_band: float = 0.01
41
+ hedge_contract_multiplier: float = 1.0
42
+ metadata: Dict = field(default_factory=dict)
43
+
44
+ def __post_init__(self) -> None:
45
+ object.__setattr__(self, "policy", _coerce_policy(self.policy))
46
+ if self.threshold < 0.0 or self.enter_band < 0.0 or self.exit_band < 0.0:
47
+ raise ValueError("threshold and bands must be >= 0")
48
+ if self.exit_band > self.enter_band:
49
+ raise ValueError("exit_band must be <= enter_band")
50
+ if self.rebalance_interval_ns < 0:
51
+ raise ValueError("rebalance_interval_ns must be >= 0")
52
+ if self.realized_vol_window <= 1:
53
+ raise ValueError("realized_vol_window must be > 1")
54
+ if self.realized_vol_multiplier < 0.0 or self.min_band < 0.0:
55
+ raise ValueError("realized_vol_multiplier and min_band must be >= 0")
56
+ if self.hedge_contract_multiplier <= 0.0:
57
+ raise ValueError("hedge_contract_multiplier must be > 0")
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class HedgeDecision:
62
+ timestamp_ns: int
63
+ net_option_delta: float
64
+ previous_hedge_qty: float
65
+ target_hedge_qty: float
66
+ trade_qty: float
67
+ should_rebalance: bool
68
+ reason: str
69
+ band: float
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class HedgePathResult:
74
+ hedge_report: pd.DataFrame
75
+ final_hedge_qty: float
76
+ hedge_pnl: float
77
+ decisions: tuple[HedgeDecision, ...]
78
+ metadata: Dict
79
+
80
+
81
+ def compute_net_option_delta(
82
+ ledger: OptionLedger,
83
+ greeks_by_symbol: Dict[str, OptionGreeks],
84
+ instruments: Dict[str, OptionInstrumentSpec],
85
+ ) -> float:
86
+ """Return portfolio option delta after package fills and Greek recompute."""
87
+ total = 0.0
88
+ for symbol, position in ledger.positions.items():
89
+ if position.is_flat:
90
+ continue
91
+ greek = greeks_by_symbol.get(symbol)
92
+ instrument = instruments.get(symbol)
93
+ if greek is None or instrument is None:
94
+ raise ValueError(f"missing Greek or instrument for {symbol}")
95
+ total += float(position.qty) * float(greek.delta) * float(instrument.multiplier)
96
+ return float(total)
97
+
98
+
99
+ def hedge_decision(
100
+ *,
101
+ timestamp_ns: int,
102
+ net_option_delta: float,
103
+ current_hedge_qty: float,
104
+ config: OptionHedgeConfig,
105
+ last_rebalance_timestamp_ns: Optional[int] = None,
106
+ underlying_prices: Optional[Sequence[float]] = None,
107
+ currently_active: bool = False,
108
+ ) -> HedgeDecision:
109
+ """Decide whether to rebalance the hedge after Greek recomputation."""
110
+ target_qty = (float(config.target_delta) - float(net_option_delta)) / float(config.hedge_contract_multiplier)
111
+ trade_qty = target_qty - float(current_hedge_qty)
112
+ band = _active_band(config, underlying_prices)
113
+ reason = "within_band"
114
+ should = False
115
+ abs_trade = abs(trade_qty)
116
+ if config.policy is OptionHedgePolicyType.FIXED_THRESHOLD:
117
+ should = abs_trade >= config.threshold
118
+ reason = "fixed_threshold" if should else reason
119
+ elif config.policy is OptionHedgePolicyType.HYSTERESIS_BAND:
120
+ threshold = config.exit_band if currently_active else config.enter_band
121
+ should = abs_trade >= threshold
122
+ reason = "hysteresis_exit_band" if currently_active and should else ("hysteresis_enter_band" if should else reason)
123
+ band = threshold
124
+ elif config.policy is OptionHedgePolicyType.TIME_BASED:
125
+ due = last_rebalance_timestamp_ns is None or int(timestamp_ns) - int(last_rebalance_timestamp_ns) >= config.rebalance_interval_ns
126
+ should = due and abs_trade > 1e-12
127
+ reason = "time_based_due" if should else "time_based_not_due"
128
+ elif config.policy is OptionHedgePolicyType.REALIZED_VOL_SCALED_BAND:
129
+ should = abs_trade >= band
130
+ reason = "realized_vol_scaled_band" if should else reason
131
+ return HedgeDecision(
132
+ timestamp_ns=int(timestamp_ns),
133
+ net_option_delta=float(net_option_delta),
134
+ previous_hedge_qty=float(current_hedge_qty),
135
+ target_hedge_qty=float(target_qty),
136
+ trade_qty=float(trade_qty if should else 0.0),
137
+ should_rebalance=bool(should),
138
+ reason=reason,
139
+ band=float(band),
140
+ )
141
+
142
+
143
+ def run_delta_hedge_path(
144
+ timestamps_ns: Sequence[int],
145
+ underlying_prices: Sequence[float],
146
+ net_option_deltas: Sequence[float],
147
+ config: OptionHedgeConfig,
148
+ *,
149
+ initial_hedge_qty: float = 0.0,
150
+ ) -> HedgePathResult:
151
+ """
152
+ Simulate hedge PnL and rebalances over a path.
153
+
154
+ At bar `t`, PnL from `price[t-1] -> price[t]` uses the hedge quantity held
155
+ at `t-1`. Only after that move do we evaluate the new option delta and
156
+ rebalance.
157
+ """
158
+ ts = np.asarray(timestamps_ns, dtype=np.int64)
159
+ prices = np.asarray(underlying_prices, dtype=np.float64)
160
+ deltas = np.asarray(net_option_deltas, dtype=np.float64)
161
+ if len(ts) == 0 or len(ts) != len(prices) or len(ts) != len(deltas):
162
+ raise ValueError("timestamps, prices and deltas must be non-empty and equal length")
163
+ if bool((prices <= 0.0).any()) or bool((~np.isfinite(prices)).any()):
164
+ raise ValueError("underlying prices must be finite and > 0")
165
+ hedge_qty = float(initial_hedge_qty)
166
+ hedge_pnl = 0.0
167
+ last_rebalance_ts: Optional[int] = None
168
+ active = abs(hedge_qty) > 1e-12
169
+ rows = []
170
+ decisions = []
171
+ for i in range(len(ts)):
172
+ pnl = 0.0
173
+ if i > 0:
174
+ pnl = hedge_qty * (prices[i] - prices[i - 1]) * config.hedge_contract_multiplier
175
+ hedge_pnl += pnl
176
+ decision = hedge_decision(
177
+ timestamp_ns=int(ts[i]),
178
+ net_option_delta=float(deltas[i]),
179
+ current_hedge_qty=hedge_qty,
180
+ config=config,
181
+ last_rebalance_timestamp_ns=last_rebalance_ts,
182
+ underlying_prices=prices[max(0, i - config.realized_vol_window + 1) : i + 1],
183
+ currently_active=active,
184
+ )
185
+ if decision.should_rebalance:
186
+ hedge_qty += decision.trade_qty
187
+ last_rebalance_ts = int(ts[i])
188
+ active = abs(hedge_qty) > 1e-12
189
+ decisions.append(decision)
190
+ rows.append(
191
+ {
192
+ "timestamp_ns": int(ts[i]),
193
+ "underlying_price": float(prices[i]),
194
+ "prior_hedge_qty": decision.previous_hedge_qty,
195
+ "net_option_delta": decision.net_option_delta,
196
+ "hedge_pnl_for_prior_move": float(pnl),
197
+ "cumulative_hedge_pnl": float(hedge_pnl),
198
+ "target_hedge_qty": decision.target_hedge_qty,
199
+ "trade_qty": decision.trade_qty,
200
+ "hedge_qty_after": float(hedge_qty),
201
+ "should_rebalance": decision.should_rebalance,
202
+ "reason": decision.reason,
203
+ "band": decision.band,
204
+ }
205
+ )
206
+ return HedgePathResult(
207
+ hedge_report=pd.DataFrame(rows),
208
+ final_hedge_qty=float(hedge_qty),
209
+ hedge_pnl=float(hedge_pnl),
210
+ decisions=tuple(decisions),
211
+ metadata={"policy": config.policy.value, "hedge_contract_multiplier": config.hedge_contract_multiplier},
212
+ )
213
+
214
+
215
+ def _active_band(config: OptionHedgeConfig, prices: Optional[Sequence[float]]) -> float:
216
+ if config.policy is not OptionHedgePolicyType.REALIZED_VOL_SCALED_BAND:
217
+ return float(config.threshold)
218
+ if prices is None or len(prices) < 2:
219
+ return float(config.min_band)
220
+ arr = np.asarray(prices, dtype=np.float64)
221
+ returns = np.diff(np.log(arr))
222
+ realized = float(np.std(returns, ddof=1)) if len(returns) > 1 else abs(float(returns[0]))
223
+ return max(float(config.min_band), realized * float(config.realized_vol_multiplier))
224
+
225
+
226
+ def _coerce_policy(value) -> OptionHedgePolicyType:
227
+ if isinstance(value, OptionHedgePolicyType):
228
+ return value
229
+ try:
230
+ return OptionHedgePolicyType(str(value))
231
+ except ValueError as exc:
232
+ raise ValueError("invalid option hedge policy") from exc