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
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Phase 7 profiling follow-up.
|
|
4
|
+
|
|
5
|
+
This script decomposes the two Phase 7 threshold misses into timing buckets so
|
|
6
|
+
optimization work can target the real layer: pandas normalization, ndarray
|
|
7
|
+
packing, order-array construction, pure Numba kernels, or result/report
|
|
8
|
+
construction.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import math
|
|
16
|
+
import statistics
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
from dataclasses import asdict, dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Dict, List, Optional, Sequence, Tuple
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
PACKAGE_DIR = Path(__file__).resolve().parents[1]
|
|
25
|
+
PROJECT_DIR = PACKAGE_DIR.parent
|
|
26
|
+
if str(PROJECT_DIR) not in sys.path:
|
|
27
|
+
sys.path.insert(0, str(PROJECT_DIR))
|
|
28
|
+
|
|
29
|
+
from quantbt.benchmarks.run_phase7 import PROFILES, BenchmarkProfile, _make_market_frames, _make_orders, _make_signals
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class ProfileStage:
|
|
34
|
+
backend: str
|
|
35
|
+
profile: str
|
|
36
|
+
stage: str
|
|
37
|
+
seconds: float
|
|
38
|
+
percent_of_profile: float
|
|
39
|
+
repeats: int
|
|
40
|
+
notes: str = ""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class BackendProfile:
|
|
45
|
+
backend: str
|
|
46
|
+
profile: str
|
|
47
|
+
bars: int
|
|
48
|
+
symbols: int
|
|
49
|
+
orders: int
|
|
50
|
+
total_seconds: float
|
|
51
|
+
stages: List[ProfileStage]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
55
|
+
parser = argparse.ArgumentParser(description="Profile QuantBT Phase 7 backend layers.")
|
|
56
|
+
parser.add_argument("--profile", choices=sorted(PROFILES), default="smoke")
|
|
57
|
+
parser.add_argument("--repeats", type=int, default=3)
|
|
58
|
+
parser.add_argument("--json-out", type=Path, default=PACKAGE_DIR / "benchmarks" / "out" / "phase7_profile.json")
|
|
59
|
+
parser.add_argument("--md-out", type=Path, default=PACKAGE_DIR / "benchmarks" / "out" / "phase7_profile.md")
|
|
60
|
+
args = parser.parse_args(argv)
|
|
61
|
+
|
|
62
|
+
base = PROFILES[args.profile]
|
|
63
|
+
profile = BenchmarkProfile(
|
|
64
|
+
name=base.name,
|
|
65
|
+
bars=base.bars,
|
|
66
|
+
symbols=base.symbols,
|
|
67
|
+
order_count=base.order_count,
|
|
68
|
+
repeats=max(1, int(args.repeats)),
|
|
69
|
+
)
|
|
70
|
+
records = [profile_native_vectorized(profile), profile_native_event(profile)]
|
|
71
|
+
write_outputs(records, args.json_out, args.md_out)
|
|
72
|
+
for record in records:
|
|
73
|
+
print(f"{record.backend}: total={record.total_seconds:.6f}s")
|
|
74
|
+
for stage in record.stages:
|
|
75
|
+
print(f" {stage.stage}: {stage.seconds:.6f}s ({stage.percent_of_profile:.1f}%)")
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def profile_native_vectorized(profile: BenchmarkProfile) -> BackendProfile:
|
|
80
|
+
import numpy as np
|
|
81
|
+
import pandas as pd
|
|
82
|
+
|
|
83
|
+
from quantbt import AccountConfig, ExecutionConfig
|
|
84
|
+
from quantbt.core.preprocessor import align_series, build_arrays, prepare_funding, validate_datetime
|
|
85
|
+
from quantbt.core.results import BacktestResultV2
|
|
86
|
+
from quantbt.core.vectorized import _engine_units_v2
|
|
87
|
+
from quantbt.sizing.fast import scale_signal_notional_matrix
|
|
88
|
+
|
|
89
|
+
idx, frames = _make_market_frames(profile.bars, profile.symbols)
|
|
90
|
+
signals = _make_signals(idx, profile.symbols)
|
|
91
|
+
symbols = list(frames.keys())
|
|
92
|
+
account = AccountConfig(initial_capital=1_000_000.0, leverage=10.0)
|
|
93
|
+
execution = ExecutionConfig()
|
|
94
|
+
|
|
95
|
+
def normalize():
|
|
96
|
+
local_idx = validate_datetime(idx)
|
|
97
|
+
closes = {symbol: frames[symbol]["close"] for symbol in symbols}
|
|
98
|
+
highs = {symbol: frames[symbol]["high"] for symbol in symbols}
|
|
99
|
+
lows = {symbol: frames[symbol]["low"] for symbol in symbols}
|
|
100
|
+
close_dict = align_series(closes, symbols, local_idx)
|
|
101
|
+
high_dict = align_series(highs, symbols, local_idx, fallback=close_dict)
|
|
102
|
+
low_dict = align_series(lows, symbols, local_idx, fallback=close_dict)
|
|
103
|
+
signal_dict = align_series(signals, symbols, local_idx, fill_val=0.0)
|
|
104
|
+
funding_dict = prepare_funding(0.0, symbols, local_idx)
|
|
105
|
+
return local_idx, close_dict, high_dict, low_dict, signal_dict, funding_dict
|
|
106
|
+
|
|
107
|
+
idx_n, close_dict, high_dict, low_dict, signal_dict, funding_dict = normalize()
|
|
108
|
+
|
|
109
|
+
def pack_arrays():
|
|
110
|
+
return build_arrays(
|
|
111
|
+
symbols=symbols,
|
|
112
|
+
idx=idx_n,
|
|
113
|
+
closes_dict=close_dict,
|
|
114
|
+
highs_dict=high_dict,
|
|
115
|
+
lows_dict=low_dict,
|
|
116
|
+
signals_dict=signal_dict,
|
|
117
|
+
funding_dict=funding_dict,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
closes_m, highs_m, lows_m, signals_m, funding_m, is_funding = pack_arrays()
|
|
121
|
+
allocs = np.full(len(symbols), 10_000.0, dtype=np.float64)
|
|
122
|
+
|
|
123
|
+
def size_targets():
|
|
124
|
+
return scale_signal_notional_matrix(signals_m, closes_m, allocs, use_pyramiding=True)
|
|
125
|
+
|
|
126
|
+
target_m = size_targets()
|
|
127
|
+
leverages = np.full(len(symbols), account.leverage, dtype=np.float64)
|
|
128
|
+
fee_rates = np.zeros(len(symbols), dtype=np.float64)
|
|
129
|
+
contract_sizes = np.ones(len(symbols), dtype=np.float64)
|
|
130
|
+
|
|
131
|
+
def kernel():
|
|
132
|
+
return _engine_units_v2(
|
|
133
|
+
n_bars=len(idx_n),
|
|
134
|
+
n_syms=len(symbols),
|
|
135
|
+
highs=highs_m,
|
|
136
|
+
lows=lows_m,
|
|
137
|
+
closes=closes_m,
|
|
138
|
+
target_units=target_m,
|
|
139
|
+
funding_rates=funding_m,
|
|
140
|
+
is_funding_bar=is_funding,
|
|
141
|
+
init_capital=account.initial_capital,
|
|
142
|
+
leverages=leverages,
|
|
143
|
+
maint_ratio=account.maintenance_ratio,
|
|
144
|
+
fee_rates=fee_rates,
|
|
145
|
+
contract_sizes=contract_sizes,
|
|
146
|
+
slippage=execution.slippage_rate,
|
|
147
|
+
use_funding=False,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
kernel_out = kernel()
|
|
151
|
+
|
|
152
|
+
def build_result():
|
|
153
|
+
(
|
|
154
|
+
equity_arr,
|
|
155
|
+
pos_arr,
|
|
156
|
+
fee_arr,
|
|
157
|
+
turnover_arr,
|
|
158
|
+
funding_arr,
|
|
159
|
+
init_margin_arr,
|
|
160
|
+
maint_margin_arr,
|
|
161
|
+
rejected_arr,
|
|
162
|
+
reject_code_arr,
|
|
163
|
+
liq_flag,
|
|
164
|
+
liq_idx,
|
|
165
|
+
_liq_reason,
|
|
166
|
+
) = kernel_out
|
|
167
|
+
equity = pd.Series(equity_arr, index=idx_n, name="equity")
|
|
168
|
+
return BacktestResultV2(
|
|
169
|
+
equity=equity,
|
|
170
|
+
returns=equity.pct_change().fillna(0.0),
|
|
171
|
+
positions=pd.DataFrame({f"Position_{s}": pos_arr[:, j] for j, s in enumerate(symbols)}, index=idx_n),
|
|
172
|
+
closes=pd.DataFrame({f"Close_{s}": closes_m[:, j] for j, s in enumerate(symbols)}, index=idx_n),
|
|
173
|
+
symbols=symbols,
|
|
174
|
+
initial_capital=account.initial_capital,
|
|
175
|
+
leverage=account.leverage,
|
|
176
|
+
liquidated=bool(liq_flag),
|
|
177
|
+
liquidation_bar=int(liq_idx),
|
|
178
|
+
fees=pd.Series(fee_arr, index=idx_n, name="fees"),
|
|
179
|
+
funding=pd.Series(funding_arr, index=idx_n, name="funding"),
|
|
180
|
+
margin=pd.DataFrame({"initial_margin": init_margin_arr, "maintenance_margin": maint_margin_arr}, index=idx_n),
|
|
181
|
+
diagnostics=pd.DataFrame(
|
|
182
|
+
{"turnover": turnover_arr, "rejected_orders": rejected_arr, "reject_code": reject_code_arr},
|
|
183
|
+
index=idx_n,
|
|
184
|
+
),
|
|
185
|
+
metadata={"backend": "native_vectorized", "engine": "units_v2_profile"},
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
stage_defs = [
|
|
189
|
+
("data_normalization", normalize, "validate_datetime + align OHLC/signals/funding"),
|
|
190
|
+
("pandas_to_ndarray", pack_arrays, "build contiguous market/signal arrays"),
|
|
191
|
+
("target_sizing", size_targets, "compute fast signal_notional target-unit matrix"),
|
|
192
|
+
("pure_numba_kernel", kernel, "compiled _engine_units_v2 only"),
|
|
193
|
+
("result_report_construction", build_result, "Series/DataFrame/BacktestResultV2 construction"),
|
|
194
|
+
]
|
|
195
|
+
return _profile_backend("native_vectorized", profile, profile.order_count, stage_defs)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def profile_native_event(profile: BenchmarkProfile) -> BackendProfile:
|
|
199
|
+
import numpy as np
|
|
200
|
+
import pandas as pd
|
|
201
|
+
|
|
202
|
+
from quantbt import AccountConfig, ExecutionConfig
|
|
203
|
+
from quantbt.core.event import _engine_event_v1
|
|
204
|
+
from quantbt.core.order_compiler import compile_order_intents
|
|
205
|
+
from quantbt.core.preprocessor import align_series, build_market_arrays, prepare_funding, validate_datetime
|
|
206
|
+
from quantbt.core.results import BacktestResultV2
|
|
207
|
+
|
|
208
|
+
idx, frames = _make_market_frames(profile.bars, profile.symbols)
|
|
209
|
+
orders = _make_orders(idx, profile.order_count, profile.symbols)
|
|
210
|
+
symbols = list(frames.keys())
|
|
211
|
+
account = AccountConfig(initial_capital=1_000_000.0, leverage=10.0)
|
|
212
|
+
execution = ExecutionConfig()
|
|
213
|
+
|
|
214
|
+
def normalize():
|
|
215
|
+
local_idx = validate_datetime(idx)
|
|
216
|
+
closes = {symbol: frames[symbol]["close"] for symbol in symbols}
|
|
217
|
+
highs = {symbol: frames[symbol]["high"] for symbol in symbols}
|
|
218
|
+
lows = {symbol: frames[symbol]["low"] for symbol in symbols}
|
|
219
|
+
close_dict = align_series(closes, symbols, local_idx)
|
|
220
|
+
high_dict = align_series(highs, symbols, local_idx, fallback=close_dict)
|
|
221
|
+
low_dict = align_series(lows, symbols, local_idx, fallback=close_dict)
|
|
222
|
+
funding_dict = prepare_funding(0.0, symbols, local_idx)
|
|
223
|
+
return local_idx, close_dict, high_dict, low_dict, funding_dict
|
|
224
|
+
|
|
225
|
+
idx_n, close_dict, high_dict, low_dict, funding_dict = normalize()
|
|
226
|
+
|
|
227
|
+
def pack_arrays():
|
|
228
|
+
return build_market_arrays(
|
|
229
|
+
symbols=symbols,
|
|
230
|
+
idx=idx_n,
|
|
231
|
+
closes_dict=close_dict,
|
|
232
|
+
highs_dict=high_dict,
|
|
233
|
+
lows_dict=low_dict,
|
|
234
|
+
funding_dict=funding_dict,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
market_arrays = pack_arrays()
|
|
238
|
+
symbol_to_col = {symbol: j for j, symbol in enumerate(symbols)}
|
|
239
|
+
|
|
240
|
+
def build_order_arrays():
|
|
241
|
+
return compile_order_intents(idx=idx_n, orders=orders, symbol_to_col=symbol_to_col)
|
|
242
|
+
|
|
243
|
+
order_arrays = build_order_arrays()
|
|
244
|
+
leverages = np.full(len(symbols), account.leverage, dtype=np.float64)
|
|
245
|
+
fee_rates = np.zeros(len(symbols), dtype=np.float64)
|
|
246
|
+
contract_sizes = np.ones(len(symbols), dtype=np.float64)
|
|
247
|
+
|
|
248
|
+
def kernel():
|
|
249
|
+
return _engine_event_v1(
|
|
250
|
+
n_bars=len(idx_n),
|
|
251
|
+
n_syms=len(symbols),
|
|
252
|
+
n_orders=len(orders),
|
|
253
|
+
order_ptr=order_arrays.order_ptr,
|
|
254
|
+
order_symbol=order_arrays.order_symbol,
|
|
255
|
+
order_side=order_arrays.order_side,
|
|
256
|
+
order_type=order_arrays.order_type,
|
|
257
|
+
order_qty=order_arrays.order_qty,
|
|
258
|
+
order_price=order_arrays.order_price,
|
|
259
|
+
order_tif=order_arrays.order_tif,
|
|
260
|
+
highs=market_arrays.highs,
|
|
261
|
+
lows=market_arrays.lows,
|
|
262
|
+
closes=market_arrays.closes,
|
|
263
|
+
funding_rates=market_arrays.funding,
|
|
264
|
+
is_funding_bar=market_arrays.is_funding_bar,
|
|
265
|
+
init_capital=account.initial_capital,
|
|
266
|
+
leverages=leverages,
|
|
267
|
+
maint_ratio=account.maintenance_ratio,
|
|
268
|
+
fee_rates=fee_rates,
|
|
269
|
+
contract_sizes=contract_sizes,
|
|
270
|
+
slippage=execution.slippage_rate,
|
|
271
|
+
use_funding=False,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
kernel_out = kernel()
|
|
275
|
+
|
|
276
|
+
def build_result():
|
|
277
|
+
(
|
|
278
|
+
equity_arr,
|
|
279
|
+
pos_arr,
|
|
280
|
+
fee_arr,
|
|
281
|
+
turnover_arr,
|
|
282
|
+
funding_arr,
|
|
283
|
+
init_margin_arr,
|
|
284
|
+
maint_margin_arr,
|
|
285
|
+
rejected_bar,
|
|
286
|
+
canceled_bar,
|
|
287
|
+
order_status,
|
|
288
|
+
reject_code,
|
|
289
|
+
fill_bar,
|
|
290
|
+
fill_qty,
|
|
291
|
+
fill_price,
|
|
292
|
+
fill_fee,
|
|
293
|
+
liq_flag,
|
|
294
|
+
liq_idx,
|
|
295
|
+
_liq_reason,
|
|
296
|
+
) = kernel_out
|
|
297
|
+
equity = pd.Series(equity_arr, index=idx_n, name="equity")
|
|
298
|
+
order_report = pd.DataFrame(
|
|
299
|
+
{
|
|
300
|
+
"original_index": order_arrays.original_index,
|
|
301
|
+
"status": order_status,
|
|
302
|
+
"reject_code": reject_code,
|
|
303
|
+
"fill_bar": fill_bar,
|
|
304
|
+
"fill_qty": fill_qty,
|
|
305
|
+
"fill_price": fill_price,
|
|
306
|
+
"fill_fee": fill_fee,
|
|
307
|
+
}
|
|
308
|
+
).sort_values("original_index", kind="stable")
|
|
309
|
+
return BacktestResultV2(
|
|
310
|
+
equity=equity,
|
|
311
|
+
returns=equity.pct_change().fillna(0.0),
|
|
312
|
+
positions=pd.DataFrame({f"Position_{s}": pos_arr[:, j] for j, s in enumerate(symbols)}, index=idx_n),
|
|
313
|
+
closes=pd.DataFrame({f"Close_{s}": market_arrays.closes[:, j] for j, s in enumerate(symbols)}, index=idx_n),
|
|
314
|
+
symbols=symbols,
|
|
315
|
+
initial_capital=account.initial_capital,
|
|
316
|
+
leverage=account.leverage,
|
|
317
|
+
liquidated=bool(liq_flag),
|
|
318
|
+
liquidation_bar=int(liq_idx),
|
|
319
|
+
orders=tuple(orders),
|
|
320
|
+
fees=pd.Series(fee_arr, index=idx_n, name="fees"),
|
|
321
|
+
funding=pd.Series(funding_arr, index=idx_n, name="funding"),
|
|
322
|
+
margin=pd.DataFrame({"initial_margin": init_margin_arr, "maintenance_margin": maint_margin_arr}, index=idx_n),
|
|
323
|
+
diagnostics=pd.DataFrame(
|
|
324
|
+
{"turnover": turnover_arr, "rejected_orders": rejected_bar, "canceled_orders": canceled_bar},
|
|
325
|
+
index=idx_n,
|
|
326
|
+
),
|
|
327
|
+
metadata={"backend": "native_event", "engine": "event_v1_profile", "order_report": order_report},
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
stage_defs = [
|
|
331
|
+
("data_normalization", normalize, "validate_datetime + align OHLC/funding"),
|
|
332
|
+
("pandas_to_ndarray", pack_arrays, "build contiguous market arrays"),
|
|
333
|
+
("order_array_construction", build_order_arrays, "compile orders with vectorized timestamp mapping"),
|
|
334
|
+
("pure_numba_kernel", kernel, "compiled _engine_event_v1 only"),
|
|
335
|
+
("result_report_construction", build_result, "order report + Series/DataFrame/BacktestResultV2"),
|
|
336
|
+
]
|
|
337
|
+
return _profile_backend("native_event", profile, len(orders), stage_defs)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _profile_backend(
|
|
341
|
+
backend: str,
|
|
342
|
+
profile: BenchmarkProfile,
|
|
343
|
+
orders: int,
|
|
344
|
+
stage_defs: Sequence[Tuple[str, object, str]],
|
|
345
|
+
) -> BackendProfile:
|
|
346
|
+
raw: List[Tuple[str, float, str]] = []
|
|
347
|
+
for name, fn, notes in stage_defs:
|
|
348
|
+
fn()
|
|
349
|
+
timings = []
|
|
350
|
+
for _ in range(profile.repeats):
|
|
351
|
+
start = time.perf_counter()
|
|
352
|
+
fn()
|
|
353
|
+
timings.append(time.perf_counter() - start)
|
|
354
|
+
raw.append((name, statistics.mean(timings), notes))
|
|
355
|
+
total = sum(seconds for _, seconds, _ in raw)
|
|
356
|
+
stages = [
|
|
357
|
+
ProfileStage(
|
|
358
|
+
backend=backend,
|
|
359
|
+
profile=profile.name,
|
|
360
|
+
stage=name,
|
|
361
|
+
seconds=seconds,
|
|
362
|
+
percent_of_profile=(seconds / total * 100.0) if total > 0.0 else 0.0,
|
|
363
|
+
repeats=profile.repeats,
|
|
364
|
+
notes=notes,
|
|
365
|
+
)
|
|
366
|
+
for name, seconds, notes in raw
|
|
367
|
+
]
|
|
368
|
+
return BackendProfile(
|
|
369
|
+
backend=backend,
|
|
370
|
+
profile=profile.name,
|
|
371
|
+
bars=profile.bars,
|
|
372
|
+
symbols=profile.symbols,
|
|
373
|
+
orders=orders,
|
|
374
|
+
total_seconds=total,
|
|
375
|
+
stages=stages,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def write_outputs(records: Sequence[BackendProfile], json_out: Path, md_out: Path) -> None:
|
|
380
|
+
json_out.parent.mkdir(parents=True, exist_ok=True)
|
|
381
|
+
md_out.parent.mkdir(parents=True, exist_ok=True)
|
|
382
|
+
payload = {"records": [asdict(record) for record in records]}
|
|
383
|
+
json_out.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
|
|
384
|
+
md_out.write_text(markdown_report(records), encoding="utf-8")
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def markdown_report(records: Sequence[BackendProfile]) -> str:
|
|
388
|
+
lines = [
|
|
389
|
+
"# Phase 7 Profiling Results",
|
|
390
|
+
"",
|
|
391
|
+
"| backend | stage | seconds | share | notes |",
|
|
392
|
+
"| --- | --- | ---: | ---: | --- |",
|
|
393
|
+
]
|
|
394
|
+
for record in records:
|
|
395
|
+
for stage in record.stages:
|
|
396
|
+
lines.append(
|
|
397
|
+
f"| `{stage.backend}` | `{stage.stage}` | {_fmt(stage.seconds)} | {stage.percent_of_profile:.1f}% | {stage.notes} |"
|
|
398
|
+
)
|
|
399
|
+
lines.extend(
|
|
400
|
+
[
|
|
401
|
+
"",
|
|
402
|
+
"Interpretation rule: optimize the largest measured bucket first. Cython/C++ is only justified after pure Numba kernel profiling remains the bottleneck.",
|
|
403
|
+
]
|
|
404
|
+
)
|
|
405
|
+
return "\n".join(lines) + "\n"
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _fmt(value: Optional[float]) -> str:
|
|
409
|
+
if value is None or (isinstance(value, float) and math.isnan(value)):
|
|
410
|
+
return "-"
|
|
411
|
+
return f"{value:.6f}"
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
if __name__ == "__main__":
|
|
415
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Phase E arbitrage benchmark smoke runner.
|
|
3
|
+
|
|
4
|
+
Run from repository root:
|
|
5
|
+
|
|
6
|
+
PYTHONPATH=/root/bobby/pool_alpha python3 quantbt/benchmarks/run_arbitrage_phase_e.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from time import perf_counter
|
|
13
|
+
|
|
14
|
+
import pandas as pd
|
|
15
|
+
|
|
16
|
+
from quantbt import (
|
|
17
|
+
ArbExecutionPolicy,
|
|
18
|
+
ArbitrageLeg,
|
|
19
|
+
BasisArbitrageSpec,
|
|
20
|
+
ContractType,
|
|
21
|
+
HedgePolicy,
|
|
22
|
+
HedgePolicyKind,
|
|
23
|
+
NativeEventBackend,
|
|
24
|
+
NativeEventConfig,
|
|
25
|
+
NativeVectorizedBackend,
|
|
26
|
+
NativeVectorizedConfig,
|
|
27
|
+
PackageExecutionKind,
|
|
28
|
+
SizingPolicy,
|
|
29
|
+
SizingPolicyKind,
|
|
30
|
+
StatArbPairSpec,
|
|
31
|
+
)
|
|
32
|
+
from quantbt.core.schema import AccountConfig
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class ArbBenchmarkProfile:
|
|
37
|
+
name: str
|
|
38
|
+
bars: int
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
PROFILES = {
|
|
42
|
+
"smoke": ArbBenchmarkProfile(name="smoke", bars=512),
|
|
43
|
+
"standard": ArbBenchmarkProfile(name="standard", bars=10_000),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def run(profile: ArbBenchmarkProfile = PROFILES["smoke"]) -> list[dict]:
|
|
48
|
+
idx = pd.date_range("2024-01-01", periods=profile.bars, freq="1h", tz="UTC")
|
|
49
|
+
records = []
|
|
50
|
+
for name, runner in (
|
|
51
|
+
("basis_event", _run_basis_event),
|
|
52
|
+
("basis_vectorized", _run_basis_vectorized),
|
|
53
|
+
("stat_event", _run_stat_event),
|
|
54
|
+
("stat_vectorized", _run_stat_vectorized),
|
|
55
|
+
):
|
|
56
|
+
started = perf_counter()
|
|
57
|
+
result = runner(idx)
|
|
58
|
+
elapsed = perf_counter() - started
|
|
59
|
+
records.append(
|
|
60
|
+
{
|
|
61
|
+
"name": name,
|
|
62
|
+
"bars": profile.bars,
|
|
63
|
+
"seconds": elapsed,
|
|
64
|
+
"final_equity": float(result.equity.iloc[-1]),
|
|
65
|
+
"engine": result.metadata["engine"],
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
return records
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _basis_spec():
|
|
72
|
+
return BasisArbitrageSpec(
|
|
73
|
+
arb_id="BENCH_BASIS",
|
|
74
|
+
legs=(
|
|
75
|
+
ArbitrageLeg(
|
|
76
|
+
symbol="PERP",
|
|
77
|
+
ratio=-1.0,
|
|
78
|
+
role="perp",
|
|
79
|
+
contract_type=ContractType.LINEAR,
|
|
80
|
+
qty_step=0.001,
|
|
81
|
+
min_qty=0.001,
|
|
82
|
+
),
|
|
83
|
+
ArbitrageLeg(
|
|
84
|
+
symbol="QUARTERLY",
|
|
85
|
+
ratio=1.0,
|
|
86
|
+
role="quarterly",
|
|
87
|
+
contract_type=ContractType.LINEAR,
|
|
88
|
+
qty_step=0.001,
|
|
89
|
+
min_qty=0.001,
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
hedge_policy=HedgePolicy(kind=HedgePolicyKind.BASE_QTY_EQUAL),
|
|
93
|
+
sizing_policy=SizingPolicy(
|
|
94
|
+
kind=SizingPolicyKind.TARGET_NOTIONAL_TO_BASE_QTY,
|
|
95
|
+
notional=10_000.0,
|
|
96
|
+
reference_symbol="PERP",
|
|
97
|
+
),
|
|
98
|
+
execution_policy=ArbExecutionPolicy(kind=PackageExecutionKind.ATOMIC_ALL_OR_NONE),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _stat_spec():
|
|
103
|
+
return StatArbPairSpec(
|
|
104
|
+
arb_id="BENCH_STAT",
|
|
105
|
+
legs=(ArbitrageLeg(symbol="BASE", ratio=1.0), ArbitrageLeg(symbol="HEDGE", ratio=-0.5)),
|
|
106
|
+
hedge_policy=HedgePolicy(kind=HedgePolicyKind.BETA_NEUTRAL),
|
|
107
|
+
sizing_policy=SizingPolicy(kind=SizingPolicyKind.TARGET_GROSS_NOTIONAL, notional=10_000.0),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _basis_data(idx):
|
|
112
|
+
steps = pd.Series(range(len(idx)), index=idx, dtype=float)
|
|
113
|
+
base = 100.0 + (steps % 31) * 0.1
|
|
114
|
+
return {"PERP": base, "QUARTERLY": base + 2.0}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _stat_data(idx):
|
|
118
|
+
steps = pd.Series(range(len(idx)), index=idx, dtype=float)
|
|
119
|
+
base = 50.0 + (steps % 17) * 0.2
|
|
120
|
+
return {"BASE": base, "HEDGE": base * 2.0 + 1.0}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _signal(idx):
|
|
124
|
+
signal = pd.Series(0.0, index=idx)
|
|
125
|
+
signal.iloc[1::200] = 1.0
|
|
126
|
+
signal.iloc[100::200] = 0.0
|
|
127
|
+
return signal.ffill()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _event_backend():
|
|
131
|
+
return NativeEventBackend(
|
|
132
|
+
NativeEventConfig(account=AccountConfig(initial_capital=100_000.0, leverage=10.0), use_funding=False)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _vectorized_backend():
|
|
137
|
+
return NativeVectorizedBackend(
|
|
138
|
+
NativeVectorizedConfig(account=AccountConfig(initial_capital=100_000.0, leverage=10.0), use_funding=False)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _run_basis_event(idx):
|
|
143
|
+
return _event_backend().run_basis_arbitrage(idx, _basis_spec(), _signal(idx), _basis_data(idx))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _run_basis_vectorized(idx):
|
|
147
|
+
return _vectorized_backend().run_basis_arbitrage(idx, _basis_spec(), _signal(idx), _basis_data(idx))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _run_stat_event(idx):
|
|
151
|
+
return _event_backend().run_stat_arb_pair_arbitrage(idx, _stat_spec(), _signal(idx), _stat_data(idx))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _run_stat_vectorized(idx):
|
|
155
|
+
return _vectorized_backend().run_stat_arb_pair_arbitrage(idx, _stat_spec(), _signal(idx), _stat_data(idx))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
for record in run(PROFILES["standard"]):
|
|
160
|
+
print(record)
|