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.
- quantbt/__init__.py +862 -0
- quantbt/adapters/__init__.py +5 -0
- quantbt/adapters/nautilus/__init__.py +42 -0
- quantbt/adapters/nautilus/_dependency.py +53 -0
- quantbt/adapters/nautilus/backend.py +639 -0
- quantbt/adapters/nautilus/instruments.py +275 -0
- quantbt/adapters/nautilus/options.py +465 -0
- quantbt/adapters/nautilus/reports.py +231 -0
- quantbt/backends/__init__.py +29 -0
- quantbt/backends/_native_event_rust.py +2168 -0
- quantbt/backends/native_event.py +5048 -0
- quantbt/backends/native_option.py +796 -0
- quantbt/backends/native_portfolio.py +928 -0
- quantbt/backends/native_vectorized.py +1299 -0
- quantbt/backtester.py +485 -0
- quantbt/benchmarks/__init__.py +1 -0
- quantbt/benchmarks/compare_phase9_parity.py +302 -0
- quantbt/benchmarks/gamma_scalping_backtestsample.py +802 -0
- quantbt/benchmarks/profile_phase7.py +415 -0
- quantbt/benchmarks/run_arbitrage_phase_e.py +160 -0
- quantbt/benchmarks/run_optimization_overhead.py +195 -0
- quantbt/benchmarks/run_options_engine.py +272 -0
- quantbt/benchmarks/run_pct_equity_nautilus_smoke.py +227 -0
- quantbt/benchmarks/run_phase12_arbitrage_cert.py +402 -0
- quantbt/benchmarks/run_phase12_benchmark_nautilus_cert.py +430 -0
- quantbt/benchmarks/run_phase13_portfolio_report.py +97 -0
- quantbt/benchmarks/run_phase13_wfo_cache.py +162 -0
- quantbt/benchmarks/run_phase14_service_loop.py +694 -0
- quantbt/benchmarks/run_phase15a_nautilus_certification.py +514 -0
- quantbt/benchmarks/run_phase15b_synthetic_depth.py +175 -0
- quantbt/benchmarks/run_phase16_performance_debt.py +389 -0
- quantbt/benchmarks/run_phase30e_reactive_runner.py +161 -0
- quantbt/benchmarks/run_phase31_intrabar.py +441 -0
- quantbt/benchmarks/run_phase34a_native_event_memory.py +179 -0
- quantbt/benchmarks/run_phase34b_native_event_prepared_score.py +173 -0
- quantbt/benchmarks/run_phase34c_native_event_single_pass.py +176 -0
- quantbt/benchmarks/run_phase7.py +654 -0
- quantbt/benchmarks/run_portfolio_real_parity.py +537 -0
- quantbt/core/__init__.py +323 -0
- quantbt/core/arbitrage.py +767 -0
- quantbt/core/basket.py +234 -0
- quantbt/core/certification.py +274 -0
- quantbt/core/constraints.py +155 -0
- quantbt/core/engine.py +1144 -0
- quantbt/core/event.py +869 -0
- quantbt/core/execution_contract.py +197 -0
- quantbt/core/execution_depth.py +613 -0
- quantbt/core/intrabar_kernel.py +1906 -0
- quantbt/core/intrabar_reference.py +907 -0
- quantbt/core/intrabar_session.py +156 -0
- quantbt/core/market_tape.py +480 -0
- quantbt/core/native_event_capabilities.py +121 -0
- quantbt/core/native_event_parity.py +336 -0
- quantbt/core/order_compiler.py +422 -0
- quantbt/core/orders.py +319 -0
- quantbt/core/portfolio.py +392 -0
- quantbt/core/preprocessor.py +256 -0
- quantbt/core/reactive.py +173 -0
- quantbt/core/results.py +341 -0
- quantbt/core/schema.py +211 -0
- quantbt/core/scopes.py +96 -0
- quantbt/core/structured_orders.py +378 -0
- quantbt/core/types.py +87 -0
- quantbt/core/vectorized.py +203 -0
- quantbt/endpoint.py +4465 -0
- quantbt/engines.py +1014 -0
- quantbt/metrics/__init__.py +45 -0
- quantbt/metrics/options_analytics.py +46 -0
- quantbt/metrics/performance.py +541 -0
- quantbt/optimization/__init__.py +94 -0
- quantbt/optimization/callbacks.py +116 -0
- quantbt/optimization/candidate_selection.py +397 -0
- quantbt/optimization/config.py +84 -0
- quantbt/optimization/constraints.py +22 -0
- quantbt/optimization/evaluator.py +21 -0
- quantbt/optimization/evaluators/__init__.py +32 -0
- quantbt/optimization/evaluators/arbitrage.py +22 -0
- quantbt/optimization/evaluators/generic.py +34 -0
- quantbt/optimization/evaluators/grid_dca.py +22 -0
- quantbt/optimization/evaluators/intrabar.py +54 -0
- quantbt/optimization/evaluators/native_event.py +51 -0
- quantbt/optimization/evaluators/options.py +22 -0
- quantbt/optimization/evaluators/portfolio.py +42 -0
- quantbt/optimization/evaluators/signal.py +43 -0
- quantbt/optimization/multiseed.py +176 -0
- quantbt/optimization/objectives.py +221 -0
- quantbt/optimization/optimizer.py +500 -0
- quantbt/optimization/result.py +80 -0
- quantbt/optimization/samplers.py +84 -0
- quantbt/optimization/space.py +223 -0
- quantbt/options/__init__.py +215 -0
- quantbt/options/cache.py +116 -0
- quantbt/options/conventions.py +158 -0
- quantbt/options/data.py +177 -0
- quantbt/options/execution.py +600 -0
- quantbt/options/fees.py +135 -0
- quantbt/options/greeks.py +173 -0
- quantbt/options/hedging.py +232 -0
- quantbt/options/iv.py +188 -0
- quantbt/options/ledger.py +263 -0
- quantbt/options/lifecycle.py +94 -0
- quantbt/options/margin.py +311 -0
- quantbt/options/packages.py +147 -0
- quantbt/options/pricing.py +191 -0
- quantbt/options/schema.py +227 -0
- quantbt/options/selectors.py +281 -0
- quantbt/options/strategy.py +249 -0
- quantbt/options/surface.py +142 -0
- quantbt/options/tape.py +226 -0
- quantbt/options/templates/__init__.py +33 -0
- quantbt/options/templates/packages.py +354 -0
- quantbt/portfolio.py +606 -0
- quantbt/py.typed +1 -0
- quantbt/reporting/__init__.py +38 -0
- quantbt/reporting/arbitrage_audit.py +211 -0
- quantbt/reporting/nautilus_bundle.py +758 -0
- quantbt/reporting/nautilus_certification.py +226 -0
- quantbt/reporting/nautilus_diagnostics.py +225 -0
- quantbt/reporting/parity.py +496 -0
- quantbt/reporting/portfolio_audit.py +206 -0
- quantbt/reporting/portfolio_nautilus.py +140 -0
- quantbt/sizing/__init__.py +3 -0
- quantbt/sizing/fast.py +67 -0
- quantbt/sizing/modes.py +162 -0
- quantbt/viz/__init__.py +4 -0
- quantbt/viz/plots.py +310 -0
- quantbt/viz/themes.py +102 -0
- quantbt/walkforward.py +3136 -0
- quantbt_engine-1.0.7.dist-info/METADATA +891 -0
- quantbt_engine-1.0.7.dist-info/RECORD +133 -0
- quantbt_engine-1.0.7.dist-info/WHEEL +5 -0
- quantbt_engine-1.0.7.dist-info/licenses/LICENSE +21 -0
- quantbt_engine-1.0.7.dist-info/top_level.txt +1 -0
quantbt/core/reactive.py
ADDED
|
@@ -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
|
+
...
|
quantbt/core/results.py
ADDED
|
@@ -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)
|