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,389 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Phase 16 performance-debt closure benchmark.
|
|
4
|
+
|
|
5
|
+
This runner measures the remaining facade/service-loop overhead after Phase
|
|
6
|
+
13/14 and verifies that prepared service contexts do not change accounting.
|
|
7
|
+
It is intentionally focused on pandas normalization/report construction, not on
|
|
8
|
+
changing domain kernels.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
import tracemalloc
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Callable, Dict, List
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
import pandas as pd
|
|
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 import QuantBTEndpoint # noqa: E402
|
|
30
|
+
from quantbt.benchmarks.run_phase14_service_loop import run_benchmark as run_phase14_benchmark # noqa: E402
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def run_phase16_benchmark(
|
|
34
|
+
*,
|
|
35
|
+
rows: int = 1_440,
|
|
36
|
+
symbols: int = 6,
|
|
37
|
+
replays: int = 8,
|
|
38
|
+
repeats: int = 2,
|
|
39
|
+
include_large_wfo: bool = True,
|
|
40
|
+
) -> Dict:
|
|
41
|
+
single = _single_service_context_benchmark(rows=rows, replays=replays, repeats=repeats)
|
|
42
|
+
portfolio = _portfolio_service_context_benchmark(rows=rows, symbols=symbols, replays=replays, repeats=repeats)
|
|
43
|
+
report = _portfolio_report_benchmark(rows=rows, symbols=symbols, repeats=repeats)
|
|
44
|
+
large_wfo = (
|
|
45
|
+
run_phase14_benchmark(
|
|
46
|
+
rows=max(rows, 1_440),
|
|
47
|
+
symbols=max(symbols, 6),
|
|
48
|
+
trials=max(8, replays),
|
|
49
|
+
order_count=max(240, rows // 4),
|
|
50
|
+
repeats=max(1, repeats),
|
|
51
|
+
)
|
|
52
|
+
if include_large_wfo
|
|
53
|
+
else {"status": "skipped", "reason": "include_large_wfo=False"}
|
|
54
|
+
)
|
|
55
|
+
parity = {
|
|
56
|
+
"single_service_context": bool(single["parity_passed"]),
|
|
57
|
+
"portfolio_service_context": bool(portfolio["parity_passed"]),
|
|
58
|
+
"portfolio_report_levels": bool(report["parity_passed"]),
|
|
59
|
+
"large_wfo_service_loop": bool(large_wfo.get("status") == "pass") if include_large_wfo else True,
|
|
60
|
+
}
|
|
61
|
+
status = "pass" if all(parity.values()) else "fail"
|
|
62
|
+
return {
|
|
63
|
+
"phase": "16",
|
|
64
|
+
"status": status,
|
|
65
|
+
"rows": int(rows),
|
|
66
|
+
"symbols": int(symbols),
|
|
67
|
+
"replays": int(replays),
|
|
68
|
+
"repeats": int(repeats),
|
|
69
|
+
"service_context": {
|
|
70
|
+
"single_signal_notional": single,
|
|
71
|
+
"native_portfolio": portfolio,
|
|
72
|
+
},
|
|
73
|
+
"report_construction": report,
|
|
74
|
+
"large_wfo_service_loop": _compact_phase14(large_wfo),
|
|
75
|
+
"parity": parity,
|
|
76
|
+
"cython_cpp_recommendation": _cython_cpp_recommendation(large_wfo),
|
|
77
|
+
"closed_debt": [
|
|
78
|
+
"facade-level repeated pandas market normalization can now be avoided with endpoint.prepare_service_context(...)",
|
|
79
|
+
"report construction has an explicit full/minimal benchmark and parity guard",
|
|
80
|
+
"larger WFO/service-loop benchmark is archived before any Cython/C++ decision",
|
|
81
|
+
],
|
|
82
|
+
"remaining_notes": [
|
|
83
|
+
"normal endpoint.backtest(...) remains backward-compatible and still normalizes defensively per call",
|
|
84
|
+
"prepared service context is opt-in and currently covers native_vectorized signal_notional plus native_portfolio",
|
|
85
|
+
"Cython/C++ should wait until pure kernels, not pandas/report facades, dominate measured runtime",
|
|
86
|
+
],
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def make_markdown(report: Dict) -> str:
|
|
91
|
+
single = report["service_context"]["single_signal_notional"]
|
|
92
|
+
portfolio = report["service_context"]["native_portfolio"]
|
|
93
|
+
rpt = report["report_construction"]
|
|
94
|
+
lines = [
|
|
95
|
+
"# Phase 16 Performance Debt Closure",
|
|
96
|
+
"",
|
|
97
|
+
f"Status: **{report['status']}**",
|
|
98
|
+
"",
|
|
99
|
+
"## Prepared Service Context",
|
|
100
|
+
"",
|
|
101
|
+
"| workload | normal seconds | prepared seconds | speedup | peak MB | parity |",
|
|
102
|
+
"| --- | ---: | ---: | ---: | ---: | --- |",
|
|
103
|
+
_row("single signal_notional", single),
|
|
104
|
+
_row("native portfolio", portfolio),
|
|
105
|
+
"",
|
|
106
|
+
"## Report Construction",
|
|
107
|
+
"",
|
|
108
|
+
"| workload | full seconds | minimal seconds | speedup | parity |",
|
|
109
|
+
"| --- | ---: | ---: | ---: | --- |",
|
|
110
|
+
"| native portfolio reports | `{full_seconds:.6f}` | `{minimal_seconds:.6f}` | `{speedup:.3f}x` | `{parity}` |".format(
|
|
111
|
+
full_seconds=float(rpt["full_seconds"]),
|
|
112
|
+
minimal_seconds=float(rpt["minimal_seconds"]),
|
|
113
|
+
speedup=float(rpt["speedup"]),
|
|
114
|
+
parity=bool(rpt["parity_passed"]),
|
|
115
|
+
),
|
|
116
|
+
"",
|
|
117
|
+
"## Large WFO / Service Loop",
|
|
118
|
+
"",
|
|
119
|
+
f"- Status: `{report['large_wfo_service_loop'].get('status')}`",
|
|
120
|
+
f"- Rows: `{report['large_wfo_service_loop'].get('rows')}`",
|
|
121
|
+
f"- Symbols: `{report['large_wfo_service_loop'].get('symbols')}`",
|
|
122
|
+
f"- Cython/C++ recommendation: {report['cython_cpp_recommendation']}",
|
|
123
|
+
"",
|
|
124
|
+
"## Closed Debt",
|
|
125
|
+
"",
|
|
126
|
+
]
|
|
127
|
+
for item in report["closed_debt"]:
|
|
128
|
+
lines.append(f"- {item}")
|
|
129
|
+
lines.extend(["", "## Remaining Notes", ""])
|
|
130
|
+
for item in report["remaining_notes"]:
|
|
131
|
+
lines.append(f"- {item}")
|
|
132
|
+
lines.append("")
|
|
133
|
+
return "\n".join(lines)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _row(label: str, item: Dict) -> str:
|
|
137
|
+
return "| {label} | `{normal:.6f}` | `{prepared:.6f}` | `{speedup:.3f}x` | `{peak:.3f}` | `{parity}` |".format(
|
|
138
|
+
label=label,
|
|
139
|
+
normal=float(item["normal_seconds"]),
|
|
140
|
+
prepared=float(item["prepared_seconds"]),
|
|
141
|
+
speedup=float(item["speedup"]),
|
|
142
|
+
peak=float(item["peak_memory_mb"]),
|
|
143
|
+
parity=bool(item["parity_passed"]),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _single_service_context_benchmark(*, rows: int, replays: int, repeats: int) -> Dict:
|
|
148
|
+
data = _single_frame(rows)
|
|
149
|
+
signals = _single_signals(data.index, replays)
|
|
150
|
+
|
|
151
|
+
normal_endpoint = _single_endpoint()
|
|
152
|
+
prepared_endpoint = _single_endpoint()
|
|
153
|
+
context = prepared_endpoint.prepare_service_context(data=data, symbols=["BTC"])
|
|
154
|
+
|
|
155
|
+
normal_results = _run_single_replays(normal_endpoint, data, signals)
|
|
156
|
+
prepared_results = _run_single_context_replays(context, signals)
|
|
157
|
+
normal_seconds = _timeit(lambda: _run_single_replays(normal_endpoint, data, signals), repeats)
|
|
158
|
+
prepared_seconds = _timeit(lambda: _run_single_context_replays(context, signals), repeats)
|
|
159
|
+
peak = _peak_memory_mb(lambda: _run_single_context_replays(context, signals))
|
|
160
|
+
equity_diff = max(
|
|
161
|
+
float(abs(normal.equity.iloc[-1] - prepared.equity.iloc[-1]))
|
|
162
|
+
for normal, prepared in zip(normal_results, prepared_results)
|
|
163
|
+
)
|
|
164
|
+
position_diff = max(
|
|
165
|
+
float(np.max(np.abs(normal.positions.to_numpy() - prepared.positions.to_numpy())))
|
|
166
|
+
for normal, prepared in zip(normal_results, prepared_results)
|
|
167
|
+
)
|
|
168
|
+
return {
|
|
169
|
+
"normal_seconds": float(normal_seconds),
|
|
170
|
+
"prepared_seconds": float(prepared_seconds),
|
|
171
|
+
"speedup": float(normal_seconds / prepared_seconds) if prepared_seconds > 0.0 else 0.0,
|
|
172
|
+
"peak_memory_mb": float(peak),
|
|
173
|
+
"parity_passed": bool(equity_diff <= 1e-9 and position_diff <= 1e-12),
|
|
174
|
+
"final_equity_max_abs_diff": equity_diff,
|
|
175
|
+
"position_max_abs_diff": position_diff,
|
|
176
|
+
"context_metadata": context.metadata,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _portfolio_service_context_benchmark(*, rows: int, symbols: int, replays: int, repeats: int) -> Dict:
|
|
181
|
+
data, positions_list, symbol_list = _portfolio_inputs(rows, symbols, replays)
|
|
182
|
+
normal_endpoint = _portfolio_endpoint(symbol_list, report_level="minimal")
|
|
183
|
+
prepared_endpoint = _portfolio_endpoint(symbol_list, report_level="minimal")
|
|
184
|
+
context = prepared_endpoint.prepare_service_context(data=data, symbols=symbol_list)
|
|
185
|
+
|
|
186
|
+
normal_results = _run_portfolio_replays(normal_endpoint, data, positions_list, symbol_list)
|
|
187
|
+
prepared_results = _run_portfolio_context_replays(context, positions_list)
|
|
188
|
+
normal_seconds = _timeit(lambda: _run_portfolio_replays(normal_endpoint, data, positions_list, symbol_list), repeats)
|
|
189
|
+
prepared_seconds = _timeit(lambda: _run_portfolio_context_replays(context, positions_list), repeats)
|
|
190
|
+
peak = _peak_memory_mb(lambda: _run_portfolio_context_replays(context, positions_list))
|
|
191
|
+
equity_diff = max(
|
|
192
|
+
float(abs(normal.equity.iloc[-1] - prepared.equity.iloc[-1]))
|
|
193
|
+
for normal, prepared in zip(normal_results, prepared_results)
|
|
194
|
+
)
|
|
195
|
+
margin_diff = max(
|
|
196
|
+
float(np.max(np.abs(normal.margin.to_numpy() - prepared.margin.to_numpy())))
|
|
197
|
+
for normal, prepared in zip(normal_results, prepared_results)
|
|
198
|
+
)
|
|
199
|
+
return {
|
|
200
|
+
"normal_seconds": float(normal_seconds),
|
|
201
|
+
"prepared_seconds": float(prepared_seconds),
|
|
202
|
+
"speedup": float(normal_seconds / prepared_seconds) if prepared_seconds > 0.0 else 0.0,
|
|
203
|
+
"peak_memory_mb": float(peak),
|
|
204
|
+
"parity_passed": bool(equity_diff <= 1e-8 and margin_diff <= 1e-8),
|
|
205
|
+
"final_equity_max_abs_diff": equity_diff,
|
|
206
|
+
"margin_max_abs_diff": margin_diff,
|
|
207
|
+
"context_metadata": context.metadata,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _portfolio_report_benchmark(*, rows: int, symbols: int, repeats: int) -> Dict:
|
|
212
|
+
data, positions_list, symbol_list = _portfolio_inputs(rows, symbols, 1)
|
|
213
|
+
full_endpoint = _portfolio_endpoint(symbol_list, report_level="full")
|
|
214
|
+
minimal_endpoint = _portfolio_endpoint(symbol_list, report_level="minimal")
|
|
215
|
+
full = full_endpoint.backtest(data=data, positions=positions_list[0], symbols=symbol_list)
|
|
216
|
+
minimal = minimal_endpoint.backtest(data=data, positions=positions_list[0], symbols=symbol_list)
|
|
217
|
+
full_seconds = _timeit(lambda: full_endpoint.backtest(data=data, positions=positions_list[0], symbols=symbol_list), repeats)
|
|
218
|
+
minimal_seconds = _timeit(lambda: minimal_endpoint.backtest(data=data, positions=positions_list[0], symbols=symbol_list), repeats)
|
|
219
|
+
equity_diff = float(np.max(np.abs(full.equity.to_numpy() - minimal.equity.to_numpy())))
|
|
220
|
+
positions_diff = float(np.max(np.abs(full.positions.to_numpy() - minimal.positions.to_numpy())))
|
|
221
|
+
return {
|
|
222
|
+
"full_seconds": float(full_seconds),
|
|
223
|
+
"minimal_seconds": float(minimal_seconds),
|
|
224
|
+
"speedup": float(full_seconds / minimal_seconds) if minimal_seconds > 0.0 else 0.0,
|
|
225
|
+
"parity_passed": bool(equity_diff <= 1e-8 and positions_diff <= 1e-12),
|
|
226
|
+
"equity_max_abs_diff": equity_diff,
|
|
227
|
+
"positions_max_abs_diff": positions_diff,
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _single_endpoint() -> QuantBTEndpoint:
|
|
232
|
+
return QuantBTEndpoint.signal_notional(
|
|
233
|
+
initial_capital=20_000.0,
|
|
234
|
+
leverage=4.0,
|
|
235
|
+
alloc_per_trade=5_000.0,
|
|
236
|
+
fee_rate=0.0002,
|
|
237
|
+
use_funding=False,
|
|
238
|
+
slippage=0.0001,
|
|
239
|
+
use_pyramiding=True,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _portfolio_endpoint(symbols: List[str], *, report_level: str) -> QuantBTEndpoint:
|
|
244
|
+
return QuantBTEndpoint.portfolio(
|
|
245
|
+
portfolio_mode="market_neutral",
|
|
246
|
+
backend="native_portfolio",
|
|
247
|
+
hedge_type="signal_notional",
|
|
248
|
+
initial_capital=100_000.0,
|
|
249
|
+
leverage=4.0,
|
|
250
|
+
alloc_per_trade={symbol: 5_000.0 for symbol in symbols},
|
|
251
|
+
fee=0.0004,
|
|
252
|
+
use_funding=False,
|
|
253
|
+
report_level=report_level,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _single_frame(rows: int) -> pd.DataFrame:
|
|
258
|
+
idx = pd.date_range("2021-01-01", periods=int(rows), freq="1h", tz="UTC")
|
|
259
|
+
close = 100.0 + np.cumsum(np.sin(np.linspace(0.0, 32.0, len(idx))) * 0.03 + 0.002)
|
|
260
|
+
return pd.DataFrame(
|
|
261
|
+
{
|
|
262
|
+
"open": close,
|
|
263
|
+
"high": close * 1.002,
|
|
264
|
+
"low": close * 0.998,
|
|
265
|
+
"close": close,
|
|
266
|
+
"volume": 1_000.0,
|
|
267
|
+
},
|
|
268
|
+
index=idx,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _single_signals(idx: pd.DatetimeIndex, replays: int) -> List[pd.Series]:
|
|
273
|
+
out = []
|
|
274
|
+
base = np.linspace(0.0, 20.0, len(idx))
|
|
275
|
+
for replay in range(int(replays)):
|
|
276
|
+
out.append(pd.Series(np.sign(np.sin(base + replay * 0.3)), index=idx))
|
|
277
|
+
return out
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _portfolio_inputs(rows: int, symbols: int, replays: int):
|
|
281
|
+
idx = pd.date_range("2021-01-01", periods=int(rows), freq="1h", tz="UTC")
|
|
282
|
+
symbol_list = [f"S{i:02d}" for i in range(int(symbols))]
|
|
283
|
+
data = {}
|
|
284
|
+
for j, symbol in enumerate(symbol_list):
|
|
285
|
+
close = 100.0 + j * 5.0 + np.cumsum(np.sin(np.linspace(0.0, 18.0, len(idx)) + j) * 0.02 + 0.001)
|
|
286
|
+
data[symbol] = pd.DataFrame(
|
|
287
|
+
{
|
|
288
|
+
"open": close,
|
|
289
|
+
"high": close * 1.002,
|
|
290
|
+
"low": close * 0.998,
|
|
291
|
+
"close": close,
|
|
292
|
+
"volume": 1_000.0,
|
|
293
|
+
},
|
|
294
|
+
index=idx,
|
|
295
|
+
)
|
|
296
|
+
positions = []
|
|
297
|
+
base = np.linspace(0.0, 16.0, len(idx))
|
|
298
|
+
for replay in range(int(replays)):
|
|
299
|
+
matrix = {
|
|
300
|
+
symbol: np.sign(np.sin(base + replay * 0.2 + j * 0.5))
|
|
301
|
+
for j, symbol in enumerate(symbol_list)
|
|
302
|
+
}
|
|
303
|
+
positions.append(pd.DataFrame(matrix, index=idx))
|
|
304
|
+
return data, positions, symbol_list
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _run_single_replays(endpoint: QuantBTEndpoint, data: pd.DataFrame, signals: List[pd.Series]):
|
|
308
|
+
return [endpoint.backtest(data=data, signal=signal, symbols=["BTC"]) for signal in signals]
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _run_single_context_replays(context, signals: List[pd.Series]):
|
|
312
|
+
return [context.backtest(signal=signal) for signal in signals]
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _run_portfolio_replays(endpoint: QuantBTEndpoint, data, positions_list, symbols):
|
|
316
|
+
return [endpoint.backtest(data=data, positions=positions, symbols=symbols) for positions in positions_list]
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _run_portfolio_context_replays(context, positions_list):
|
|
320
|
+
return [context.backtest(positions=positions) for positions in positions_list]
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _timeit(func: Callable[[], object], repeats: int) -> float:
|
|
324
|
+
values = []
|
|
325
|
+
for _ in range(max(1, int(repeats))):
|
|
326
|
+
start = time.perf_counter()
|
|
327
|
+
func()
|
|
328
|
+
values.append(time.perf_counter() - start)
|
|
329
|
+
return float(min(values))
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _peak_memory_mb(func: Callable[[], object]) -> float:
|
|
333
|
+
tracemalloc.start()
|
|
334
|
+
func()
|
|
335
|
+
_, peak = tracemalloc.get_traced_memory()
|
|
336
|
+
tracemalloc.stop()
|
|
337
|
+
return float(peak / (1024 * 1024))
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _compact_phase14(report: Dict) -> Dict:
|
|
341
|
+
if report.get("status") == "skipped":
|
|
342
|
+
return report
|
|
343
|
+
return {
|
|
344
|
+
"status": report.get("status"),
|
|
345
|
+
"rows": report.get("rows"),
|
|
346
|
+
"symbols": report.get("symbols"),
|
|
347
|
+
"trials": report.get("trials"),
|
|
348
|
+
"order_count": report.get("order_count"),
|
|
349
|
+
"parity": report.get("parity"),
|
|
350
|
+
"cython_cpp_recommendation": report.get("cython_cpp_recommendation"),
|
|
351
|
+
"next_optimization_targets": report.get("next_optimization_targets"),
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _cython_cpp_recommendation(large_wfo: Dict) -> str:
|
|
356
|
+
if large_wfo.get("status") != "pass":
|
|
357
|
+
return "defer; benchmark did not pass all parity/status gates"
|
|
358
|
+
text = str(large_wfo.get("cython_cpp_recommendation", "")).lower()
|
|
359
|
+
if "not justified" in text or "not yet" in text:
|
|
360
|
+
return "not justified yet; facade/report overhead remains the larger measured bucket"
|
|
361
|
+
return large_wfo.get("cython_cpp_recommendation", "defer until pure kernel bottleneck is proven")
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def main() -> None:
|
|
365
|
+
parser = argparse.ArgumentParser()
|
|
366
|
+
parser.add_argument("--rows", type=int, default=1_440)
|
|
367
|
+
parser.add_argument("--symbols", type=int, default=6)
|
|
368
|
+
parser.add_argument("--replays", type=int, default=8)
|
|
369
|
+
parser.add_argument("--repeats", type=int, default=2)
|
|
370
|
+
parser.add_argument("--skip-large-wfo", action="store_true")
|
|
371
|
+
parser.add_argument("--output-json", default=str(PACKAGE_DIR / "benchmarks" / "phase16_performance_debt.json"))
|
|
372
|
+
parser.add_argument("--output-md", default=str(PACKAGE_DIR / "benchmarks" / "phase16_performance_debt.md"))
|
|
373
|
+
args = parser.parse_args()
|
|
374
|
+
report = run_phase16_benchmark(
|
|
375
|
+
rows=args.rows,
|
|
376
|
+
symbols=args.symbols,
|
|
377
|
+
replays=args.replays,
|
|
378
|
+
repeats=args.repeats,
|
|
379
|
+
include_large_wfo=not args.skip_large_wfo,
|
|
380
|
+
)
|
|
381
|
+
json_path = Path(args.output_json)
|
|
382
|
+
md_path = Path(args.output_md)
|
|
383
|
+
json_path.write_text(json.dumps(report, indent=2, default=str), encoding="utf-8")
|
|
384
|
+
md_path.write_text(make_markdown(report), encoding="utf-8")
|
|
385
|
+
print(json.dumps(report, indent=2, default=str))
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
if __name__ == "__main__":
|
|
389
|
+
main()
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
from quantbt import OrderCommand, OrderSide, OrderType, QuantBTEndpoint, TimeInForce
|
|
12
|
+
from quantbt.core.orders import OrderAction
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _bars(n: int) -> pd.DataFrame:
|
|
16
|
+
idx = pd.date_range("2024-01-01", periods=n, freq="1h", tz="UTC")
|
|
17
|
+
x = np.arange(n, dtype=np.float64)
|
|
18
|
+
close = 100.0 + 0.002 * x + 2.0 * np.sin(x / 27.0) + 0.7 * np.sin(x / 7.0)
|
|
19
|
+
return pd.DataFrame(
|
|
20
|
+
{
|
|
21
|
+
"open": close,
|
|
22
|
+
"high": close + 1.25,
|
|
23
|
+
"low": close - 1.25,
|
|
24
|
+
"close": close,
|
|
25
|
+
"volume": 10_000.0 + 100.0 * np.cos(x / 11.0),
|
|
26
|
+
},
|
|
27
|
+
index=idx,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ReactiveGridStrategy:
|
|
32
|
+
def __init__(self, *, levels: int, reseed_every: int) -> None:
|
|
33
|
+
self.levels = int(levels)
|
|
34
|
+
self.reseed_every = int(reseed_every)
|
|
35
|
+
self.cycle = 0
|
|
36
|
+
|
|
37
|
+
def on_bar_close(self, context):
|
|
38
|
+
commands = []
|
|
39
|
+
if context.bar_index % self.reseed_every == 0:
|
|
40
|
+
self.cycle += 1
|
|
41
|
+
commands.append(
|
|
42
|
+
OrderCommand(
|
|
43
|
+
timestamp=context.timestamp,
|
|
44
|
+
action=OrderAction.CANCEL_ALL,
|
|
45
|
+
symbol=context.symbols[0],
|
|
46
|
+
tag_prefix="GRID-",
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
center = float(context.close[0])
|
|
50
|
+
for level in range(1, self.levels + 1):
|
|
51
|
+
commands.append(
|
|
52
|
+
OrderCommand(
|
|
53
|
+
timestamp=context.timestamp,
|
|
54
|
+
symbol=context.symbols[0],
|
|
55
|
+
side=OrderSide.BUY,
|
|
56
|
+
order_type=OrderType.LIMIT,
|
|
57
|
+
qty=0.01,
|
|
58
|
+
price=center - 0.05 * level,
|
|
59
|
+
tif=TimeInForce.GTC,
|
|
60
|
+
order_id=f"grid-{self.cycle}-{level}",
|
|
61
|
+
tag=f"GRID-C{self.cycle}-L{level}",
|
|
62
|
+
metadata={"campaign_id": "GRID", "cycle_id": str(self.cycle), "level_id": str(level)},
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
if context.positions[context.symbols[0]] > 0.0 and context.bar_index % (self.reseed_every + 7) == 0:
|
|
66
|
+
commands.append(
|
|
67
|
+
OrderCommand(
|
|
68
|
+
timestamp=context.timestamp,
|
|
69
|
+
symbol=context.symbols[0],
|
|
70
|
+
side=OrderSide.SELL,
|
|
71
|
+
order_type=OrderType.MARKET,
|
|
72
|
+
qty=abs(float(context.positions[context.symbols[0]])),
|
|
73
|
+
tif=TimeInForce.IOC,
|
|
74
|
+
reduce_only=True,
|
|
75
|
+
order_id=f"flatten-{context.bar_index}",
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
return commands
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def run(*, bars: int, levels: int, reseed_every: int, out_dir: Path) -> dict:
|
|
82
|
+
data = _bars(bars)
|
|
83
|
+
strategy = ReactiveGridStrategy(levels=levels, reseed_every=reseed_every)
|
|
84
|
+
endpoint = QuantBTEndpoint.native_event_strategy(initial_capital=100_000, leverage=5, use_funding=False)
|
|
85
|
+
|
|
86
|
+
t0 = time.perf_counter()
|
|
87
|
+
reactive = endpoint.simulate(data=data, strategy=strategy, symbols=["BTC"])
|
|
88
|
+
reactive_seconds = time.perf_counter() - t0
|
|
89
|
+
|
|
90
|
+
t1 = time.perf_counter()
|
|
91
|
+
replay = QuantBTEndpoint.native_event_lifecycle(initial_capital=100_000, leverage=5, use_funding=False).simulate(
|
|
92
|
+
data=data,
|
|
93
|
+
order_commands=reactive.metadata["emitted_command_tape"],
|
|
94
|
+
symbols=["BTC"],
|
|
95
|
+
)
|
|
96
|
+
replay_seconds = time.perf_counter() - t1
|
|
97
|
+
|
|
98
|
+
equity_diff = float(np.max(np.abs(reactive.equity.to_numpy() - replay.equity.to_numpy())))
|
|
99
|
+
pos_diff = float(
|
|
100
|
+
np.max(
|
|
101
|
+
np.abs(
|
|
102
|
+
reactive.positions["Position_BTC"].to_numpy()
|
|
103
|
+
- replay.positions["Position_BTC"].to_numpy()
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
report = {
|
|
108
|
+
"phase": "30E",
|
|
109
|
+
"bars": int(bars),
|
|
110
|
+
"levels": int(levels),
|
|
111
|
+
"reseed_every": int(reseed_every),
|
|
112
|
+
"emitted_commands": int(reactive.metadata["emitted_command_count"]),
|
|
113
|
+
"fills": int(len(reactive.fills)),
|
|
114
|
+
"reactive_seconds": reactive_seconds,
|
|
115
|
+
"static_replay_seconds": replay_seconds,
|
|
116
|
+
"total_seconds": reactive_seconds + replay_seconds,
|
|
117
|
+
"equity_max_abs_diff": equity_diff,
|
|
118
|
+
"position_max_abs_diff": pos_diff,
|
|
119
|
+
"context_builder": reactive.metadata["reactive_context_builder"],
|
|
120
|
+
"incremental_compile_replays": reactive.metadata["reactive_incremental_compile_replays"],
|
|
121
|
+
"final_equity": float(reactive.equity.iloc[-1]),
|
|
122
|
+
}
|
|
123
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
124
|
+
json_path = out_dir / "phase30e_reactive_runner.json"
|
|
125
|
+
md_path = out_dir / "phase30e_reactive_runner.md"
|
|
126
|
+
json_path.write_text(json.dumps(report, indent=2, sort_keys=True), encoding="utf-8")
|
|
127
|
+
md_path.write_text(
|
|
128
|
+
"\n".join(
|
|
129
|
+
[
|
|
130
|
+
"# Phase 30E Reactive Runner Benchmark",
|
|
131
|
+
"",
|
|
132
|
+
f"- Bars: {bars:,}",
|
|
133
|
+
f"- Grid levels: {levels}",
|
|
134
|
+
f"- Emitted commands: {report['emitted_commands']:,}",
|
|
135
|
+
f"- Fills: {report['fills']:,}",
|
|
136
|
+
f"- Reactive runner seconds: {reactive_seconds:.6f}",
|
|
137
|
+
f"- Static replay seconds: {replay_seconds:.6f}",
|
|
138
|
+
f"- Max equity diff: {equity_diff:.12f}",
|
|
139
|
+
f"- Max position diff: {pos_diff:.12f}",
|
|
140
|
+
f"- Context builder: {report['context_builder']}",
|
|
141
|
+
"",
|
|
142
|
+
"Final accounting is still produced by one static native-event v2 replay.",
|
|
143
|
+
]
|
|
144
|
+
),
|
|
145
|
+
encoding="utf-8",
|
|
146
|
+
)
|
|
147
|
+
return report
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def main() -> None:
|
|
151
|
+
parser = argparse.ArgumentParser()
|
|
152
|
+
parser.add_argument("--bars", type=int, default=25_000)
|
|
153
|
+
parser.add_argument("--levels", type=int, default=20)
|
|
154
|
+
parser.add_argument("--reseed-every", type=int, default=50)
|
|
155
|
+
parser.add_argument("--out-dir", type=Path, default=Path("benchmarks/out"))
|
|
156
|
+
args = parser.parse_args()
|
|
157
|
+
print(json.dumps(run(bars=args.bars, levels=args.levels, reseed_every=args.reseed_every, out_dir=args.out_dir), indent=2))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == "__main__":
|
|
161
|
+
main()
|