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
@@ -0,0 +1,270 @@
1
+ """
2
+ Simple, focused test for futures mark-to-market accounting.
3
+
4
+ Tests a SINGLE trade from start to finish:
5
+ 1. Buy 1 MES contract
6
+ 2. Hold for several hours
7
+ 3. Track cash and portfolio value at each iteration
8
+ 4. Verify they track MES price movements correctly
9
+ 5. Sell the contract
10
+ 6. Verify final P&L matches price change
11
+
12
+ This test should give us confidence that the basic mechanics are correct.
13
+ """
14
+ import datetime
15
+ import pytest
16
+ import pytz
17
+ from dotenv import load_dotenv
18
+
19
+ # Load environment variables from .env file
20
+ load_dotenv()
21
+
22
+ from lumibot.backtesting import BacktestingBroker
23
+ from lumibot.data_sources.databento_data_polars_backtesting import DataBentoDataPolarsBacktesting
24
+ from lumibot.entities import Asset, TradingFee
25
+ from lumibot.strategies import Strategy
26
+ from lumibot.traders import Trader
27
+ from lumibot.credentials import DATABENTO_CONFIG
28
+
29
+ DATABENTO_API_KEY = DATABENTO_CONFIG.get("API_KEY")
30
+
31
+ # Expected MES contract specs
32
+ MES_MULTIPLIER = 5 # $5 per point
33
+ MES_MARGIN = 1300 # ~$1,300 initial margin per contract
34
+
35
+
36
+ class SingleTradeTracker(Strategy):
37
+ """
38
+ Extremely simple strategy:
39
+ - Buy 1 MES contract on first iteration
40
+ - Hold for several hours
41
+ - Sell after a fixed number of iterations
42
+ - Track everything along the way
43
+ """
44
+
45
+ def initialize(self):
46
+ self.sleeptime = "15M" # Check every 15 minutes
47
+ self.set_market("us_futures")
48
+
49
+ # Create MES continuous future asset
50
+ self.mes = Asset("MES", asset_type=Asset.AssetType.CONT_FUTURE)
51
+
52
+ # Tracking variables
53
+ self.iteration_count = 0
54
+ self.entry_price = None
55
+ self.entry_cash = None
56
+ self.entry_portfolio = None
57
+
58
+ # Track state at each iteration
59
+ self.snapshots = []
60
+
61
+ # When to sell (after N iterations)
62
+ self.hold_iterations = 8 # Hold for ~2 hours (8 * 15min)
63
+
64
+ def on_trading_iteration(self):
65
+ self.iteration_count += 1
66
+
67
+ # Get current state
68
+ price = self.get_last_price(self.mes)
69
+ cash = self.get_cash()
70
+ portfolio = self.get_portfolio_value()
71
+ position = self.get_position(self.mes)
72
+ dt = self.get_datetime()
73
+
74
+ has_position = position is not None and position.quantity > 0
75
+
76
+ # Record snapshot
77
+ snapshot = {
78
+ "iteration": self.iteration_count,
79
+ "datetime": dt,
80
+ "price": float(price) if price else None,
81
+ "cash": cash,
82
+ "portfolio": portfolio,
83
+ "has_position": has_position,
84
+ "position_qty": position.quantity if position else 0,
85
+ }
86
+ self.snapshots.append(snapshot)
87
+
88
+ # BUY on first iteration
89
+ if self.iteration_count == 1:
90
+ self.entry_price = float(price)
91
+ self.entry_cash = cash
92
+ self.entry_portfolio = portfolio
93
+
94
+ order = self.create_order(self.mes, quantity=1, side="buy")
95
+ self.submit_order(order)
96
+
97
+ # SELL after holding period
98
+ elif self.iteration_count == self.hold_iterations and has_position:
99
+ order = self.create_order(self.mes, quantity=1, side="sell")
100
+ self.submit_order(order)
101
+
102
+
103
+ class TestFuturesSingleTrade:
104
+ """Test a single futures trade from start to finish"""
105
+
106
+ @pytest.mark.apitest
107
+ @pytest.mark.skipif(
108
+ not DATABENTO_API_KEY or DATABENTO_API_KEY == '<your key here>',
109
+ reason="This test requires a Databento API key"
110
+ )
111
+ def test_single_mes_trade_tracking(self):
112
+ """
113
+ Test a single MES trade and verify:
114
+ 1. Initial margin is deducted on entry
115
+ 2. Cash changes with mark-to-market during hold
116
+ 3. Portfolio value tracks cash (not adding notional value)
117
+ 4. Final P&L matches price movement * multiplier
118
+ """
119
+ # Use a single trading day
120
+ tzinfo = pytz.timezone("America/New_York")
121
+ backtesting_start = tzinfo.localize(datetime.datetime(2024, 1, 3, 9, 30))
122
+ backtesting_end = tzinfo.localize(datetime.datetime(2024, 1, 3, 16, 0))
123
+
124
+ data_source = DataBentoDataPolarsBacktesting(
125
+ datetime_start=backtesting_start,
126
+ datetime_end=backtesting_end,
127
+ databento_key=DATABENTO_API_KEY,
128
+ )
129
+
130
+ broker = BacktestingBroker(data_source=data_source)
131
+
132
+ # Set trading fee
133
+ fee = TradingFee(flat_fee=0.50)
134
+
135
+ strat = SingleTradeTracker(
136
+ broker=broker,
137
+ buy_trading_fees=[fee],
138
+ sell_trading_fees=[fee],
139
+ )
140
+
141
+ trader = Trader(logfile="", backtest=True)
142
+ trader.add_strategy(strat)
143
+ results = trader.run_all(
144
+ show_plot=False,
145
+ show_tearsheet=False,
146
+ show_indicators=False,
147
+ save_tearsheet=False
148
+ )
149
+
150
+ # Verify we got snapshots
151
+ assert len(strat.snapshots) >= 8, f"Expected at least 8 iterations, got {len(strat.snapshots)}"
152
+
153
+ print("\n" + "="*80)
154
+ print("SINGLE TRADE ANALYSIS")
155
+ print("="*80)
156
+
157
+ # Analyze snapshots
158
+ for i, snap in enumerate(strat.snapshots):
159
+ print(f"\nIteration {snap['iteration']} @ {snap['datetime']}")
160
+ print(f" Price: ${snap['price']:.2f}")
161
+ print(f" Cash: ${snap['cash']:,.2f}")
162
+ print(f" Portfolio: ${snap['portfolio']:,.2f}")
163
+ print(f" Has Position: {snap['has_position']}")
164
+
165
+ if i == 0:
166
+ # Before trade
167
+ assert snap['cash'] == 100000, "Starting cash should be $100k"
168
+ assert snap['portfolio'] == 100000, "Starting portfolio should be $100k"
169
+ assert not snap['has_position'], "Should have no position initially"
170
+
171
+ elif i == 1:
172
+ # Just after entry
173
+ # Cash should have decreased by margin + fee
174
+ expected_cash_change = -(MES_MARGIN + 0.50)
175
+ actual_cash_change = snap['cash'] - strat.snapshots[0]['cash']
176
+ cash_diff = abs(expected_cash_change - actual_cash_change)
177
+
178
+ print(f" Expected cash change: ${expected_cash_change:,.2f}")
179
+ print(f" Actual cash change: ${actual_cash_change:,.2f}")
180
+ print(f" Difference: ${cash_diff:,.2f}")
181
+
182
+ assert cash_diff < 10, f"Cash change after entry should be ~${expected_cash_change}, got ${actual_cash_change}"
183
+ assert snap['has_position'], "Should have position after entry"
184
+
185
+ # Portfolio should equal cash + margin + unrealized P&L
186
+ # At entry, unrealized P&L should be near 0, so portfolio ≈ cash + margin
187
+ expected_portfolio = snap['cash'] + MES_MARGIN
188
+ portfolio_diff = abs(snap['portfolio'] - expected_portfolio)
189
+ print(f" Portfolio: ${snap['portfolio']:,.2f}")
190
+ print(f" Expected (cash + margin): ${expected_portfolio:,.2f}")
191
+ print(f" Difference: ${portfolio_diff:,.2f}")
192
+ assert portfolio_diff < 500, f"Portfolio should equal cash + margin at entry, diff was ${portfolio_diff}"
193
+
194
+ elif snap['has_position']:
195
+ # During hold period - verify mark-to-market is working
196
+ # Get the entry snapshot (iteration 2, right after entry)
197
+ entry_snap = strat.snapshots[1] # First snapshot with position
198
+ entry_fill_price = entry_snap['price'] # This should be close to fill price
199
+
200
+ # Calculate unrealized P&L from actual fill
201
+ price_change = snap['price'] - strat.entry_price
202
+ expected_pnl = price_change * MES_MULTIPLIER
203
+
204
+ # Portfolio should be: Cash + Margin + Unrealized P&L
205
+ # (Cash has margin deducted, so portfolio adds it back plus unrealized P&L)
206
+ expected_portfolio = snap['cash'] + MES_MARGIN + expected_pnl
207
+ actual_portfolio = snap['portfolio']
208
+ portfolio_diff = abs(expected_portfolio - actual_portfolio)
209
+
210
+ print(f" Price change since entry: ${price_change:.2f}")
211
+ print(f" Expected P&L: ${expected_pnl:.2f}")
212
+ print(f" Expected portfolio: ${expected_portfolio:,.2f}")
213
+ print(f" Portfolio diff: ${portfolio_diff:,.2f}")
214
+
215
+ # Allow some tolerance for MTM timing and fill price differences
216
+ assert portfolio_diff < 500, f"Portfolio should equal Cash + Unrealized P&L, diff was ${portfolio_diff}"
217
+
218
+ # For futures with MTM, portfolio = cash + margin + unrealized P&L
219
+ # Portfolio will be ~$1,300 higher than cash (the margin), so don't check ratio
220
+
221
+ # Find exit snapshot (last one or when position closes)
222
+ exit_snap = None
223
+ for i in range(len(strat.snapshots) - 1, 0, -1):
224
+ if not strat.snapshots[i]['has_position'] and strat.snapshots[i-1]['has_position']:
225
+ exit_snap = strat.snapshots[i]
226
+ entry_snap = strat.snapshots[1] # Right after entry
227
+ break
228
+
229
+ if exit_snap:
230
+ print("\n" + "="*80)
231
+ print("EXIT ANALYSIS")
232
+ print("="*80)
233
+
234
+ # Calculate expected P&L
235
+ entry_price = strat.entry_price
236
+ exit_price = strat.snapshots[-2]['price'] # Price before exit
237
+ price_change = exit_price - entry_price
238
+ expected_pnl = price_change * MES_MULTIPLIER
239
+
240
+ # Final cash should be: starting cash + P&L - fees
241
+ expected_final_cash = 100000 + expected_pnl - 1.00 # 2 fees ($0.50 each)
242
+ actual_final_cash = exit_snap['cash']
243
+ cash_diff = abs(expected_final_cash - actual_final_cash)
244
+
245
+ print(f"Entry price: ${entry_price:.2f}")
246
+ print(f"Exit price: ${exit_price:.2f}")
247
+ print(f"Price change: ${price_change:.2f}")
248
+ print(f"Expected P&L: ${expected_pnl:.2f}")
249
+ print(f"Fees: $1.00")
250
+ print(f"Expected final cash: ${expected_final_cash:,.2f}")
251
+ print(f"Actual final cash: ${actual_final_cash:,.2f}")
252
+ print(f"Difference: ${cash_diff:,.2f}")
253
+
254
+ # Verify final cash is correct (allow tolerance for fill price differences)
255
+ assert cash_diff < 150, f"Final cash should match expected P&L, diff was ${cash_diff}"
256
+
257
+ # Verify portfolio equals cash at end
258
+ portfolio_diff = abs(exit_snap['portfolio'] - exit_snap['cash'])
259
+ print(f"Final portfolio-cash diff: ${portfolio_diff:,.2f}")
260
+ assert portfolio_diff < 10, f"Final portfolio should equal cash, diff was ${portfolio_diff}"
261
+
262
+ print("\n" + "="*80)
263
+ print("✓ ALL CHECKS PASSED")
264
+ print("="*80)
265
+
266
+
267
+ if __name__ == "__main__":
268
+ # Run the test directly
269
+ test = TestFuturesSingleTrade()
270
+ test.test_single_mes_trade_tracking()
@@ -0,0 +1,191 @@
1
+ """
2
+ ULTRA SIMPLE futures test - checking ONE thing at a time.
3
+
4
+ No complex strategies, no indicators, no bracket orders.
5
+ Just: Buy 1 contract → hold → sell → verify numbers match reality.
6
+ """
7
+ import datetime
8
+ import pytest
9
+ import pytz
10
+
11
+ from lumibot.backtesting import BacktestingBroker
12
+ from lumibot.data_sources.databento_data_polars_backtesting import DataBentoDataPolarsBacktesting
13
+ from lumibot.entities import Asset, TradingFee
14
+ from lumibot.strategies import Strategy
15
+ from lumibot.traders import Trader
16
+ from lumibot.credentials import DATABENTO_CONFIG
17
+
18
+ DATABENTO_API_KEY = DATABENTO_CONFIG.get("API_KEY")
19
+
20
+
21
+ class UltraSimpleStrategy(Strategy):
22
+ """Buy on iteration 1, sell on iteration 5. That's it."""
23
+
24
+ def initialize(self):
25
+ self.sleeptime = "30M" # Every 30 minutes
26
+ self.set_market("us_futures")
27
+ self.mes = Asset("MES", asset_type=Asset.AssetType.CONT_FUTURE)
28
+ self.iteration = 0
29
+ self.snapshots = []
30
+
31
+ def on_trading_iteration(self):
32
+ self.iteration += 1
33
+
34
+ # Get state
35
+ price = self.get_last_price(self.mes)
36
+ cash = self.get_cash()
37
+ portfolio = self.get_portfolio_value()
38
+ position = self.get_position(self.mes)
39
+
40
+ # Save snapshot
41
+ self.snapshots.append({
42
+ "iteration": self.iteration,
43
+ "datetime": self.get_datetime(),
44
+ "price": float(price) if price else None,
45
+ "cash": cash,
46
+ "portfolio": portfolio,
47
+ "position_qty": position.quantity if position else 0,
48
+ })
49
+
50
+ # Buy on iteration 1
51
+ if self.iteration == 1:
52
+ print(f"[ITER 1] BEFORE BUY: Cash=${cash:,.2f}, Portfolio=${portfolio:,.2f}, Price=${price:.2f}")
53
+ order = self.create_order(self.mes, 1, "buy")
54
+ self.submit_order(order)
55
+
56
+ # Sell on iteration 5
57
+ elif self.iteration == 5 and position and position.quantity > 0:
58
+ print(f"[ITER 5] BEFORE SELL: Cash=${cash:,.2f}, Portfolio=${portfolio:,.2f}, Price=${price:.2f}")
59
+ order = self.create_order(self.mes, 1, "sell")
60
+ self.submit_order(order)
61
+
62
+ def on_filled_order(self, position, order, price, quantity, multiplier):
63
+ """Track fills"""
64
+ cash_after = self.get_cash()
65
+ portfolio_after = self.get_portfolio_value()
66
+ print(f"[FILL] {order.side} @ ${price:.2f} → Cash=${cash_after:,.2f}, Portfolio=${portfolio_after:,.2f}")
67
+
68
+
69
+ @pytest.mark.skipif(
70
+ not DATABENTO_API_KEY or DATABENTO_API_KEY == '<your key here>',
71
+ reason="Requires DataBento API key for futures data"
72
+ )
73
+ def test_ultra_simple_buy_hold_sell():
74
+ """
75
+ The simplest possible test:
76
+ 1. Start with $100k
77
+ 2. Buy 1 MES contract
78
+ 3. Hold for a few iterations
79
+ 4. Sell 1 MES contract
80
+ 5. Print everything and manually verify it makes sense
81
+ """
82
+ print("\n" + "="*80)
83
+ print("ULTRA SIMPLE FUTURES TEST")
84
+ print("="*80)
85
+
86
+ # Single day
87
+ tzinfo = pytz.timezone("America/New_York")
88
+ backtesting_start = tzinfo.localize(datetime.datetime(2024, 1, 3, 9, 30))
89
+ backtesting_end = tzinfo.localize(datetime.datetime(2024, 1, 3, 16, 0))
90
+
91
+ data_source = DataBentoDataPolarsBacktesting(
92
+ datetime_start=backtesting_start,
93
+ datetime_end=backtesting_end,
94
+ databento_key=DATABENTO_API_KEY,
95
+ )
96
+
97
+ broker = BacktestingBroker(data_source=data_source)
98
+ fee = TradingFee(flat_fee=0.50)
99
+
100
+ strat = UltraSimpleStrategy(
101
+ broker=broker,
102
+ buy_trading_fees=[fee],
103
+ sell_trading_fees=[fee],
104
+ )
105
+
106
+ trader = Trader(logfile="", backtest=True)
107
+ trader.add_strategy(strat)
108
+ results = trader.run_all(
109
+ show_plot=False,
110
+ show_tearsheet=False,
111
+ show_indicators=False,
112
+ save_tearsheet=False
113
+ )
114
+
115
+ print("\n" + "="*80)
116
+ print("SNAPSHOT ANALYSIS")
117
+ print("="*80)
118
+
119
+ for snap in strat.snapshots:
120
+ print(f"\nIteration {snap['iteration']} @ {snap['datetime']}")
121
+ print(f" Price: ${snap['price']:.2f}")
122
+ print(f" Cash: ${snap['cash']:,.2f}")
123
+ print(f" Portfolio: ${snap['portfolio']:,.2f}")
124
+ print(f" Position: {snap['position_qty']} contracts")
125
+
126
+ # Calculate what we expect
127
+ if snap['iteration'] == 1:
128
+ print(f" ✓ Starting state (no position yet)")
129
+
130
+ elif snap['iteration'] == 2:
131
+ # After buy
132
+ print(f" → After BUY:")
133
+ print(f" - Cash should drop by margin (~$1,300) + fee ($0.50)")
134
+ print(f" - Portfolio should equal Cash (not cash + notional value)")
135
+
136
+ elif snap['position_qty'] > 0:
137
+ # Holding position
138
+ print(f" → HOLDING position:")
139
+ print(f" - Portfolio should track price movements")
140
+ print(f" - Portfolio should equal Cash (mark-to-market)")
141
+
142
+ elif snap['iteration'] > 5 and snap['position_qty'] == 0:
143
+ # After sell
144
+ print(f" → After SELL:")
145
+ print(f" - Margin should be released")
146
+ print(f" - Cash should reflect total P&L minus fees")
147
+
148
+ print("\n" + "="*80)
149
+ print("MANUAL CHECKS TO DO:")
150
+ print("="*80)
151
+ print("1. Does cash drop by ~$1,300 after buying? (margin)")
152
+ print("2. Does portfolio equal cash while holding? (not cash + $23,000 notional)")
153
+ print("3. Does portfolio move with price changes?")
154
+ print("4. Does final cash = starting cash + price_change*5 - $1.00 in fees?")
155
+ print("="*80)
156
+
157
+ # Calculate final P&L
158
+ if len(strat.snapshots) >= 6:
159
+ entry_snap = strat.snapshots[1] # After buy
160
+ exit_snap = strat.snapshots[5] # After sell
161
+
162
+ # Get prices from the snapshots when we had position
163
+ entry_price = entry_snap['price']
164
+ exit_price = exit_snap['price']
165
+
166
+ price_change = exit_price - entry_price
167
+ expected_pnl = price_change * 5 # MES multiplier
168
+ expected_final_cash = 100000 + expected_pnl - 1.00 # Starting + P&L - fees
169
+
170
+ actual_final_cash = exit_snap['cash']
171
+
172
+ print(f"\nFINAL P&L CALCULATION:")
173
+ print(f" Entry price: ${entry_price:.2f}")
174
+ print(f" Exit price: ${exit_price:.2f}")
175
+ print(f" Price change: ${price_change:.2f}")
176
+ print(f" Expected P&L: ${expected_pnl:.2f} (price_change * 5)")
177
+ print(f" Total fees: $1.00 (2 * $0.50)")
178
+ print(f" Expected final cash: ${expected_final_cash:,.2f}")
179
+ print(f" Actual final cash: ${actual_final_cash:,.2f}")
180
+ print(f" Difference: ${abs(expected_final_cash - actual_final_cash):.2f}")
181
+ print()
182
+
183
+ # Simple pass/fail
184
+ if abs(expected_final_cash - actual_final_cash) < 100:
185
+ print("✓ PASS: Final cash matches expected P&L")
186
+ else:
187
+ print("✗ FAIL: Final cash does NOT match expected P&L")
188
+
189
+
190
+ if __name__ == "__main__":
191
+ test_ultra_simple_buy_hold_sell()