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,451 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Edge case tests for futures trading:
|
|
3
|
+
1. Short selling (sell → buy to cover)
|
|
4
|
+
2. Multiple simultaneous positions
|
|
5
|
+
3. Rapid entry/exit cycles
|
|
6
|
+
"""
|
|
7
|
+
import datetime
|
|
8
|
+
import shutil
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
import pytz
|
|
13
|
+
from dotenv import load_dotenv
|
|
14
|
+
|
|
15
|
+
# Load environment variables from .env file
|
|
16
|
+
load_dotenv()
|
|
17
|
+
|
|
18
|
+
from lumibot.backtesting import BacktestingBroker
|
|
19
|
+
from lumibot.backtesting.databento_backtesting import (
|
|
20
|
+
DataBentoDataBacktesting as DataBentoDataBacktestingPandas,
|
|
21
|
+
)
|
|
22
|
+
from lumibot.data_sources.databento_data_polars_backtesting import DataBentoDataPolarsBacktesting
|
|
23
|
+
from lumibot.entities import Asset, TradingFee
|
|
24
|
+
from lumibot.strategies import Strategy
|
|
25
|
+
from lumibot.traders import Trader
|
|
26
|
+
from lumibot.credentials import DATABENTO_CONFIG
|
|
27
|
+
from lumibot.tools.databento_helper_polars import LUMIBOT_DATABENTO_CACHE_FOLDER
|
|
28
|
+
|
|
29
|
+
DATABENTO_API_KEY = DATABENTO_CONFIG.get("API_KEY")
|
|
30
|
+
|
|
31
|
+
# Contract specs
|
|
32
|
+
MES_MULTIPLIER = 5
|
|
33
|
+
ES_MULTIPLIER = 50
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _clear_polars_cache():
|
|
37
|
+
cache_path = Path(LUMIBOT_DATABENTO_CACHE_FOLDER)
|
|
38
|
+
if cache_path.exists():
|
|
39
|
+
shutil.rmtree(cache_path)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ShortSellingStrategy(Strategy):
|
|
43
|
+
"""
|
|
44
|
+
Test short selling:
|
|
45
|
+
- Sell 1 MES contract (open short position)
|
|
46
|
+
- Hold for several iterations
|
|
47
|
+
- Buy to cover (close short position)
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def initialize(self):
|
|
51
|
+
self.sleeptime = "15M"
|
|
52
|
+
self.set_market("us_futures")
|
|
53
|
+
self.mes = Asset("MES", asset_type=Asset.AssetType.CONT_FUTURE)
|
|
54
|
+
|
|
55
|
+
self.iteration = 0
|
|
56
|
+
self.snapshots = []
|
|
57
|
+
self.trades = []
|
|
58
|
+
|
|
59
|
+
def on_trading_iteration(self):
|
|
60
|
+
self.iteration += 1
|
|
61
|
+
|
|
62
|
+
price = self.get_last_price(self.mes)
|
|
63
|
+
cash = self.get_cash()
|
|
64
|
+
portfolio = self.get_portfolio_value()
|
|
65
|
+
position = self.get_position(self.mes)
|
|
66
|
+
|
|
67
|
+
self.snapshots.append({
|
|
68
|
+
"iteration": self.iteration,
|
|
69
|
+
"datetime": self.get_datetime(),
|
|
70
|
+
"price": float(price) if price else None,
|
|
71
|
+
"cash": cash,
|
|
72
|
+
"portfolio": portfolio,
|
|
73
|
+
"position_qty": position.quantity if position else 0,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
# Sell to open short on iteration 1
|
|
77
|
+
if self.iteration == 1:
|
|
78
|
+
order = self.create_order(self.mes, 1, "sell")
|
|
79
|
+
self.submit_order(order)
|
|
80
|
+
|
|
81
|
+
# Buy to cover on iteration 6
|
|
82
|
+
elif self.iteration == 6 and position and position.quantity < 0:
|
|
83
|
+
order = self.create_order(self.mes, 1, "buy")
|
|
84
|
+
self.submit_order(order)
|
|
85
|
+
|
|
86
|
+
def on_filled_order(self, position, order, price, quantity, multiplier):
|
|
87
|
+
self.trades.append({
|
|
88
|
+
"datetime": self.get_datetime(),
|
|
89
|
+
"side": order.side,
|
|
90
|
+
"quantity": quantity,
|
|
91
|
+
"price": price,
|
|
92
|
+
"multiplier": multiplier,
|
|
93
|
+
"cash_after": self.get_cash(),
|
|
94
|
+
"portfolio_after": self.get_portfolio_value(),
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class MultiplePositionsStrategy(Strategy):
|
|
99
|
+
"""
|
|
100
|
+
Test holding multiple positions simultaneously:
|
|
101
|
+
- Buy MES
|
|
102
|
+
- Buy ES (while still holding MES)
|
|
103
|
+
- Sell MES
|
|
104
|
+
- Sell ES
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def initialize(self):
|
|
108
|
+
self.sleeptime = "15M"
|
|
109
|
+
self.set_market("us_futures")
|
|
110
|
+
|
|
111
|
+
self.mes = Asset("MES", asset_type=Asset.AssetType.CONT_FUTURE)
|
|
112
|
+
self.es = Asset("ES", asset_type=Asset.AssetType.CONT_FUTURE)
|
|
113
|
+
|
|
114
|
+
self.iteration = 0
|
|
115
|
+
self.snapshots = []
|
|
116
|
+
self.trades = []
|
|
117
|
+
|
|
118
|
+
def on_trading_iteration(self):
|
|
119
|
+
self.iteration += 1
|
|
120
|
+
|
|
121
|
+
mes_price = self.get_last_price(self.mes)
|
|
122
|
+
es_price = self.get_last_price(self.es)
|
|
123
|
+
cash = self.get_cash()
|
|
124
|
+
portfolio = self.get_portfolio_value()
|
|
125
|
+
mes_pos = self.get_position(self.mes)
|
|
126
|
+
es_pos = self.get_position(self.es)
|
|
127
|
+
|
|
128
|
+
self.snapshots.append({
|
|
129
|
+
"iteration": self.iteration,
|
|
130
|
+
"datetime": self.get_datetime(),
|
|
131
|
+
"mes_price": float(mes_price) if mes_price else None,
|
|
132
|
+
"es_price": float(es_price) if es_price else None,
|
|
133
|
+
"cash": cash,
|
|
134
|
+
"portfolio": portfolio,
|
|
135
|
+
"mes_qty": mes_pos.quantity if mes_pos else 0,
|
|
136
|
+
"es_qty": es_pos.quantity if es_pos else 0,
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
# Buy MES on iteration 1
|
|
140
|
+
if self.iteration == 1:
|
|
141
|
+
order = self.create_order(self.mes, 1, "buy")
|
|
142
|
+
self.submit_order(order)
|
|
143
|
+
|
|
144
|
+
# Buy ES on iteration 3 (while holding MES)
|
|
145
|
+
elif self.iteration == 3:
|
|
146
|
+
order = self.create_order(self.es, 1, "buy")
|
|
147
|
+
self.submit_order(order)
|
|
148
|
+
|
|
149
|
+
# Sell MES on iteration 5
|
|
150
|
+
elif self.iteration == 5 and mes_pos and mes_pos.quantity > 0:
|
|
151
|
+
order = self.create_order(self.mes, 1, "sell")
|
|
152
|
+
self.submit_order(order)
|
|
153
|
+
|
|
154
|
+
# Sell ES on iteration 7
|
|
155
|
+
elif self.iteration == 7 and es_pos and es_pos.quantity > 0:
|
|
156
|
+
order = self.create_order(self.es, 1, "sell")
|
|
157
|
+
self.submit_order(order)
|
|
158
|
+
|
|
159
|
+
def on_filled_order(self, position, order, price, quantity, multiplier):
|
|
160
|
+
self.trades.append({
|
|
161
|
+
"datetime": self.get_datetime(),
|
|
162
|
+
"asset": position.asset.symbol,
|
|
163
|
+
"side": order.side,
|
|
164
|
+
"quantity": quantity,
|
|
165
|
+
"price": price,
|
|
166
|
+
"multiplier": multiplier,
|
|
167
|
+
"cash_after": self.get_cash(),
|
|
168
|
+
"portfolio_after": self.get_portfolio_value(),
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TestFuturesEdgeCases:
|
|
173
|
+
"""Test edge cases in futures trading"""
|
|
174
|
+
|
|
175
|
+
@pytest.mark.apitest
|
|
176
|
+
@pytest.mark.skipif(
|
|
177
|
+
not DATABENTO_API_KEY or DATABENTO_API_KEY == '<your key here>',
|
|
178
|
+
reason="This test requires a Databento API key"
|
|
179
|
+
)
|
|
180
|
+
@pytest.mark.parametrize(
|
|
181
|
+
"datasource_cls",
|
|
182
|
+
[
|
|
183
|
+
DataBentoDataPolarsBacktesting,
|
|
184
|
+
DataBentoDataBacktestingPandas,
|
|
185
|
+
],
|
|
186
|
+
)
|
|
187
|
+
def test_short_selling(self, datasource_cls):
|
|
188
|
+
"""
|
|
189
|
+
Test short selling:
|
|
190
|
+
1. Sell 1 MES contract (open short)
|
|
191
|
+
2. Hold for several iterations
|
|
192
|
+
3. Buy 1 MES contract (cover short)
|
|
193
|
+
4. Verify P&L is inverse of long trade
|
|
194
|
+
"""
|
|
195
|
+
print("\n" + "="*80)
|
|
196
|
+
print("EDGE CASE TEST: SHORT SELLING")
|
|
197
|
+
print("="*80)
|
|
198
|
+
|
|
199
|
+
tzinfo = pytz.timezone("America/New_York")
|
|
200
|
+
backtesting_start = tzinfo.localize(datetime.datetime(2024, 1, 3, 9, 30))
|
|
201
|
+
backtesting_end = tzinfo.localize(datetime.datetime(2024, 1, 3, 16, 0))
|
|
202
|
+
|
|
203
|
+
if datasource_cls is DataBentoDataPolarsBacktesting:
|
|
204
|
+
_clear_polars_cache()
|
|
205
|
+
|
|
206
|
+
data_source = datasource_cls(
|
|
207
|
+
datetime_start=backtesting_start,
|
|
208
|
+
datetime_end=backtesting_end,
|
|
209
|
+
api_key=DATABENTO_API_KEY,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
broker = BacktestingBroker(data_source=data_source)
|
|
213
|
+
fee = TradingFee(flat_fee=0.50)
|
|
214
|
+
|
|
215
|
+
strat = ShortSellingStrategy(
|
|
216
|
+
broker=broker,
|
|
217
|
+
buy_trading_fees=[fee],
|
|
218
|
+
sell_trading_fees=[fee],
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
trader = Trader(logfile="", backtest=True)
|
|
222
|
+
trader.add_strategy(strat)
|
|
223
|
+
results = trader.run_all(
|
|
224
|
+
show_plot=False,
|
|
225
|
+
show_tearsheet=False,
|
|
226
|
+
show_indicators=False,
|
|
227
|
+
save_tearsheet=False
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
print(f"\n✓ Backtest completed")
|
|
231
|
+
print(f" Snapshots: {len(strat.snapshots)}")
|
|
232
|
+
print(f" Trades: {len(strat.trades)}")
|
|
233
|
+
|
|
234
|
+
# Verify we got at least 2 trades (sell to open, buy to cover)
|
|
235
|
+
assert len(strat.trades) >= 2, f"Expected at least 2 trades, got {len(strat.trades)}"
|
|
236
|
+
|
|
237
|
+
# Analyze trades
|
|
238
|
+
print("\n" + "-"*80)
|
|
239
|
+
print("TRADE ANALYSIS")
|
|
240
|
+
print("-"*80)
|
|
241
|
+
|
|
242
|
+
for i, trade in enumerate(strat.trades):
|
|
243
|
+
print(f"\nTrade {i+1}:")
|
|
244
|
+
print(f" Side: {trade['side']}")
|
|
245
|
+
print(f" Price: ${trade['price']:.2f}")
|
|
246
|
+
print(f" Multiplier: {trade['multiplier']}")
|
|
247
|
+
print(f" Cash after: ${trade['cash_after']:,.2f}")
|
|
248
|
+
print(f" Portfolio after: ${trade['portfolio_after']:,.2f}")
|
|
249
|
+
|
|
250
|
+
# Verify multipliers
|
|
251
|
+
for trade in strat.trades:
|
|
252
|
+
assert trade['multiplier'] == MES_MULTIPLIER, \
|
|
253
|
+
f"MES multiplier should be {MES_MULTIPLIER}, got {trade['multiplier']}"
|
|
254
|
+
|
|
255
|
+
# If we have both entry and exit, verify P&L
|
|
256
|
+
if len(strat.trades) >= 2:
|
|
257
|
+
entry = strat.trades[0] # Sell to open
|
|
258
|
+
exit_trade = strat.trades[1] # Buy to cover
|
|
259
|
+
|
|
260
|
+
print("\n" + "-"*80)
|
|
261
|
+
print("P&L VERIFICATION (SHORT TRADE)")
|
|
262
|
+
print("-"*80)
|
|
263
|
+
|
|
264
|
+
# For short: P&L = (Entry - Exit) × Qty × Multiplier (inverted!)
|
|
265
|
+
entry_price = entry['price']
|
|
266
|
+
exit_price = exit_trade['price']
|
|
267
|
+
price_change = entry_price - exit_price # Inverted for short
|
|
268
|
+
expected_pnl = price_change * MES_MULTIPLIER
|
|
269
|
+
|
|
270
|
+
print(f" Entry (sell): ${entry_price:.2f}")
|
|
271
|
+
print(f" Exit (buy): ${exit_price:.2f}")
|
|
272
|
+
print(f" Price change (entry - exit): ${price_change:.2f}")
|
|
273
|
+
print(f" Expected P&L: ${expected_pnl:.2f} (inverted for short)")
|
|
274
|
+
|
|
275
|
+
# Verify final cash
|
|
276
|
+
starting_cash = 100000
|
|
277
|
+
total_fees = 1.00 # 2 × $0.50
|
|
278
|
+
expected_final_cash = starting_cash + expected_pnl - total_fees
|
|
279
|
+
|
|
280
|
+
# Get final snapshot cash
|
|
281
|
+
final_cash = strat.snapshots[-1]['cash']
|
|
282
|
+
cash_diff = abs(expected_final_cash - final_cash)
|
|
283
|
+
|
|
284
|
+
print(f"\n Starting cash: ${starting_cash:,.2f}")
|
|
285
|
+
print(f" Expected final cash: ${expected_final_cash:,.2f}")
|
|
286
|
+
print(f" Actual final cash: ${final_cash:,.2f}")
|
|
287
|
+
print(f" Difference: ${cash_diff:.2f}")
|
|
288
|
+
|
|
289
|
+
# Allow tolerance for timing/fill differences
|
|
290
|
+
assert cash_diff < 150, f"Cash difference too large: ${cash_diff:.2f}"
|
|
291
|
+
print(f"\n✓ PASS: Short selling P&L is correct")
|
|
292
|
+
|
|
293
|
+
print("\n" + "="*80)
|
|
294
|
+
print("✓ SHORT SELLING TEST PASSED")
|
|
295
|
+
print("="*80)
|
|
296
|
+
|
|
297
|
+
@pytest.mark.apitest
|
|
298
|
+
@pytest.mark.skipif(
|
|
299
|
+
not DATABENTO_API_KEY or DATABENTO_API_KEY == '<your key here>',
|
|
300
|
+
reason="This test requires a Databento API key"
|
|
301
|
+
)
|
|
302
|
+
@pytest.mark.parametrize(
|
|
303
|
+
"datasource_cls",
|
|
304
|
+
[
|
|
305
|
+
DataBentoDataPolarsBacktesting,
|
|
306
|
+
DataBentoDataBacktestingPandas,
|
|
307
|
+
],
|
|
308
|
+
)
|
|
309
|
+
def test_multiple_simultaneous_positions(self, datasource_cls):
|
|
310
|
+
"""
|
|
311
|
+
Test holding multiple positions at once:
|
|
312
|
+
1. Buy MES
|
|
313
|
+
2. Buy ES (while holding MES)
|
|
314
|
+
3. Verify both positions tracked correctly
|
|
315
|
+
4. Sell MES
|
|
316
|
+
5. Sell ES
|
|
317
|
+
6. Verify final cash is correct
|
|
318
|
+
"""
|
|
319
|
+
print("\n" + "="*80)
|
|
320
|
+
print("EDGE CASE TEST: MULTIPLE SIMULTANEOUS POSITIONS")
|
|
321
|
+
print("="*80)
|
|
322
|
+
|
|
323
|
+
tzinfo = pytz.timezone("America/New_York")
|
|
324
|
+
backtesting_start = tzinfo.localize(datetime.datetime(2024, 1, 3, 9, 30))
|
|
325
|
+
backtesting_end = tzinfo.localize(datetime.datetime(2024, 1, 3, 16, 0))
|
|
326
|
+
|
|
327
|
+
if datasource_cls is DataBentoDataPolarsBacktesting:
|
|
328
|
+
_clear_polars_cache()
|
|
329
|
+
|
|
330
|
+
data_source = datasource_cls(
|
|
331
|
+
datetime_start=backtesting_start,
|
|
332
|
+
datetime_end=backtesting_end,
|
|
333
|
+
api_key=DATABENTO_API_KEY,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
broker = BacktestingBroker(data_source=data_source)
|
|
337
|
+
fee = TradingFee(flat_fee=0.50)
|
|
338
|
+
|
|
339
|
+
strat = MultiplePositionsStrategy(
|
|
340
|
+
broker=broker,
|
|
341
|
+
buy_trading_fees=[fee],
|
|
342
|
+
sell_trading_fees=[fee],
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
trader = Trader(logfile="", backtest=True)
|
|
346
|
+
trader.add_strategy(strat)
|
|
347
|
+
results = trader.run_all(
|
|
348
|
+
show_plot=False,
|
|
349
|
+
show_tearsheet=False,
|
|
350
|
+
show_indicators=False,
|
|
351
|
+
save_tearsheet=False
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
print(f"\n✓ Backtest completed")
|
|
355
|
+
print(f" Snapshots: {len(strat.snapshots)}")
|
|
356
|
+
print(f" Trades: {len(strat.trades)}")
|
|
357
|
+
|
|
358
|
+
# Verify we got 4 trades
|
|
359
|
+
assert len(strat.trades) == 4, f"Expected 4 trades, got {len(strat.trades)}"
|
|
360
|
+
|
|
361
|
+
# Group trades by asset
|
|
362
|
+
mes_trades = [t for t in strat.trades if t['asset'] == 'MES']
|
|
363
|
+
es_trades = [t for t in strat.trades if t['asset'] == 'ES']
|
|
364
|
+
|
|
365
|
+
print("\n" + "-"*80)
|
|
366
|
+
print("TRADE ANALYSIS")
|
|
367
|
+
print("-"*80)
|
|
368
|
+
|
|
369
|
+
print("\nMES Trades:")
|
|
370
|
+
for i, trade in enumerate(mes_trades):
|
|
371
|
+
print(f" {i+1}. {trade['side']} @ ${trade['price']:.2f}, mult={trade['multiplier']}")
|
|
372
|
+
|
|
373
|
+
print("\nES Trades:")
|
|
374
|
+
for i, trade in enumerate(es_trades):
|
|
375
|
+
print(f" {i+1}. {trade['side']} @ ${trade['price']:.2f}, mult={trade['multiplier']}")
|
|
376
|
+
|
|
377
|
+
# Verify multipliers
|
|
378
|
+
for trade in mes_trades:
|
|
379
|
+
assert trade['multiplier'] == MES_MULTIPLIER, \
|
|
380
|
+
f"MES multiplier should be {MES_MULTIPLIER}, got {trade['multiplier']}"
|
|
381
|
+
|
|
382
|
+
for trade in es_trades:
|
|
383
|
+
assert trade['multiplier'] == ES_MULTIPLIER, \
|
|
384
|
+
f"ES multiplier should be {ES_MULTIPLIER}, got {trade['multiplier']}"
|
|
385
|
+
|
|
386
|
+
# Calculate expected P&L for each instrument
|
|
387
|
+
print("\n" + "-"*80)
|
|
388
|
+
print("P&L VERIFICATION")
|
|
389
|
+
print("-"*80)
|
|
390
|
+
|
|
391
|
+
# MES P&L
|
|
392
|
+
mes_entry = mes_trades[0]
|
|
393
|
+
mes_exit = mes_trades[1]
|
|
394
|
+
mes_pnl = (mes_exit['price'] - mes_entry['price']) * MES_MULTIPLIER
|
|
395
|
+
|
|
396
|
+
print(f"\nMES:")
|
|
397
|
+
print(f" Entry: ${mes_entry['price']:.2f}")
|
|
398
|
+
print(f" Exit: ${mes_exit['price']:.2f}")
|
|
399
|
+
print(f" P&L: ${mes_pnl:.2f}")
|
|
400
|
+
|
|
401
|
+
# ES P&L
|
|
402
|
+
es_entry = es_trades[0]
|
|
403
|
+
es_exit = es_trades[1]
|
|
404
|
+
es_pnl = (es_exit['price'] - es_entry['price']) * ES_MULTIPLIER
|
|
405
|
+
|
|
406
|
+
print(f"\nES:")
|
|
407
|
+
print(f" Entry: ${es_entry['price']:.2f}")
|
|
408
|
+
print(f" Exit: ${es_exit['price']:.2f}")
|
|
409
|
+
print(f" P&L: ${es_pnl:.2f}")
|
|
410
|
+
|
|
411
|
+
# Total P&L
|
|
412
|
+
total_pnl = mes_pnl + es_pnl
|
|
413
|
+
total_fees = 4.00 # 4 trades × $0.50 each (assuming $0.50 per side)
|
|
414
|
+
|
|
415
|
+
print(f"\nTotal:")
|
|
416
|
+
print(f" Combined P&L: ${total_pnl:.2f}")
|
|
417
|
+
print(f" Total fees: ${total_fees:.2f}")
|
|
418
|
+
print(f" Net P&L: ${total_pnl - total_fees:.2f}")
|
|
419
|
+
|
|
420
|
+
# Verify final cash
|
|
421
|
+
starting_cash = 100000
|
|
422
|
+
expected_final_cash = starting_cash + total_pnl - total_fees
|
|
423
|
+
final_cash = strat.snapshots[-1]['cash']
|
|
424
|
+
cash_diff = abs(expected_final_cash - final_cash)
|
|
425
|
+
|
|
426
|
+
print(f"\n Starting cash: ${starting_cash:,.2f}")
|
|
427
|
+
print(f" Expected final cash: ${expected_final_cash:,.2f}")
|
|
428
|
+
print(f" Actual final cash: ${final_cash:,.2f}")
|
|
429
|
+
print(f" Difference: ${cash_diff:.2f}")
|
|
430
|
+
|
|
431
|
+
# Allow tolerance
|
|
432
|
+
assert cash_diff < 200, f"Cash difference too large: ${cash_diff:.2f}"
|
|
433
|
+
print(f"\n✓ PASS: Multiple simultaneous positions tracked correctly")
|
|
434
|
+
|
|
435
|
+
# Verify we had both positions at the same time (iteration 3-4)
|
|
436
|
+
snapshot_with_both = None
|
|
437
|
+
for snap in strat.snapshots:
|
|
438
|
+
if snap['mes_qty'] > 0 and snap['es_qty'] > 0:
|
|
439
|
+
snapshot_with_both = snap
|
|
440
|
+
break
|
|
441
|
+
|
|
442
|
+
assert snapshot_with_both is not None, "Should have held both positions simultaneously"
|
|
443
|
+
print(f"\n✓ Verified both positions held simultaneously at iteration {snapshot_with_both['iteration']}")
|
|
444
|
+
|
|
445
|
+
print("\n" + "="*80)
|
|
446
|
+
print("✓ MULTIPLE POSITIONS TEST PASSED")
|
|
447
|
+
print("="*80)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
if __name__ == "__main__":
|
|
451
|
+
pytest.main([__file__, "-v", "-s"])
|