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,173 @@
1
+ """
2
+ Reactive native-event strategy context.
3
+
4
+ These records are intentionally lightweight and read-only. Strategies inspect
5
+ engine state after each bar and return `OrderCommand` objects for the next bar.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Callable, Mapping, Optional, Protocol, Sequence, Tuple, runtime_checkable
12
+
13
+ import numpy as np
14
+ import pandas as pd
15
+
16
+ from .orders import OrderCommand
17
+ from .schema import OrderSide
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class NativeFillEvent:
22
+ timestamp: pd.Timestamp
23
+ symbol: str
24
+ side: OrderSide
25
+ qty: float
26
+ price: float
27
+ fee: float
28
+ order_id: Optional[str] = None
29
+ tag: Optional[str] = None
30
+ campaign_id: Optional[str] = None
31
+ cycle_id: Optional[str] = None
32
+ level_id: Optional[str] = None
33
+ parent_order_id: Optional[str] = None
34
+ oco_group_id: Optional[str] = None
35
+ metadata: Mapping = field(default_factory=dict)
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class NativeOrderEvent:
40
+ timestamp: pd.Timestamp
41
+ bar: int
42
+ event_name: str
43
+ status: int
44
+ order_id: Optional[str] = None
45
+ target_order_id: Optional[str] = None
46
+ parent_order_id: Optional[str] = None
47
+ oco_group_id: Optional[str] = None
48
+ tag: Optional[str] = None
49
+ campaign_id: Optional[str] = None
50
+ cycle_id: Optional[str] = None
51
+ level_id: Optional[str] = None
52
+ original_index: int = -1
53
+ related_original_index: int = -1
54
+ metadata: Mapping = field(default_factory=dict)
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class NativeActiveOrderSnapshot:
59
+ order_id: Optional[str]
60
+ symbol: Optional[str]
61
+ side: Optional[str]
62
+ order_type: Optional[str]
63
+ status: int
64
+ remaining_qty: float
65
+ price: float
66
+ trigger_price: float
67
+ reduce_only: bool
68
+ parent_order_id: Optional[str] = None
69
+ group_id: Optional[str] = None
70
+ oco_group_id: Optional[str] = None
71
+ tag: Optional[str] = None
72
+ campaign_id: Optional[str] = None
73
+ cycle_id: Optional[str] = None
74
+ level_id: Optional[str] = None
75
+
76
+
77
+ @dataclass(frozen=True, slots=True)
78
+ class NativeCommandBatch:
79
+ """Optional compact callback container for reactive command batches.
80
+
81
+ Existing strategies may continue returning ``list[OrderCommand]`` or a
82
+ tuple. This wrapper makes the batch boundary explicit for strategies that
83
+ already build a fixed command tuple, without changing command semantics or
84
+ the public ``OrderCommand`` type.
85
+ """
86
+
87
+ commands: Tuple[OrderCommand, ...] = field(default_factory=tuple)
88
+
89
+ @classmethod
90
+ def from_commands(cls, commands: Sequence[OrderCommand]) -> "NativeCommandBatch":
91
+ return cls(tuple(commands))
92
+
93
+ def __iter__(self):
94
+ return iter(self.commands)
95
+
96
+ def __len__(self) -> int:
97
+ return len(self.commands)
98
+
99
+ def __bool__(self) -> bool:
100
+ return bool(self.commands)
101
+
102
+
103
+ @dataclass(frozen=True)
104
+ class NativeStrategyContext:
105
+ bar_index: int
106
+ timestamp: pd.Timestamp
107
+ open: np.ndarray
108
+ high: np.ndarray
109
+ low: np.ndarray
110
+ close: np.ndarray
111
+ volume: np.ndarray
112
+ equity: float
113
+ available_equity: float
114
+ initial_margin: float
115
+ maintenance_margin: float
116
+ positions: Mapping[str, float]
117
+ fills_this_bar: Sequence[NativeFillEvent]
118
+ order_events_this_bar: Sequence[NativeOrderEvent]
119
+ active_orders: Sequence[NativeActiveOrderSnapshot]
120
+ liquidated: bool
121
+ symbols: Tuple[str, ...] = field(default_factory=tuple)
122
+ size_order: Callable[..., float] = field(default=lambda **_: 0.0, repr=False, compare=False)
123
+
124
+
125
+ class NativeEventStrategyError(RuntimeError):
126
+ """Raised when a reactive strategy callback fails."""
127
+
128
+ def __init__(self, callback: str, bar_index: int, timestamp: pd.Timestamp, original: Exception):
129
+ self.callback = callback
130
+ self.bar_index = int(bar_index)
131
+ self.timestamp = timestamp
132
+ self.original = original
133
+ super().__init__(
134
+ f"native-event strategy callback {callback!r} failed at "
135
+ f"bar_index={bar_index}, timestamp={timestamp}: {type(original).__name__}: {original}"
136
+ )
137
+
138
+
139
+ class NativeEventStrategyProtocol:
140
+ """
141
+ Optional protocol-like base class for user strategies.
142
+
143
+ Subclassing is not required; duck typing is used by the backend.
144
+ """
145
+
146
+ def initialize(self, context: NativeStrategyContext) -> Sequence[OrderCommand]:
147
+ return ()
148
+
149
+ def on_bar_close(self, context: NativeStrategyContext) -> Sequence[OrderCommand]:
150
+ return ()
151
+
152
+ def finalize(self, context: NativeStrategyContext) -> Sequence[OrderCommand]:
153
+ return ()
154
+
155
+
156
+ @runtime_checkable
157
+ class NativeEventStrategy(Protocol):
158
+ """Public structural protocol for stateful native-event strategies.
159
+
160
+ Implementations are discovered by duck typing; subclassing this protocol
161
+ is optional. A strategy may optionally declare
162
+ ``native_context_requirements`` to reduce callback context materialization
163
+ for score/optimization runs.
164
+ """
165
+
166
+ def initialize(self, context: NativeStrategyContext) -> Sequence[OrderCommand]:
167
+ ...
168
+
169
+ def on_bar_close(self, context: NativeStrategyContext) -> Sequence[OrderCommand]:
170
+ ...
171
+
172
+ def finalize(self, context: NativeStrategyContext) -> Sequence[OrderCommand]:
173
+ ...
@@ -0,0 +1,341 @@
1
+ """
2
+ quantbt.core.results
3
+ --------------------
4
+ Richer result contract for upgraded backends.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import Dict, List, Mapping, Sequence
11
+
12
+ import numpy as np
13
+ import pandas as pd
14
+
15
+ from .orders import Fill, OrderIntent, Trade
16
+ from .types import BacktestResult
17
+
18
+
19
+ @dataclass
20
+ class BacktestResultV2:
21
+ equity: pd.Series
22
+ returns: pd.Series
23
+ positions: pd.DataFrame
24
+ closes: pd.DataFrame
25
+ symbols: List[str]
26
+ initial_capital: float
27
+ leverage: float = 1.0
28
+ liquidated: bool = False
29
+ liquidation_bar: int = -1
30
+ orders: Sequence[OrderIntent] = field(default_factory=tuple)
31
+ fills: Sequence[Fill] = field(default_factory=tuple)
32
+ trades: Sequence[Trade] = field(default_factory=tuple)
33
+ fees: pd.Series = field(default_factory=lambda: pd.Series(dtype=float))
34
+ funding: pd.Series = field(default_factory=lambda: pd.Series(dtype=float))
35
+ margin: pd.DataFrame = field(default_factory=pd.DataFrame)
36
+ diagnostics: pd.DataFrame = field(default_factory=pd.DataFrame)
37
+ metadata: Dict = field(default_factory=dict)
38
+
39
+ def __post_init__(self) -> None:
40
+ if self.initial_capital <= 0.0:
41
+ raise ValueError("initial_capital must be > 0")
42
+ if self.leverage <= 0.0:
43
+ raise ValueError("leverage must be > 0")
44
+ if not isinstance(self.equity.index, pd.DatetimeIndex):
45
+ raise ValueError("equity must be indexed by DatetimeIndex")
46
+ if len(self.returns) != len(self.equity):
47
+ raise ValueError("returns must have the same length as equity")
48
+ if len(self.positions) != len(self.equity):
49
+ raise ValueError("positions must have the same length as equity")
50
+ if len(self.closes) != len(self.equity):
51
+ raise ValueError("closes must have the same length as equity")
52
+
53
+ @property
54
+ def drawdown(self) -> pd.Series:
55
+ peak = self.equity.cummax()
56
+ return (peak - self.equity) / peak.replace(0, np.nan)
57
+
58
+ @property
59
+ def daily_equity(self) -> pd.Series:
60
+ return self.equity.resample("1D").last().ffill().dropna()
61
+
62
+ @property
63
+ def daily_returns(self) -> pd.Series:
64
+ return self.daily_equity.pct_change().dropna()
65
+
66
+ def full_report(self, trading_days: int = 365, scope: str = "auto") -> Dict:
67
+ """Return the standard QuantBT metrics dictionary for this result."""
68
+ from .scopes import scoped_result
69
+ from ..metrics.performance import full_report
70
+
71
+ return full_report(scoped_result(self, scope=scope), trading_days=trading_days)
72
+
73
+ def show_metrics(self, trading_days: int = 365, scope: str = "auto") -> Dict:
74
+ """Print a legacy-style metrics report and return the metrics dict."""
75
+ from ..endpoint import format_metrics_report
76
+
77
+ report = self.full_report(trading_days=trading_days, scope=scope)
78
+ print(format_metrics_report(report))
79
+ return report
80
+
81
+ def quick_plot(self, theme: str = "dark", figsize: tuple = (14, 6), scope: str = "auto"):
82
+ """Plot cumulative return and drawdown for this result."""
83
+ from ..viz import quick_plot
84
+
85
+ return quick_plot(self, theme=theme, figsize=figsize, scope=scope)
86
+
87
+ def tearsheet(self, theme: str = "dark", benchmark=None, scope: str = "auto"):
88
+ """Render the QuantBT tearsheet for this result."""
89
+ from ..viz import tearsheet
90
+
91
+ return tearsheet(self, theme=theme, benchmark=benchmark, scope=scope)
92
+
93
+ @classmethod
94
+ def from_legacy(cls, result: BacktestResult) -> "BacktestResultV2":
95
+ return cls(
96
+ equity=result.equity.copy(),
97
+ returns=result.returns.copy(),
98
+ positions=result.positions.copy(),
99
+ closes=result.closes.copy(),
100
+ symbols=list(result.symbols),
101
+ initial_capital=float(result.initial_capital),
102
+ leverage=float(result.leverage),
103
+ liquidated=bool(result.liquidated),
104
+ liquidation_bar=int(result.liquidation_bar),
105
+ metadata=dict(result.metadata),
106
+ )
107
+
108
+ def to_legacy(self) -> BacktestResult:
109
+ return BacktestResult(
110
+ equity=self.equity.copy(),
111
+ returns=self.returns.copy(),
112
+ positions=self.positions.copy(),
113
+ closes=self.closes.copy(),
114
+ symbols=list(self.symbols),
115
+ initial_capital=float(self.initial_capital),
116
+ leverage=float(self.leverage),
117
+ liquidated=bool(self.liquidated),
118
+ liquidation_bar=int(self.liquidation_bar),
119
+ metadata=dict(self.metadata),
120
+ )
121
+
122
+
123
+ @dataclass(frozen=True)
124
+ class NativeAccountingArrays:
125
+ timestamps: np.ndarray
126
+ equity: np.ndarray
127
+ returns: np.ndarray
128
+ positions: np.ndarray
129
+ fees: np.ndarray
130
+ funding: np.ndarray
131
+ initial_margin: np.ndarray
132
+ maintenance_margin: np.ndarray
133
+ symbols: tuple[str, ...]
134
+ initial_capital: float
135
+ leverage: float = 1.0
136
+ liquidated: bool = False
137
+ liquidation_bar: int = -1
138
+
139
+ @classmethod
140
+ def from_result(cls, result: BacktestResultV2) -> "NativeAccountingArrays":
141
+ position_cols = [f"Position_{symbol}" for symbol in result.symbols]
142
+ return cls(
143
+ timestamps=result.equity.index.view("int64").copy(),
144
+ equity=result.equity.to_numpy(dtype=np.float64, copy=True),
145
+ returns=result.returns.to_numpy(dtype=np.float64, copy=True),
146
+ positions=result.positions[position_cols].to_numpy(dtype=np.float64, copy=True),
147
+ fees=result.fees.to_numpy(dtype=np.float64, copy=True),
148
+ funding=result.funding.to_numpy(dtype=np.float64, copy=True),
149
+ initial_margin=result.margin.get("initial_margin", pd.Series(0.0, index=result.equity.index)).to_numpy(
150
+ dtype=np.float64,
151
+ copy=True,
152
+ ),
153
+ maintenance_margin=result.margin.get(
154
+ "maintenance_margin",
155
+ pd.Series(0.0, index=result.equity.index),
156
+ ).to_numpy(dtype=np.float64, copy=True),
157
+ symbols=tuple(result.symbols),
158
+ initial_capital=float(result.initial_capital),
159
+ leverage=float(result.leverage),
160
+ liquidated=bool(result.liquidated),
161
+ liquidation_bar=int(result.liquidation_bar),
162
+ )
163
+
164
+ @property
165
+ def datetime_index(self) -> pd.DatetimeIndex:
166
+ return pd.DatetimeIndex(self.timestamps)
167
+
168
+
169
+ @dataclass(frozen=True)
170
+ class NativeEventScoreResult:
171
+ accounting: NativeAccountingArrays
172
+ final_positions: np.ndarray
173
+ fill_count: int
174
+ rejection_count: int
175
+ cancellation_count: int
176
+ liquidated: bool
177
+ liquidation_bar: int
178
+ metrics: Mapping[str, float]
179
+ metadata: Mapping[str, object] = field(default_factory=dict)
180
+
181
+ @property
182
+ def equity(self) -> np.ndarray:
183
+ return self.accounting.equity
184
+
185
+ @property
186
+ def returns(self) -> np.ndarray:
187
+ return self.accounting.returns
188
+
189
+ @property
190
+ def positions(self) -> np.ndarray:
191
+ return self.accounting.positions
192
+
193
+ @property
194
+ def fees(self) -> np.ndarray:
195
+ return self.accounting.fees
196
+
197
+ @property
198
+ def funding(self) -> np.ndarray:
199
+ return self.accounting.funding
200
+
201
+ @property
202
+ def initial_margin(self) -> np.ndarray:
203
+ return self.accounting.initial_margin
204
+
205
+ @property
206
+ def maintenance_margin(self) -> np.ndarray:
207
+ return self.accounting.maintenance_margin
208
+
209
+ def full_report(self, trading_days: int = 365, scope: str = "auto") -> Dict:
210
+ if str(scope).lower().strip() not in {"auto", "full"}:
211
+ raise ValueError("NativeEventScoreResult supports scope='auto' or scope='full'")
212
+
213
+ from ..metrics.performance import compute_performance_metrics
214
+
215
+ return compute_performance_metrics(
216
+ timestamps=self.accounting.datetime_index,
217
+ equity=self.accounting.equity,
218
+ returns=self.accounting.returns,
219
+ positions=self.accounting.positions,
220
+ symbols=self.accounting.symbols,
221
+ initial_capital=float(self.accounting.initial_capital),
222
+ liquidated=bool(self.liquidated),
223
+ trading_days=trading_days,
224
+ )
225
+
226
+
227
+ @dataclass(frozen=True, slots=True)
228
+ class NativeEventScalarScoreResult:
229
+ """Low-retention score contract for prepared native-event optimization.
230
+
231
+ Unlike :class:`NativeEventScoreResult`, this result does not retain an
232
+ equity, position, fee, funding, or margin path. The reactive session
233
+ computes the same report metrics online and keeps only scalar accounting
234
+ state. Public audit runs and the compatibility ``score()`` contract keep
235
+ using ``NativeEventScoreResult`` with ndarray accounting.
236
+ """
237
+
238
+ final_equity: float
239
+ final_positions: np.ndarray
240
+ fill_count: int
241
+ rejection_count: int
242
+ cancellation_count: int
243
+ liquidated: bool
244
+ liquidation_bar: int
245
+ metrics: Mapping[str, float]
246
+ metadata: Mapping[str, object] = field(default_factory=dict)
247
+
248
+ def _lifecycle_counter(self, name: str, default: int = 0) -> int:
249
+ counters = self.metadata.get("lifecycle_counters", {})
250
+ return int(counters.get(name, default)) if isinstance(counters, Mapping) else int(default)
251
+
252
+ @property
253
+ def event_count(self) -> int:
254
+ """Number of lifecycle events emitted by the scalar run."""
255
+
256
+ return self._lifecycle_counter("event_count")
257
+
258
+ @property
259
+ def rejected_count(self) -> int:
260
+ """Number of rejected commands in the scalar run."""
261
+
262
+ return int(self.rejection_count)
263
+
264
+ @property
265
+ def canceled_count(self) -> int:
266
+ """Number of canceled commands in the scalar run."""
267
+
268
+ return int(self.cancellation_count)
269
+
270
+ @property
271
+ def max_initial_margin(self) -> float:
272
+ return float(self.metrics.get("max_initial_margin", 0.0))
273
+
274
+ @property
275
+ def max_maintenance_margin(self) -> float:
276
+ return float(self.metrics.get("max_maintenance_margin", 0.0))
277
+
278
+ @property
279
+ def total_fee(self) -> float:
280
+ return float(self.metadata.get("total_fee", 0.0))
281
+
282
+ @property
283
+ def total_turnover(self) -> float:
284
+ return float(self.metadata.get("total_turnover", 0.0))
285
+
286
+ def full_report(self, trading_days: int = 365, scope: str = "auto") -> Dict:
287
+ """Return the online report captured for this score run.
288
+
289
+ A scalar score has no path from which to recompute a different
290
+ annualization convention. Callers requesting a different
291
+ ``trading_days`` value must rerun the score with that value.
292
+ """
293
+ if str(scope).lower().strip() not in {"auto", "full"}:
294
+ raise ValueError("NativeEventScalarScoreResult supports scope='auto' or scope='full'")
295
+ recorded_days = int(self.metadata.get("trading_days", trading_days))
296
+ if int(trading_days) != recorded_days:
297
+ raise ValueError(
298
+ "scalar score metrics were computed with trading_days="
299
+ f"{recorded_days}; rerun the score to use trading_days={int(trading_days)}"
300
+ )
301
+ return dict(self.metrics)
302
+
303
+
304
+ @dataclass
305
+ class OptionBacktestResult(BacktestResultV2):
306
+ """
307
+ Backtest result contract for native option simulations.
308
+
309
+ It intentionally remains a `BacktestResultV2` so existing report helpers
310
+ keep working, while exposing option-domain audit tables explicitly.
311
+ """
312
+
313
+ fills_report: pd.DataFrame = field(default_factory=pd.DataFrame)
314
+ packages_report: pd.DataFrame = field(default_factory=pd.DataFrame)
315
+ cash_report: pd.DataFrame = field(default_factory=pd.DataFrame)
316
+ marks_report: pd.DataFrame = field(default_factory=pd.DataFrame)
317
+ greeks_report: pd.DataFrame = field(default_factory=pd.DataFrame)
318
+ settlements_report: pd.DataFrame = field(default_factory=pd.DataFrame)
319
+ margin_report: pd.DataFrame = field(default_factory=pd.DataFrame)
320
+ attribution_report: pd.DataFrame = field(default_factory=pd.DataFrame)
321
+ hedge_report: pd.DataFrame = field(default_factory=pd.DataFrame)
322
+ option_equity: pd.Series = field(default_factory=lambda: pd.Series(dtype=float))
323
+ combined_equity: pd.Series = field(default_factory=lambda: pd.Series(dtype=float))
324
+ combined_returns: pd.Series = field(default_factory=lambda: pd.Series(dtype=float))
325
+ run_manifest: Dict = field(default_factory=dict)
326
+
327
+ def __post_init__(self) -> None:
328
+ super().__post_init__()
329
+ self.metadata.setdefault("fills_report", self.fills_report)
330
+ self.metadata.setdefault("packages_report", self.packages_report)
331
+ self.metadata.setdefault("cash_report", self.cash_report)
332
+ self.metadata.setdefault("marks_report", self.marks_report)
333
+ self.metadata.setdefault("greeks_report", self.greeks_report)
334
+ self.metadata.setdefault("settlements_report", self.settlements_report)
335
+ self.metadata.setdefault("margin_report", self.margin_report)
336
+ self.metadata.setdefault("attribution_report", self.attribution_report)
337
+ self.metadata.setdefault("hedge_report", self.hedge_report)
338
+ self.metadata.setdefault("option_equity", self.option_equity)
339
+ self.metadata.setdefault("combined_equity", self.combined_equity)
340
+ self.metadata.setdefault("combined_returns", self.combined_returns)
341
+ self.metadata.setdefault("run_manifest", self.run_manifest)