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.
Files changed (133) hide show
  1. quantbt/__init__.py +862 -0
  2. quantbt/adapters/__init__.py +5 -0
  3. quantbt/adapters/nautilus/__init__.py +42 -0
  4. quantbt/adapters/nautilus/_dependency.py +53 -0
  5. quantbt/adapters/nautilus/backend.py +639 -0
  6. quantbt/adapters/nautilus/instruments.py +275 -0
  7. quantbt/adapters/nautilus/options.py +465 -0
  8. quantbt/adapters/nautilus/reports.py +231 -0
  9. quantbt/backends/__init__.py +29 -0
  10. quantbt/backends/_native_event_rust.py +2168 -0
  11. quantbt/backends/native_event.py +5048 -0
  12. quantbt/backends/native_option.py +796 -0
  13. quantbt/backends/native_portfolio.py +928 -0
  14. quantbt/backends/native_vectorized.py +1299 -0
  15. quantbt/backtester.py +485 -0
  16. quantbt/benchmarks/__init__.py +1 -0
  17. quantbt/benchmarks/compare_phase9_parity.py +302 -0
  18. quantbt/benchmarks/gamma_scalping_backtestsample.py +802 -0
  19. quantbt/benchmarks/profile_phase7.py +415 -0
  20. quantbt/benchmarks/run_arbitrage_phase_e.py +160 -0
  21. quantbt/benchmarks/run_optimization_overhead.py +195 -0
  22. quantbt/benchmarks/run_options_engine.py +272 -0
  23. quantbt/benchmarks/run_pct_equity_nautilus_smoke.py +227 -0
  24. quantbt/benchmarks/run_phase12_arbitrage_cert.py +402 -0
  25. quantbt/benchmarks/run_phase12_benchmark_nautilus_cert.py +430 -0
  26. quantbt/benchmarks/run_phase13_portfolio_report.py +97 -0
  27. quantbt/benchmarks/run_phase13_wfo_cache.py +162 -0
  28. quantbt/benchmarks/run_phase14_service_loop.py +694 -0
  29. quantbt/benchmarks/run_phase15a_nautilus_certification.py +514 -0
  30. quantbt/benchmarks/run_phase15b_synthetic_depth.py +175 -0
  31. quantbt/benchmarks/run_phase16_performance_debt.py +389 -0
  32. quantbt/benchmarks/run_phase30e_reactive_runner.py +161 -0
  33. quantbt/benchmarks/run_phase31_intrabar.py +441 -0
  34. quantbt/benchmarks/run_phase34a_native_event_memory.py +179 -0
  35. quantbt/benchmarks/run_phase34b_native_event_prepared_score.py +173 -0
  36. quantbt/benchmarks/run_phase34c_native_event_single_pass.py +176 -0
  37. quantbt/benchmarks/run_phase7.py +654 -0
  38. quantbt/benchmarks/run_portfolio_real_parity.py +537 -0
  39. quantbt/core/__init__.py +323 -0
  40. quantbt/core/arbitrage.py +767 -0
  41. quantbt/core/basket.py +234 -0
  42. quantbt/core/certification.py +274 -0
  43. quantbt/core/constraints.py +155 -0
  44. quantbt/core/engine.py +1144 -0
  45. quantbt/core/event.py +869 -0
  46. quantbt/core/execution_contract.py +197 -0
  47. quantbt/core/execution_depth.py +613 -0
  48. quantbt/core/intrabar_kernel.py +1906 -0
  49. quantbt/core/intrabar_reference.py +907 -0
  50. quantbt/core/intrabar_session.py +156 -0
  51. quantbt/core/market_tape.py +480 -0
  52. quantbt/core/native_event_capabilities.py +121 -0
  53. quantbt/core/native_event_parity.py +336 -0
  54. quantbt/core/order_compiler.py +422 -0
  55. quantbt/core/orders.py +319 -0
  56. quantbt/core/portfolio.py +392 -0
  57. quantbt/core/preprocessor.py +256 -0
  58. quantbt/core/reactive.py +173 -0
  59. quantbt/core/results.py +341 -0
  60. quantbt/core/schema.py +211 -0
  61. quantbt/core/scopes.py +96 -0
  62. quantbt/core/structured_orders.py +378 -0
  63. quantbt/core/types.py +87 -0
  64. quantbt/core/vectorized.py +203 -0
  65. quantbt/endpoint.py +4465 -0
  66. quantbt/engines.py +1014 -0
  67. quantbt/metrics/__init__.py +45 -0
  68. quantbt/metrics/options_analytics.py +46 -0
  69. quantbt/metrics/performance.py +541 -0
  70. quantbt/optimization/__init__.py +94 -0
  71. quantbt/optimization/callbacks.py +116 -0
  72. quantbt/optimization/candidate_selection.py +397 -0
  73. quantbt/optimization/config.py +84 -0
  74. quantbt/optimization/constraints.py +22 -0
  75. quantbt/optimization/evaluator.py +21 -0
  76. quantbt/optimization/evaluators/__init__.py +32 -0
  77. quantbt/optimization/evaluators/arbitrage.py +22 -0
  78. quantbt/optimization/evaluators/generic.py +34 -0
  79. quantbt/optimization/evaluators/grid_dca.py +22 -0
  80. quantbt/optimization/evaluators/intrabar.py +54 -0
  81. quantbt/optimization/evaluators/native_event.py +51 -0
  82. quantbt/optimization/evaluators/options.py +22 -0
  83. quantbt/optimization/evaluators/portfolio.py +42 -0
  84. quantbt/optimization/evaluators/signal.py +43 -0
  85. quantbt/optimization/multiseed.py +176 -0
  86. quantbt/optimization/objectives.py +221 -0
  87. quantbt/optimization/optimizer.py +500 -0
  88. quantbt/optimization/result.py +80 -0
  89. quantbt/optimization/samplers.py +84 -0
  90. quantbt/optimization/space.py +223 -0
  91. quantbt/options/__init__.py +215 -0
  92. quantbt/options/cache.py +116 -0
  93. quantbt/options/conventions.py +158 -0
  94. quantbt/options/data.py +177 -0
  95. quantbt/options/execution.py +600 -0
  96. quantbt/options/fees.py +135 -0
  97. quantbt/options/greeks.py +173 -0
  98. quantbt/options/hedging.py +232 -0
  99. quantbt/options/iv.py +188 -0
  100. quantbt/options/ledger.py +263 -0
  101. quantbt/options/lifecycle.py +94 -0
  102. quantbt/options/margin.py +311 -0
  103. quantbt/options/packages.py +147 -0
  104. quantbt/options/pricing.py +191 -0
  105. quantbt/options/schema.py +227 -0
  106. quantbt/options/selectors.py +281 -0
  107. quantbt/options/strategy.py +249 -0
  108. quantbt/options/surface.py +142 -0
  109. quantbt/options/tape.py +226 -0
  110. quantbt/options/templates/__init__.py +33 -0
  111. quantbt/options/templates/packages.py +354 -0
  112. quantbt/portfolio.py +606 -0
  113. quantbt/py.typed +1 -0
  114. quantbt/reporting/__init__.py +38 -0
  115. quantbt/reporting/arbitrage_audit.py +211 -0
  116. quantbt/reporting/nautilus_bundle.py +758 -0
  117. quantbt/reporting/nautilus_certification.py +226 -0
  118. quantbt/reporting/nautilus_diagnostics.py +225 -0
  119. quantbt/reporting/parity.py +496 -0
  120. quantbt/reporting/portfolio_audit.py +206 -0
  121. quantbt/reporting/portfolio_nautilus.py +140 -0
  122. quantbt/sizing/__init__.py +3 -0
  123. quantbt/sizing/fast.py +67 -0
  124. quantbt/sizing/modes.py +162 -0
  125. quantbt/viz/__init__.py +4 -0
  126. quantbt/viz/plots.py +310 -0
  127. quantbt/viz/themes.py +102 -0
  128. quantbt/walkforward.py +3136 -0
  129. quantbt_engine-1.0.7.dist-info/METADATA +891 -0
  130. quantbt_engine-1.0.7.dist-info/RECORD +133 -0
  131. quantbt_engine-1.0.7.dist-info/WHEEL +5 -0
  132. quantbt_engine-1.0.7.dist-info/licenses/LICENSE +21 -0
  133. quantbt_engine-1.0.7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env python3
2
+ """Phase 32C optimization overhead and prepared-evaluator benchmark."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ from pathlib import Path
9
+ import sys
10
+ import time
11
+
12
+ import numpy as np
13
+ import pandas as pd
14
+
15
+ PACKAGE_DIR = Path(__file__).resolve().parents[1]
16
+ PROJECT_DIR = PACKAGE_DIR.parent
17
+ if str(PROJECT_DIR) not in sys.path:
18
+ sys.path.insert(0, str(PROJECT_DIR))
19
+
20
+ from quantbt import ( # noqa: E402
21
+ GenericEndpointEvaluator,
22
+ IntrabarIntentTape,
23
+ ObjectiveResult,
24
+ OptimizationConfig,
25
+ OptunaOptimizer,
26
+ PreparedSignalEvaluator,
27
+ QuantBTEndpoint,
28
+ SamplerConfig,
29
+ )
30
+
31
+
32
+ def run_benchmark(rows: int = 360, trials: int = 24, loops: int = 24) -> dict:
33
+ df = _frame(rows)
34
+ optimizer_seconds = _optimizer_overhead(trials)
35
+ normal_seconds, prepared_seconds, signal_diff = _signal_replay_benchmark(df, loops)
36
+ first_intrabar, warm_intrabar, intrabar_diff = _intrabar_compile_benchmark(df)
37
+ status = "pass" if signal_diff <= 1e-9 and intrabar_diff <= 1e-9 else "fail"
38
+ return {
39
+ "status": status,
40
+ "rows": int(rows),
41
+ "trials": int(trials),
42
+ "loops": int(loops),
43
+ "optimizer_overhead_seconds": float(optimizer_seconds),
44
+ "optimizer_overhead_per_trial_seconds": float(optimizer_seconds / max(1, trials)),
45
+ "normal_signal_replay_seconds": float(normal_seconds),
46
+ "prepared_signal_replay_seconds": float(prepared_seconds),
47
+ "prepared_signal_speedup": float(normal_seconds / prepared_seconds) if prepared_seconds > 0 else 0.0,
48
+ "signal_final_equity_diff": float(signal_diff),
49
+ "intrabar_first_run_seconds": float(first_intrabar),
50
+ "intrabar_warm_run_seconds": float(warm_intrabar),
51
+ "intrabar_compile_to_warm_ratio": float(first_intrabar / warm_intrabar) if warm_intrabar > 0 else 0.0,
52
+ "intrabar_final_equity_diff": float(intrabar_diff),
53
+ }
54
+
55
+
56
+ def make_markdown(report: dict) -> str:
57
+ return "\n".join(
58
+ [
59
+ "# Phase 32C Optimization Overhead Benchmark",
60
+ "",
61
+ f"Status: **{report['status']}**",
62
+ "",
63
+ "| Measurement | Value |",
64
+ "|---|---:|",
65
+ f"| Optimizer overhead | `{report['optimizer_overhead_seconds']:.6f}s` |",
66
+ f"| Optimizer overhead / trial | `{report['optimizer_overhead_per_trial_seconds']:.6f}s` |",
67
+ f"| Normal signal replays | `{report['normal_signal_replay_seconds']:.6f}s` |",
68
+ f"| Prepared signal replays | `{report['prepared_signal_replay_seconds']:.6f}s` |",
69
+ f"| Prepared signal speedup | `{report['prepared_signal_speedup']:.3f}x` |",
70
+ f"| Intrabar first run | `{report['intrabar_first_run_seconds']:.6f}s` |",
71
+ f"| Intrabar warm run | `{report['intrabar_warm_run_seconds']:.6f}s` |",
72
+ f"| Intrabar first/warm ratio | `{report['intrabar_compile_to_warm_ratio']:.3f}x` |",
73
+ "",
74
+ "Parity checks:",
75
+ "",
76
+ f"- Signal final equity diff: `{report['signal_final_equity_diff']}`",
77
+ f"- Intrabar final equity diff: `{report['intrabar_final_equity_diff']}`",
78
+ "",
79
+ "This benchmark measures facade/optimizer overhead, not strategy quality.",
80
+ ]
81
+ ) + "\n"
82
+
83
+
84
+ def _optimizer_overhead(trials: int) -> float:
85
+ evaluator = GenericEndpointEvaluator(
86
+ build_run_inputs=lambda params: {"value": float(params["x"])},
87
+ run_func=lambda value: value,
88
+ objective_builder=lambda result, params: ObjectiveResult.scalar(float(result), metrics={"score": float(result)}),
89
+ )
90
+ optimizer = OptunaOptimizer(
91
+ evaluator=evaluator,
92
+ config=OptimizationConfig(
93
+ study_name=f"phase32c_overhead_{time.time_ns()}",
94
+ n_trials=int(trials),
95
+ seed=42,
96
+ show_progress_bar=False,
97
+ duplicate_policy="allow",
98
+ ),
99
+ sampler_config=SamplerConfig(name="random"),
100
+ )
101
+ start = time.perf_counter()
102
+ optimizer.optimize(param_ranges={"x": (0.0, 1.0)})
103
+ return time.perf_counter() - start
104
+
105
+
106
+ def _signal_replay_benchmark(df: pd.DataFrame, loops: int):
107
+ endpoint = QuantBTEndpoint.signal_notional(
108
+ backend="native_vectorized",
109
+ initial_capital=20_000.0,
110
+ leverage=5.0,
111
+ alloc_per_trade=1_000.0,
112
+ fee_rate=0.0,
113
+ use_funding=False,
114
+ )
115
+ signal = pd.Series(np.where(df["close"].diff().fillna(0.0) > 0.0, 1.0, 0.0), index=df.index)
116
+ normal = endpoint.backtest(data=df, signal=signal, symbols=["BTC"])
117
+ prepared = endpoint.prepare_service_context(data=df, symbols=["BTC"])
118
+ prepared_result = prepared.backtest(signal=signal)
119
+ diff = abs(float(normal.equity.iloc[-1]) - float(prepared_result.equity.iloc[-1]))
120
+
121
+ start = time.perf_counter()
122
+ for _ in range(int(loops)):
123
+ endpoint.backtest(data=df, signal=signal, symbols=["BTC"])
124
+ normal_seconds = time.perf_counter() - start
125
+
126
+ evaluator = PreparedSignalEvaluator(
127
+ prepared_context=prepared,
128
+ strategy_func=lambda params: signal,
129
+ objective_builder=lambda result, params: ObjectiveResult.scalar(float(result.equity.iloc[-1])),
130
+ )
131
+ start = time.perf_counter()
132
+ for _ in range(int(loops)):
133
+ evaluator.evaluate({})
134
+ prepared_seconds = time.perf_counter() - start
135
+ return normal_seconds, prepared_seconds, diff
136
+
137
+
138
+ def _intrabar_compile_benchmark(df: pd.DataFrame):
139
+ endpoint = QuantBTEndpoint.intrabar_bracket(
140
+ initial_capital=20_000.0,
141
+ leverage=5.0,
142
+ fee_rate=0.0,
143
+ slippage_bps=0.0,
144
+ use_funding=False,
145
+ report_level="minimal",
146
+ )
147
+ runner = endpoint.prepare_intrabar(data=df, symbols=["BTC"])
148
+ entry = np.zeros(len(df))
149
+ entry[0] = 1.0
150
+ intent = IntrabarIntentTape.from_arrays(entry_side=entry, entry_size=np.abs(entry))
151
+
152
+ start = time.perf_counter()
153
+ first = runner.run(intent, report_level="minimal")
154
+ first_seconds = time.perf_counter() - start
155
+ start = time.perf_counter()
156
+ warm = runner.run(intent, report_level="minimal")
157
+ warm_seconds = time.perf_counter() - start
158
+ diff = abs(float(first.equity.iloc[-1]) - float(warm.equity.iloc[-1]))
159
+ return first_seconds, warm_seconds, diff
160
+
161
+
162
+ def _frame(rows: int) -> pd.DataFrame:
163
+ idx = pd.date_range("2024-01-01", periods=int(rows), freq="1h", tz="UTC")
164
+ x = np.linspace(0.0, 16.0, len(idx))
165
+ close = 100.0 + np.sin(x) * 2.0 + np.arange(len(idx)) * 0.01
166
+ return pd.DataFrame(
167
+ {
168
+ "open": close,
169
+ "high": close * 1.01,
170
+ "low": close * 0.99,
171
+ "close": close,
172
+ "volume": 1_000.0,
173
+ },
174
+ index=idx,
175
+ )
176
+
177
+
178
+ def main() -> int:
179
+ parser = argparse.ArgumentParser()
180
+ parser.add_argument("--rows", type=int, default=360)
181
+ parser.add_argument("--trials", type=int, default=24)
182
+ parser.add_argument("--loops", type=int, default=24)
183
+ parser.add_argument("--json", type=Path, default=PACKAGE_DIR / "benchmarks" / "results" / "optimization_overhead.json")
184
+ parser.add_argument("--markdown", type=Path, default=PACKAGE_DIR / "benchmarks" / "results" / "optimization_overhead.md")
185
+ args = parser.parse_args()
186
+ report = run_benchmark(rows=args.rows, trials=args.trials, loops=args.loops)
187
+ args.json.parent.mkdir(parents=True, exist_ok=True)
188
+ args.json.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n")
189
+ args.markdown.write_text(make_markdown(report))
190
+ print(json.dumps(report, indent=2, sort_keys=True))
191
+ return 0 if report["status"] == "pass" else 1
192
+
193
+
194
+ if __name__ == "__main__":
195
+ raise SystemExit(main())
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env python3
2
+ """Phase 10 options-engine benchmark and parity guard."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import sys
9
+ import time
10
+ import tracemalloc
11
+ from pathlib import Path
12
+ from typing import Dict, Sequence
13
+
14
+ import numpy as np
15
+ import pandas as pd
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 import ( # noqa: E402
23
+ ExerciseStyle,
24
+ NativeOptionBackend,
25
+ NativeOptionConfig,
26
+ OptionInstrumentRegistry,
27
+ OptionInstrumentSpec,
28
+ OptionKind,
29
+ OptionPackageIntent,
30
+ OptionPackageLeg,
31
+ OptionPreparedRunCache,
32
+ OrderSide,
33
+ PremiumConvention,
34
+ SettlementStyle,
35
+ )
36
+
37
+
38
+ def run_benchmark(*, snapshots: int, contracts: int, packages: int, repeats: int, seed: int) -> Dict:
39
+ rng = np.random.default_rng(seed)
40
+ registry = _registry(contracts)
41
+ chain = _chain(registry, snapshots=snapshots, rng=rng)
42
+ package_list = _packages(registry, chain, packages=packages)
43
+ config = NativeOptionConfig(initial_balances={"USD": 100_000.0}, reporting_currency="USD", random_seed=seed)
44
+ backend = NativeOptionBackend(config)
45
+
46
+ uncached = backend.run(chain=chain, instruments=registry, packages=package_list)
47
+ cache = OptionPreparedRunCache.from_chain(chain, registry)
48
+ cached = backend.run(chain=chain, instruments=registry, packages=package_list, prepared_cache=cache)
49
+ parity = _parity(uncached, cached)
50
+
51
+ uncached_seconds = _timeit(lambda: backend.run(chain=chain, instruments=registry, packages=package_list), repeats)
52
+ cached_seconds = _timeit(lambda: backend.run(chain=chain, instruments=registry, packages=package_list, prepared_cache=cache), repeats)
53
+ peak_mb = _peak_memory_mb(lambda: backend.run(chain=chain, instruments=registry, packages=package_list, prepared_cache=cache))
54
+ return {
55
+ "phase": "options_phase10",
56
+ "status": "pass" if parity["passed"] else "fail",
57
+ "seed": int(seed),
58
+ "snapshots": int(snapshots),
59
+ "contracts": int(contracts),
60
+ "quotes": int(len(chain)),
61
+ "packages": int(len(package_list)),
62
+ "fills": int(len(cached.fills_report)),
63
+ "hedges": 0,
64
+ "memory_peak_mb": float(peak_mb),
65
+ "uncached_seconds": float(uncached_seconds),
66
+ "cached_seconds": float(cached_seconds),
67
+ "cache_speedup": float(uncached_seconds / cached_seconds) if cached_seconds > 0.0 else 0.0,
68
+ "package_cache_size": int(cache.package_cache_size),
69
+ "parity": parity,
70
+ "run_manifest": cached.run_manifest,
71
+ "cython_cpp_recommendation": (
72
+ "not_recommended_yet: Phase 10 benchmark still targets pandas/tape/package facade and cache reuse; "
73
+ "collect pure-kernel profile evidence before Cython/C++."
74
+ ),
75
+ }
76
+
77
+
78
+ def make_markdown(report: Dict) -> str:
79
+ lines = [
80
+ "# Options Engine Phase 10 Benchmark",
81
+ "",
82
+ f"Status: **{report['status']}**",
83
+ "",
84
+ "| metric | value |",
85
+ "| --- | ---: |",
86
+ f"| snapshots | `{report['snapshots']}` |",
87
+ f"| contracts | `{report['contracts']}` |",
88
+ f"| quotes | `{report['quotes']}` |",
89
+ f"| packages | `{report['packages']}` |",
90
+ f"| fills | `{report['fills']}` |",
91
+ f"| hedges | `{report['hedges']}` |",
92
+ f"| peak memory MB | `{report['memory_peak_mb']:.3f}` |",
93
+ f"| uncached seconds | `{report['uncached_seconds']:.6f}` |",
94
+ f"| cached seconds | `{report['cached_seconds']:.6f}` |",
95
+ f"| cache speedup | `{report['cache_speedup']:.3f}x` |",
96
+ f"| package cache size | `{report['package_cache_size']}` |",
97
+ "",
98
+ "## Parity Guard",
99
+ "",
100
+ f"- Passed: `{report['parity']['passed']}`",
101
+ f"- Final equity abs diff: `{report['parity']['final_equity_abs_diff']:.12f}`",
102
+ f"- Position max abs diff: `{report['parity']['position_max_abs_diff']:.12f}`",
103
+ f"- Fills equal: `{report['parity']['fills_equal']}`",
104
+ "",
105
+ "## Manifest",
106
+ "",
107
+ f"- Data hash: `{report['run_manifest'].get('data_hash')}`",
108
+ f"- Margin model: `{report['run_manifest'].get('margin_model')}`",
109
+ f"- Pricing model: `{report['run_manifest'].get('pricing_model')}`",
110
+ f"- Fidelity: `{report['run_manifest'].get('fidelity_manifest')}`",
111
+ "",
112
+ "## Cython / C++ Decision",
113
+ "",
114
+ report["cython_cpp_recommendation"],
115
+ "",
116
+ ]
117
+ return "\n".join(lines)
118
+
119
+
120
+ def _registry(contracts: int) -> OptionInstrumentRegistry:
121
+ expiry = int(pd.Timestamp("2026-03-01 08:00:00", tz="UTC").value)
122
+ specs = []
123
+ for i in range(contracts):
124
+ strike = 80_000.0 + 1_000.0 * i
125
+ kind = OptionKind.CALL if i % 2 == 0 else OptionKind.PUT
126
+ specs.append(
127
+ OptionInstrumentSpec(
128
+ symbol=f"BTC-O{i:04d}.TEST",
129
+ venue="test",
130
+ underlying_id="BTC-PERP.TEST",
131
+ underlying_index_id="BTC-INDEX.TEST",
132
+ option_kind=kind,
133
+ exercise_style=ExerciseStyle.EUROPEAN,
134
+ premium_convention=PremiumConvention.LINEAR_QUOTE,
135
+ settlement_style=SettlementStyle.CASH,
136
+ strike=strike,
137
+ expiry_ns=expiry,
138
+ settlement_currency="USD",
139
+ premium_currency="USD",
140
+ quote_currency="USD",
141
+ multiplier=1.0,
142
+ contract_size=1.0,
143
+ qty_step=1.0,
144
+ tick_size=0.01,
145
+ convention_version="phase10_linear_benchmark_v1",
146
+ )
147
+ )
148
+ return OptionInstrumentRegistry.from_iterable(specs)
149
+
150
+
151
+ def _chain(registry: OptionInstrumentRegistry, *, snapshots: int, rng) -> pd.DataFrame:
152
+ start = pd.Timestamp("2026-01-01 00:00:00", tz="UTC")
153
+ rows = []
154
+ for t in range(snapshots):
155
+ ts = int((start + pd.Timedelta(minutes=15 * t)).value)
156
+ index_price = 100_000.0 + 100.0 * np.sin(t / 10.0)
157
+ for code, spec in enumerate(registry.instruments):
158
+ intrinsic = max(index_price - spec.strike, 0.0) if spec.option_kind is OptionKind.CALL else max(spec.strike - index_price, 0.0)
159
+ time_value = 500.0 + 5.0 * code + float(rng.normal(0.0, 1.0))
160
+ mark = max(intrinsic + time_value, 1.0)
161
+ rows.append(
162
+ {
163
+ "timestamp_ns": ts,
164
+ "instrument_id": spec.symbol,
165
+ "venue": "TEST",
166
+ "underlying_id": spec.underlying_id,
167
+ "expiry_ns": spec.expiry_ns,
168
+ "strike": spec.strike,
169
+ "option_kind": spec.option_kind.value,
170
+ "bid_price": mark * 0.995,
171
+ "bid_size": 50.0,
172
+ "ask_price": mark * 1.005,
173
+ "ask_size": 50.0,
174
+ "mark_price": mark,
175
+ "last_price": mark,
176
+ "index_price": index_price,
177
+ "forward_price": index_price,
178
+ "mark_iv": 0.6,
179
+ "bid_iv": 0.59,
180
+ "ask_iv": 0.61,
181
+ "delta": 0.5 if spec.option_kind is OptionKind.CALL else -0.5,
182
+ "gamma": 0.0001,
183
+ "vega": 100.0,
184
+ "theta": -10.0,
185
+ "open_interest": 1000.0,
186
+ "volume": 100.0,
187
+ "quote_currency": "USD",
188
+ "settlement_currency": "USD",
189
+ "sequence_id": code,
190
+ "source_latency_ns": 1_000_000,
191
+ }
192
+ )
193
+ return pd.DataFrame(rows)
194
+
195
+
196
+ def _packages(registry: OptionInstrumentRegistry, chain: pd.DataFrame, *, packages: int) -> Sequence[OptionPackageIntent]:
197
+ timestamps = sorted(chain["timestamp_ns"].unique())
198
+ symbols = list(registry.symbols)
199
+ out = []
200
+ for i in range(packages):
201
+ ts = int(timestamps[i % len(timestamps)])
202
+ symbol = symbols[i % len(symbols)]
203
+ side = OrderSide.BUY if i % 2 == 0 else OrderSide.SELL
204
+ out.append(
205
+ OptionPackageIntent(
206
+ timestamp_ns=ts,
207
+ package_id=f"bench-{i:05d}",
208
+ legs=(OptionPackageLeg(symbol, side, 1.0),),
209
+ quantity=1.0,
210
+ )
211
+ )
212
+ return tuple(out)
213
+
214
+
215
+ def _parity(a, b) -> Dict:
216
+ equity_diff = float(abs(a.equity.iloc[-1] - b.equity.iloc[-1]))
217
+ position_diff = float(np.max(np.abs(a.positions.to_numpy() - b.positions.to_numpy())))
218
+ fills_equal = bool(a.fills_report.equals(b.fills_report))
219
+ return {
220
+ "passed": bool(equity_diff <= 1e-9 and position_diff <= 1e-12 and fills_equal),
221
+ "final_equity_abs_diff": equity_diff,
222
+ "position_max_abs_diff": position_diff,
223
+ "fills_equal": fills_equal,
224
+ }
225
+
226
+
227
+ def _timeit(fn, repeats: int) -> float:
228
+ durations = []
229
+ for _ in range(max(1, repeats)):
230
+ start = time.perf_counter()
231
+ fn()
232
+ durations.append(time.perf_counter() - start)
233
+ return float(min(durations))
234
+
235
+
236
+ def _peak_memory_mb(fn) -> float:
237
+ tracemalloc.start()
238
+ try:
239
+ fn()
240
+ _, peak = tracemalloc.get_traced_memory()
241
+ finally:
242
+ tracemalloc.stop()
243
+ return peak / 1_000_000.0
244
+
245
+
246
+ def main() -> None:
247
+ parser = argparse.ArgumentParser()
248
+ parser.add_argument("--snapshots", type=int, default=96)
249
+ parser.add_argument("--contracts", type=int, default=48)
250
+ parser.add_argument("--packages", type=int, default=96)
251
+ parser.add_argument("--repeats", type=int, default=3)
252
+ parser.add_argument("--seed", type=int, default=42)
253
+ parser.add_argument("--output-json", type=Path, default=PACKAGE_DIR / "benchmarks" / "options_phase10_baseline.json")
254
+ parser.add_argument("--output-md", type=Path, default=PACKAGE_DIR / "benchmarks" / "options_phase10_baseline.md")
255
+ args = parser.parse_args()
256
+
257
+ report = run_benchmark(
258
+ snapshots=args.snapshots,
259
+ contracts=args.contracts,
260
+ packages=args.packages,
261
+ repeats=args.repeats,
262
+ seed=args.seed,
263
+ )
264
+ args.output_json.write_text(json.dumps(report, indent=2, default=str) + "\n", encoding="utf-8")
265
+ args.output_md.write_text(make_markdown(report), encoding="utf-8")
266
+ print(json.dumps({"status": report["status"], "cache_speedup": report["cache_speedup"]}, indent=2))
267
+ if report["status"] != "pass":
268
+ raise SystemExit(1)
269
+
270
+
271
+ if __name__ == "__main__":
272
+ main()
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env python3
2
+ """Smoke compare native legacy `%_equity` and Nautilus `%_equity` validation."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Dict, Optional
11
+
12
+ import numpy as np
13
+ import pandas as pd
14
+
15
+ PACKAGE_DIR = Path(__file__).resolve().parents[1]
16
+ PROJECT_DIR = PACKAGE_DIR.parent
17
+ if str(PROJECT_DIR) not in sys.path:
18
+ sys.path.insert(0, str(PROJECT_DIR))
19
+
20
+ from quantbt import QuantBTEndpoint # noqa: E402
21
+ from quantbt.adapters.nautilus import NautilusBackendConfig # noqa: E402
22
+
23
+
24
+ def run_smoke(rows: int = 300) -> Dict:
25
+ data = _synthetic_eth_data(rows=rows)
26
+ scenarios = [
27
+ {
28
+ "name": "aligned_fee_no_funding_no_slippage",
29
+ "native_fee_round_trip": 0.0008,
30
+ "native_use_funding": False,
31
+ "native_slippage": 0.0,
32
+ "nautilus_fee_rate": 0.0004,
33
+ "nautilus_use_funding": False,
34
+ "nautilus_slippage": 0.0,
35
+ "note": "Native one-way fee approximates ETH taker fee; custom Nautilus fee_rate is not applied.",
36
+ },
37
+ {
38
+ "name": "user_like_mismatch",
39
+ "native_fee_round_trip": 0.0005,
40
+ "native_use_funding": True,
41
+ "native_slippage": 0.0002,
42
+ "nautilus_fee_rate": 0.0005,
43
+ "nautilus_use_funding": False,
44
+ "nautilus_slippage": 0.0002,
45
+ "note": "Matches the observed notebook-style mismatch: fee convention, funding, and slippage differ.",
46
+ },
47
+ ]
48
+ results = []
49
+ for scenario in scenarios:
50
+ results.append(_run_scenario(data, scenario))
51
+ return {
52
+ "status": "pass",
53
+ "rows": int(rows),
54
+ "symbol": "ETHUSDT-PERP.BINANCE",
55
+ "scenarios": results,
56
+ "conclusion": _conclusion(results),
57
+ }
58
+
59
+
60
+ def make_markdown(report: Dict) -> str:
61
+ lines = [
62
+ "# `%_equity` Native vs Nautilus Smoke",
63
+ "",
64
+ f"Status: **{report['status']}**",
65
+ f"Rows: `{report['rows']}`",
66
+ f"Symbol: `{report['symbol']}`",
67
+ "",
68
+ ]
69
+ for item in report["scenarios"]:
70
+ lines.extend(
71
+ [
72
+ f"## {item['name']}",
73
+ "",
74
+ f"- Native final equity: `{item['native']['final_equity']:.6f}`",
75
+ f"- Nautilus final equity: `{item['nautilus']['final_equity']:.6f}`",
76
+ f"- Final equity diff: `{item['final_equity_diff']:.6f}`",
77
+ f"- Native trades: `{item['native']['num_trades']}`",
78
+ f"- Nautilus trades: `{item['nautilus']['num_trades']}`",
79
+ f"- Signal transitions: `{item['diagnostic']['signal']['effective_transition_count']}`",
80
+ f"- Nautilus orders/fills: `{item['diagnostic']['orders']['orders_count']}` / `{item['diagnostic']['orders']['fills_count']}`",
81
+ f"- Checks: `{item['diagnostic']['checks']}`",
82
+ f"- Note: {item['note']}",
83
+ "",
84
+ ]
85
+ )
86
+ lines.extend(["## Conclusion", "", report["conclusion"], ""])
87
+ return "\n".join(lines)
88
+
89
+
90
+ def _run_scenario(data: pd.DataFrame, scenario: Dict) -> Dict:
91
+ native = QuantBTEndpoint.pct_equity(
92
+ initial_capital=20_000,
93
+ leverage=5,
94
+ maintenance_ratio=0.005,
95
+ contract_size=1.0,
96
+ use_funding=bool(scenario["native_use_funding"]),
97
+ funding_rate=0.0001,
98
+ alloc_per_trade=0.5,
99
+ fee=float(scenario["native_fee_round_trip"]),
100
+ slippage=float(scenario["native_slippage"]),
101
+ use_pyramiding=False,
102
+ )
103
+ native_result = native.backtest(data=data, signal_col="pos_weight")
104
+
105
+ nautilus = QuantBTEndpoint.nautilus_validation(
106
+ initial_capital=20_000,
107
+ leverage=5,
108
+ alloc_per_trade=0.5,
109
+ hedge_type="%_equity",
110
+ fee_rate=float(scenario["nautilus_fee_rate"]),
111
+ use_funding=bool(scenario["nautilus_use_funding"]),
112
+ use_pyramiding=False,
113
+ slippage=float(scenario["nautilus_slippage"]),
114
+ nautilus_config=NautilusBackendConfig(
115
+ timeframe="1h",
116
+ starting_balance=20_000,
117
+ trade_notional=0.5,
118
+ close_positions_on_stop=False,
119
+ bypass_logging=True,
120
+ log_level="ERROR",
121
+ ),
122
+ )
123
+ nautilus_result = nautilus.simulate(
124
+ data=data,
125
+ signal_col="pos_weight",
126
+ symbols=["ETHUSDT-PERP.BINANCE"],
127
+ show_order_logs=False,
128
+ )
129
+ diagnostic = nautilus.nautilus_pct_equity_diagnostic(
130
+ data=data,
131
+ signal_col="pos_weight",
132
+ native_fee_round_trip=float(scenario["native_fee_round_trip"]),
133
+ native_use_funding=bool(scenario["native_use_funding"]),
134
+ native_slippage=float(scenario["native_slippage"]),
135
+ )
136
+ native_report = native_result.full_report()
137
+ nautilus_report = nautilus_result.full_report()
138
+ return {
139
+ "name": scenario["name"],
140
+ "note": scenario["note"],
141
+ "native": {
142
+ "final_equity": float(native_result.equity.iloc[-1]),
143
+ "total_return_pct": float(native_report["total_return_pct"]),
144
+ "num_trades": int(native_report["num_trades"]),
145
+ },
146
+ "nautilus": {
147
+ "final_equity": float(nautilus_result.equity.iloc[-1]),
148
+ "total_return_pct": float(nautilus_report["total_return_pct"]),
149
+ "num_trades": int(nautilus_report["num_trades"]),
150
+ },
151
+ "final_equity_diff": float(nautilus_result.equity.iloc[-1] - native_result.equity.iloc[-1]),
152
+ "diagnostic": _jsonable_diagnostic(diagnostic),
153
+ }
154
+
155
+
156
+ def _synthetic_eth_data(rows: int) -> pd.DataFrame:
157
+ idx = pd.date_range("2024-01-01", periods=rows, freq="1h", tz="UTC")
158
+ grid = np.arange(rows)
159
+ close = pd.Series(2000 + 80 * np.sin(grid / 18) + 0.8 * grid + 20 * np.sin(grid / 5), index=idx)
160
+ signal = pd.Series(0.0, index=idx)
161
+ signal.iloc[10 : min(80, rows)] = 1.0
162
+ signal.iloc[min(110, rows) : min(170, rows)] = -1.0
163
+ signal.iloc[min(210, rows) : min(260, rows)] = 1.0
164
+ return pd.DataFrame(
165
+ {
166
+ "open": close,
167
+ "high": close * 1.002,
168
+ "low": close * 0.998,
169
+ "close": close,
170
+ "volume": 10_000.0,
171
+ "pos_weight": signal,
172
+ },
173
+ index=idx,
174
+ )
175
+
176
+
177
+ def _conclusion(results) -> str:
178
+ aligned = next(item for item in results if item["name"] == "aligned_fee_no_funding_no_slippage")
179
+ mismatch = next(item for item in results if item["name"] == "user_like_mismatch")
180
+ return (
181
+ "When fee/funding/slippage semantics are aligned as closely as the current adapters allow, "
182
+ f"the synthetic final-equity gap is only `{aligned['final_equity_diff']:.6f}` USD and order/fill counts match. "
183
+ "The user-like setup intentionally differs: legacy `fee` is round-trip, Nautilus `fee_rate` is metadata today, "
184
+ "native funding/slippage are applied while Nautilus signal validation does not apply custom funding/slippage. "
185
+ f"That scenario shows a larger synthetic gap of `{mismatch['final_equity_diff']:.6f}` USD. "
186
+ "Large real-alpha gaps should be audited with the diagnostic helper first; if transition counts match, the next "
187
+ "production task is implementing custom fee/slippage/funding in the Nautilus signal adapter."
188
+ )
189
+
190
+
191
+ def _jsonable_diagnostic(diagnostic: Dict) -> Dict:
192
+ out = dict(diagnostic)
193
+ out["signal"] = dict(out["signal"])
194
+ transition = out["signal"].pop("transition_report")
195
+ out["signal"]["transition_report_head"] = transition.head(20).to_dict(orient="records")
196
+ return out
197
+
198
+
199
+ def _json_default(value):
200
+ if isinstance(value, (np.integer,)):
201
+ return int(value)
202
+ if isinstance(value, (np.floating,)):
203
+ return float(value)
204
+ if isinstance(value, (np.bool_,)):
205
+ return bool(value)
206
+ if isinstance(value, pd.Timestamp):
207
+ return value.isoformat()
208
+ raise TypeError(f"{type(value).__name__} is not JSON serializable")
209
+
210
+
211
+ def main(argv: Optional[list[str]] = None) -> int:
212
+ parser = argparse.ArgumentParser(description=__doc__)
213
+ parser.add_argument("--rows", type=int, default=300)
214
+ parser.add_argument("--json-out", type=Path, default=PACKAGE_DIR / "benchmarks" / "pct_equity_nautilus_smoke.json")
215
+ parser.add_argument("--md-out", type=Path, default=PACKAGE_DIR / "benchmarks" / "pct_equity_nautilus_smoke.md")
216
+ args = parser.parse_args(argv)
217
+ report = run_smoke(rows=args.rows)
218
+ args.json_out.parent.mkdir(parents=True, exist_ok=True)
219
+ args.md_out.parent.mkdir(parents=True, exist_ok=True)
220
+ args.json_out.write_text(json.dumps(report, indent=2, default=_json_default) + "\n", encoding="utf-8")
221
+ args.md_out.write_text(make_markdown(report), encoding="utf-8")
222
+ print(make_markdown(report))
223
+ return 0 if report["status"] == "pass" else 1
224
+
225
+
226
+ if __name__ == "__main__":
227
+ raise SystemExit(main())