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,430 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Phase 12B benchmark follow-up and Nautilus portfolio certification runner.
|
|
4
|
+
|
|
5
|
+
The runner keeps production claims narrow and auditable:
|
|
6
|
+
|
|
7
|
+
* benchmark stages separate full facade time from array preparation, pure
|
|
8
|
+
Numba portfolio kernel time, and report-construction residual time;
|
|
9
|
+
* Nautilus portfolio validation is optional because it depends on the external
|
|
10
|
+
NautilusTrader package and venue adapter state;
|
|
11
|
+
* all-or-none basket package semantics are certified through the deterministic
|
|
12
|
+
QuantBT depth preflight used before Nautilus package replay.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import statistics
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Dict, List, Optional
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
import pandas as pd
|
|
27
|
+
|
|
28
|
+
PACKAGE_DIR = Path(__file__).resolve().parents[1]
|
|
29
|
+
PROJECT_DIR = PACKAGE_DIR.parent
|
|
30
|
+
if str(PROJECT_DIR) not in sys.path:
|
|
31
|
+
sys.path.insert(0, str(PROJECT_DIR))
|
|
32
|
+
|
|
33
|
+
from quantbt import ( # noqa: E402
|
|
34
|
+
AccountConfig,
|
|
35
|
+
NautilusExecutionDepthConfig,
|
|
36
|
+
OrderIntent,
|
|
37
|
+
OrderSide,
|
|
38
|
+
OrderType,
|
|
39
|
+
PortfolioBacktestEngine,
|
|
40
|
+
QuantBTEndpoint,
|
|
41
|
+
TimeInForce,
|
|
42
|
+
simulate_nautilus_order_package_depth,
|
|
43
|
+
)
|
|
44
|
+
from quantbt.backends import NativePortfolioBackend, NativePortfolioConfig # noqa: E402
|
|
45
|
+
from quantbt.core.engine import _engine_portfolio # noqa: E402
|
|
46
|
+
from quantbt.core.preprocessor import align_series, build_market_arrays, build_signal_matrix, prepare_funding, validate_datetime # noqa: E402
|
|
47
|
+
from quantbt.sizing.fast import scale_signal_notional_matrix # noqa: E402
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run_certification(
|
|
51
|
+
*,
|
|
52
|
+
rows: int = 2_000,
|
|
53
|
+
symbols: int = 6,
|
|
54
|
+
repeats: int = 3,
|
|
55
|
+
include_nautilus: bool = False,
|
|
56
|
+
) -> Dict:
|
|
57
|
+
benchmark = _benchmark_native_portfolio(rows=rows, symbols=symbols, repeats=repeats)
|
|
58
|
+
all_or_none = _all_or_none_basket_depth_smoke()
|
|
59
|
+
nautilus = _optional_real_nautilus_portfolio(include_nautilus=include_nautilus)
|
|
60
|
+
passed = (
|
|
61
|
+
benchmark["status"] == "pass"
|
|
62
|
+
and all_or_none["status"] == "pass"
|
|
63
|
+
and nautilus["status"] in {"pass", "skipped", "diff"}
|
|
64
|
+
)
|
|
65
|
+
return {
|
|
66
|
+
"status": "pass" if passed else "fail",
|
|
67
|
+
"benchmark_followup": benchmark,
|
|
68
|
+
"all_or_none_basket": all_or_none,
|
|
69
|
+
"nautilus_portfolio": nautilus,
|
|
70
|
+
"cython_cpp_recommendation": _cython_cpp_recommendation(benchmark),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def make_markdown(report: Dict) -> str:
|
|
75
|
+
bench = report["benchmark_followup"]
|
|
76
|
+
lines = [
|
|
77
|
+
"# Phase 12B Benchmark And Nautilus Portfolio Certification",
|
|
78
|
+
"",
|
|
79
|
+
f"Status: **{report['status']}**",
|
|
80
|
+
"",
|
|
81
|
+
"## Benchmark Follow-Up",
|
|
82
|
+
"",
|
|
83
|
+
f"- Bars: `{bench['rows']}`",
|
|
84
|
+
f"- Symbols: `{bench['symbols']}`",
|
|
85
|
+
f"- Repeats: `{bench['repeats']}`",
|
|
86
|
+
f"- Full facade seconds: `{bench['stages']['full_facade_seconds']:.6f}`",
|
|
87
|
+
f"- Prepared reuse facade seconds: `{bench['stages']['prepared_reuse_facade_seconds']:.6f}`",
|
|
88
|
+
f"- Prepared reuse speedup: `{bench['stages']['prepared_reuse_speedup']:.3f}x`",
|
|
89
|
+
f"- Array preparation seconds: `{bench['stages']['array_preparation_seconds']:.6f}`",
|
|
90
|
+
f"- Pure Numba kernel seconds: `{bench['stages']['pure_numba_kernel_seconds']:.6f}`",
|
|
91
|
+
f"- Report construction residual seconds: `{bench['stages']['report_construction_estimate_seconds']:.6f}`",
|
|
92
|
+
f"- Pure kernel share: `{bench['stages']['pure_kernel_share_pct']:.2f}%`",
|
|
93
|
+
"",
|
|
94
|
+
"## Nautilus Portfolio",
|
|
95
|
+
"",
|
|
96
|
+
f"- Status: `{report['nautilus_portfolio']['status']}`",
|
|
97
|
+
f"- Validation status: `{report['nautilus_portfolio'].get('validation_status')}`",
|
|
98
|
+
f"- Equity tolerance profile: `{report['nautilus_portfolio'].get('equity_tolerance')}`",
|
|
99
|
+
f"- Position tolerance profile: `{report['nautilus_portfolio'].get('position_tolerance')}`",
|
|
100
|
+
f"- Final equity diff: `{report['nautilus_portfolio'].get('final_equity_diff')}`",
|
|
101
|
+
f"- Max position diff: `{report['nautilus_portfolio'].get('max_abs_position_diff')}`",
|
|
102
|
+
"",
|
|
103
|
+
"## All-Or-None Basket",
|
|
104
|
+
"",
|
|
105
|
+
f"- Status: `{report['all_or_none_basket']['status']}`",
|
|
106
|
+
f"- Input orders: `{report['all_or_none_basket']['input_orders']}`",
|
|
107
|
+
f"- Accepted orders: `{report['all_or_none_basket']['accepted_orders']}`",
|
|
108
|
+
f"- Rejected orders: `{report['all_or_none_basket']['rejected_orders']}`",
|
|
109
|
+
f"- Depth model: `{report['all_or_none_basket']['depth_model']}`",
|
|
110
|
+
"",
|
|
111
|
+
"## Cython/C++ Decision",
|
|
112
|
+
"",
|
|
113
|
+
report["cython_cpp_recommendation"],
|
|
114
|
+
]
|
|
115
|
+
return "\n".join(lines) + "\n"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _benchmark_native_portfolio(rows: int, symbols: int, repeats: int) -> Dict:
|
|
119
|
+
idx, positions, closes, highs, lows = _make_portfolio_fixture(rows, symbols)
|
|
120
|
+
account = AccountConfig(initial_capital=250_000.0, leverage=5.0, maintenance_ratio=0.005)
|
|
121
|
+
alloc = 10_000.0
|
|
122
|
+
fee_rate = 0.0002
|
|
123
|
+
fee_oneway = fee_rate / 2.0
|
|
124
|
+
|
|
125
|
+
def full_facade():
|
|
126
|
+
return PortfolioBacktestEngine(
|
|
127
|
+
positions=positions,
|
|
128
|
+
closes=closes,
|
|
129
|
+
highs=highs,
|
|
130
|
+
lows=lows,
|
|
131
|
+
datetime_index=idx,
|
|
132
|
+
mode="longshort",
|
|
133
|
+
backend="native_portfolio",
|
|
134
|
+
account=account,
|
|
135
|
+
fee_rate=fee_rate,
|
|
136
|
+
alloc_per_trade=alloc,
|
|
137
|
+
hedge_type="signal_notional",
|
|
138
|
+
use_funding=False,
|
|
139
|
+
).result
|
|
140
|
+
|
|
141
|
+
backend = NativePortfolioBackend(NativePortfolioConfig(account=account, fee_rate=fee_oneway, use_funding=False))
|
|
142
|
+
symbol_list = list(positions.keys())
|
|
143
|
+
prepared_market = backend.prepare_market_arrays(
|
|
144
|
+
datetime_index=idx,
|
|
145
|
+
closes=closes,
|
|
146
|
+
highs=highs,
|
|
147
|
+
lows=lows,
|
|
148
|
+
funding_rate=0.0,
|
|
149
|
+
symbols=symbol_list,
|
|
150
|
+
)
|
|
151
|
+
prepared_signals = backend.prepare_signal_matrix(positions, idx, symbol_list)
|
|
152
|
+
|
|
153
|
+
def prepared_reuse():
|
|
154
|
+
return backend.run_signals(
|
|
155
|
+
positions=None,
|
|
156
|
+
closes=closes,
|
|
157
|
+
highs=highs,
|
|
158
|
+
lows=lows,
|
|
159
|
+
datetime_index=idx,
|
|
160
|
+
mode="longshort",
|
|
161
|
+
alloc_per_trade=alloc,
|
|
162
|
+
contract_size=1.0,
|
|
163
|
+
hedge_type="signal_notional",
|
|
164
|
+
funding_rate=0.0,
|
|
165
|
+
leverage=account.leverage,
|
|
166
|
+
maintenance_ratio=account.maintenance_ratio,
|
|
167
|
+
symbols=symbol_list,
|
|
168
|
+
use_pyramiding=True,
|
|
169
|
+
market_arrays=prepared_market,
|
|
170
|
+
raw_signal_matrix=prepared_signals,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
prepared = _prepare_portfolio_arrays(idx, positions, closes, highs, lows, account, alloc, fee_oneway)
|
|
174
|
+
_kernel_portfolio(prepared)
|
|
175
|
+
full_facade()
|
|
176
|
+
prepared_reuse()
|
|
177
|
+
|
|
178
|
+
prep_seconds = _timeit(lambda: _prepare_portfolio_arrays(idx, positions, closes, highs, lows, account, alloc, fee_oneway), repeats)
|
|
179
|
+
kernel_seconds = _timeit(lambda: _kernel_portfolio(prepared), repeats)
|
|
180
|
+
full_seconds = _timeit(full_facade, repeats)
|
|
181
|
+
prepared_reuse_seconds = _timeit(prepared_reuse, repeats)
|
|
182
|
+
report_seconds = max(0.0, full_seconds - prep_seconds - kernel_seconds)
|
|
183
|
+
status = "pass" if full_seconds > 0.0 and kernel_seconds > 0.0 else "fail"
|
|
184
|
+
return {
|
|
185
|
+
"status": status,
|
|
186
|
+
"rows": int(rows),
|
|
187
|
+
"symbols": int(symbols),
|
|
188
|
+
"bar_symbols": int(rows * symbols),
|
|
189
|
+
"repeats": int(repeats),
|
|
190
|
+
"stages": {
|
|
191
|
+
"full_facade_seconds": float(full_seconds),
|
|
192
|
+
"prepared_reuse_facade_seconds": float(prepared_reuse_seconds),
|
|
193
|
+
"array_preparation_seconds": float(prep_seconds),
|
|
194
|
+
"pure_numba_kernel_seconds": float(kernel_seconds),
|
|
195
|
+
"report_construction_estimate_seconds": float(report_seconds),
|
|
196
|
+
"prepared_reuse_speedup": float(full_seconds / prepared_reuse_seconds) if prepared_reuse_seconds > 0.0 else 0.0,
|
|
197
|
+
"array_preparation_share_pct": float(prep_seconds / full_seconds * 100.0) if full_seconds > 0.0 else 0.0,
|
|
198
|
+
"pure_kernel_share_pct": float(kernel_seconds / full_seconds * 100.0) if full_seconds > 0.0 else 0.0,
|
|
199
|
+
"report_construction_share_pct": float(report_seconds / full_seconds * 100.0) if full_seconds > 0.0 else 0.0,
|
|
200
|
+
},
|
|
201
|
+
"notes": (
|
|
202
|
+
"Prepared-array cache targets WFO/service loops. Pure Numba kernel "
|
|
203
|
+
"remains separated from pandas normalization and report construction."
|
|
204
|
+
),
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _optional_real_nautilus_portfolio(include_nautilus: bool) -> Dict:
|
|
209
|
+
if not include_nautilus:
|
|
210
|
+
return {"status": "skipped", "reason": "run with --include-nautilus"}
|
|
211
|
+
try:
|
|
212
|
+
from quantbt.adapters.nautilus import NautilusBackendConfig, NautilusBacktestEngine
|
|
213
|
+
|
|
214
|
+
NautilusBacktestEngine.check_available()
|
|
215
|
+
idx, raw_positions, raw_closes, raw_highs, raw_lows = _make_portfolio_fixture(rows=96, symbols=2)
|
|
216
|
+
symbols = ["BTCUSDT-PERP.BINANCE", "ETHUSDT-PERP.BINANCE"]
|
|
217
|
+
raw_symbols = list(raw_positions.keys())
|
|
218
|
+
positions = {symbols[i]: raw_positions[raw_symbols[i]] for i in range(2)}
|
|
219
|
+
closes = {symbols[i]: raw_closes[raw_symbols[i]] for i in range(2)}
|
|
220
|
+
highs = {symbols[i]: raw_highs[raw_symbols[i]] for i in range(2)}
|
|
221
|
+
lows = {symbols[i]: raw_lows[raw_symbols[i]] for i in range(2)}
|
|
222
|
+
data = {
|
|
223
|
+
symbol: pd.DataFrame(
|
|
224
|
+
{
|
|
225
|
+
"open": closes[symbol],
|
|
226
|
+
"high": highs[symbol],
|
|
227
|
+
"low": lows[symbol],
|
|
228
|
+
"close": closes[symbol],
|
|
229
|
+
"volume": 1_000.0,
|
|
230
|
+
},
|
|
231
|
+
index=idx,
|
|
232
|
+
)
|
|
233
|
+
for symbol in symbols
|
|
234
|
+
}
|
|
235
|
+
endpoint = QuantBTEndpoint.portfolio(
|
|
236
|
+
portfolio_mode="market_neutral",
|
|
237
|
+
backend="nautilus",
|
|
238
|
+
initial_capital=100_000.0,
|
|
239
|
+
leverage=3.0,
|
|
240
|
+
fee_rate=0.0002,
|
|
241
|
+
use_funding=False,
|
|
242
|
+
hedge_type="signal_notional",
|
|
243
|
+
alloc_per_trade={symbols[0]: 1_000_000.0, symbols[1]: 750_000.0},
|
|
244
|
+
metadata={
|
|
245
|
+
"portfolio_nautilus_equity_tolerance": 1.0,
|
|
246
|
+
"portfolio_nautilus_position_tolerance": 0.005,
|
|
247
|
+
},
|
|
248
|
+
nautilus_config=NautilusBackendConfig(instrument_id=symbols[0], timeframe="1h", bypass_risk=True),
|
|
249
|
+
)
|
|
250
|
+
result = endpoint.simulate(data=data, positions=pd.DataFrame(positions), symbols=symbols)
|
|
251
|
+
validation = result.metadata.get("portfolio_nautilus_validation_report", {})
|
|
252
|
+
return {
|
|
253
|
+
"status": "pass" if validation.get("status") == "pass" else "diff",
|
|
254
|
+
"validation_status": validation.get("status"),
|
|
255
|
+
"checks": validation.get("checks", {}),
|
|
256
|
+
"equity_tolerance": validation.get("equity_tolerance"),
|
|
257
|
+
"position_tolerance": validation.get("position_tolerance"),
|
|
258
|
+
"expected_order_count": validation.get("expected_order_count"),
|
|
259
|
+
"nautilus_orders": validation.get("nautilus_orders"),
|
|
260
|
+
"nautilus_fills": validation.get("nautilus_fills"),
|
|
261
|
+
"final_equity_diff": validation.get("final_equity_diff"),
|
|
262
|
+
"max_abs_position_diff": validation.get("max_abs_position_diff"),
|
|
263
|
+
}
|
|
264
|
+
except Exception as exc:
|
|
265
|
+
return {"status": "skipped", "reason": f"{type(exc).__name__}: {exc}"}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _all_or_none_basket_depth_smoke() -> Dict:
|
|
269
|
+
idx = pd.date_range("2024-01-01", periods=4, freq="1h", tz="UTC")
|
|
270
|
+
data = {
|
|
271
|
+
"BTC": _depth_frame(idx, close=100.0, high=101.0, low=95.0),
|
|
272
|
+
"ETH": _depth_frame(idx, close=50.0, high=51.0, low=49.0),
|
|
273
|
+
}
|
|
274
|
+
meta = {"package_id": "PHASE12-BASKET", "package_type": "basket_package"}
|
|
275
|
+
orders = (
|
|
276
|
+
OrderIntent(idx[1], "BTC", OrderSide.BUY, OrderType.LIMIT, qty=1.0, price=96.0, tif=TimeInForce.GTC, metadata=meta),
|
|
277
|
+
OrderIntent(idx[1], "ETH", OrderSide.BUY, OrderType.LIMIT, qty=1.0, price=45.0, tif=TimeInForce.GTC, metadata=meta),
|
|
278
|
+
)
|
|
279
|
+
result = simulate_nautilus_order_package_depth(
|
|
280
|
+
orders,
|
|
281
|
+
data,
|
|
282
|
+
NautilusExecutionDepthConfig(all_or_none_packages=True),
|
|
283
|
+
)
|
|
284
|
+
package_status = result.package_report["status"].tolist() if not result.package_report.empty else []
|
|
285
|
+
passed = len(result.orders) == 0 and result.metadata.get("rejected_orders") == 2 and package_status == ["rejected"]
|
|
286
|
+
return {
|
|
287
|
+
"status": "pass" if passed else "fail",
|
|
288
|
+
"input_orders": int(result.metadata.get("input_orders", len(orders))),
|
|
289
|
+
"accepted_orders": int(result.metadata.get("accepted_orders", len(result.orders))),
|
|
290
|
+
"rejected_orders": int(result.metadata.get("rejected_orders", 0)),
|
|
291
|
+
"package_status": package_status,
|
|
292
|
+
"depth_model": result.metadata.get("depth_model"),
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _make_portfolio_fixture(rows: int, symbols: int, symbol_prefix: str = "SYM"):
|
|
297
|
+
idx = pd.date_range("2022-01-01", periods=rows, freq="1h", tz="UTC")
|
|
298
|
+
grid = np.arange(rows)
|
|
299
|
+
base = 100.0 + np.cumsum(np.sin(grid / 19.0) * 0.08 + np.cos(grid / 37.0) * 0.02)
|
|
300
|
+
positions = {}
|
|
301
|
+
closes = {}
|
|
302
|
+
highs = {}
|
|
303
|
+
lows = {}
|
|
304
|
+
for j in range(symbols):
|
|
305
|
+
symbol = f"{symbol_prefix}{j:03d}" if symbol_prefix.endswith("SYM") else f"{symbol_prefix}{j}"
|
|
306
|
+
close = pd.Series(base * (1.0 + j * 0.015) + j * 3.0, index=idx)
|
|
307
|
+
raw = np.where(((grid // (18 + j % 4)) + j) % 4 == 0, 1.0, 0.0)
|
|
308
|
+
sign = 1.0 if j % 2 == 0 else -1.0
|
|
309
|
+
positions[symbol] = pd.Series(raw * sign, index=idx)
|
|
310
|
+
closes[symbol] = close
|
|
311
|
+
highs[symbol] = close * 1.002
|
|
312
|
+
lows[symbol] = close * 0.998
|
|
313
|
+
return idx, positions, closes, highs, lows
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _prepare_portfolio_arrays(idx, positions, closes, highs, lows, account, alloc, fee_rate):
|
|
317
|
+
idx = validate_datetime(idx)
|
|
318
|
+
symbols = list(positions.keys())
|
|
319
|
+
close_dict = align_series(closes, symbols, idx)
|
|
320
|
+
high_dict = align_series(highs, symbols, idx, fallback=close_dict)
|
|
321
|
+
low_dict = align_series(lows, symbols, idx, fallback=close_dict)
|
|
322
|
+
pos_dict = align_series(positions, symbols, idx, fill_val=0.0)
|
|
323
|
+
funding_dict = prepare_funding(0.0, symbols, idx)
|
|
324
|
+
market = build_market_arrays(symbols, idx, close_dict, high_dict, low_dict, funding_dict)
|
|
325
|
+
raw_signals = build_signal_matrix(symbols, idx, pos_dict)
|
|
326
|
+
alloc_arr = np.full(len(symbols), float(alloc), dtype=np.float64)
|
|
327
|
+
contract_sizes = np.ones(len(symbols), dtype=np.float64)
|
|
328
|
+
leverages = np.full(len(symbols), float(account.leverage), dtype=np.float64)
|
|
329
|
+
target_units = scale_signal_notional_matrix(raw_signals, market.closes, alloc_arr, use_pyramiding=True)
|
|
330
|
+
return {
|
|
331
|
+
"n_bars": len(idx),
|
|
332
|
+
"n_syms": len(symbols),
|
|
333
|
+
"highs": market.highs,
|
|
334
|
+
"lows": market.lows,
|
|
335
|
+
"closes": market.closes,
|
|
336
|
+
"target_units": target_units,
|
|
337
|
+
"funding": market.funding,
|
|
338
|
+
"is_funding_bar": market.is_funding_bar,
|
|
339
|
+
"initial_capital": float(account.initial_capital),
|
|
340
|
+
"leverages": leverages,
|
|
341
|
+
"maintenance_ratio": float(account.maintenance_ratio),
|
|
342
|
+
"fee_rate": float(fee_rate),
|
|
343
|
+
"slippage_rate": 0.0,
|
|
344
|
+
"contract_sizes": contract_sizes,
|
|
345
|
+
"tradable": np.ones_like(market.closes, dtype=np.bool_),
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _kernel_portfolio(prepared: Dict):
|
|
350
|
+
return _engine_portfolio(
|
|
351
|
+
n_bars=prepared["n_bars"],
|
|
352
|
+
n_syms=prepared["n_syms"],
|
|
353
|
+
highs=prepared["highs"],
|
|
354
|
+
lows=prepared["lows"],
|
|
355
|
+
closes=prepared["closes"],
|
|
356
|
+
target_pos=prepared["target_units"],
|
|
357
|
+
funding_rates=prepared["funding"],
|
|
358
|
+
is_funding_bar=prepared["is_funding_bar"],
|
|
359
|
+
init_capital=prepared["initial_capital"],
|
|
360
|
+
leverages=prepared["leverages"],
|
|
361
|
+
maint_ratio=prepared["maintenance_ratio"],
|
|
362
|
+
fee_rate=prepared["fee_rate"],
|
|
363
|
+
slippage_rate=prepared["slippage_rate"],
|
|
364
|
+
contract_sizes=prepared["contract_sizes"],
|
|
365
|
+
use_funding=False,
|
|
366
|
+
tradable=prepared["tradable"],
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _depth_frame(idx, close: float, high: float, low: float) -> pd.DataFrame:
|
|
371
|
+
return pd.DataFrame(
|
|
372
|
+
{
|
|
373
|
+
"open": close,
|
|
374
|
+
"high": high,
|
|
375
|
+
"low": low,
|
|
376
|
+
"close": close,
|
|
377
|
+
"volume": 100.0,
|
|
378
|
+
},
|
|
379
|
+
index=idx,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _timeit(fn, repeats: int) -> float:
|
|
384
|
+
samples: List[float] = []
|
|
385
|
+
for _ in range(max(1, int(repeats))):
|
|
386
|
+
start = time.perf_counter()
|
|
387
|
+
fn()
|
|
388
|
+
samples.append(time.perf_counter() - start)
|
|
389
|
+
return float(statistics.mean(samples))
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _cython_cpp_recommendation(benchmark: Dict) -> str:
|
|
393
|
+
share = benchmark.get("stages", {}).get("pure_kernel_share_pct", 100.0)
|
|
394
|
+
if share >= 35.0:
|
|
395
|
+
return "Pure kernel share is large enough to justify investigating Cython/C++ after correctness locks."
|
|
396
|
+
return "Cython/C++ is not justified yet; optimize cached array preparation and report construction first."
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _json_default(value):
|
|
400
|
+
if isinstance(value, (np.bool_,)):
|
|
401
|
+
return bool(value)
|
|
402
|
+
if isinstance(value, (np.integer,)):
|
|
403
|
+
return int(value)
|
|
404
|
+
if isinstance(value, (np.floating,)):
|
|
405
|
+
return float(value)
|
|
406
|
+
if isinstance(value, pd.Timestamp):
|
|
407
|
+
return value.isoformat()
|
|
408
|
+
raise TypeError(f"{type(value).__name__} is not JSON serializable")
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
412
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
413
|
+
parser.add_argument("--rows", type=int, default=2_000)
|
|
414
|
+
parser.add_argument("--symbols", type=int, default=6)
|
|
415
|
+
parser.add_argument("--repeats", type=int, default=3)
|
|
416
|
+
parser.add_argument("--include-nautilus", action="store_true")
|
|
417
|
+
parser.add_argument("--json-out", type=Path, default=PACKAGE_DIR / "benchmarks" / "phase12_benchmark_nautilus_cert.json")
|
|
418
|
+
parser.add_argument("--md-out", type=Path, default=PACKAGE_DIR / "benchmarks" / "phase12_benchmark_nautilus_cert.md")
|
|
419
|
+
args = parser.parse_args(argv)
|
|
420
|
+
report = run_certification(rows=args.rows, symbols=args.symbols, repeats=args.repeats, include_nautilus=args.include_nautilus)
|
|
421
|
+
args.json_out.parent.mkdir(parents=True, exist_ok=True)
|
|
422
|
+
args.md_out.parent.mkdir(parents=True, exist_ok=True)
|
|
423
|
+
args.json_out.write_text(json.dumps(report, indent=2, default=_json_default) + "\n", encoding="utf-8")
|
|
424
|
+
args.md_out.write_text(make_markdown(report), encoding="utf-8")
|
|
425
|
+
print(make_markdown(report))
|
|
426
|
+
return 0 if report["status"] == "pass" else 1
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
if __name__ == "__main__":
|
|
430
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Phase 13B native portfolio report-construction benchmark.
|
|
4
|
+
|
|
5
|
+
The runner reuses the Phase 12B decomposition and writes a focused artifact for
|
|
6
|
+
the report-construction optimization pass.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Dict
|
|
16
|
+
|
|
17
|
+
PACKAGE_DIR = Path(__file__).resolve().parents[1]
|
|
18
|
+
PROJECT_DIR = PACKAGE_DIR.parent
|
|
19
|
+
if str(PROJECT_DIR) not in sys.path:
|
|
20
|
+
sys.path.insert(0, str(PROJECT_DIR))
|
|
21
|
+
|
|
22
|
+
from quantbt.benchmarks.run_phase12_benchmark_nautilus_cert import run_certification # noqa: E402
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run_report(rows: int = 2_000, symbols: int = 6, repeats: int = 3) -> Dict:
|
|
26
|
+
report = run_certification(rows=rows, symbols=symbols, repeats=repeats, include_nautilus=False)
|
|
27
|
+
bench = report["benchmark_followup"]
|
|
28
|
+
stages = bench["stages"]
|
|
29
|
+
return {
|
|
30
|
+
"status": "pass" if report["status"] == "pass" and bench["status"] == "pass" else "fail",
|
|
31
|
+
"rows": int(rows),
|
|
32
|
+
"symbols": int(symbols),
|
|
33
|
+
"repeats": int(repeats),
|
|
34
|
+
"full_facade_seconds": float(stages["full_facade_seconds"]),
|
|
35
|
+
"prepared_reuse_facade_seconds": float(stages["prepared_reuse_facade_seconds"]),
|
|
36
|
+
"array_preparation_seconds": float(stages["array_preparation_seconds"]),
|
|
37
|
+
"pure_numba_kernel_seconds": float(stages["pure_numba_kernel_seconds"]),
|
|
38
|
+
"report_construction_estimate_seconds": float(stages["report_construction_estimate_seconds"]),
|
|
39
|
+
"report_construction_share_pct": float(stages["report_construction_share_pct"]),
|
|
40
|
+
"pure_kernel_share_pct": float(stages["pure_kernel_share_pct"]),
|
|
41
|
+
"prepared_reuse_speedup": float(stages["prepared_reuse_speedup"]),
|
|
42
|
+
"cython_cpp_recommendation": report["cython_cpp_recommendation"],
|
|
43
|
+
"notes": (
|
|
44
|
+
"Phase 13B keeps accounting unchanged and optimizes report construction "
|
|
45
|
+
"with ndarray-first calculations for funding, diagnostics, exposure, "
|
|
46
|
+
"and rebalance reports."
|
|
47
|
+
),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def make_markdown(report: Dict) -> str:
|
|
52
|
+
return "\n".join(
|
|
53
|
+
[
|
|
54
|
+
"# Phase 13B Native Portfolio Report Construction",
|
|
55
|
+
"",
|
|
56
|
+
f"Status: **{report['status']}**",
|
|
57
|
+
"",
|
|
58
|
+
f"- Rows: `{report['rows']}`",
|
|
59
|
+
f"- Symbols: `{report['symbols']}`",
|
|
60
|
+
f"- Repeats: `{report['repeats']}`",
|
|
61
|
+
f"- Full facade seconds: `{report['full_facade_seconds']:.6f}`",
|
|
62
|
+
f"- Prepared reuse seconds: `{report['prepared_reuse_facade_seconds']:.6f}`",
|
|
63
|
+
f"- Array preparation seconds: `{report['array_preparation_seconds']:.6f}`",
|
|
64
|
+
f"- Pure Numba kernel seconds: `{report['pure_numba_kernel_seconds']:.6f}`",
|
|
65
|
+
f"- Report construction residual seconds: `{report['report_construction_estimate_seconds']:.6f}`",
|
|
66
|
+
f"- Report construction share: `{report['report_construction_share_pct']:.2f}%`",
|
|
67
|
+
f"- Pure kernel share: `{report['pure_kernel_share_pct']:.2f}%`",
|
|
68
|
+
f"- Prepared reuse speedup: `{report['prepared_reuse_speedup']:.3f}x`",
|
|
69
|
+
"",
|
|
70
|
+
"## Notes",
|
|
71
|
+
"",
|
|
72
|
+
report["notes"],
|
|
73
|
+
"",
|
|
74
|
+
"## Cython/C++ Decision",
|
|
75
|
+
"",
|
|
76
|
+
report["cython_cpp_recommendation"],
|
|
77
|
+
]
|
|
78
|
+
) + "\n"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main() -> int:
|
|
82
|
+
parser = argparse.ArgumentParser()
|
|
83
|
+
parser.add_argument("--rows", type=int, default=2_000)
|
|
84
|
+
parser.add_argument("--symbols", type=int, default=6)
|
|
85
|
+
parser.add_argument("--repeats", type=int, default=3)
|
|
86
|
+
parser.add_argument("--json", type=Path, default=PACKAGE_DIR / "benchmarks" / "phase13_portfolio_report.json")
|
|
87
|
+
parser.add_argument("--markdown", type=Path, default=PACKAGE_DIR / "benchmarks" / "phase13_portfolio_report.md")
|
|
88
|
+
args = parser.parse_args()
|
|
89
|
+
report = run_report(rows=args.rows, symbols=args.symbols, repeats=args.repeats)
|
|
90
|
+
args.json.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n")
|
|
91
|
+
args.markdown.write_text(make_markdown(report))
|
|
92
|
+
print(json.dumps(report, indent=2, sort_keys=True))
|
|
93
|
+
return 0 if report["status"] == "pass" else 1
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
raise SystemExit(main())
|