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,802 @@
1
+ import argparse
2
+ import json
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import pandas as pd
7
+ import numpy as np
8
+ from datetime import datetime, timedelta
9
+
10
+
11
+ PACKAGE_DIR = Path(__file__).resolve().parents[1]
12
+ PROJECT_DIR = PACKAGE_DIR.parent
13
+ if str(PROJECT_DIR) not in sys.path:
14
+ sys.path.insert(0, str(PROJECT_DIR))
15
+
16
+ from quantbt import ( # noqa: E402
17
+ ExerciseStyle,
18
+ GammaScalpingConfig,
19
+ OptionHedgeConfig,
20
+ OptionHedgePolicyType,
21
+ OptionInstrumentRegistry,
22
+ OptionInstrumentSpec,
23
+ OptionKind,
24
+ OptionPackageIntent,
25
+ OptionPackageLeg,
26
+ OptionPreparedRunCache,
27
+ OrderSide,
28
+ PremiumConvention,
29
+ QuantBTEndpoint,
30
+ SettlementStyle,
31
+ build_gamma_scalping_strategy_run,
32
+ )
33
+
34
+ def filter_atm_options(df: pd.DataFrame, iv_rank_threshold: float = 101.0, # Tạm set cao để bypass IV rank check
35
+ use_rv_condition: bool = False, # Thêm flag tắt RV > IV
36
+ rv_window: int = 20, iv_window: int = 252, min_oi: int = 50, # Giảm OI tạm
37
+ min_dte: int = 2, max_dte: int = 14) -> pd.DataFrame: # Mở rộng DTE
38
+ df = df.copy()
39
+ df['snapshot_time'] = pd.to_datetime(df['time']).dt.tz_localize(None).dt.normalize()
40
+ df['expiration_date'] = pd.to_datetime(df['expiration']).dt.tz_localize(None).dt.normalize()
41
+ df['spot_price'] = df['close']
42
+ df['strike'] = df['strike'].astype(float).astype(int)
43
+ df['mid_price'] = (df['bid'] + df['ask']) / 2
44
+ df['dte'] = (df['expiration_date'] - df['snapshot_time']).dt.days
45
+
46
+ df_sorted = df.sort_values('snapshot_time')
47
+ df_sorted['log_return'] = np.log(df_sorted['spot_price'] / df_sorted['spot_price'].shift(1))
48
+ df_sorted['rv'] = df_sorted['log_return'].ewm(span=rv_window).std() * np.sqrt(252)
49
+
50
+ # IV rank chỉ tính nếu window đủ, nhưng bypass check
51
+ df_sorted['iv_rank'] = df_sorted.groupby('snapshot_time')['implied_volatility'].transform(
52
+ lambda x: x.rank(pct=True).iloc[-1] * 100 if len(x) > 0 else np.nan
53
+ ) # Simple rank per day, hoặc giữ rolling nhưng skip NaN
54
+
55
+ result_rows = []
56
+ for (time_snapshot, underlying), group_df in df_sorted.groupby(['snapshot_time', 'underlying']):
57
+ current_iv = group_df['implied_volatility'].mean()
58
+ current_rv = group_df['rv'].mean() if 'rv' in group_df else np.nan # Mean để tránh NaN
59
+ current_iv_rank = group_df['iv_rank'].mean()
60
+
61
+ # Bypass condition tạm
62
+ if current_iv_rank >= iv_rank_threshold:
63
+ continue # Chỉ skip nếu rank cao, nhưng set threshold=101 để không skip
64
+ if use_rv_condition and (pd.isna(current_rv) or current_rv <= current_iv):
65
+ continue
66
+
67
+ filtered_df = group_df[(group_df['dte'] >= min_dte) & (group_df['dte'] <= max_dte) & (group_df['open_interest'] >= min_oi)].copy()
68
+ if filtered_df.empty:
69
+ continue
70
+
71
+ current_spot = filtered_df['spot_price'].iloc[0]
72
+ paired_strikes = filtered_df.groupby(['expiration_date', 'strike']).filter(
73
+ lambda x: set(x['type'].values) == {'call', 'put'} # Chính xác hơn: đúng 1 call + 1 put
74
+ )
75
+ if paired_strikes.empty:
76
+ continue
77
+
78
+ paired_strikes['atm_distance'] = abs(paired_strikes['strike'] - current_spot)
79
+ min_expiry_date = paired_strikes['dte'].min() # Ưu tiên DTE nhỏ nhất
80
+ best_expiry = paired_strikes[paired_strikes['dte'] == min_expiry_date]['expiration_date'].iloc[0]
81
+ best_strikes = paired_strikes[paired_strikes['expiration_date'] == best_expiry]
82
+ best_strike = best_strikes.loc[best_strikes['atm_distance'].idxmin(), 'strike']
83
+
84
+ final_pair = filtered_df[
85
+ (filtered_df['expiration_date'] == best_expiry) &
86
+ (filtered_df['strike'] == best_strike) &
87
+ (filtered_df['type'].isin(['call', 'put']))
88
+ ]
89
+ if len(final_pair) == 2:
90
+ result_rows.append(final_pair)
91
+
92
+ if result_rows:
93
+ return pd.concat(result_rows, ignore_index=True)
94
+ else:
95
+ print("No straddle found after all filters - check data has paired call/put ATM short-dated")
96
+ return pd.DataFrame(columns=df.columns)
97
+
98
+ def normalize_greeks(df_straddle: pd.DataFrame) -> pd.DataFrame:
99
+ """
100
+ Chuẩn hóa dựa vendor: delta/gamma *100 (per $1), theta per day USD, vega *100 (per 1.0 IV).
101
+ """
102
+ df = df_straddle.copy()
103
+ df['delta_norm'] = df['delta'] * 100
104
+ df['gamma_norm'] = df['gamma'] * 100
105
+ df['theta_norm'] = df['theta']
106
+ df['vega_norm'] = df['vega'] * 100
107
+ return df
108
+
109
+ def aggregate_straddle_greeks(df: pd.DataFrame, position_type: str = 'long', notional: int = 100) -> pd.DataFrame:
110
+ """
111
+ Aggregate Greeks, scale by notional và sign.
112
+ """
113
+ sign = 1 if position_type == 'long' else -1
114
+ df['time'] = pd.to_datetime(df['time'])
115
+ df = df.set_index('time').sort_index()
116
+
117
+ straddle_df = df.groupby(level=0).agg({
118
+ 'spot_price': 'first',
119
+ 'delta_norm': 'sum',
120
+ 'gamma_norm': 'sum',
121
+ 'theta_norm': 'sum',
122
+ 'vega_norm': 'sum',
123
+ 'implied_volatility': 'mean',
124
+ 'mid_price': 'sum',
125
+ 'dte': 'first'
126
+ })
127
+
128
+ for col in ['delta_norm', 'gamma_norm', 'theta_norm', 'vega_norm', 'mid_price']:
129
+ straddle_df[col] *= sign * notional
130
+
131
+ straddle_df.rename(columns={'implied_volatility': 'iv_straddle'}, inplace=True)
132
+ return straddle_df
133
+
134
+ def simulate_paths(S0: float, mu: float, sigma_rv: float, T: float, dt: float, n_paths: int = 1000) -> np.ndarray:
135
+ """GBM paths for backtest."""
136
+ n_steps = int(T / dt)
137
+ paths = np.zeros((n_paths, n_steps + 1))
138
+ paths[:, 0] = S0
139
+ for t in range(1, n_steps + 1):
140
+ Z = np.random.standard_normal(n_paths)
141
+ paths[:, t] = paths[:, t-1] * np.exp((mu - 0.5 * sigma_rv**2) * dt + sigma_rv * np.sqrt(dt) * Z)
142
+ return paths
143
+
144
+ def gamma_pnl_factor(gamma_norm: float, S: float, rv: float, iv: float, dt: float, notional: int = 100) -> float:
145
+ """Gamma P&L attribution."""
146
+ return 0.5 * gamma_norm * S**2 * (rv**2 - iv**2) * dt * notional
147
+
148
+ def hedge_and_pnl(df_straddle: pd.DataFrame,
149
+ position_type: str = 'long',
150
+ notional: int = 100,
151
+ hedge_threshold: float = 0.05,
152
+ min_dte: int = 2,
153
+ sim_paths: bool = False,
154
+ option_commission_per_straddle: float = 3.0, # USD per straddle round-trip (2 legs)
155
+ hedge_commission_per_unit_delta: float = 0.05 # USD per 1.0 delta rebalanced
156
+ ) -> pd.DataFrame:
157
+ """
158
+ P&L realistic với commission:
159
+ - Option fee: khi open/rollover straddle mới
160
+ - Hedge fee: mỗi lần re-hedge delta
161
+ """
162
+ df = normalize_greeks(df_straddle)
163
+ df = aggregate_straddle_greeks(df, position_type, notional)
164
+
165
+ df['portfolio_delta'] = df['delta_norm']
166
+ df['pnl'] = 0.0
167
+ df['cum_pnl'] = 0.0
168
+ df['cum_return'] = 0.0
169
+ df['gamma_attrib'] = 0.0
170
+ df['hedge_pnl'] = 0.0
171
+ df['mtm_change'] = 0.0
172
+ df['commission'] = 0.0 # NEW: track commission
173
+ df['commission_option'] = 0.0 # Phí từ option
174
+ df['commission_hedge'] = 0.0 # Phí từ hedge
175
+
176
+ if sim_paths:
177
+ dt_base = 1/252
178
+ T_total = len(df) * dt_base
179
+ paths = simulate_paths(df['spot_price'].iloc[0], mu=0.1, sigma_rv=0.3, T=T_total, dt=dt_base, n_paths=1)
180
+ df['spot_price'] = pd.Series(paths[0, :len(df)], index=df.index)
181
+
182
+ if df.empty:
183
+ return df
184
+
185
+ # Initial capital = giá trị straddle khi entry (mid_price đầu tiên)
186
+ initial_capital = abs(df.iloc[0]['mid_price']) # abs để tránh âm nếu short
187
+ if initial_capital == 0:
188
+ initial_capital = 1.0 # Tránh chia 0
189
+
190
+ current_position_value = df.iloc[0]['mid_price']
191
+ prev_delta_for_hedge = df.iloc[0]['delta_norm'] # Để tính delta change khi hedge
192
+
193
+ for i in range(1, len(df)):
194
+ row_prev, row = df.iloc[i-1], df.iloc[i]
195
+
196
+ S_prev, S = row_prev['spot_price'], row['spot_price']
197
+ ds = S - S_prev
198
+ dt_actual = (row.name - row_prev.name).days
199
+
200
+ rv_actual = abs(ds / S_prev) * np.sqrt(252) if dt_actual > 0 and S_prev != 0 else 0.0
201
+
202
+ commission_today = 0.0
203
+ commission_option_today = 0.0
204
+ commission_hedge_today = 0.0
205
+
206
+ # === ROLLOVER: close old straddle, open new ===
207
+ if row_prev['dte'] < min_dte:
208
+ close_pnl = row_prev['mid_price'] - current_position_value
209
+ df.at[row_prev.name, 'pnl'] += close_pnl
210
+ df.at[row_prev.name, 'mtm_change'] = close_pnl
211
+
212
+ # Commission khi rollover: open new straddle (2 legs)
213
+ commission_option_today = option_commission_per_straddle * notional
214
+ commission_today += commission_option_today
215
+
216
+ # Reset position
217
+ current_position_value = row['mid_price']
218
+ prev_delta_for_hedge = row['delta_norm'] # Delta mới sau rollover
219
+ df.at[row.name, 'portfolio_delta'] = row['delta_norm']
220
+ else:
221
+ current_position_value = row['mid_price']
222
+
223
+ # === DAILY MTM CHANGE ===
224
+ mtm_change = row['mid_price'] - row_prev['mid_price']
225
+ df.at[row.name, 'mtm_change'] = mtm_change
226
+
227
+ # === DISCRETE HEDGE ===
228
+ prev_delta = row_prev['portfolio_delta']
229
+ hedge_pnl = 0.0
230
+ if abs(prev_delta) > hedge_threshold:
231
+ hedge_pnl = -prev_delta * ds
232
+ delta_change = abs(row['delta_norm'] - prev_delta) # Amount rebalanced
233
+ commission_hedge_today = delta_change * hedge_commission_per_unit_delta
234
+ commission_today += commission_hedge_today
235
+
236
+ df.at[row.name, 'portfolio_delta'] = row['delta_norm'] # Rebalanced to new delta
237
+ prev_delta_for_hedge = row['delta_norm']
238
+
239
+ df.at[row.name, 'hedge_pnl'] = hedge_pnl
240
+
241
+ # === TOTAL PNL SAU COMMISSION ===
242
+ gross_pnl = mtm_change + hedge_pnl
243
+ net_pnl = gross_pnl - commission_today
244
+ df.at[row.name, 'pnl'] = net_pnl
245
+
246
+
247
+ # CUM PNL & CUM RETURN
248
+ df.at[row.name, 'cum_pnl'] = df.at[row_prev.name, 'cum_pnl'] + net_pnl
249
+ df.at[row.name, 'cum_return'] = df.at[row.name, 'cum_pnl'] / initial_capital # % return
250
+
251
+ # === COMMISSION BREAKDOWN ===
252
+ df.at[row.name, 'commission'] = commission_today
253
+ df.at[row.name, 'commission_option'] = commission_option_today
254
+ df.at[row.name, 'commission_hedge'] = commission_hedge_today
255
+
256
+ # === GAMMA ATTRIB (tạm giữ, bạn sẽ fix sau) ===
257
+ df.at[row.name, 'gamma_attrib'] = gamma_pnl_factor(
258
+ row_prev['gamma_norm'], S_prev, rv_actual, row_prev['iv_straddle'], dt_actual, notional
259
+ )
260
+
261
+ df.iloc[0]['cum_return'] = 0.0
262
+ df.iloc[0]['cum_pnl'] = 0.0
263
+
264
+ return df
265
+
266
+
267
+ def build_synthetic_gamma_scalping_case(
268
+ *,
269
+ snapshots: int = 90,
270
+ seed: int = 42,
271
+ initial_spot: float = 100_000.0,
272
+ strike: float = 100_000.0,
273
+ ) -> tuple[pd.DataFrame, OptionInstrumentRegistry, list[OptionPackageIntent]]:
274
+ """
275
+ Build a deterministic ATM long-straddle case for the native option engine.
276
+
277
+ The sample intentionally keeps one listed call/put alive across the whole
278
+ tape. This isolates option-package execution, quote-side fills, MTM,
279
+ prepared-cache replay, and delta-hedge accounting without mixing in
280
+ selection/rollover noise.
281
+ """
282
+ rng = np.random.default_rng(seed)
283
+ start = pd.Timestamp("2026-01-01 00:00:00", tz="UTC")
284
+ expiry = int((start + pd.Timedelta(days=max(30, snapshots + 10))).value)
285
+ call_id = "BTC-26MAR26-100000-C.TEST"
286
+ put_id = "BTC-26MAR26-100000-P.TEST"
287
+ registry = OptionInstrumentRegistry.from_iterable(
288
+ (
289
+ _linear_option_spec(call_id, strike, OptionKind.CALL, expiry),
290
+ _linear_option_spec(put_id, strike, OptionKind.PUT, expiry),
291
+ )
292
+ )
293
+
294
+ rows = []
295
+ spot = float(initial_spot)
296
+ for i in range(snapshots):
297
+ ts = start + pd.Timedelta(days=i)
298
+ timestamp_ns = int(ts.value)
299
+ spot *= float(np.exp(0.0002 + rng.normal(0.0, 0.018)))
300
+ dte = max((expiry - timestamp_ns) / (24 * 60 * 60 * 1_000_000_000), 1.0)
301
+ time_value = max(800.0 * np.sqrt(dte / 365.0), 80.0)
302
+ skew = np.tanh((spot - strike) / (0.08 * strike))
303
+ call_delta = float(np.clip(0.50 + 0.35 * skew, 0.05, 0.95))
304
+ put_delta = call_delta - 1.0
305
+
306
+ call_mark = max(spot - strike, 0.0) + time_value
307
+ put_mark = max(strike - spot, 0.0) + time_value * 0.98
308
+ for sequence_id, instrument_id, option_kind, mark, delta in (
309
+ (0, call_id, "call", call_mark, call_delta),
310
+ (1, put_id, "put", put_mark, put_delta),
311
+ ):
312
+ spread = max(mark * 0.004, 2.0)
313
+ rows.append(
314
+ {
315
+ "timestamp_ns": timestamp_ns,
316
+ "instrument_id": instrument_id,
317
+ "venue": "TEST",
318
+ "underlying_id": "BTC-PERP.TEST",
319
+ "expiry_ns": expiry,
320
+ "strike": strike,
321
+ "option_kind": option_kind,
322
+ "bid_price": max(mark - 0.5 * spread, 0.01),
323
+ "bid_size": 100.0,
324
+ "ask_price": mark + 0.5 * spread,
325
+ "ask_size": 100.0,
326
+ "mark_price": mark,
327
+ "last_price": mark,
328
+ "index_price": spot,
329
+ "forward_price": spot,
330
+ "mark_iv": 0.55,
331
+ "bid_iv": 0.54,
332
+ "ask_iv": 0.56,
333
+ "delta": delta,
334
+ "gamma": 0.00008,
335
+ "vega": 90.0,
336
+ "theta": -8.0,
337
+ "open_interest": 500.0,
338
+ "volume": 100.0,
339
+ "quote_currency": "USD",
340
+ "settlement_currency": "USD",
341
+ "sequence_id": sequence_id,
342
+ "source_latency_ns": 1_000_000,
343
+ }
344
+ )
345
+
346
+ chain = pd.DataFrame(rows)
347
+ timestamps = sorted(chain["timestamp_ns"].unique())
348
+ packages = [
349
+ OptionPackageIntent(
350
+ timestamp_ns=int(timestamps[0]),
351
+ package_id="gamma-open-long-straddle",
352
+ legs=(
353
+ OptionPackageLeg(call_id, OrderSide.BUY, 1.0, role="long_call"),
354
+ OptionPackageLeg(put_id, OrderSide.BUY, 1.0, role="long_put"),
355
+ ),
356
+ quantity=1.0,
357
+ tag="gamma_scalping_entry",
358
+ metadata={"strategy": "gamma_scalping", "action": "open"},
359
+ ),
360
+ OptionPackageIntent(
361
+ timestamp_ns=int(timestamps[-1]),
362
+ package_id="gamma-close-long-straddle",
363
+ legs=(
364
+ OptionPackageLeg(call_id, OrderSide.SELL, 1.0, role="close_call"),
365
+ OptionPackageLeg(put_id, OrderSide.SELL, 1.0, role="close_put"),
366
+ ),
367
+ quantity=1.0,
368
+ tag="gamma_scalping_exit",
369
+ metadata={"strategy": "gamma_scalping", "action": "close"},
370
+ ),
371
+ ]
372
+ return chain, registry, packages
373
+
374
+
375
+ def run_quantbt_gamma_scalping_sample(*, snapshots: int = 90, seed: int = 42) -> dict:
376
+ """Run the synthetic gamma-scalping sample through the public options endpoint."""
377
+ chain, registry, _ = build_synthetic_gamma_scalping_case(snapshots=snapshots, seed=seed)
378
+ strategy_run = build_gamma_scalping_strategy_run(
379
+ chain,
380
+ registry,
381
+ GammaScalpingConfig(
382
+ hedge_policy=OptionHedgeConfig(policy=OptionHedgePolicyType.FIXED_THRESHOLD, threshold=0.05),
383
+ ),
384
+ )
385
+ cache = OptionPreparedRunCache.from_chain(chain, registry)
386
+ underlying = chain.groupby("timestamp_ns", sort=True)["index_price"].first()
387
+ underlying.index = pd.to_datetime(underlying.index, utc=True).tz_convert(None)
388
+ bt = QuantBTEndpoint.options(
389
+ initial_capital=100_000.0,
390
+ reporting_currency="USD",
391
+ initial_balances={"USD": 100_000.0},
392
+ fee_rate=0.0002,
393
+ metadata={"sample": "gamma_scalping_backtestsample", "seed": seed},
394
+ )
395
+ uncached = bt.backtest(chain=chain, instruments=registry, strategy_run=strategy_run, underlying=underlying)
396
+ cached = bt.backtest(chain=chain, instruments=registry, strategy_run=strategy_run, underlying=underlying, prepared_cache=cache)
397
+
398
+ final_equity_diff = float(abs(uncached.equity.iloc[-1] - cached.equity.iloc[-1]))
399
+ fills_equal = bool(uncached.fills_report.equals(cached.fills_report))
400
+ if final_equity_diff > 1e-9 or not fills_equal:
401
+ raise RuntimeError("prepared-cache gamma sample parity failed")
402
+
403
+ report = {
404
+ "status": "pass",
405
+ "sample": "gamma_scalping_backtestsample",
406
+ "snapshots": int(snapshots),
407
+ "chain_rows": int(len(chain)),
408
+ "packages": int(len(strategy_run.packages)),
409
+ "fills": int(len(cached.fills_report)),
410
+ "initial_equity": float(cached.equity.iloc[0]),
411
+ "final_equity": float(cached.equity.iloc[-1]),
412
+ "option_pnl": float(cached.option_equity.iloc[-1] - cached.option_equity.iloc[0]),
413
+ "hedge_pnl": float(cached.hedge_report["cumulative_hedge_pnl"].iloc[-1]),
414
+ "combined_option_plus_hedge_pnl": float(cached.equity.iloc[-1] - cached.equity.iloc[0]),
415
+ "hedge_rebalances": int(cached.hedge_report["should_rebalance"].sum()),
416
+ "selected_contracts": cached.metadata["selected_contracts"].to_dict("records"),
417
+ "prepared_cache_used": bool(cached.metadata.get("prepared_cache_used")),
418
+ "package_cache_size": int(cached.metadata.get("package_cache_size", 0)),
419
+ "parity": {
420
+ "final_equity_abs_diff": final_equity_diff,
421
+ "fills_equal": fills_equal,
422
+ },
423
+ "run_manifest": cached.run_manifest,
424
+ }
425
+ return report
426
+
427
+
428
+ def run_real_binance_gamma_scalping_sample(
429
+ *,
430
+ options_csv: Path,
431
+ underlying_source: str = "spot",
432
+ hedge_timeframe: str = "1h",
433
+ ) -> dict:
434
+ """
435
+ Run the gamma-scalping sample on a real Binance options snapshot CSV.
436
+
437
+ The CSV is converted into QuantBT's canonical option-chain schema. BTCUSDT
438
+ spot/perp candles are loaded from `_get_data` for the hedge path; if that
439
+ loader is unavailable for the requested range, the snapshot `spot_BTCUSDT`
440
+ column is used as a transparent fallback.
441
+ """
442
+ raw = pd.read_csv(options_csv, compression="gzip")
443
+ chain, registry = canonicalize_binance_options_history(raw)
444
+ strategy_run = build_gamma_scalping_strategy_run(
445
+ chain,
446
+ registry,
447
+ GammaScalpingConfig(
448
+ min_dte_days=10.0,
449
+ max_dte_days=21.0,
450
+ max_spread_bps=2_000.0,
451
+ hedge_policy=OptionHedgeConfig(policy=OptionHedgePolicyType.FIXED_THRESHOLD, threshold=0.05),
452
+ metadata={"source": "real_binance_csv"},
453
+ ),
454
+ )
455
+ cache = OptionPreparedRunCache.from_chain(chain, registry)
456
+ hedge_prices, hedge_price_source = load_underlying_prices_for_chain(
457
+ chain,
458
+ source=underlying_source,
459
+ timeframe=hedge_timeframe,
460
+ )
461
+
462
+ bt = QuantBTEndpoint.options(
463
+ initial_capital=100_000.0,
464
+ reporting_currency="USD",
465
+ initial_balances={"USD": 100_000.0},
466
+ fee_rate=0.0002,
467
+ metadata={
468
+ "sample": "real_binance_gamma_scalping",
469
+ "source_file": str(options_csv),
470
+ "underlying_source": underlying_source,
471
+ "hedge_timeframe": hedge_timeframe,
472
+ },
473
+ )
474
+ uncached = bt.backtest(chain=chain, instruments=registry, strategy_run=strategy_run, underlying=hedge_prices)
475
+ cached = bt.backtest(
476
+ chain=chain,
477
+ instruments=registry,
478
+ strategy_run=strategy_run,
479
+ underlying=hedge_prices,
480
+ prepared_cache=cache,
481
+ )
482
+ final_equity_diff = float(abs(uncached.equity.iloc[-1] - cached.equity.iloc[-1]))
483
+ fills_equal = bool(uncached.fills_report.equals(cached.fills_report))
484
+ if final_equity_diff > 1e-9 or not fills_equal:
485
+ raise RuntimeError("real Binance options prepared-cache parity failed")
486
+
487
+ selected_contracts = cached.metadata["selected_contracts"].to_dict("records")
488
+
489
+ report = {
490
+ "status": "pass",
491
+ "sample": "real_binance_gamma_scalping",
492
+ "source_file": str(options_csv),
493
+ "snapshots": int(chain["timestamp_ns"].nunique()),
494
+ "chain_rows": int(len(chain)),
495
+ "contracts": int(len(registry.instruments)),
496
+ "packages": int(len(strategy_run.packages)),
497
+ "fills": int(len(cached.fills_report)),
498
+ "selected": selected_contracts,
499
+ "initial_equity": float(cached.equity.iloc[0]),
500
+ "final_equity": float(cached.equity.iloc[-1]),
501
+ "option_pnl": float(cached.option_equity.iloc[-1] - cached.option_equity.iloc[0]),
502
+ "hedge_pnl": float(cached.hedge_report["cumulative_hedge_pnl"].iloc[-1]),
503
+ "combined_option_plus_hedge_pnl": float(cached.equity.iloc[-1] - cached.equity.iloc[0]),
504
+ "hedge_rebalances": int(cached.hedge_report["should_rebalance"].sum()),
505
+ "hedge_price_source": hedge_price_source,
506
+ "prepared_cache_used": bool(cached.metadata.get("prepared_cache_used")),
507
+ "package_cache_size": int(cached.metadata.get("package_cache_size", 0)),
508
+ "parity": {
509
+ "final_equity_abs_diff": final_equity_diff,
510
+ "fills_equal": fills_equal,
511
+ },
512
+ "run_manifest": cached.run_manifest,
513
+ }
514
+ return report
515
+
516
+
517
+ def canonicalize_binance_options_history(raw: pd.DataFrame) -> tuple[pd.DataFrame, OptionInstrumentRegistry]:
518
+ """Convert the legacy Binance option snapshot CSV into QuantBT canonical schema."""
519
+ required = {
520
+ "snapshot_time",
521
+ "symbol",
522
+ "spot_BTCUSDT",
523
+ "markPrice",
524
+ "bidPrice",
525
+ "askPrice",
526
+ "bidIV",
527
+ "askIV",
528
+ "markIV",
529
+ "delta",
530
+ "theta",
531
+ "gamma",
532
+ "vega",
533
+ "volume",
534
+ "strikePrice",
535
+ }
536
+ missing = sorted(required.difference(raw.columns))
537
+ if missing:
538
+ raise ValueError(f"real options CSV missing required columns: {missing}")
539
+
540
+ df = raw.copy()
541
+ df["snapshot_time"] = pd.to_datetime(df["snapshot_time"], utc=True, errors="coerce")
542
+ df = df.dropna(subset=["snapshot_time", "symbol"])
543
+ parsed = df["symbol"].astype(str).str.extract(r"^(?P<underlying>[A-Z]+)-(?P<expiry>\d{6})-(?P<strike>\d+(?:\.\d+)?)-(?P<kind>[CP])$")
544
+ df = df.join(parsed)
545
+ df = df.dropna(subset=["underlying", "expiry", "strike", "kind"])
546
+ df["timestamp_ns"] = df["snapshot_time"].astype("int64")
547
+ df["expiry_ns"] = df["expiry"].map(_binance_expiry_to_ns).astype("int64")
548
+ df["strike"] = pd.to_numeric(df["strike"], errors="coerce")
549
+
550
+ numeric_pairs = {
551
+ "bidPrice": "bid_price",
552
+ "askPrice": "ask_price",
553
+ "markPrice": "mark_price",
554
+ "spot_BTCUSDT": "index_price",
555
+ "exercisePrice": "forward_price",
556
+ "bidIV": "bid_iv",
557
+ "askIV": "ask_iv",
558
+ "markIV": "mark_iv",
559
+ "delta": "delta",
560
+ "gamma": "gamma",
561
+ "vega": "vega",
562
+ "theta": "theta",
563
+ "volume": "volume",
564
+ }
565
+ for source, target in numeric_pairs.items():
566
+ df[target] = pd.to_numeric(df[source], errors="coerce")
567
+ df["forward_price"] = df["forward_price"].fillna(df["index_price"])
568
+ if "lastPrice" in df:
569
+ df["last_price"] = pd.to_numeric(df["lastPrice"], errors="coerce").fillna(df["mark_price"])
570
+ else:
571
+ df["last_price"] = df["mark_price"]
572
+ df["bid_size"] = pd.to_numeric(df.get("lastQty", 1.0), errors="coerce").fillna(1.0).clip(lower=1.0)
573
+ df["ask_size"] = df["bid_size"]
574
+ df["open_interest"] = 1.0
575
+ if "amount" in df:
576
+ df["open_interest"] = pd.to_numeric(df["amount"], errors="coerce").fillna(1.0).clip(lower=1.0)
577
+
578
+ df = df[(df["bid_price"] > 0.0) & (df["ask_price"] > 0.0)]
579
+ df = df[df["ask_price"] >= df["bid_price"]]
580
+ df = df[df["expiry_ns"] > df["timestamp_ns"]]
581
+ df = df.dropna(subset=["strike", "index_price", "forward_price", "mark_price"])
582
+ df = df.sort_values(["timestamp_ns", "symbol"]).reset_index(drop=True)
583
+ df["sequence_id"] = df.groupby("timestamp_ns").cumcount().astype("int64")
584
+ df["source_latency_ns"] = 1_000_000
585
+ df["option_kind"] = np.where(df["kind"] == "C", "call", "put")
586
+ df["instrument_id"] = df["symbol"].astype(str) + ".BINANCE"
587
+ df["underlying_id"] = df["underlying"].astype(str) + "USDT.BINANCE"
588
+ df["venue"] = "BINANCE"
589
+ df["quote_currency"] = "USD"
590
+ df["settlement_currency"] = "USD"
591
+
592
+ canonical = df[
593
+ [
594
+ "timestamp_ns",
595
+ "instrument_id",
596
+ "venue",
597
+ "underlying_id",
598
+ "expiry_ns",
599
+ "strike",
600
+ "option_kind",
601
+ "bid_price",
602
+ "bid_size",
603
+ "ask_price",
604
+ "ask_size",
605
+ "mark_price",
606
+ "last_price",
607
+ "index_price",
608
+ "forward_price",
609
+ "mark_iv",
610
+ "bid_iv",
611
+ "ask_iv",
612
+ "delta",
613
+ "gamma",
614
+ "vega",
615
+ "theta",
616
+ "open_interest",
617
+ "volume",
618
+ "quote_currency",
619
+ "settlement_currency",
620
+ "sequence_id",
621
+ "source_latency_ns",
622
+ ]
623
+ ].copy()
624
+
625
+ specs = []
626
+ static = canonical.drop_duplicates("instrument_id").sort_values("instrument_id")
627
+ for row in static.itertuples(index=False):
628
+ specs.append(
629
+ OptionInstrumentSpec(
630
+ symbol=row.instrument_id,
631
+ venue="binance",
632
+ underlying_id=row.underlying_id,
633
+ underlying_index_id="BTCUSDT-INDEX.BINANCE",
634
+ option_kind=OptionKind.CALL if row.option_kind == "call" else OptionKind.PUT,
635
+ exercise_style=ExerciseStyle.EUROPEAN,
636
+ premium_convention=PremiumConvention.LINEAR_QUOTE,
637
+ settlement_style=SettlementStyle.CASH,
638
+ strike=float(row.strike),
639
+ expiry_ns=int(row.expiry_ns),
640
+ settlement_currency="USD",
641
+ premium_currency="USD",
642
+ quote_currency="USD",
643
+ multiplier=1.0,
644
+ contract_size=1.0,
645
+ qty_step=0.001,
646
+ tick_size=0.01,
647
+ convention_version="binance_options_history_csv_v1",
648
+ )
649
+ )
650
+ return canonical, OptionInstrumentRegistry.from_iterable(specs)
651
+
652
+
653
+ def build_real_atm_straddle_packages(chain: pd.DataFrame) -> tuple[list[OptionPackageIntent], dict]:
654
+ """Select a real ATM call/put pair available at entry and exit."""
655
+ timestamps = sorted(chain["timestamp_ns"].unique())
656
+ entry_ts = int(timestamps[0])
657
+ exit_ts = int(timestamps[-1])
658
+ entry = chain[chain["timestamp_ns"] == entry_ts].copy()
659
+ exit_symbols = set(chain.loc[chain["timestamp_ns"] == exit_ts, "instrument_id"])
660
+ entry = entry[entry["instrument_id"].isin(exit_symbols)]
661
+ pair_counts = entry.groupby(["expiry_ns", "strike"])["option_kind"].agg(lambda values: set(values))
662
+ valid_pairs = [key for key, kinds in pair_counts.items() if kinds == {"call", "put"}]
663
+ if not valid_pairs:
664
+ raise ValueError("no entry ATM straddle pair survives until final snapshot")
665
+ spot = float(entry["index_price"].median())
666
+ expiry_ns, strike = min(valid_pairs, key=lambda key: (abs(float(key[1]) - spot), int(key[0])))
667
+ selected_rows = entry[(entry["expiry_ns"] == expiry_ns) & (entry["strike"] == strike)]
668
+ call_id = str(selected_rows.loc[selected_rows["option_kind"] == "call", "instrument_id"].iloc[0])
669
+ put_id = str(selected_rows.loc[selected_rows["option_kind"] == "put", "instrument_id"].iloc[0])
670
+ selected = {
671
+ "entry_timestamp_ns": entry_ts,
672
+ "exit_timestamp_ns": exit_ts,
673
+ "entry_time": str(pd.Timestamp(entry_ts, tz="UTC")),
674
+ "exit_time": str(pd.Timestamp(exit_ts, tz="UTC")),
675
+ "spot": spot,
676
+ "strike": float(strike),
677
+ "expiry": str(pd.Timestamp(int(expiry_ns), tz="UTC")),
678
+ "call_id": call_id,
679
+ "put_id": put_id,
680
+ }
681
+ packages = [
682
+ OptionPackageIntent(
683
+ timestamp_ns=entry_ts,
684
+ package_id="real-gamma-open-long-straddle",
685
+ legs=(
686
+ OptionPackageLeg(call_id, OrderSide.BUY, 1.0, role="long_call"),
687
+ OptionPackageLeg(put_id, OrderSide.BUY, 1.0, role="long_put"),
688
+ ),
689
+ quantity=1.0,
690
+ tag="real_gamma_scalping_entry",
691
+ metadata={"strategy": "gamma_scalping", "action": "open", **selected},
692
+ ),
693
+ OptionPackageIntent(
694
+ timestamp_ns=exit_ts,
695
+ package_id="real-gamma-close-long-straddle",
696
+ legs=(
697
+ OptionPackageLeg(call_id, OrderSide.SELL, 1.0, role="close_call"),
698
+ OptionPackageLeg(put_id, OrderSide.SELL, 1.0, role="close_put"),
699
+ ),
700
+ quantity=1.0,
701
+ tag="real_gamma_scalping_exit",
702
+ metadata={"strategy": "gamma_scalping", "action": "close", **selected},
703
+ ),
704
+ ]
705
+ return packages, selected
706
+
707
+
708
+ def selected_straddle_delta_path(chain: pd.DataFrame, selected: dict) -> pd.Series:
709
+ active = chain[chain["instrument_id"].isin([selected["call_id"], selected["put_id"]])]
710
+ delta = active.groupby("timestamp_ns")["delta"].sum().sort_index()
711
+ delta.loc[int(selected["exit_timestamp_ns"])] = 0.0
712
+ return delta.sort_index()
713
+
714
+
715
+ def load_underlying_prices_for_chain(chain: pd.DataFrame, *, source: str, timeframe: str) -> tuple[pd.Series, str]:
716
+ timestamps = sorted(chain["timestamp_ns"].unique())
717
+ start = pd.Timestamp(int(timestamps[0]), tz="UTC").tz_localize(None)
718
+ end = pd.Timestamp(int(timestamps[-1]), tz="UTC").tz_localize(None)
719
+ dataset = "binance_spot_1m" if source == "spot" else "crypto_1m"
720
+ try:
721
+ get_data_path = Path("/root/bobby/pool_alpha/alphas_storage/_get_data")
722
+ if str(get_data_path) not in sys.path:
723
+ sys.path.insert(0, str(get_data_path))
724
+ from data_loader import load_data # type: ignore
725
+
726
+ ohlcv = load_data(
727
+ dataset,
728
+ symbols="BTCUSDT",
729
+ start_date=str(start),
730
+ end_date=str(end),
731
+ timeframe=timeframe,
732
+ check_val=False,
733
+ )
734
+ if not ohlcv.empty:
735
+ out = ohlcv.copy()
736
+ out["timestamp_ns"] = pd.to_datetime(out["time"], utc=True).astype("int64")
737
+ series = out.set_index("timestamp_ns")["close"].sort_index()
738
+ return series, dataset
739
+ except Exception as exc:
740
+ fallback = chain.groupby("timestamp_ns")["index_price"].first().sort_index()
741
+ return fallback, f"option_chain_index_price_fallback:{exc}"
742
+ fallback = chain.groupby("timestamp_ns")["index_price"].first().sort_index()
743
+ return fallback, "option_chain_index_price_fallback:no_loader_rows"
744
+
745
+
746
+ def _binance_expiry_to_ns(value: str) -> int:
747
+ text = str(value)
748
+ year = 2000 + int(text[:2])
749
+ month = int(text[2:4])
750
+ day = int(text[4:6])
751
+ return int(pd.Timestamp(year=year, month=month, day=day, hour=8, tz="UTC").value)
752
+
753
+
754
+ def _linear_option_spec(symbol: str, strike: float, kind: OptionKind, expiry_ns: int) -> OptionInstrumentSpec:
755
+ return OptionInstrumentSpec(
756
+ symbol=symbol,
757
+ venue="test",
758
+ underlying_id="BTC-PERP.TEST",
759
+ underlying_index_id="BTC-INDEX.TEST",
760
+ option_kind=kind,
761
+ exercise_style=ExerciseStyle.EUROPEAN,
762
+ premium_convention=PremiumConvention.LINEAR_QUOTE,
763
+ settlement_style=SettlementStyle.CASH,
764
+ strike=strike,
765
+ expiry_ns=expiry_ns,
766
+ settlement_currency="USD",
767
+ premium_currency="USD",
768
+ quote_currency="USD",
769
+ multiplier=1.0,
770
+ contract_size=1.0,
771
+ qty_step=1.0,
772
+ tick_size=0.01,
773
+ convention_version="gamma_scalping_synthetic_linear_v1",
774
+ )
775
+
776
+
777
+ def main() -> None:
778
+ parser = argparse.ArgumentParser(description="Run a QuantBT options gamma-scalping smoke sample.")
779
+ parser.add_argument("--snapshots", type=int, default=90)
780
+ parser.add_argument("--seed", type=int, default=42)
781
+ parser.add_argument("--real-options-csv", type=Path, default=None)
782
+ parser.add_argument("--underlying-source", choices=("spot", "perp"), default="spot")
783
+ parser.add_argument("--hedge-timeframe", default="1h")
784
+ parser.add_argument("--output-json", type=Path, default=None)
785
+ args = parser.parse_args()
786
+
787
+ if args.real_options_csv is not None:
788
+ report = run_real_binance_gamma_scalping_sample(
789
+ options_csv=args.real_options_csv,
790
+ underlying_source=args.underlying_source,
791
+ hedge_timeframe=args.hedge_timeframe,
792
+ )
793
+ else:
794
+ report = run_quantbt_gamma_scalping_sample(snapshots=args.snapshots, seed=args.seed)
795
+ payload = json.dumps(report, indent=2, default=str)
796
+ if args.output_json is not None:
797
+ args.output_json.write_text(payload + "\n", encoding="utf-8")
798
+ print(payload)
799
+
800
+
801
+ if __name__ == "__main__":
802
+ main()