lumibot 4.0.22__py3-none-any.whl → 4.1.0__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.
- lumibot/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/__pycache__/constants.cpython-312.pyc +0 -0
- lumibot/__pycache__/credentials.cpython-312.pyc +0 -0
- lumibot/backtesting/__init__.py +6 -5
- lumibot/backtesting/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/alpaca_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/alpha_vantage_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/backtesting_broker.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/ccxt_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/databento_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/interactive_brokers_rest_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/pandas_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/polygon_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/thetadata_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/yahoo_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/backtesting_broker.py +209 -9
- lumibot/backtesting/databento_backtesting.py +141 -24
- lumibot/backtesting/thetadata_backtesting.py +63 -42
- lumibot/brokers/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/alpaca.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/bitunix.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/broker.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/ccxt.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/example_broker.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/interactive_brokers.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/interactive_brokers_rest.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/projectx.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/schwab.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/tradier.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/tradovate.cpython-312.pyc +0 -0
- lumibot/brokers/alpaca.py +11 -1
- lumibot/brokers/tradeovate.py +475 -0
- lumibot/components/grok_news_helper.py +284 -0
- lumibot/components/options_helper.py +90 -34
- lumibot/credentials.py +3 -0
- lumibot/data_sources/__init__.py +2 -1
- lumibot/data_sources/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/alpaca_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/alpha_vantage_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/bitunix_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/ccxt_backtesting_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/ccxt_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/data_source.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/data_source_backtesting.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/databento_data_polars_backtesting.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/databento_data_polars_live.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/example_broker_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/exceptions.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/interactive_brokers_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/interactive_brokers_rest_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/pandas_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/polars_mixin.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/polygon_data_polars.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/projectx_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/schwab_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/tradier_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/tradovate_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/yahoo_data_polars.cpython-312.pyc +0 -0
- lumibot/data_sources/data_source_backtesting.py +3 -5
- lumibot/data_sources/databento_data.py +5 -5
- lumibot/data_sources/databento_data_polars_backtesting.py +636 -0
- lumibot/data_sources/databento_data_polars_live.py +793 -0
- lumibot/data_sources/pandas_data.py +6 -3
- lumibot/data_sources/polars_mixin.py +126 -21
- lumibot/data_sources/tradeovate_data.py +80 -0
- lumibot/data_sources/tradier_data.py +2 -1
- lumibot/entities/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/asset.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/bar.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/bars.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/chains.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/data.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/dataline.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/order.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/position.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/quote.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/trading_fee.cpython-312.pyc +0 -0
- lumibot/entities/asset.py +8 -0
- lumibot/entities/order.py +1 -1
- lumibot/entities/quote.py +14 -0
- lumibot/example_strategies/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/example_strategies/__pycache__/test_broker_functions.cpython-312-pytest-8.4.1.pyc +0 -0
- lumibot/strategies/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/strategies/__pycache__/_strategy.cpython-312.pyc +0 -0
- lumibot/strategies/__pycache__/strategy.cpython-312.pyc +0 -0
- lumibot/strategies/__pycache__/strategy_executor.cpython-312.pyc +0 -0
- lumibot/strategies/_strategy.py +95 -27
- lumibot/strategies/strategy.py +5 -6
- lumibot/strategies/strategy_executor.py +2 -2
- lumibot/tools/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/alpaca_helpers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/bitunix_helpers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/black_scholes.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/ccxt_data_store.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/databento_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/databento_helper_polars.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/debugers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/decorators.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/helpers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/indicators.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/lumibot_logger.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/pandas.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/polygon_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/polygon_helper_async.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/polygon_helper_polars_optimized.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/projectx_helpers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/schwab_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/thetadata_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/types.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/yahoo_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/yahoo_helper_polars_optimized.cpython-312.pyc +0 -0
- lumibot/tools/databento_helper.py +384 -133
- lumibot/tools/databento_helper_polars.py +218 -156
- lumibot/tools/databento_roll.py +216 -0
- lumibot/tools/lumibot_logger.py +32 -17
- lumibot/tools/polygon_helper.py +65 -0
- lumibot/tools/thetadata_helper.py +588 -70
- lumibot/traders/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/traders/__pycache__/trader.cpython-312.pyc +0 -0
- lumibot/traders/trader.py +1 -1
- lumibot/trading_builtins/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/trading_builtins/__pycache__/custom_stream.cpython-312.pyc +0 -0
- lumibot/trading_builtins/__pycache__/safe_list.cpython-312.pyc +0 -0
- {lumibot-4.0.22.dist-info → lumibot-4.1.0.dist-info}/METADATA +1 -2
- {lumibot-4.0.22.dist-info → lumibot-4.1.0.dist-info}/RECORD +164 -46
- tests/backtest/check_timing_offset.py +198 -0
- tests/backtest/check_volume_spike.py +112 -0
- tests/backtest/comprehensive_comparison.py +166 -0
- tests/backtest/debug_comparison.py +91 -0
- tests/backtest/diagnose_price_difference.py +97 -0
- tests/backtest/direct_api_comparison.py +203 -0
- tests/backtest/profile_thetadata_vs_polygon.py +255 -0
- tests/backtest/root_cause_analysis.py +109 -0
- tests/backtest/test_accuracy_verification.py +244 -0
- tests/backtest/test_daily_data_timestamp_comparison.py +801 -0
- tests/backtest/test_databento.py +57 -0
- tests/backtest/test_databento_comprehensive_trading.py +564 -0
- tests/backtest/test_debug_avg_fill_price.py +112 -0
- tests/backtest/test_dividends.py +8 -3
- tests/backtest/test_example_strategies.py +54 -47
- tests/backtest/test_futures_edge_cases.py +451 -0
- tests/backtest/test_futures_single_trade.py +270 -0
- tests/backtest/test_futures_ultra_simple.py +191 -0
- tests/backtest/test_index_data_verification.py +348 -0
- tests/backtest/test_polygon.py +45 -24
- tests/backtest/test_thetadata.py +246 -60
- tests/backtest/test_thetadata_comprehensive.py +729 -0
- tests/backtest/test_thetadata_vs_polygon.py +557 -0
- tests/backtest/test_yahoo.py +1 -2
- tests/conftest.py +20 -0
- tests/test_backtesting_data_source_env.py +249 -0
- tests/test_backtesting_quiet_logs_complete.py +10 -11
- tests/test_databento_helper.py +73 -86
- tests/test_databento_live.py +10 -10
- tests/test_databento_timezone_fixes.py +21 -4
- tests/test_get_historical_prices.py +6 -6
- tests/test_options_helper.py +162 -40
- tests/test_polygon_helper.py +21 -13
- tests/test_quiet_logs_requirements.py +5 -5
- tests/test_thetadata_helper.py +487 -171
- tests/test_yahoo_data.py +125 -0
- {lumibot-4.0.22.dist-info → lumibot-4.1.0.dist-info}/LICENSE +0 -0
- {lumibot-4.0.22.dist-info → lumibot-4.1.0.dist-info}/WHEEL +0 -0
- {lumibot-4.0.22.dist-info → lumibot-4.1.0.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()
|