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,441 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Phase 31D intrabar execution benchmark and certification summary.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import gc
|
|
10
|
+
import json
|
|
11
|
+
import statistics
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
from dataclasses import asdict, dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Dict, List
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import pandas as pd
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
PACKAGE_DIR = Path(__file__).resolve().parents[1]
|
|
23
|
+
PROJECT_DIR = PACKAGE_DIR.parent
|
|
24
|
+
if str(PROJECT_DIR) not in sys.path:
|
|
25
|
+
sys.path.insert(0, str(PROJECT_DIR))
|
|
26
|
+
|
|
27
|
+
from quantbt import ( # noqa: E402
|
|
28
|
+
AccountConfig,
|
|
29
|
+
BacktestEngineV2,
|
|
30
|
+
ExecutionContract,
|
|
31
|
+
FillReplayTape,
|
|
32
|
+
IntrabarIntentTape,
|
|
33
|
+
IntrabarSessionTape,
|
|
34
|
+
OrderIntent,
|
|
35
|
+
OrderSide,
|
|
36
|
+
OrderType,
|
|
37
|
+
SessionExecutionPolicy,
|
|
38
|
+
prepare_market_tape,
|
|
39
|
+
run_fill_replay_kernel,
|
|
40
|
+
run_intrabar_kernel,
|
|
41
|
+
run_intrabar_reference,
|
|
42
|
+
run_intrabar_session_kernel,
|
|
43
|
+
)
|
|
44
|
+
from quantbt.core.vectorized import _engine_units_v2 # noqa: E402
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class Phase31BenchmarkRecord:
|
|
49
|
+
route: str
|
|
50
|
+
rows: int
|
|
51
|
+
symbols: int
|
|
52
|
+
fills_or_orders: int
|
|
53
|
+
warmup_seconds: float
|
|
54
|
+
runtime_seconds: float
|
|
55
|
+
runtime_min_seconds: float
|
|
56
|
+
runtime_max_seconds: float
|
|
57
|
+
bars_per_second: float
|
|
58
|
+
ratio_vs_close_target: float | None = None
|
|
59
|
+
ratio_vs_intrabar_minimal: float | None = None
|
|
60
|
+
speedup_vs_reference: float | None = None
|
|
61
|
+
parity: str = "n/a"
|
|
62
|
+
notes: str = ""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def run_benchmark(*, rows: int = 25_000, repeats: int = 3, seed: int = 31) -> Dict:
|
|
66
|
+
df, intent = _make_intrabar_fixture(rows=rows, seed=seed)
|
|
67
|
+
tape = prepare_market_tape(data=df, symbols=["BTC"], use_funding=False)
|
|
68
|
+
account = AccountConfig(initial_capital=100_000.0, leverage=10.0)
|
|
69
|
+
contract = ExecutionContract.intrabar_bracket(close_on_last_bar=True)
|
|
70
|
+
|
|
71
|
+
records: list[Phase31BenchmarkRecord] = []
|
|
72
|
+
close_stats = _measure(lambda: _run_close_target_kernel(tape, intent, account), repeats=repeats)
|
|
73
|
+
records.append(_record("close_target_v2_pure_kernel", rows, 1, 0, close_stats, baseline=close_stats["best"], parity="baseline"))
|
|
74
|
+
|
|
75
|
+
minimal_stats = _measure(
|
|
76
|
+
lambda: run_intrabar_kernel(tape=tape, intent=intent, account=account, contract=contract, report_level="minimal"),
|
|
77
|
+
repeats=repeats,
|
|
78
|
+
)
|
|
79
|
+
minimal_result = run_intrabar_kernel(tape=tape, intent=intent, account=account, contract=contract, report_level="minimal")
|
|
80
|
+
records.append(
|
|
81
|
+
_record(
|
|
82
|
+
"intrabar_bracket_v1_minimal",
|
|
83
|
+
rows,
|
|
84
|
+
1,
|
|
85
|
+
minimal_result.fill_count,
|
|
86
|
+
minimal_stats,
|
|
87
|
+
baseline=close_stats["best"],
|
|
88
|
+
parity="oracle_checked_in_tests",
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
audit_stats = _measure(
|
|
93
|
+
lambda: run_intrabar_kernel(tape=tape, intent=intent, account=account, contract=contract, report_level="audit"),
|
|
94
|
+
repeats=repeats,
|
|
95
|
+
)
|
|
96
|
+
audit_result = run_intrabar_kernel(tape=tape, intent=intent, account=account, contract=contract, report_level="audit")
|
|
97
|
+
records.append(
|
|
98
|
+
_record(
|
|
99
|
+
"intrabar_bracket_v1_audit",
|
|
100
|
+
rows,
|
|
101
|
+
1,
|
|
102
|
+
audit_result.fill_count,
|
|
103
|
+
audit_stats,
|
|
104
|
+
baseline=close_stats["best"],
|
|
105
|
+
intrabar_minimal=minimal_stats["best"],
|
|
106
|
+
parity="pass" if np.allclose(audit_result.equity, minimal_result.equity, atol=1e-9, rtol=0.0) else "fail",
|
|
107
|
+
notes="two_pass_sparse_fills",
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
session_tape = IntrabarSessionTape(
|
|
112
|
+
session_id=np.arange(rows, dtype=np.int64) // 24,
|
|
113
|
+
entry_allowed_at_open=np.ones(rows, dtype=np.bool_),
|
|
114
|
+
force_flat_at_open=(np.arange(rows, dtype=np.int64) % 24) == 23,
|
|
115
|
+
)
|
|
116
|
+
session_policy = SessionExecutionPolicy(max_long_entries_per_session=2)
|
|
117
|
+
session_minimal_stats = _measure(
|
|
118
|
+
lambda: run_intrabar_session_kernel(
|
|
119
|
+
tape=tape,
|
|
120
|
+
intent=intent,
|
|
121
|
+
account=account,
|
|
122
|
+
contract=contract,
|
|
123
|
+
session_policy=session_policy,
|
|
124
|
+
session_tape=session_tape,
|
|
125
|
+
report_level="minimal",
|
|
126
|
+
),
|
|
127
|
+
repeats=repeats,
|
|
128
|
+
)
|
|
129
|
+
session_minimal = run_intrabar_session_kernel(
|
|
130
|
+
tape=tape,
|
|
131
|
+
intent=intent,
|
|
132
|
+
account=account,
|
|
133
|
+
contract=contract,
|
|
134
|
+
session_policy=session_policy,
|
|
135
|
+
session_tape=session_tape,
|
|
136
|
+
report_level="minimal",
|
|
137
|
+
)
|
|
138
|
+
records.append(
|
|
139
|
+
_record(
|
|
140
|
+
"intrabar_session_bracket_v1_minimal",
|
|
141
|
+
rows,
|
|
142
|
+
1,
|
|
143
|
+
session_minimal.fill_count,
|
|
144
|
+
session_minimal_stats,
|
|
145
|
+
baseline=close_stats["best"],
|
|
146
|
+
intrabar_minimal=minimal_stats["best"],
|
|
147
|
+
parity="reference_checked_in_tests",
|
|
148
|
+
notes="session_state_kernel",
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
session_audit_stats = _measure(
|
|
153
|
+
lambda: run_intrabar_session_kernel(
|
|
154
|
+
tape=tape,
|
|
155
|
+
intent=intent,
|
|
156
|
+
account=account,
|
|
157
|
+
contract=contract,
|
|
158
|
+
session_policy=session_policy,
|
|
159
|
+
session_tape=session_tape,
|
|
160
|
+
report_level="audit",
|
|
161
|
+
),
|
|
162
|
+
repeats=repeats,
|
|
163
|
+
)
|
|
164
|
+
session_audit = run_intrabar_session_kernel(
|
|
165
|
+
tape=tape,
|
|
166
|
+
intent=intent,
|
|
167
|
+
account=account,
|
|
168
|
+
contract=contract,
|
|
169
|
+
session_policy=session_policy,
|
|
170
|
+
session_tape=session_tape,
|
|
171
|
+
report_level="audit",
|
|
172
|
+
)
|
|
173
|
+
records.append(
|
|
174
|
+
_record(
|
|
175
|
+
"intrabar_session_bracket_v1_audit",
|
|
176
|
+
rows,
|
|
177
|
+
1,
|
|
178
|
+
session_audit.fill_count,
|
|
179
|
+
session_audit_stats,
|
|
180
|
+
baseline=close_stats["best"],
|
|
181
|
+
intrabar_minimal=minimal_stats["best"],
|
|
182
|
+
parity="pass" if np.allclose(session_audit.equity, session_minimal.equity, atol=1e-9, rtol=0.0) else "fail",
|
|
183
|
+
notes="session_two_pass_sparse_fills",
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
reference_stats = _measure(
|
|
188
|
+
lambda: run_intrabar_reference(tape=tape, intent=intent, account=account, contract=contract),
|
|
189
|
+
repeats=max(1, min(2, repeats)),
|
|
190
|
+
)
|
|
191
|
+
records.append(
|
|
192
|
+
_record(
|
|
193
|
+
"intrabar_reference_python",
|
|
194
|
+
rows,
|
|
195
|
+
1,
|
|
196
|
+
audit_result.fill_count,
|
|
197
|
+
reference_stats,
|
|
198
|
+
baseline=close_stats["best"],
|
|
199
|
+
intrabar_minimal=minimal_stats["best"],
|
|
200
|
+
parity="truth_model",
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
fill_tape = FillReplayTape.from_frame(audit_result.fills_report)
|
|
205
|
+
fill_replay_stats = _measure(
|
|
206
|
+
lambda: run_fill_replay_kernel(tape=tape, fill_tape=fill_tape, account=account),
|
|
207
|
+
repeats=repeats,
|
|
208
|
+
)
|
|
209
|
+
records.append(
|
|
210
|
+
_record(
|
|
211
|
+
"fill_replay_v1_kernel",
|
|
212
|
+
rows,
|
|
213
|
+
1,
|
|
214
|
+
len(fill_tape.bar_index),
|
|
215
|
+
fill_replay_stats,
|
|
216
|
+
baseline=close_stats["best"],
|
|
217
|
+
intrabar_minimal=minimal_stats["best"],
|
|
218
|
+
parity="accounting_only",
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
native_event_stats = _measure(lambda: _run_native_event_orders(df, audit_result.fills_report), repeats=max(1, min(2, repeats)))
|
|
223
|
+
records.append(
|
|
224
|
+
_record(
|
|
225
|
+
"native_event_explicit_orders_facade",
|
|
226
|
+
rows,
|
|
227
|
+
1,
|
|
228
|
+
int(len(audit_result.fills_report)),
|
|
229
|
+
native_event_stats,
|
|
230
|
+
baseline=close_stats["best"],
|
|
231
|
+
intrabar_minimal=minimal_stats["best"],
|
|
232
|
+
parity="speed_reference_not_semantic_claim",
|
|
233
|
+
notes="full_facade_order_replay",
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
reference = next(r for r in records if r.route == "intrabar_reference_python")
|
|
238
|
+
for record in records:
|
|
239
|
+
if record.route.startswith("intrabar_bracket_v1") or record.route.startswith("intrabar_session_bracket_v1"):
|
|
240
|
+
record.speedup_vs_reference = reference.runtime_seconds / record.runtime_seconds
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
"rows": rows,
|
|
244
|
+
"repeats": repeats,
|
|
245
|
+
"seed": seed,
|
|
246
|
+
"records": [asdict(record) for record in records],
|
|
247
|
+
"summary": _summary(records),
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def make_markdown(report: Dict) -> str:
|
|
252
|
+
lines = [
|
|
253
|
+
"# Phase 31 Intrabar Benchmark",
|
|
254
|
+
"",
|
|
255
|
+
f"- Rows: `{report['rows']}`",
|
|
256
|
+
f"- Repeats: `{report['repeats']}`",
|
|
257
|
+
f"- Seed: `{report['seed']}`",
|
|
258
|
+
"",
|
|
259
|
+
"| Route | Runtime | Bars/s | Ratio vs close-target | Ratio vs intrabar minimal | Speedup vs Python oracle | Fills/orders | Parity | Notes |",
|
|
260
|
+
"|---|---:|---:|---:|---:|---:|---:|---|---|",
|
|
261
|
+
]
|
|
262
|
+
for record in report["records"]:
|
|
263
|
+
lines.append(
|
|
264
|
+
"| `{route}` | {runtime:.6f}s | {bps:,.0f} | {rclose} | {rmin} | {speedup} | {fills} | {parity} | {notes} |".format(
|
|
265
|
+
route=record["route"],
|
|
266
|
+
runtime=record["runtime_seconds"],
|
|
267
|
+
bps=record["bars_per_second"],
|
|
268
|
+
rclose=_fmt_ratio(record["ratio_vs_close_target"]),
|
|
269
|
+
rmin=_fmt_ratio(record["ratio_vs_intrabar_minimal"]),
|
|
270
|
+
speedup=_fmt_ratio(record["speedup_vs_reference"]),
|
|
271
|
+
fills=record["fills_or_orders"],
|
|
272
|
+
parity=record["parity"],
|
|
273
|
+
notes=record["notes"] or "",
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
lines.extend(
|
|
277
|
+
[
|
|
278
|
+
"",
|
|
279
|
+
"## Summary",
|
|
280
|
+
"",
|
|
281
|
+
f"- Fast intrabar minimal vs Python oracle: `{_fmt_ratio(report['summary']['intrabar_minimal_speedup_vs_reference'])}` faster.",
|
|
282
|
+
f"- Fast intrabar audit vs minimal: `{_fmt_ratio(report['summary']['intrabar_audit_ratio_vs_minimal'])}` runtime ratio.",
|
|
283
|
+
f"- Fast intrabar minimal vs close-target pure kernel: `{_fmt_ratio(report['summary']['intrabar_minimal_ratio_vs_close_target'])}` runtime ratio.",
|
|
284
|
+
"",
|
|
285
|
+
"Interpretation: close-target remains the fastest narrow contract. The new intrabar kernel is the fast path for alpha logic that needs next-open entry, intrabar SL/TP/trailing, and audit fills without falling back to Python event loops.",
|
|
286
|
+
]
|
|
287
|
+
)
|
|
288
|
+
return "\n".join(lines) + "\n"
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _make_intrabar_fixture(*, rows: int, seed: int):
|
|
292
|
+
rng = np.random.default_rng(seed)
|
|
293
|
+
idx = pd.date_range("2020-01-01", periods=rows, freq="1h", tz="UTC")
|
|
294
|
+
ret = rng.normal(0.0, 0.0015, size=rows)
|
|
295
|
+
close = 100.0 * np.exp(np.cumsum(ret))
|
|
296
|
+
open_ = np.r_[close[0], close[:-1] * (1.0 + rng.normal(0.0, 0.0002, size=rows - 1))]
|
|
297
|
+
high = np.maximum(open_, close) * (1.0 + rng.uniform(0.0005, 0.006, size=rows))
|
|
298
|
+
low = np.minimum(open_, close) * (1.0 - rng.uniform(0.0005, 0.006, size=rows))
|
|
299
|
+
df = pd.DataFrame({"open": open_, "high": high, "low": low, "close": close, "volume": 100.0}, index=idx)
|
|
300
|
+
entry_side = np.zeros(rows, dtype=np.int8)
|
|
301
|
+
entry_size = np.zeros(rows, dtype=np.float64)
|
|
302
|
+
entry_side[5::50] = 1
|
|
303
|
+
entry_size[5::50] = 1.0
|
|
304
|
+
entry_side[30::50] = -1
|
|
305
|
+
entry_size[30::50] = 1.0
|
|
306
|
+
stop = np.full(rows, 0.012, dtype=np.float64)
|
|
307
|
+
tp = np.full(rows, 0.018, dtype=np.float64)
|
|
308
|
+
trailing = np.full(rows, 0.010, dtype=np.float64)
|
|
309
|
+
technical_exit = np.zeros(rows, dtype=np.bool_)
|
|
310
|
+
technical_exit[45::50] = True
|
|
311
|
+
intent = IntrabarIntentTape.from_arrays(
|
|
312
|
+
entry_side=entry_side,
|
|
313
|
+
entry_size=entry_size,
|
|
314
|
+
stop_value=stop,
|
|
315
|
+
take_profit_value=tp,
|
|
316
|
+
trailing_value=trailing,
|
|
317
|
+
technical_exit=technical_exit,
|
|
318
|
+
)
|
|
319
|
+
return df, intent
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _run_close_target_kernel(tape, intent, account):
|
|
323
|
+
target = np.zeros((tape.n_bars, 1), dtype=np.float64)
|
|
324
|
+
current = 0.0
|
|
325
|
+
for i in range(tape.n_bars):
|
|
326
|
+
if intent.entry_side[i] != 0 and intent.entry_size[i] > 0.0:
|
|
327
|
+
current = float(intent.entry_side[i]) * float(intent.entry_size[i])
|
|
328
|
+
target[i, 0] = current
|
|
329
|
+
return _engine_units_v2(
|
|
330
|
+
tape.n_bars,
|
|
331
|
+
1,
|
|
332
|
+
tape.highs,
|
|
333
|
+
tape.lows,
|
|
334
|
+
tape.closes,
|
|
335
|
+
target,
|
|
336
|
+
tape.funding_rates,
|
|
337
|
+
tape.funding_event_mask,
|
|
338
|
+
account.initial_capital,
|
|
339
|
+
np.array([account.leverage], dtype=np.float64),
|
|
340
|
+
account.maintenance_ratio,
|
|
341
|
+
np.array([0.0], dtype=np.float64),
|
|
342
|
+
np.array([1.0], dtype=np.float64),
|
|
343
|
+
0.0,
|
|
344
|
+
False,
|
|
345
|
+
)[0][-1]
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _run_native_event_orders(df: pd.DataFrame, fills: pd.DataFrame):
|
|
349
|
+
orders = []
|
|
350
|
+
idx = df.index
|
|
351
|
+
for row in fills.itertuples(index=False):
|
|
352
|
+
bar = int(row.bar_index)
|
|
353
|
+
side = OrderSide.BUY if int(row.side) > 0 else OrderSide.SELL
|
|
354
|
+
orders.append(OrderIntent(idx[bar], "BTC", side, OrderType.MARKET, qty=float(row.qty)))
|
|
355
|
+
engine = BacktestEngineV2(
|
|
356
|
+
data=df,
|
|
357
|
+
symbols=["BTC"],
|
|
358
|
+
backend="native_event",
|
|
359
|
+
orders=orders,
|
|
360
|
+
account=AccountConfig(initial_capital=100_000.0, leverage=10.0),
|
|
361
|
+
use_funding=False,
|
|
362
|
+
fee_rate=0.0,
|
|
363
|
+
)
|
|
364
|
+
return engine.result.equity.iloc[-1]
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _measure(workload, *, repeats: int) -> Dict[str, float]:
|
|
368
|
+
gc.collect()
|
|
369
|
+
start = time.perf_counter()
|
|
370
|
+
workload()
|
|
371
|
+
warmup = time.perf_counter() - start
|
|
372
|
+
runtimes = []
|
|
373
|
+
for _ in range(max(1, repeats)):
|
|
374
|
+
gc.collect()
|
|
375
|
+
start = time.perf_counter()
|
|
376
|
+
workload()
|
|
377
|
+
runtimes.append(time.perf_counter() - start)
|
|
378
|
+
return {
|
|
379
|
+
"best": float(min(runtimes)),
|
|
380
|
+
"worst": float(max(runtimes)),
|
|
381
|
+
"median": float(statistics.median(runtimes)),
|
|
382
|
+
"warmup": float(warmup),
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _record(route, rows, symbols, fills, stats, *, baseline, intrabar_minimal=None, parity="n/a", notes=""):
|
|
387
|
+
runtime = stats["best"]
|
|
388
|
+
return Phase31BenchmarkRecord(
|
|
389
|
+
route=route,
|
|
390
|
+
rows=rows,
|
|
391
|
+
symbols=symbols,
|
|
392
|
+
fills_or_orders=int(fills),
|
|
393
|
+
warmup_seconds=float(stats["warmup"]),
|
|
394
|
+
runtime_seconds=float(runtime),
|
|
395
|
+
runtime_min_seconds=float(stats["best"]),
|
|
396
|
+
runtime_max_seconds=float(stats["worst"]),
|
|
397
|
+
bars_per_second=float(rows / runtime) if runtime > 0 else float("inf"),
|
|
398
|
+
ratio_vs_close_target=float(runtime / baseline) if baseline and runtime else None,
|
|
399
|
+
ratio_vs_intrabar_minimal=float(runtime / intrabar_minimal) if intrabar_minimal and runtime else None,
|
|
400
|
+
parity=parity,
|
|
401
|
+
notes=notes,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _summary(records: List[Phase31BenchmarkRecord]) -> Dict:
|
|
406
|
+
lookup = {record.route: record for record in records}
|
|
407
|
+
minimal = lookup["intrabar_bracket_v1_minimal"]
|
|
408
|
+
audit = lookup["intrabar_bracket_v1_audit"]
|
|
409
|
+
reference = lookup["intrabar_reference_python"]
|
|
410
|
+
close_target = lookup["close_target_v2_pure_kernel"]
|
|
411
|
+
return {
|
|
412
|
+
"intrabar_minimal_speedup_vs_reference": reference.runtime_seconds / minimal.runtime_seconds,
|
|
413
|
+
"intrabar_audit_ratio_vs_minimal": audit.runtime_seconds / minimal.runtime_seconds,
|
|
414
|
+
"intrabar_minimal_ratio_vs_close_target": minimal.runtime_seconds / close_target.runtime_seconds,
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _fmt_ratio(value) -> str:
|
|
419
|
+
if value is None:
|
|
420
|
+
return "-"
|
|
421
|
+
return f"{float(value):.2f}x"
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def main(argv=None) -> int:
|
|
425
|
+
parser = argparse.ArgumentParser(description="Run Phase 31 intrabar benchmark.")
|
|
426
|
+
parser.add_argument("--rows", type=int, default=25_000)
|
|
427
|
+
parser.add_argument("--repeats", type=int, default=3)
|
|
428
|
+
parser.add_argument("--seed", type=int, default=31)
|
|
429
|
+
parser.add_argument("--json-out", type=Path, default=PACKAGE_DIR / "benchmarks" / "phase31_intrabar_benchmark.json")
|
|
430
|
+
parser.add_argument("--md-out", type=Path, default=PACKAGE_DIR / "benchmarks" / "phase31_intrabar_benchmark.md")
|
|
431
|
+
args = parser.parse_args(argv)
|
|
432
|
+
|
|
433
|
+
report = run_benchmark(rows=args.rows, repeats=args.repeats, seed=args.seed)
|
|
434
|
+
args.json_out.write_text(json.dumps(report, indent=2), encoding="utf-8")
|
|
435
|
+
args.md_out.write_text(make_markdown(report), encoding="utf-8")
|
|
436
|
+
print(make_markdown(report))
|
|
437
|
+
return 0
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
if __name__ == "__main__":
|
|
441
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import resource
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
from quantbt import AccountConfig, ExecutionConfig, NativeEventBackend, NativeEventConfig
|
|
15
|
+
from quantbt.core.orders import OrderAction, OrderCommand
|
|
16
|
+
from quantbt.core.schema import OrderSide, OrderType, TimeInForce
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _rss_mb() -> float:
|
|
20
|
+
value = float(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss)
|
|
21
|
+
if sys.platform == "darwin":
|
|
22
|
+
return value / (1024.0 * 1024.0)
|
|
23
|
+
return value / 1024.0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _market(rows: int):
|
|
27
|
+
idx = pd.date_range("2020-01-01", periods=rows, freq="15min", tz="UTC")
|
|
28
|
+
x = np.arange(rows, dtype=np.float64)
|
|
29
|
+
close = pd.Series(100.0 + np.sin(x / 17.0) * 2.0 + x * 0.0001, index=idx)
|
|
30
|
+
high = close + 1.2
|
|
31
|
+
low = close - 1.2
|
|
32
|
+
return idx, {"BTC": close}, {"BTC": high}, {"BTC": low}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _commands(idx: pd.DatetimeIndex, levels: int, cycle: int):
|
|
36
|
+
commands = []
|
|
37
|
+
order_id = 0
|
|
38
|
+
for bar in range(1, len(idx), cycle):
|
|
39
|
+
commands.append(OrderCommand(timestamp=idx[bar], action=OrderAction.CANCEL_ALL, symbol="BTC"))
|
|
40
|
+
anchor = 100.0 + np.sin(bar / 17.0) * 2.0 + bar * 0.0001
|
|
41
|
+
for level in range(1, levels + 1):
|
|
42
|
+
commands.append(
|
|
43
|
+
OrderCommand(
|
|
44
|
+
timestamp=idx[bar],
|
|
45
|
+
symbol="BTC",
|
|
46
|
+
side=OrderSide.BUY,
|
|
47
|
+
order_type=OrderType.LIMIT,
|
|
48
|
+
qty=0.01,
|
|
49
|
+
price=float(anchor - 0.08 * level),
|
|
50
|
+
tif=TimeInForce.GTC,
|
|
51
|
+
order_id=f"entry-{order_id}",
|
|
52
|
+
tag=f"GRID-C{bar}-L{level}",
|
|
53
|
+
metadata={"campaign_id": f"C{bar}", "level_id": str(level)},
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
order_id += 1
|
|
57
|
+
commands.append(
|
|
58
|
+
OrderCommand(
|
|
59
|
+
timestamp=idx[bar],
|
|
60
|
+
symbol="BTC",
|
|
61
|
+
side=OrderSide.SELL,
|
|
62
|
+
order_type=OrderType.LIMIT,
|
|
63
|
+
qty=0.01,
|
|
64
|
+
price=float(anchor + 0.08 * level),
|
|
65
|
+
tif=TimeInForce.GTC,
|
|
66
|
+
reduce_only=True,
|
|
67
|
+
order_id=f"exit-{order_id}",
|
|
68
|
+
tag=f"GRID-C{bar}-X{level}",
|
|
69
|
+
metadata={"campaign_id": f"C{bar}", "level_id": str(level), "leg_role": "take_profit"},
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
order_id += 1
|
|
73
|
+
return tuple(commands)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _run_child(args) -> dict:
|
|
77
|
+
idx, close, high, low = _market(args.rows)
|
|
78
|
+
commands = _commands(idx, levels=args.levels, cycle=args.cycle)
|
|
79
|
+
backend = NativeEventBackend(
|
|
80
|
+
NativeEventConfig(
|
|
81
|
+
account=AccountConfig(initial_capital=100_000.0, leverage=10.0),
|
|
82
|
+
execution=ExecutionConfig(slippage_bps=0.0),
|
|
83
|
+
fee_rate=0.0,
|
|
84
|
+
use_funding=False,
|
|
85
|
+
report_level=args.report_level,
|
|
86
|
+
audit_sink=args.audit_sink,
|
|
87
|
+
audit_sink_path=args.audit_sink_path,
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
start = time.perf_counter()
|
|
91
|
+
result = backend.run_order_commands(idx, commands, close, high, low, symbols=["BTC"])
|
|
92
|
+
elapsed = time.perf_counter() - start
|
|
93
|
+
payload = {
|
|
94
|
+
"report_level": result.metadata["report_level"],
|
|
95
|
+
"audit_sink": result.metadata["audit_sink"],
|
|
96
|
+
"rows": int(args.rows),
|
|
97
|
+
"levels": int(args.levels),
|
|
98
|
+
"commands": int(len(commands)),
|
|
99
|
+
"fills": int(result.metadata["lifecycle_counters"]["fill_count"]),
|
|
100
|
+
"events": int(result.metadata["lifecycle_counters"]["event_count"]),
|
|
101
|
+
"seconds": float(elapsed),
|
|
102
|
+
"peak_rss_mb": float(_rss_mb()),
|
|
103
|
+
"command_report_rows": int(len(result.metadata["command_report"])),
|
|
104
|
+
"order_event_rows": int(len(result.metadata["order_events"])),
|
|
105
|
+
"fills_materialized": int(len(result.fills)),
|
|
106
|
+
"orders_materialized": int(len(result.orders)),
|
|
107
|
+
"final_equity": float(result.equity.iloc[-1]),
|
|
108
|
+
}
|
|
109
|
+
print(json.dumps(payload, sort_keys=True))
|
|
110
|
+
return payload
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _run_parent(args) -> list[dict]:
|
|
114
|
+
rows = []
|
|
115
|
+
for level in ("minimal", "standard", "audit"):
|
|
116
|
+
cmd = [
|
|
117
|
+
sys.executable,
|
|
118
|
+
__file__,
|
|
119
|
+
"--child",
|
|
120
|
+
"--rows",
|
|
121
|
+
str(args.rows),
|
|
122
|
+
"--levels",
|
|
123
|
+
str(args.levels),
|
|
124
|
+
"--cycle",
|
|
125
|
+
str(args.cycle),
|
|
126
|
+
"--report-level",
|
|
127
|
+
level,
|
|
128
|
+
]
|
|
129
|
+
completed = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
130
|
+
rows.append(json.loads(completed.stdout.strip().splitlines()[-1]))
|
|
131
|
+
if args.json_out:
|
|
132
|
+
Path(args.json_out).write_text(json.dumps(rows, indent=2, sort_keys=True) + "\n")
|
|
133
|
+
if args.md_out:
|
|
134
|
+
lines = [
|
|
135
|
+
"# Phase 34A Native Event Artifact Memory Benchmark",
|
|
136
|
+
"",
|
|
137
|
+
"| report_level | seconds | peak RSS MB | commands | fills | events | command rows | event rows | fills obj | orders obj |",
|
|
138
|
+
"|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|",
|
|
139
|
+
]
|
|
140
|
+
for row in rows:
|
|
141
|
+
lines.append(
|
|
142
|
+
"| {report_level} | {seconds:.6f} | {peak_rss_mb:.3f} | {commands} | {fills} | {events} | "
|
|
143
|
+
"{command_report_rows} | {order_event_rows} | {fills_materialized} | {orders_materialized} |".format(**row)
|
|
144
|
+
)
|
|
145
|
+
lines.extend(
|
|
146
|
+
[
|
|
147
|
+
"",
|
|
148
|
+
"Notes:",
|
|
149
|
+
"",
|
|
150
|
+
"- Each row runs in a fresh subprocess.",
|
|
151
|
+
"- Peak RSS includes Python import, pandas, and Numba/cache overhead; on small workloads it is not expected to be monotonic by artifact level.",
|
|
152
|
+
"- The artifact contract is verified by command/event row counts and materialized Python object counts; larger command-heavy runs are needed for stable RSS deltas.",
|
|
153
|
+
]
|
|
154
|
+
)
|
|
155
|
+
Path(args.md_out).write_text("\n".join(lines) + "\n")
|
|
156
|
+
print(json.dumps(rows, indent=2, sort_keys=True))
|
|
157
|
+
return rows
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def main() -> None:
|
|
161
|
+
parser = argparse.ArgumentParser()
|
|
162
|
+
parser.add_argument("--child", action="store_true")
|
|
163
|
+
parser.add_argument("--rows", type=int, default=5_000)
|
|
164
|
+
parser.add_argument("--levels", type=int, default=15)
|
|
165
|
+
parser.add_argument("--cycle", type=int, default=50)
|
|
166
|
+
parser.add_argument("--report-level", default="audit")
|
|
167
|
+
parser.add_argument("--audit-sink", default="memory")
|
|
168
|
+
parser.add_argument("--audit-sink-path", default=None)
|
|
169
|
+
parser.add_argument("--json-out", default="benchmarks/phase34a_native_event_memory.json")
|
|
170
|
+
parser.add_argument("--md-out", default="benchmarks/phase34a_native_event_memory.md")
|
|
171
|
+
args = parser.parse_args()
|
|
172
|
+
if args.child:
|
|
173
|
+
_run_child(args)
|
|
174
|
+
else:
|
|
175
|
+
_run_parent(args)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
main()
|