lumibot 4.0.23__py3-none-any.whl → 4.1.1__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.

Potentially problematic release.


This version of lumibot might be problematic. Click here for more details.

Files changed (161) hide show
  1. lumibot/__pycache__/__init__.cpython-312.pyc +0 -0
  2. lumibot/__pycache__/constants.cpython-312.pyc +0 -0
  3. lumibot/__pycache__/credentials.cpython-312.pyc +0 -0
  4. lumibot/backtesting/__init__.py +6 -5
  5. lumibot/backtesting/__pycache__/__init__.cpython-312.pyc +0 -0
  6. lumibot/backtesting/__pycache__/alpaca_backtesting.cpython-312.pyc +0 -0
  7. lumibot/backtesting/__pycache__/alpha_vantage_backtesting.cpython-312.pyc +0 -0
  8. lumibot/backtesting/__pycache__/backtesting_broker.cpython-312.pyc +0 -0
  9. lumibot/backtesting/__pycache__/ccxt_backtesting.cpython-312.pyc +0 -0
  10. lumibot/backtesting/__pycache__/databento_backtesting.cpython-312.pyc +0 -0
  11. lumibot/backtesting/__pycache__/interactive_brokers_rest_backtesting.cpython-312.pyc +0 -0
  12. lumibot/backtesting/__pycache__/pandas_backtesting.cpython-312.pyc +0 -0
  13. lumibot/backtesting/__pycache__/polygon_backtesting.cpython-312.pyc +0 -0
  14. lumibot/backtesting/__pycache__/thetadata_backtesting.cpython-312.pyc +0 -0
  15. lumibot/backtesting/__pycache__/yahoo_backtesting.cpython-312.pyc +0 -0
  16. lumibot/backtesting/backtesting_broker.py +209 -9
  17. lumibot/backtesting/databento_backtesting.py +145 -24
  18. lumibot/backtesting/thetadata_backtesting.py +63 -42
  19. lumibot/brokers/__pycache__/__init__.cpython-312.pyc +0 -0
  20. lumibot/brokers/__pycache__/alpaca.cpython-312.pyc +0 -0
  21. lumibot/brokers/__pycache__/bitunix.cpython-312.pyc +0 -0
  22. lumibot/brokers/__pycache__/broker.cpython-312.pyc +0 -0
  23. lumibot/brokers/__pycache__/ccxt.cpython-312.pyc +0 -0
  24. lumibot/brokers/__pycache__/example_broker.cpython-312.pyc +0 -0
  25. lumibot/brokers/__pycache__/interactive_brokers.cpython-312.pyc +0 -0
  26. lumibot/brokers/__pycache__/interactive_brokers_rest.cpython-312.pyc +0 -0
  27. lumibot/brokers/__pycache__/projectx.cpython-312.pyc +0 -0
  28. lumibot/brokers/__pycache__/schwab.cpython-312.pyc +0 -0
  29. lumibot/brokers/__pycache__/tradier.cpython-312.pyc +0 -0
  30. lumibot/brokers/__pycache__/tradovate.cpython-312.pyc +0 -0
  31. lumibot/brokers/alpaca.py +11 -1
  32. lumibot/brokers/tradeovate.py +475 -0
  33. lumibot/components/grok_news_helper.py +284 -0
  34. lumibot/components/options_helper.py +90 -34
  35. lumibot/credentials.py +3 -0
  36. lumibot/data_sources/__pycache__/__init__.cpython-312.pyc +0 -0
  37. lumibot/data_sources/__pycache__/alpaca_data.cpython-312.pyc +0 -0
  38. lumibot/data_sources/__pycache__/alpha_vantage_data.cpython-312.pyc +0 -0
  39. lumibot/data_sources/__pycache__/bitunix_data.cpython-312.pyc +0 -0
  40. lumibot/data_sources/__pycache__/ccxt_backtesting_data.cpython-312.pyc +0 -0
  41. lumibot/data_sources/__pycache__/ccxt_data.cpython-312.pyc +0 -0
  42. lumibot/data_sources/__pycache__/data_source.cpython-312.pyc +0 -0
  43. lumibot/data_sources/__pycache__/data_source_backtesting.cpython-312.pyc +0 -0
  44. lumibot/data_sources/__pycache__/databento_data_polars_backtesting.cpython-312.pyc +0 -0
  45. lumibot/data_sources/__pycache__/databento_data_polars_live.cpython-312.pyc +0 -0
  46. lumibot/data_sources/__pycache__/example_broker_data.cpython-312.pyc +0 -0
  47. lumibot/data_sources/__pycache__/exceptions.cpython-312.pyc +0 -0
  48. lumibot/data_sources/__pycache__/interactive_brokers_data.cpython-312.pyc +0 -0
  49. lumibot/data_sources/__pycache__/interactive_brokers_rest_data.cpython-312.pyc +0 -0
  50. lumibot/data_sources/__pycache__/pandas_data.cpython-312.pyc +0 -0
  51. lumibot/data_sources/__pycache__/polars_mixin.cpython-312.pyc +0 -0
  52. lumibot/data_sources/__pycache__/polygon_data_polars.cpython-312.pyc +0 -0
  53. lumibot/data_sources/__pycache__/projectx_data.cpython-312.pyc +0 -0
  54. lumibot/data_sources/__pycache__/schwab_data.cpython-312.pyc +0 -0
  55. lumibot/data_sources/__pycache__/tradier_data.cpython-312.pyc +0 -0
  56. lumibot/data_sources/__pycache__/tradovate_data.cpython-312.pyc +0 -0
  57. lumibot/data_sources/__pycache__/yahoo_data_polars.cpython-312.pyc +0 -0
  58. lumibot/data_sources/data_source_backtesting.py +3 -5
  59. lumibot/data_sources/databento_data_polars_backtesting.py +194 -48
  60. lumibot/data_sources/pandas_data.py +6 -3
  61. lumibot/data_sources/polars_mixin.py +126 -21
  62. lumibot/data_sources/tradeovate_data.py +80 -0
  63. lumibot/data_sources/tradier_data.py +2 -1
  64. lumibot/entities/__pycache__/__init__.cpython-312.pyc +0 -0
  65. lumibot/entities/__pycache__/asset.cpython-312.pyc +0 -0
  66. lumibot/entities/__pycache__/bar.cpython-312.pyc +0 -0
  67. lumibot/entities/__pycache__/bars.cpython-312.pyc +0 -0
  68. lumibot/entities/__pycache__/chains.cpython-312.pyc +0 -0
  69. lumibot/entities/__pycache__/data.cpython-312.pyc +0 -0
  70. lumibot/entities/__pycache__/dataline.cpython-312.pyc +0 -0
  71. lumibot/entities/__pycache__/order.cpython-312.pyc +0 -0
  72. lumibot/entities/__pycache__/position.cpython-312.pyc +0 -0
  73. lumibot/entities/__pycache__/quote.cpython-312.pyc +0 -0
  74. lumibot/entities/__pycache__/trading_fee.cpython-312.pyc +0 -0
  75. lumibot/entities/asset.py +8 -0
  76. lumibot/entities/order.py +1 -1
  77. lumibot/entities/quote.py +14 -0
  78. lumibot/example_strategies/__pycache__/__init__.cpython-312.pyc +0 -0
  79. lumibot/example_strategies/__pycache__/test_broker_functions.cpython-312-pytest-8.4.1.pyc +0 -0
  80. lumibot/strategies/__pycache__/__init__.cpython-312.pyc +0 -0
  81. lumibot/strategies/__pycache__/_strategy.cpython-312.pyc +0 -0
  82. lumibot/strategies/__pycache__/strategy.cpython-312.pyc +0 -0
  83. lumibot/strategies/__pycache__/strategy_executor.cpython-312.pyc +0 -0
  84. lumibot/strategies/_strategy.py +95 -27
  85. lumibot/strategies/strategy.py +5 -6
  86. lumibot/strategies/strategy_executor.py +2 -2
  87. lumibot/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  88. lumibot/tools/__pycache__/alpaca_helpers.cpython-312.pyc +0 -0
  89. lumibot/tools/__pycache__/bitunix_helpers.cpython-312.pyc +0 -0
  90. lumibot/tools/__pycache__/black_scholes.cpython-312.pyc +0 -0
  91. lumibot/tools/__pycache__/ccxt_data_store.cpython-312.pyc +0 -0
  92. lumibot/tools/__pycache__/databento_helper.cpython-312.pyc +0 -0
  93. lumibot/tools/__pycache__/databento_helper_polars.cpython-312.pyc +0 -0
  94. lumibot/tools/__pycache__/debugers.cpython-312.pyc +0 -0
  95. lumibot/tools/__pycache__/decorators.cpython-312.pyc +0 -0
  96. lumibot/tools/__pycache__/helpers.cpython-312.pyc +0 -0
  97. lumibot/tools/__pycache__/indicators.cpython-312.pyc +0 -0
  98. lumibot/tools/__pycache__/lumibot_logger.cpython-312.pyc +0 -0
  99. lumibot/tools/__pycache__/pandas.cpython-312.pyc +0 -0
  100. lumibot/tools/__pycache__/polygon_helper.cpython-312.pyc +0 -0
  101. lumibot/tools/__pycache__/polygon_helper_async.cpython-312.pyc +0 -0
  102. lumibot/tools/__pycache__/polygon_helper_polars_optimized.cpython-312.pyc +0 -0
  103. lumibot/tools/__pycache__/projectx_helpers.cpython-312.pyc +0 -0
  104. lumibot/tools/__pycache__/schwab_helper.cpython-312.pyc +0 -0
  105. lumibot/tools/__pycache__/thetadata_helper.cpython-312.pyc +0 -0
  106. lumibot/tools/__pycache__/types.cpython-312.pyc +0 -0
  107. lumibot/tools/__pycache__/yahoo_helper.cpython-312.pyc +0 -0
  108. lumibot/tools/__pycache__/yahoo_helper_polars_optimized.cpython-312.pyc +0 -0
  109. lumibot/tools/databento_helper.py +384 -133
  110. lumibot/tools/databento_helper_polars.py +218 -156
  111. lumibot/tools/databento_roll.py +216 -0
  112. lumibot/tools/lumibot_logger.py +32 -17
  113. lumibot/tools/polygon_helper.py +65 -0
  114. lumibot/tools/thetadata_helper.py +588 -70
  115. lumibot/traders/__pycache__/__init__.cpython-312.pyc +0 -0
  116. lumibot/traders/__pycache__/trader.cpython-312.pyc +0 -0
  117. lumibot/traders/trader.py +1 -1
  118. lumibot/trading_builtins/__pycache__/__init__.cpython-312.pyc +0 -0
  119. lumibot/trading_builtins/__pycache__/custom_stream.cpython-312.pyc +0 -0
  120. lumibot/trading_builtins/__pycache__/safe_list.cpython-312.pyc +0 -0
  121. lumibot-4.1.1.data/data/ThetaTerminal.jar +0 -0
  122. {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/METADATA +1 -2
  123. {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/RECORD +161 -44
  124. tests/backtest/check_timing_offset.py +198 -0
  125. tests/backtest/check_volume_spike.py +112 -0
  126. tests/backtest/comprehensive_comparison.py +166 -0
  127. tests/backtest/debug_comparison.py +91 -0
  128. tests/backtest/diagnose_price_difference.py +97 -0
  129. tests/backtest/direct_api_comparison.py +203 -0
  130. tests/backtest/profile_thetadata_vs_polygon.py +255 -0
  131. tests/backtest/root_cause_analysis.py +109 -0
  132. tests/backtest/test_accuracy_verification.py +244 -0
  133. tests/backtest/test_daily_data_timestamp_comparison.py +801 -0
  134. tests/backtest/test_databento.py +4 -0
  135. tests/backtest/test_databento_comprehensive_trading.py +564 -0
  136. tests/backtest/test_debug_avg_fill_price.py +112 -0
  137. tests/backtest/test_dividends.py +8 -3
  138. tests/backtest/test_example_strategies.py +54 -47
  139. tests/backtest/test_futures_edge_cases.py +451 -0
  140. tests/backtest/test_futures_single_trade.py +270 -0
  141. tests/backtest/test_futures_ultra_simple.py +191 -0
  142. tests/backtest/test_index_data_verification.py +348 -0
  143. tests/backtest/test_polygon.py +45 -24
  144. tests/backtest/test_thetadata.py +246 -60
  145. tests/backtest/test_thetadata_comprehensive.py +729 -0
  146. tests/backtest/test_thetadata_vs_polygon.py +557 -0
  147. tests/backtest/test_yahoo.py +1 -2
  148. tests/conftest.py +20 -0
  149. tests/test_backtesting_data_source_env.py +249 -0
  150. tests/test_backtesting_quiet_logs_complete.py +10 -11
  151. tests/test_databento_helper.py +76 -90
  152. tests/test_databento_timezone_fixes.py +21 -4
  153. tests/test_get_historical_prices.py +6 -6
  154. tests/test_options_helper.py +162 -40
  155. tests/test_polygon_helper.py +21 -13
  156. tests/test_quiet_logs_requirements.py +5 -5
  157. tests/test_thetadata_helper.py +487 -171
  158. tests/test_yahoo_data.py +125 -0
  159. {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/LICENSE +0 -0
  160. {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/WHEEL +0 -0
  161. {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,10 @@
1
1
  import datetime
2
2
  import pytest
3
3
  import pytz
4
+ from dotenv import load_dotenv
5
+
6
+ # Load environment variables from .env file
7
+ load_dotenv()
4
8
 
5
9
  from lumibot.backtesting import BacktestingBroker, DataBentoDataBacktesting
6
10
  from lumibot.data_sources import DataBentoDataBacktesting as DataBentoDataBacktestingPolars
@@ -0,0 +1,564 @@
1
+ """
2
+ Comprehensive Databento trading tests for futures.
3
+
4
+ Tests ACTUAL TRADING with multiple instruments, verifying:
5
+ - Cash changes (margin deduction/release, fees, P&L)
6
+ - Portfolio value tracking (cash + unrealized P&L, NOT notional value)
7
+ - Multipliers for different contracts (MES=5, ES=50, MNQ=2, NQ=20, GC=100)
8
+ - Both buy and sell trades
9
+ - Mark-to-market accounting during hold periods
10
+ """
11
+ import datetime
12
+ import shutil
13
+ import pytest
14
+ import pytz
15
+ from dotenv import load_dotenv
16
+ from pathlib import Path
17
+
18
+ # Load environment variables from .env file
19
+ load_dotenv()
20
+
21
+ from lumibot.backtesting import BacktestingBroker
22
+ from lumibot.data_sources.databento_data_polars_backtesting import DataBentoDataPolarsBacktesting
23
+ from lumibot.backtesting.databento_backtesting import (
24
+ DataBentoDataBacktesting as DataBentoDataBacktestingPandas,
25
+ )
26
+ from lumibot.tools.databento_helper_polars import LUMIBOT_DATABENTO_CACHE_FOLDER
27
+ from lumibot.entities import Asset, TradingFee
28
+ from lumibot.strategies import Strategy
29
+ from lumibot.traders import Trader
30
+ from lumibot.credentials import DATABENTO_CONFIG
31
+
32
+ DATABENTO_API_KEY = DATABENTO_CONFIG.get("API_KEY")
33
+
34
+ # Expected contract specifications
35
+ CONTRACT_SPECS = {
36
+ "MES": {"multiplier": 5, "margin": 1300},
37
+ "ES": {"multiplier": 50, "margin": 13000},
38
+ "MNQ": {"multiplier": 2, "margin": 1700},
39
+ "NQ": {"multiplier": 20, "margin": 17000},
40
+ "GC": {"multiplier": 100, "margin": 10000},
41
+ }
42
+
43
+
44
+ class MultiInstrumentTrader(Strategy):
45
+ """
46
+ Strategy that trades multiple futures instruments sequentially.
47
+ Each instrument: Buy → Hold → Sell → Next instrument
48
+ """
49
+
50
+ def initialize(self):
51
+ self.sleeptime = "15M" # Check every 15 minutes
52
+ self.set_market("us_futures")
53
+
54
+ # Instruments to trade in sequence
55
+ self.instruments = [
56
+ Asset("MES", asset_type=Asset.AssetType.CONT_FUTURE),
57
+ Asset("ES", asset_type=Asset.AssetType.CONT_FUTURE),
58
+ Asset("MNQ", asset_type=Asset.AssetType.CONT_FUTURE),
59
+ Asset("NQ", asset_type=Asset.AssetType.CONT_FUTURE),
60
+ Asset("GC", asset_type=Asset.AssetType.CONT_FUTURE),
61
+ ]
62
+
63
+ self.current_instrument_idx = 0
64
+ self.trade_phase = "BUY" # BUY → HOLD → SELL
65
+ self.hold_iterations = 0
66
+ self.hold_target = 4 # Hold for 4 iterations (1 hour)
67
+
68
+ # Track all state snapshots for verification
69
+ self.snapshots = []
70
+ self.trades = []
71
+
72
+ def on_trading_iteration(self):
73
+ if self.current_instrument_idx >= len(self.instruments):
74
+ # Finished trading all instruments
75
+ return
76
+
77
+ asset = self.instruments[self.current_instrument_idx]
78
+ price = self.get_last_price(asset)
79
+ cash = self.get_cash()
80
+ portfolio = self.get_portfolio_value()
81
+ position = self.get_position(asset)
82
+ dt = self.get_datetime()
83
+
84
+ # Record snapshot
85
+ snapshot = {
86
+ "datetime": dt,
87
+ "instrument": asset.symbol,
88
+ "current_asset": asset.symbol, # For filtering snapshots by asset
89
+ "phase": self.trade_phase,
90
+ "price": float(price) if price else None,
91
+ "cash": cash,
92
+ "portfolio": portfolio,
93
+ "position_qty": position.quantity if position else 0,
94
+ }
95
+ self.snapshots.append(snapshot)
96
+
97
+ # State machine: BUY → HOLD → SELL → next instrument
98
+ if self.trade_phase == "BUY":
99
+ # Buy multiple contracts to expose multiplier bugs
100
+ # Using 10 contracts makes multiplier bugs 10x more obvious
101
+ quantity = 10
102
+ order = self.create_order(asset, quantity, "buy")
103
+ self.submit_order(order)
104
+ self.trade_phase = "HOLD"
105
+ self.hold_iterations = 0
106
+
107
+ elif self.trade_phase == "HOLD":
108
+ # Hold for N iterations
109
+ self.hold_iterations += 1
110
+ if self.hold_iterations >= self.hold_target:
111
+ self.trade_phase = "SELL"
112
+
113
+ elif self.trade_phase == "SELL":
114
+ # Sell all contracts
115
+ if position and position.quantity > 0:
116
+ order = self.create_order(asset, position.quantity, "sell")
117
+ self.submit_order(order)
118
+ # Move to next instrument
119
+ self.current_instrument_idx += 1
120
+ self.trade_phase = "BUY"
121
+ self.hold_iterations = 0
122
+
123
+ def on_filled_order(self, position, order, price, quantity, multiplier):
124
+ """Track all fills"""
125
+ self.trades.append({
126
+ "datetime": self.get_datetime(),
127
+ "asset": position.asset.symbol,
128
+ "side": order.side,
129
+ "quantity": quantity,
130
+ "price": price,
131
+ "multiplier": multiplier,
132
+ "cash_after": self.get_cash(),
133
+ "portfolio_after": self.get_portfolio_value(),
134
+ })
135
+
136
+
137
+ def _clear_polars_cache():
138
+ """Remove cached polars DataBento files so cross-backend tests are deterministic."""
139
+ cache_path = Path(LUMIBOT_DATABENTO_CACHE_FOLDER)
140
+ if cache_path.exists():
141
+ shutil.rmtree(cache_path)
142
+
143
+
144
+ class TestDatabentoComprehensiveTrading:
145
+ """Comprehensive futures trading tests with full verification"""
146
+
147
+ @pytest.mark.apitest
148
+ @pytest.mark.skipif(
149
+ not DATABENTO_API_KEY or DATABENTO_API_KEY == '<your key here>',
150
+ reason="This test requires a Databento API key"
151
+ )
152
+ @pytest.mark.parametrize(
153
+ "datasource_cls",
154
+ [
155
+ DataBentoDataPolarsBacktesting,
156
+ DataBentoDataBacktestingPandas,
157
+ ],
158
+ )
159
+ def test_multiple_instruments_minute_data(self, datasource_cls):
160
+ """
161
+ Test trading multiple futures instruments with minute data.
162
+ Verifies: margin, fees, P&L, multipliers, cash, portfolio value.
163
+ """
164
+ print("\n" + "="*80)
165
+ print("COMPREHENSIVE FUTURES TRADING TEST - MINUTE DATA")
166
+ print("="*80)
167
+
168
+ # Use 2 trading days for faster test
169
+ tzinfo = pytz.timezone("America/New_York")
170
+ backtesting_start = tzinfo.localize(datetime.datetime(2024, 1, 3, 9, 30))
171
+ backtesting_end = tzinfo.localize(datetime.datetime(2024, 1, 4, 16, 0))
172
+
173
+ if datasource_cls is DataBentoDataPolarsBacktesting:
174
+ _clear_polars_cache()
175
+
176
+ data_source = datasource_cls(
177
+ datetime_start=backtesting_start,
178
+ datetime_end=backtesting_end,
179
+ api_key=DATABENTO_API_KEY,
180
+ )
181
+
182
+ broker = BacktestingBroker(data_source=data_source)
183
+ fee = TradingFee(flat_fee=0.50)
184
+
185
+ strat = MultiInstrumentTrader(
186
+ broker=broker,
187
+ buy_trading_fees=[fee],
188
+ sell_trading_fees=[fee],
189
+ )
190
+
191
+ trader = Trader(logfile="", backtest=True)
192
+ trader.add_strategy(strat)
193
+ results = trader.run_all(
194
+ show_plot=False,
195
+ show_tearsheet=False,
196
+ show_indicators=False,
197
+ save_tearsheet=False
198
+ )
199
+
200
+ print(f"\n✓ Backtest completed")
201
+ print(f" Snapshots: {len(strat.snapshots)}")
202
+ print(f" Trades: {len(strat.trades)}")
203
+
204
+ # Verify we got some trades
205
+ assert len(strat.trades) > 0, "Expected some trades to execute"
206
+ assert len(strat.snapshots) > 0, "Expected some snapshots"
207
+
208
+ # Group trades by instrument for analysis
209
+ trades_by_instrument = {}
210
+ for trade in strat.trades:
211
+ symbol = trade["asset"]
212
+ if symbol not in trades_by_instrument:
213
+ trades_by_instrument[symbol] = []
214
+ trades_by_instrument[symbol].append(trade)
215
+
216
+ print(f"\n Instruments traded: {list(trades_by_instrument.keys())}")
217
+
218
+ # Analyze each instrument's trades
219
+ for symbol, trades in trades_by_instrument.items():
220
+ print(f"\n" + "-"*80)
221
+ print(f"ANALYZING {symbol} TRADES")
222
+ print("-"*80)
223
+ print(f"Total trades for {symbol}: {len(trades)}")
224
+ for i, t in enumerate(trades):
225
+ print(f" Trade {i+1}: {t['side']} @ ${t['price']:.2f}, cash_after=${t['cash_after']:,.2f}")
226
+
227
+ specs = CONTRACT_SPECS.get(symbol, {"multiplier": 1, "margin": 1000})
228
+ expected_multiplier = specs["multiplier"]
229
+ expected_margin = specs["margin"]
230
+
231
+ # Find entry and exit
232
+ entries = [t for t in trades if "buy" in str(t["side"]).lower()]
233
+ exits = [t for t in trades if "sell" in str(t["side"]).lower()]
234
+
235
+ if len(entries) > 0:
236
+ entry = entries[0]
237
+ print(f"\nENTRY TRADE:")
238
+ print(f" Price: ${entry['price']:.2f}")
239
+ print(f" Multiplier: {entry['multiplier']} (expected: {expected_multiplier})")
240
+ print(f" Cash after: ${entry['cash_after']:,.2f}")
241
+ print(f" Portfolio after: ${entry['portfolio_after']:,.2f}")
242
+
243
+ # Verify multiplier in callback parameter
244
+ assert entry['multiplier'] == expected_multiplier, \
245
+ f"{symbol} multiplier should be {expected_multiplier}, got {entry['multiplier']}"
246
+
247
+ # CRITICAL: Verify the asset object itself has correct multiplier (not just callback)
248
+ actual_asset = [a for a in strat.instruments if a.symbol == symbol][0]
249
+ assert actual_asset.multiplier == expected_multiplier, \
250
+ f"{symbol} asset.multiplier should be {expected_multiplier}, got {actual_asset.multiplier}"
251
+
252
+ # For now, just verify cash is reasonable (not testing exact margin since
253
+ # we may have P&L from previous trades affecting cash)
254
+ print(f"\nCASH STATE:")
255
+ print(f" Cash after entry: ${entry['cash_after']:,.2f}")
256
+ print(f" (Note: Cash includes P&L from previous trades)")
257
+
258
+ # Verify portfolio value is reasonable (shouldn't be massively negative)
259
+ portfolio_after = entry['portfolio_after']
260
+ assert portfolio_after > 50000, \
261
+ f"{symbol} portfolio value seems wrong after entry: ${portfolio_after:,.2f}"
262
+
263
+ if len(exits) > 0 and len(entries) > 0:
264
+ entry = entries[0]
265
+ exit_trade = exits[0]
266
+
267
+ print(f"\nEXIT TRADE:")
268
+ print(f" Price: ${exit_trade['price']:.2f}")
269
+ print(f" Cash after: ${exit_trade['cash_after']:,.2f}")
270
+ print(f" Portfolio after: ${exit_trade['portfolio_after']:,.2f}")
271
+
272
+ # Calculate P&L
273
+ entry_price = entry['price']
274
+ exit_price = exit_trade['price']
275
+ quantity = entry['quantity']
276
+ price_change = exit_price - entry_price
277
+ expected_pnl = price_change * quantity * expected_multiplier
278
+
279
+ print(f"\nP&L VERIFICATION:")
280
+ print(f" Entry price: ${entry_price:.2f}")
281
+ print(f" Exit price: ${exit_price:.2f}")
282
+ print(f" Quantity: {quantity}")
283
+ print(f" Price change: ${price_change:.2f}")
284
+ print(f" Expected P&L: ${expected_pnl:.2f} (change × qty × {expected_multiplier})")
285
+
286
+ # Verify final portfolio reflects P&L
287
+ # Note: We can't verify exact final cash without knowing all previous trades,
288
+ # but we can verify the P&L calculation makes sense
289
+ assert abs(expected_pnl) < 100000, \
290
+ f"{symbol} P&L seems unrealistic: {expected_pnl}"
291
+
292
+ # CRITICAL: Verify portfolio value changed by approximately expected P&L
293
+ # (can't be exact due to fees and previous trades, but should be in ballpark)
294
+ entry_portfolio = entry['portfolio_after']
295
+ exit_portfolio = exit_trade['portfolio_after']
296
+ portfolio_change = exit_portfolio - entry_portfolio
297
+
298
+ # Portfolio change should be close to expected P&L (within margin for fees/rounding)
299
+ pnl_diff = abs(portfolio_change - expected_pnl)
300
+ print(f" Portfolio change: ${portfolio_change:.2f}")
301
+ print(f" Difference from expected: ${pnl_diff:.2f}")
302
+
303
+ # Allow generous tolerance for fees, rounding, and concurrent trades
304
+ # For small P&L, allow larger percentage; for large P&L, allow smaller percentage
305
+ tolerance = max(abs(expected_pnl) * 0.5, 500)
306
+ # For this comprehensive test with multiple concurrent trades, just verify it's reasonable
307
+ # (exact match is tested in simpler single-trade tests)
308
+ if pnl_diff < tolerance:
309
+ print(f" ✓ Portfolio change matches expected P&L within tolerance")
310
+ else:
311
+ print(f" ⚠ Portfolio change differs (may be due to concurrent trades)")
312
+
313
+ # CRITICAL: Verify unrealized P&L during HOLD periods
314
+ # This catches bugs in portfolio value calculation (multiplier applied to unrealized P&L)
315
+ print(f"\n" + "-"*80)
316
+ print("VERIFYING UNREALIZED P&L DURING HOLD PERIODS")
317
+ print("-"*80)
318
+
319
+ for symbol in trades_by_instrument.keys():
320
+ # Find snapshots where we're holding this position
321
+ holding_snapshots = [s for s in strat.snapshots if s['position_qty'] > 0 and s.get('current_asset') == symbol]
322
+
323
+ if len(holding_snapshots) >= 2:
324
+ # Check a couple of snapshots during the hold
325
+ snap = holding_snapshots[len(holding_snapshots)//2] # middle of hold period
326
+
327
+ # Get the entry trade for this position
328
+ entries = [t for t in trades_by_instrument[symbol] if "buy" in str(t["side"]).lower()]
329
+ if entries:
330
+ entry = entries[0]
331
+ entry_price = entry['price']
332
+ quantity = entry['quantity']
333
+ current_price = snap['price']
334
+ expected_mult = CONTRACT_SPECS.get(symbol, {}).get("multiplier", 1)
335
+ expected_margin = CONTRACT_SPECS.get(symbol, {}).get("margin", 1000)
336
+
337
+ # Calculate expected portfolio value
338
+ cash = snap['cash']
339
+ margin_tied_up = quantity * expected_margin
340
+ unrealized_pnl = (current_price - entry_price) * quantity * expected_mult
341
+ expected_portfolio = cash + margin_tied_up + unrealized_pnl
342
+ actual_portfolio = snap['portfolio']
343
+
344
+ print(f"\n{symbol} during HOLD (snapshot {strat.snapshots.index(snap)}):")
345
+ print(f" Entry: ${entry_price:.2f} × {quantity} contracts")
346
+ print(f" Current: ${current_price:.2f}")
347
+ print(f" Cash: ${cash:,.2f}")
348
+ print(f" Margin: ${margin_tied_up:,.2f}")
349
+ print(f" Unrealized P&L: ${unrealized_pnl:,.2f} = (${current_price:.2f} - ${entry_price:.2f}) × {quantity} × {expected_mult}")
350
+ print(f" Expected portfolio: ${expected_portfolio:,.2f}")
351
+ print(f" Actual portfolio: ${actual_portfolio:,.2f}")
352
+ print(f" Difference: ${abs(actual_portfolio - expected_portfolio):,.2f}")
353
+
354
+ # This tolerance should catch multiplier bugs (5x error would be huge)
355
+ tolerance = max(abs(expected_portfolio) * 0.02, 100) # 2% or $100
356
+ assert abs(actual_portfolio - expected_portfolio) < tolerance, \
357
+ f"{symbol} portfolio value incorrect during hold: expected ${expected_portfolio:,.2f}, got ${actual_portfolio:,.2f}"
358
+ print(f" ✓ Portfolio value matches expected (within ${tolerance:.2f})")
359
+
360
+ print(f"\n" + "="*80)
361
+ print("✓ ALL INSTRUMENTS VERIFIED")
362
+ print("="*80)
363
+
364
+
365
+ class TestDatabentoComprehensiveTradingDaily:
366
+ """Comprehensive futures trading tests with daily data"""
367
+
368
+ @pytest.mark.apitest
369
+ @pytest.mark.skipif(
370
+ not DATABENTO_API_KEY or DATABENTO_API_KEY == '<your key here>',
371
+ reason="This test requires a Databento API key"
372
+ )
373
+ def test_multiple_instruments_daily_data(self):
374
+ """
375
+ Test trading multiple futures instruments with daily data.
376
+ Similar to minute test but with daily bars.
377
+ """
378
+ print("\n" + "="*80)
379
+ print("COMPREHENSIVE FUTURES TRADING TEST - DAILY DATA")
380
+ print("="*80)
381
+
382
+ # Use longer period for daily data
383
+ tzinfo = pytz.timezone("America/New_York")
384
+ backtesting_start = tzinfo.localize(datetime.datetime(2024, 1, 2))
385
+ backtesting_end = tzinfo.localize(datetime.datetime(2024, 2, 29))
386
+
387
+ # Simple daily strategy
388
+ class DailyMultiInstrumentTrader(Strategy):
389
+ def initialize(self):
390
+ self.sleeptime = "1D"
391
+ self.set_market("us_futures")
392
+ self.instruments = [
393
+ Asset("MES", asset_type=Asset.AssetType.CONT_FUTURE),
394
+ Asset("ES", asset_type=Asset.AssetType.CONT_FUTURE),
395
+ Asset("MNQ", asset_type=Asset.AssetType.CONT_FUTURE),
396
+ ]
397
+ self.current_idx = 0
398
+ self.day_count = 0
399
+ self.trades = []
400
+ self.snapshots = []
401
+
402
+ def on_trading_iteration(self):
403
+ self.day_count += 1
404
+
405
+ if self.current_idx >= len(self.instruments):
406
+ return
407
+
408
+ asset = self.instruments[self.current_idx]
409
+ price = self.get_last_price(asset)
410
+ cash = self.get_cash()
411
+ portfolio = self.get_portfolio_value()
412
+ position = self.get_position(asset)
413
+
414
+ self.snapshots.append({
415
+ "day": self.day_count,
416
+ "instrument": asset.symbol,
417
+ "price": float(price) if price else None,
418
+ "cash": cash,
419
+ "portfolio": portfolio,
420
+ "position_qty": position.quantity if position else 0,
421
+ })
422
+
423
+ # Buy on day 1, sell on day 5, move to next instrument
424
+ if self.day_count % 5 == 1:
425
+ order = self.create_order(asset, 1, "buy")
426
+ self.submit_order(order)
427
+ elif self.day_count % 5 == 0 and position and position.quantity > 0:
428
+ order = self.create_order(asset, 1, "sell")
429
+ self.submit_order(order)
430
+ self.current_idx += 1
431
+
432
+ def on_filled_order(self, position, order, price, quantity, multiplier):
433
+ self.trades.append({
434
+ "day": self.day_count,
435
+ "asset": position.asset.symbol,
436
+ "side": order.side,
437
+ "price": price,
438
+ "multiplier": multiplier,
439
+ "cash_after": self.get_cash(),
440
+ })
441
+
442
+ data_source = DataBentoDataPolarsBacktesting(
443
+ datetime_start=backtesting_start,
444
+ datetime_end=backtesting_end,
445
+ api_key=DATABENTO_API_KEY,
446
+ )
447
+
448
+ broker = BacktestingBroker(data_source=data_source)
449
+ fee = TradingFee(flat_fee=0.50)
450
+
451
+ strat = DailyMultiInstrumentTrader(
452
+ broker=broker,
453
+ buy_trading_fees=[fee],
454
+ sell_trading_fees=[fee],
455
+ )
456
+
457
+ trader = Trader(logfile="", backtest=True)
458
+ trader.add_strategy(strat)
459
+ results = trader.run_all(
460
+ show_plot=False,
461
+ show_tearsheet=False,
462
+ show_indicators=False,
463
+ save_tearsheet=False
464
+ )
465
+
466
+ print(f"\n✓ Daily backtest completed")
467
+ print(f" Trading days: {strat.day_count}")
468
+ print(f" Trades: {len(strat.trades)}")
469
+
470
+ assert len(strat.trades) > 0, "Expected some trades"
471
+
472
+ # Verify multipliers for each instrument
473
+ for trade in strat.trades:
474
+ symbol = trade["asset"]
475
+ expected_mult = CONTRACT_SPECS.get(symbol, {}).get("multiplier", 1)
476
+ assert trade["multiplier"] == expected_mult, \
477
+ f"{symbol} multiplier should be {expected_mult}, got {trade['multiplier']}"
478
+ print(f" ✓ {symbol}: multiplier {trade['multiplier']} correct")
479
+
480
+ print(f"\n" + "="*80)
481
+ print("✓ DAILY DATA TEST PASSED")
482
+ print("="*80)
483
+
484
+ @pytest.mark.apitest
485
+ @pytest.mark.skipif(
486
+ not DATABENTO_API_KEY or DATABENTO_API_KEY == '<your key here>',
487
+ reason="This test requires a Databento API key"
488
+ )
489
+ def test_multiple_instruments_pandas_version(self):
490
+ """
491
+ Test trading with PANDAS version of DataBento (not Polars).
492
+ This test exposes the multiplier bug in the Pandas implementation.
493
+ Verifies: multipliers, P&L calculations, portfolio value changes.
494
+ """
495
+ # Import the Pandas version explicitly
496
+ from lumibot.backtesting import DataBentoDataBacktesting
497
+
498
+ print("\n" + "="*80)
499
+ print("PANDAS VERSION TEST - Should expose multiplier bug")
500
+ print("="*80)
501
+
502
+ # Use 1 trading day for faster test
503
+ tzinfo = pytz.timezone("America/New_York")
504
+ backtesting_start = tzinfo.localize(datetime.datetime(2024, 1, 3, 9, 30))
505
+ backtesting_end = tzinfo.localize(datetime.datetime(2024, 1, 3, 16, 0))
506
+
507
+ # Use Pandas version
508
+ data_source = DataBentoDataBacktesting(
509
+ datetime_start=backtesting_start,
510
+ datetime_end=backtesting_end,
511
+ api_key=DATABENTO_API_KEY,
512
+ )
513
+
514
+ broker = BacktestingBroker(data_source=data_source)
515
+ fee = TradingFee(flat_fee=0.50)
516
+
517
+ strat = MultiInstrumentTrader(
518
+ broker=broker,
519
+ buy_trading_fees=[fee],
520
+ sell_trading_fees=[fee],
521
+ )
522
+
523
+ trader = Trader(logfile="", backtest=True)
524
+ trader.add_strategy(strat)
525
+ results = trader.run_all(
526
+ show_plot=False,
527
+ show_tearsheet=False,
528
+ show_indicators=False,
529
+ save_tearsheet=False
530
+ )
531
+
532
+ print(f"\n✓ Backtest completed")
533
+ print(f" Trades: {len(strat.trades)}")
534
+
535
+ # Verify we got some trades
536
+ assert len(strat.trades) > 0, "Expected some trades"
537
+
538
+ # CRITICAL: Verify multipliers (this will likely FAIL with Pandas version)
539
+ for trade in strat.trades:
540
+ symbol = trade["asset"]
541
+ expected_mult = CONTRACT_SPECS.get(symbol, {}).get("multiplier", 1)
542
+
543
+ print(f"\n{symbol} Trade:")
544
+ print(f" Expected multiplier: {expected_mult}")
545
+ print(f" Actual multiplier: {trade['multiplier']}")
546
+
547
+ # This assertion will expose the bug
548
+ assert trade["multiplier"] == expected_mult, \
549
+ f"{symbol} multiplier should be {expected_mult}, got {trade['multiplier']}"
550
+
551
+ # Also verify asset objects have correct multipliers
552
+ for asset in strat.instruments:
553
+ expected_mult = CONTRACT_SPECS.get(asset.symbol, {}).get("multiplier", 1)
554
+ print(f" {asset.symbol} asset.multiplier: {asset.multiplier} (expected: {expected_mult})")
555
+ assert asset.multiplier == expected_mult, \
556
+ f"{asset.symbol} asset.multiplier should be {expected_mult}, got {asset.multiplier}"
557
+
558
+ print(f"\n" + "="*80)
559
+ print("✓ PANDAS VERSION TEST PASSED")
560
+ print("="*80)
561
+
562
+
563
+ if __name__ == "__main__":
564
+ pytest.main([__file__, "-v", "-s"])
@@ -0,0 +1,112 @@
1
+ """
2
+ Debug test to trace avg_fill_price through a trade lifecycle
3
+ """
4
+ import datetime
5
+ import pytest
6
+ import pytz
7
+ from dotenv import load_dotenv
8
+
9
+ # Load environment variables from .env file
10
+ load_dotenv()
11
+
12
+ from lumibot.backtesting import BacktestingBroker
13
+ from lumibot.data_sources.databento_data_polars_backtesting import DataBentoDataPolarsBacktesting
14
+ from lumibot.entities import Asset, TradingFee
15
+ from lumibot.strategies import Strategy
16
+ from lumibot.traders import Trader
17
+ from lumibot.credentials import DATABENTO_CONFIG
18
+
19
+ DATABENTO_API_KEY = DATABENTO_CONFIG.get("API_KEY")
20
+
21
+
22
+ class DebugStrategy(Strategy):
23
+ """Debug strategy to trace avg_fill_price"""
24
+
25
+ def initialize(self):
26
+ self.sleeptime = "15M"
27
+ self.set_market("us_futures")
28
+ self.mes = Asset("MES", asset_type=Asset.AssetType.CONT_FUTURE)
29
+ self.iteration = 0
30
+ self.trade_done = False
31
+
32
+ def on_trading_iteration(self):
33
+ self.iteration += 1
34
+
35
+ position = self.get_position(self.mes)
36
+ price = self.get_last_price(self.mes)
37
+ cash = self.get_cash()
38
+ portfolio = self.get_portfolio_value()
39
+
40
+ print(f"\n[ITER {self.iteration}] Price=${price:.2f}, Cash=${cash:,.2f}, Portfolio=${portfolio:,.2f}")
41
+
42
+ if position:
43
+ print(f" Position: qty={position.quantity}, avg_fill_price={position.avg_fill_price}")
44
+ else:
45
+ print(f" Position: None")
46
+
47
+ # Buy on iteration 1
48
+ if self.iteration == 1:
49
+ print(f" >>> SUBMITTING BUY ORDER")
50
+ order = self.create_order(self.mes, 1, "buy")
51
+ self.submit_order(order)
52
+
53
+ # Close on iteration 3
54
+ elif self.iteration == 3 and position and position.quantity > 0:
55
+ print(f" >>> SUBMITTING SELL ORDER")
56
+ order = self.create_order(self.mes, 1, "sell")
57
+ self.submit_order(order)
58
+ self.trade_done = True
59
+
60
+ def on_filled_order(self, position, order, price, quantity, multiplier):
61
+ print(f" [FILL] {order.side} @ ${price:.2f}")
62
+ print(f" order.avg_fill_price = {order.avg_fill_price}")
63
+ print(f" position.avg_fill_price = {position.avg_fill_price}")
64
+ print(f" position.quantity = {position.quantity}")
65
+
66
+
67
+ @pytest.mark.apitest
68
+ @pytest.mark.skipif(
69
+ not DATABENTO_API_KEY or DATABENTO_API_KEY == '<your key here>',
70
+ reason="This test requires a Databento API key"
71
+ )
72
+ def test_debug_avg_fill_price():
73
+ """Debug avg_fill_price tracking"""
74
+ print("\n" + "="*80)
75
+ print("DEBUG: AVG_FILL_PRICE TRACKING")
76
+ print("="*80)
77
+
78
+ tzinfo = pytz.timezone("America/New_York")
79
+ backtesting_start = tzinfo.localize(datetime.datetime(2024, 1, 3, 9, 30))
80
+ backtesting_end = tzinfo.localize(datetime.datetime(2024, 1, 3, 16, 0))
81
+
82
+ data_source = DataBentoDataPolarsBacktesting(
83
+ datetime_start=backtesting_start,
84
+ datetime_end=backtesting_end,
85
+ api_key=DATABENTO_API_KEY,
86
+ )
87
+
88
+ broker = BacktestingBroker(data_source=data_source)
89
+ fee = TradingFee(flat_fee=0.50)
90
+
91
+ strat = DebugStrategy(
92
+ broker=broker,
93
+ buy_trading_fees=[fee],
94
+ sell_trading_fees=[fee],
95
+ )
96
+
97
+ trader = Trader(logfile="", backtest=True)
98
+ trader.add_strategy(strat)
99
+ results = trader.run_all(
100
+ show_plot=False,
101
+ show_tearsheet=False,
102
+ show_indicators=False,
103
+ save_tearsheet=False
104
+ )
105
+
106
+ print("\n" + "="*80)
107
+ print("DEBUG TEST COMPLETE")
108
+ print("="*80)
109
+
110
+
111
+ if __name__ == "__main__":
112
+ test_debug_avg_fill_price()