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/options/iv.py ADDED
@@ -0,0 +1,188 @@
1
+ """
2
+ Deterministic implied-volatility solvers.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+ import math
10
+ from typing import Callable, Union
11
+
12
+ from .pricing import (
13
+ black76_intrinsic,
14
+ black76_price,
15
+ inverse_black76_intrinsic_base,
16
+ inverse_black76_price_base,
17
+ _coerce_kind,
18
+ _non_negative_float,
19
+ _positive_float,
20
+ )
21
+ from .schema import OptionKind
22
+
23
+
24
+ class IVStatus(str, Enum):
25
+ OK = "ok"
26
+ BELOW_INTRINSIC = "below_intrinsic"
27
+ ABOVE_MAX_PRICE = "above_max_price"
28
+ INVALID_INPUT = "invalid_input"
29
+ NOT_BRACKETED = "not_bracketed"
30
+ MAX_ITERATIONS = "max_iterations"
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class ImpliedVolResult:
35
+ implied_vol: float
36
+ status: IVStatus
37
+ iterations: int
38
+ model_price: float
39
+ lower_bound: float
40
+ upper_bound: float
41
+ residual: float
42
+
43
+ @property
44
+ def ok(self) -> bool:
45
+ return self.status is IVStatus.OK
46
+
47
+
48
+ def implied_vol_black76(
49
+ price: float,
50
+ forward: float,
51
+ strike: float,
52
+ time_to_expiry: float,
53
+ option_kind: Union[OptionKind, str],
54
+ *,
55
+ discount: float = 1.0,
56
+ tolerance: float = 1e-12,
57
+ max_iterations: int = 100,
58
+ vol_lower: float = 0.0,
59
+ vol_upper: float = 5.0,
60
+ max_vol_upper: float = 20.0,
61
+ ) -> ImpliedVolResult:
62
+ """Solve linear Black-76 implied volatility with bracketed bisection."""
63
+ try:
64
+ kind = _coerce_kind(option_kind)
65
+ target = _non_negative_float(price, "price")
66
+ fwd = _positive_float(forward, "forward")
67
+ strike_ = _positive_float(strike, "strike")
68
+ tau = _non_negative_float(time_to_expiry, "time_to_expiry")
69
+ df = _positive_float(discount, "discount")
70
+ except (TypeError, ValueError):
71
+ return _invalid_result(price)
72
+ lower_bound = black76_intrinsic(fwd, strike_, kind, discount=df)
73
+ upper_bound = _black76_upper_bound(fwd, strike_, kind, discount=df)
74
+ return _solve_bisection(
75
+ target,
76
+ lower_bound,
77
+ upper_bound,
78
+ lambda vol: black76_price(fwd, strike_, tau, vol, kind, discount=df),
79
+ tolerance=tolerance,
80
+ max_iterations=max_iterations,
81
+ vol_lower=vol_lower,
82
+ vol_upper=vol_upper,
83
+ max_vol_upper=max_vol_upper,
84
+ )
85
+
86
+
87
+ def implied_vol_inverse_black76_base(
88
+ price_base: float,
89
+ forward: float,
90
+ strike: float,
91
+ time_to_expiry: float,
92
+ option_kind: Union[OptionKind, str],
93
+ *,
94
+ discount: float = 1.0,
95
+ tolerance: float = 1e-12,
96
+ max_iterations: int = 100,
97
+ vol_lower: float = 0.0,
98
+ vol_upper: float = 5.0,
99
+ max_vol_upper: float = 20.0,
100
+ ) -> ImpliedVolResult:
101
+ """Solve inverse Black-76 implied volatility from base-currency price."""
102
+ try:
103
+ kind = _coerce_kind(option_kind)
104
+ target = _non_negative_float(price_base, "price_base")
105
+ fwd = _positive_float(forward, "forward")
106
+ strike_ = _positive_float(strike, "strike")
107
+ tau = _non_negative_float(time_to_expiry, "time_to_expiry")
108
+ df = _positive_float(discount, "discount")
109
+ except (TypeError, ValueError):
110
+ return _invalid_result(price_base)
111
+ lower_bound = inverse_black76_intrinsic_base(fwd, strike_, kind, discount=df)
112
+ upper_bound = _black76_upper_bound(fwd, strike_, kind, discount=df) / fwd
113
+ return _solve_bisection(
114
+ target,
115
+ lower_bound,
116
+ upper_bound,
117
+ lambda vol: inverse_black76_price_base(fwd, strike_, tau, vol, kind, discount=df),
118
+ tolerance=tolerance,
119
+ max_iterations=max_iterations,
120
+ vol_lower=vol_lower,
121
+ vol_upper=vol_upper,
122
+ max_vol_upper=max_vol_upper,
123
+ )
124
+
125
+
126
+ def _solve_bisection(
127
+ target: float,
128
+ lower_bound: float,
129
+ upper_bound: float,
130
+ price_fn: Callable[[float], float],
131
+ *,
132
+ tolerance: float,
133
+ max_iterations: int,
134
+ vol_lower: float,
135
+ vol_upper: float,
136
+ max_vol_upper: float,
137
+ ) -> ImpliedVolResult:
138
+ tol = _positive_float(tolerance, "tolerance")
139
+ if max_iterations <= 0:
140
+ return ImpliedVolResult(math.nan, IVStatus.INVALID_INPUT, 0, math.nan, lower_bound, upper_bound, math.nan)
141
+ lower_vol = _non_negative_float(vol_lower, "vol_lower")
142
+ upper_vol = _positive_float(vol_upper, "vol_upper")
143
+ max_upper = _positive_float(max_vol_upper, "max_vol_upper")
144
+ if upper_vol <= lower_vol:
145
+ return ImpliedVolResult(math.nan, IVStatus.INVALID_INPUT, 0, math.nan, lower_bound, upper_bound, math.nan)
146
+ if target < lower_bound - tol:
147
+ return ImpliedVolResult(math.nan, IVStatus.BELOW_INTRINSIC, 0, lower_bound, lower_bound, upper_bound, target - lower_bound)
148
+ if target > upper_bound + tol:
149
+ return ImpliedVolResult(math.nan, IVStatus.ABOVE_MAX_PRICE, 0, upper_bound, lower_bound, upper_bound, target - upper_bound)
150
+ if abs(target - lower_bound) <= tol:
151
+ return ImpliedVolResult(0.0, IVStatus.OK, 0, lower_bound, lower_bound, upper_bound, lower_bound - target)
152
+
153
+ lower_price = price_fn(lower_vol)
154
+ upper_price = price_fn(upper_vol)
155
+ while upper_price < target and upper_vol < max_upper:
156
+ upper_vol = min(upper_vol * 2.0, max_upper)
157
+ upper_price = price_fn(upper_vol)
158
+ if target < lower_price - tol or upper_price < target - tol:
159
+ return ImpliedVolResult(math.nan, IVStatus.NOT_BRACKETED, 0, upper_price, lower_bound, upper_bound, upper_price - target)
160
+
161
+ mid = 0.5 * (lower_vol + upper_vol)
162
+ mid_price = price_fn(mid)
163
+ for iteration in range(1, max_iterations + 1):
164
+ mid = 0.5 * (lower_vol + upper_vol)
165
+ mid_price = price_fn(mid)
166
+ residual = mid_price - target
167
+ if abs(residual) <= tol:
168
+ return ImpliedVolResult(mid, IVStatus.OK, iteration, mid_price, lower_bound, upper_bound, residual)
169
+ if mid_price < target:
170
+ lower_vol = mid
171
+ else:
172
+ upper_vol = mid
173
+ return ImpliedVolResult(mid, IVStatus.MAX_ITERATIONS, max_iterations, mid_price, lower_bound, upper_bound, mid_price - target)
174
+
175
+
176
+ def _black76_upper_bound(forward: float, strike: float, option_kind: OptionKind, *, discount: float) -> float:
177
+ if option_kind is OptionKind.CALL:
178
+ return discount * forward
179
+ return discount * strike
180
+
181
+
182
+ def _invalid_result(price: float) -> ImpliedVolResult:
183
+ try:
184
+ raw = float(price)
185
+ except (TypeError, ValueError):
186
+ raw = math.nan
187
+ target = raw if math.isfinite(raw) else math.nan
188
+ return ImpliedVolResult(math.nan, IVStatus.INVALID_INPUT, 0, math.nan, math.nan, math.nan, target)
@@ -0,0 +1,263 @@
1
+ """
2
+ Multi-currency option ledger.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass, field
8
+ from typing import Dict, Iterable, Optional
9
+
10
+ import pandas as pd
11
+
12
+ from ..core.orders import Fill
13
+ from ..core.schema import OrderSide
14
+ from .fees import OptionFeeResult
15
+ from .schema import OptionInstrumentSpec
16
+
17
+
18
+ @dataclass
19
+ class OptionPosition:
20
+ symbol: str
21
+ qty: float = 0.0
22
+ avg_entry: float = 0.0
23
+ realized_pnl: float = 0.0
24
+ premium_currency: str = ""
25
+ settlement_currency: str = ""
26
+ multiplier: float = 1.0
27
+
28
+ @property
29
+ def is_flat(self) -> bool:
30
+ return abs(self.qty) <= 1e-12
31
+
32
+
33
+ @dataclass
34
+ class OptionLedger:
35
+ cash: Dict[str, float] = field(default_factory=dict)
36
+ positions: Dict[str, OptionPosition] = field(default_factory=dict)
37
+ realized_pnl: Dict[str, float] = field(default_factory=dict)
38
+ fees: Dict[str, float] = field(default_factory=dict)
39
+ settlement_cashflows: Dict[str, float] = field(default_factory=dict)
40
+ margin_locked: Dict[str, float] = field(default_factory=dict)
41
+ events: list[Dict] = field(default_factory=list)
42
+ settled_symbols: set[str] = field(default_factory=set)
43
+
44
+ @classmethod
45
+ def from_cash(cls, balances: Dict[str, float]) -> "OptionLedger":
46
+ ledger = cls()
47
+ for currency, amount in balances.items():
48
+ ledger.cash[str(currency).upper()] = float(amount)
49
+ return ledger
50
+
51
+ def apply_fill(
52
+ self,
53
+ fill: Fill,
54
+ instrument: OptionInstrumentSpec,
55
+ *,
56
+ fee: Optional[OptionFeeResult] = None,
57
+ timestamp_ns: Optional[int] = None,
58
+ ) -> None:
59
+ """Apply premium cashflow, fee, position quantity, and realized PnL."""
60
+ premium_currency = instrument.premium_currency
61
+ fee_amount = float(fee.fee) if fee is not None else float(fill.fee)
62
+ fee_currency = fee.currency if fee is not None else premium_currency
63
+ premium = float(fill.qty) * float(fill.price) * float(instrument.multiplier)
64
+ premium_cash_delta = premium if fill.side is OrderSide.SELL else -premium
65
+ self._add_cash(premium_currency, premium_cash_delta)
66
+ if fee_amount:
67
+ self._add_cash(fee_currency, -fee_amount)
68
+ self.fees[fee_currency] = self.fees.get(fee_currency, 0.0) + fee_amount
69
+ realized = self._apply_position(fill, instrument)
70
+ if realized:
71
+ self.realized_pnl[premium_currency] = self.realized_pnl.get(premium_currency, 0.0) + realized
72
+ self.events.append(
73
+ {
74
+ "timestamp_ns": int(timestamp_ns if timestamp_ns is not None else fill.timestamp),
75
+ "event_type": "fill",
76
+ "symbol": fill.symbol,
77
+ "side": fill.side.value,
78
+ "qty": float(fill.qty),
79
+ "price": float(fill.price),
80
+ "premium_currency": premium_currency,
81
+ "premium_cashflow": float(premium_cash_delta),
82
+ "fee_currency": fee_currency,
83
+ "fee": fee_amount,
84
+ "realized_pnl": float(realized),
85
+ "cash_after": dict(self.cash),
86
+ "position_after": self.positions.get(fill.symbol).qty if fill.symbol in self.positions else 0.0,
87
+ }
88
+ )
89
+
90
+ def apply_settlement(
91
+ self,
92
+ instrument: OptionInstrumentSpec,
93
+ *,
94
+ timestamp_ns: int,
95
+ settlement_price: float,
96
+ payoff_per_unit: float,
97
+ representation: str,
98
+ ) -> float:
99
+ """Settle and close an option position exactly once."""
100
+ if instrument.symbol in self.settled_symbols:
101
+ raise ValueError(f"{instrument.symbol} has already been settled")
102
+ position = self.positions.get(instrument.symbol)
103
+ if position is None or position.is_flat:
104
+ self.settled_symbols.add(instrument.symbol)
105
+ self.events.append(
106
+ {
107
+ "timestamp_ns": int(timestamp_ns),
108
+ "event_type": "settlement",
109
+ "symbol": instrument.symbol,
110
+ "settlement_price": float(settlement_price),
111
+ "payoff_per_unit": float(payoff_per_unit),
112
+ "settlement_currency": instrument.settlement_currency,
113
+ "settlement_cashflow": 0.0,
114
+ "representation": representation,
115
+ "position_closed": True,
116
+ "cash_after": dict(self.cash),
117
+ }
118
+ )
119
+ return 0.0
120
+ cashflow = float(position.qty) * float(payoff_per_unit) * float(instrument.multiplier)
121
+ self._add_cash(instrument.settlement_currency, cashflow)
122
+ self.settlement_cashflows[instrument.settlement_currency] = (
123
+ self.settlement_cashflows.get(instrument.settlement_currency, 0.0) + cashflow
124
+ )
125
+ position.realized_pnl += cashflow
126
+ self.realized_pnl[instrument.settlement_currency] = self.realized_pnl.get(instrument.settlement_currency, 0.0) + cashflow
127
+ position.qty = 0.0
128
+ position.avg_entry = 0.0
129
+ self.settled_symbols.add(instrument.symbol)
130
+ self.events.append(
131
+ {
132
+ "timestamp_ns": int(timestamp_ns),
133
+ "event_type": "settlement",
134
+ "symbol": instrument.symbol,
135
+ "settlement_price": float(settlement_price),
136
+ "payoff_per_unit": float(payoff_per_unit),
137
+ "settlement_currency": instrument.settlement_currency,
138
+ "settlement_cashflow": float(cashflow),
139
+ "representation": representation,
140
+ "position_closed": True,
141
+ "cash_after": dict(self.cash),
142
+ }
143
+ )
144
+ return cashflow
145
+
146
+ def equity(
147
+ self,
148
+ *,
149
+ conversion_rates: Dict[str, float],
150
+ marks: Optional[Dict[str, float]] = None,
151
+ instruments: Optional[Dict[str, OptionInstrumentSpec]] = None,
152
+ reporting_currency: str = "USD",
153
+ ) -> float:
154
+ """Return marked equity in reporting currency."""
155
+ total = 0.0
156
+ for currency, amount in self.cash.items():
157
+ total += float(amount) * _conversion_rate(currency, conversion_rates, reporting_currency)
158
+ if marks and instruments:
159
+ for symbol, mark in marks.items():
160
+ position = self.positions.get(symbol)
161
+ instrument = instruments.get(symbol)
162
+ if position is None or instrument is None or position.is_flat:
163
+ continue
164
+ total += (
165
+ float(position.qty)
166
+ * float(mark)
167
+ * float(instrument.multiplier)
168
+ * _conversion_rate(instrument.premium_currency, conversion_rates, reporting_currency)
169
+ )
170
+ return float(total)
171
+
172
+ def equity_identity_report(
173
+ self,
174
+ *,
175
+ conversion_rates: Dict[str, float],
176
+ marks: Optional[Dict[str, float]] = None,
177
+ instruments: Optional[Dict[str, OptionInstrumentSpec]] = None,
178
+ reporting_currency: str = "USD",
179
+ ) -> Dict:
180
+ equity = self.equity(
181
+ conversion_rates=conversion_rates,
182
+ marks=marks,
183
+ instruments=instruments,
184
+ reporting_currency=reporting_currency,
185
+ )
186
+ cash_equity = sum(
187
+ float(amount) * _conversion_rate(currency, conversion_rates, reporting_currency)
188
+ for currency, amount in self.cash.items()
189
+ )
190
+ mark_equity = equity - cash_equity
191
+ return {
192
+ "reporting_currency": reporting_currency.upper(),
193
+ "cash_equity": float(cash_equity),
194
+ "mark_equity": float(mark_equity),
195
+ "equity": float(equity),
196
+ "cash": dict(self.cash),
197
+ "fees": dict(self.fees),
198
+ "realized_pnl": dict(self.realized_pnl),
199
+ "settlement_cashflows": dict(self.settlement_cashflows),
200
+ "margin_locked": dict(self.margin_locked),
201
+ "events": len(self.events),
202
+ "reconciled": True,
203
+ }
204
+
205
+ def event_report(self) -> pd.DataFrame:
206
+ return pd.DataFrame(self.events)
207
+
208
+ def _apply_position(self, fill: Fill, instrument: OptionInstrumentSpec) -> float:
209
+ position = self.positions.get(fill.symbol)
210
+ if position is None:
211
+ position = OptionPosition(
212
+ symbol=fill.symbol,
213
+ premium_currency=instrument.premium_currency,
214
+ settlement_currency=instrument.settlement_currency,
215
+ multiplier=instrument.multiplier,
216
+ )
217
+ self.positions[fill.symbol] = position
218
+ signed_qty = float(fill.signed_qty)
219
+ fill_price = float(fill.price)
220
+ prev_qty = float(position.qty)
221
+ realized = 0.0
222
+ if abs(prev_qty) <= 1e-12 or prev_qty * signed_qty > 0.0:
223
+ new_abs = abs(prev_qty) + abs(signed_qty)
224
+ position.avg_entry = (
225
+ (abs(prev_qty) * position.avg_entry + abs(signed_qty) * fill_price) / new_abs
226
+ if new_abs > 0.0
227
+ else 0.0
228
+ )
229
+ position.qty = prev_qty + signed_qty
230
+ return 0.0
231
+ close_qty = min(abs(prev_qty), abs(signed_qty))
232
+ if prev_qty > 0.0:
233
+ realized = (fill_price - position.avg_entry) * close_qty * float(instrument.multiplier)
234
+ else:
235
+ realized = (position.avg_entry - fill_price) * close_qty * float(instrument.multiplier)
236
+ new_qty = prev_qty + signed_qty
237
+ position.realized_pnl += realized
238
+ if abs(new_qty) <= 1e-12:
239
+ position.qty = 0.0
240
+ position.avg_entry = 0.0
241
+ elif prev_qty * new_qty > 0.0:
242
+ position.qty = new_qty
243
+ else:
244
+ position.qty = new_qty
245
+ position.avg_entry = fill_price
246
+ return float(realized)
247
+
248
+ def _add_cash(self, currency: str, amount: float) -> None:
249
+ key = str(currency).upper()
250
+ self.cash[key] = self.cash.get(key, 0.0) + float(amount)
251
+
252
+
253
+ def _conversion_rate(currency: str, conversion_rates: Dict[str, float], reporting_currency: str) -> float:
254
+ ccy = str(currency).upper()
255
+ report = str(reporting_currency).upper()
256
+ if ccy == report:
257
+ return 1.0
258
+ if ccy not in conversion_rates:
259
+ raise ValueError(f"missing conversion rate for {ccy}->{report}")
260
+ rate = float(conversion_rates[ccy])
261
+ if rate <= 0.0:
262
+ raise ValueError(f"conversion rate for {ccy}->{report} must be > 0")
263
+ return rate
@@ -0,0 +1,94 @@
1
+ """
2
+ Option lifecycle and expiry settlement.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+ from typing import Union
10
+
11
+ from .ledger import OptionLedger
12
+ from .schema import OptionInstrumentSpec, OptionKind, PremiumConvention, SettlementStyle
13
+
14
+
15
+ class OptionSettlementRepresentation(str, Enum):
16
+ ECONOMIC_CASH = "economic_cash"
17
+ FUTURE_THEN_CASH = "future_then_cash"
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class OptionSettlementResult:
22
+ symbol: str
23
+ timestamp_ns: int
24
+ settlement_price: float
25
+ payoff_per_unit: float
26
+ cashflow: float
27
+ settlement_currency: str
28
+ representation: OptionSettlementRepresentation
29
+ itm: bool
30
+ position_closed: bool
31
+
32
+
33
+ def option_expiry_payoff_per_unit(instrument: OptionInstrumentSpec, settlement_price: float) -> float:
34
+ """Return payoff per 1 option unit in the instrument settlement currency."""
35
+ price = float(settlement_price)
36
+ if price <= 0.0:
37
+ raise ValueError("settlement_price must be > 0")
38
+ strike = float(instrument.strike)
39
+ if instrument.option_kind is OptionKind.CALL:
40
+ intrinsic_quote = max(price - strike, 0.0)
41
+ else:
42
+ intrinsic_quote = max(strike - price, 0.0)
43
+ if instrument.premium_convention is PremiumConvention.INVERSE_BASE:
44
+ return intrinsic_quote / price
45
+ if instrument.premium_convention is PremiumConvention.LINEAR_QUOTE:
46
+ return intrinsic_quote
47
+ raise NotImplementedError("quanto option expiry payoff is not implemented in Phase 5")
48
+
49
+
50
+ def settle_option_expiry(
51
+ ledger: OptionLedger,
52
+ instrument: OptionInstrumentSpec,
53
+ *,
54
+ timestamp_ns: int,
55
+ settlement_price: float,
56
+ representation: Union[OptionSettlementRepresentation, str, None] = None,
57
+ ) -> OptionSettlementResult:
58
+ """Settle an option position and close it exactly once."""
59
+ rep = _resolve_representation(instrument, representation)
60
+ payoff = option_expiry_payoff_per_unit(instrument, settlement_price)
61
+ cashflow = ledger.apply_settlement(
62
+ instrument,
63
+ timestamp_ns=int(timestamp_ns),
64
+ settlement_price=float(settlement_price),
65
+ payoff_per_unit=payoff,
66
+ representation=rep.value,
67
+ )
68
+ return OptionSettlementResult(
69
+ symbol=instrument.symbol,
70
+ timestamp_ns=int(timestamp_ns),
71
+ settlement_price=float(settlement_price),
72
+ payoff_per_unit=float(payoff),
73
+ cashflow=float(cashflow),
74
+ settlement_currency=instrument.settlement_currency,
75
+ representation=rep,
76
+ itm=bool(payoff > 0.0),
77
+ position_closed=True,
78
+ )
79
+
80
+
81
+ def _resolve_representation(
82
+ instrument: OptionInstrumentSpec,
83
+ representation: Union[OptionSettlementRepresentation, str, None],
84
+ ) -> OptionSettlementRepresentation:
85
+ if representation is not None:
86
+ if isinstance(representation, OptionSettlementRepresentation):
87
+ return representation
88
+ try:
89
+ return OptionSettlementRepresentation(str(representation))
90
+ except ValueError as exc:
91
+ raise ValueError("invalid settlement representation") from exc
92
+ if instrument.settlement_style is SettlementStyle.FUTURE_THEN_CASH:
93
+ return OptionSettlementRepresentation.FUTURE_THEN_CASH
94
+ return OptionSettlementRepresentation.ECONOMIC_CASH