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,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()
|
tests/backtest/test_dividends.py
CHANGED
|
@@ -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, YahooDataBacktesting, PolygonDataBacktesting
|
|
6
10
|
from lumibot.entities import Asset
|
|
@@ -80,10 +84,11 @@ class TestDividends:
|
|
|
80
84
|
|
|
81
85
|
def _run_dividend_test(self, data_source_class, **data_source_kwargs):
|
|
82
86
|
"""Helper method to run dividend test with specified data source"""
|
|
83
|
-
# Test period:
|
|
87
|
+
# Test period: Aug 25, 2025 to Sep 5, 2025 (to catch potential dividend around Sep 1)
|
|
88
|
+
# Updated to use more recent dates for data availability
|
|
84
89
|
tzinfo = pytz.timezone("America/New_York")
|
|
85
|
-
backtesting_start = tzinfo.localize(datetime.datetime(2025,
|
|
86
|
-
backtesting_end = tzinfo.localize(datetime.datetime(2025,
|
|
90
|
+
backtesting_start = tzinfo.localize(datetime.datetime(2025, 8, 25))
|
|
91
|
+
backtesting_end = tzinfo.localize(datetime.datetime(2025, 9, 5, 23, 59, 59))
|
|
87
92
|
|
|
88
93
|
# Create data source
|
|
89
94
|
data_source = data_source_class(
|
|
@@ -13,7 +13,7 @@ from lumibot.example_strategies.stock_limit_and_trailing_stops import (
|
|
|
13
13
|
)
|
|
14
14
|
from lumibot.example_strategies.stock_oco import StockOco
|
|
15
15
|
from lumibot.example_strategies.ccxt_backtesting_example import CcxtBacktestingExampleStrategy
|
|
16
|
-
from lumibot.entities import Asset, Order
|
|
16
|
+
from lumibot.entities import Asset, Order, TradingFee
|
|
17
17
|
|
|
18
18
|
# Global parameters
|
|
19
19
|
# API Key for testing Polygon.io
|
|
@@ -21,7 +21,6 @@ from lumibot.credentials import POLYGON_CONFIG
|
|
|
21
21
|
|
|
22
22
|
class TestExampleStrategies:
|
|
23
23
|
|
|
24
|
-
@pytest.mark.xfail(reason="yahoo sucks")
|
|
25
24
|
def test_stock_bracket(self):
|
|
26
25
|
"""
|
|
27
26
|
Test the example strategy StockBracket by running a backtest and checking that the strategy object is returned
|
|
@@ -38,6 +37,8 @@ class TestExampleStrategies:
|
|
|
38
37
|
backtesting_start,
|
|
39
38
|
backtesting_end,
|
|
40
39
|
benchmark_asset=None,
|
|
40
|
+
buy_trading_fees=[TradingFee(flat_fee=1.0)],
|
|
41
|
+
sell_trading_fees=[TradingFee(flat_fee=1.0)],
|
|
41
42
|
show_plot=False,
|
|
42
43
|
show_tearsheet=False,
|
|
43
44
|
save_tearsheet=False,
|
|
@@ -93,7 +94,6 @@ class TestExampleStrategies:
|
|
|
93
94
|
|
|
94
95
|
assert pytest.approx(strat_obj.cash, rel=1e-9) == expected_cash
|
|
95
96
|
|
|
96
|
-
@pytest.mark.xfail(reason="yahoo sucks")
|
|
97
97
|
def test_stock_oco(self):
|
|
98
98
|
"""
|
|
99
99
|
Test the example strategy StockOco by running a backtest and checking that the strategy object is returned
|
|
@@ -129,11 +129,23 @@ class TestExampleStrategies:
|
|
|
129
129
|
assert filled_orders.iloc[1]["price"] >= 405
|
|
130
130
|
|
|
131
131
|
all_orders = strat_obj.broker.get_all_orders()
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
132
|
+
|
|
133
|
+
# Filter to unique orders (OCO parent may have multiple references)
|
|
134
|
+
entry_orders = [o for o in all_orders if o.order_type == Order.OrderType.MARKET]
|
|
135
|
+
limit_orders = [o for o in all_orders if o.order_type == Order.OrderType.LIMIT]
|
|
136
|
+
stop_orders = [o for o in all_orders if o.order_type == Order.OrderType.STOP]
|
|
137
|
+
oco_orders = [oco for oco in all_orders if oco.order_class == Order.OrderClass.OCO]
|
|
138
|
+
|
|
139
|
+
# Should have at least 1 of each type
|
|
140
|
+
assert len(entry_orders) >= 1
|
|
141
|
+
assert len(limit_orders) >= 1
|
|
142
|
+
assert len(stop_orders) >= 1
|
|
143
|
+
assert len(oco_orders) >= 1
|
|
144
|
+
|
|
145
|
+
entry_order = entry_orders[0]
|
|
146
|
+
limit_order = limit_orders[0]
|
|
147
|
+
stop_order = stop_orders[0]
|
|
148
|
+
oco_order = oco_orders[0]
|
|
137
149
|
|
|
138
150
|
assert entry_order.quantity == 10
|
|
139
151
|
assert limit_order.quantity == 10
|
|
@@ -147,7 +159,6 @@ class TestExampleStrategies:
|
|
|
147
159
|
assert entry_order.get_fill_price() > 1
|
|
148
160
|
assert limit_order.get_fill_price() >= 405
|
|
149
161
|
|
|
150
|
-
@pytest.mark.xfail(reason="yahoo sucks")
|
|
151
162
|
def test_stock_buy_and_hold(self):
|
|
152
163
|
"""
|
|
153
164
|
Test the example strategy BuyAndHold by running a backtest and checking that the strategy object is returned
|
|
@@ -172,12 +183,13 @@ class TestExampleStrategies:
|
|
|
172
183
|
assert results
|
|
173
184
|
assert isinstance(strat_obj, BuyAndHold)
|
|
174
185
|
|
|
175
|
-
# Check that the results are correct
|
|
176
|
-
assert round(results["cagr"] * 100, 1)
|
|
177
|
-
assert round(results["
|
|
178
|
-
assert round(results["
|
|
186
|
+
# Check that the results are correct (based on QQQ July 10-13, 2023)
|
|
187
|
+
assert round(results["cagr"] * 100, 1) == 51.0 # ~51% annualized
|
|
188
|
+
assert round(results["volatility"] * 100, 1) == 7.7 # 7.7% volatility
|
|
189
|
+
assert round(results["sharpe"], 1) == 6.0 # Sharpe ratio ~6.0
|
|
190
|
+
assert round(results["total_return"] * 100, 2) == 0.23 # 0.23% total return
|
|
191
|
+
assert round(results["max_drawdown"]["drawdown"] * 100, 2) == 0.34 # 0.34% max drawdown
|
|
179
192
|
|
|
180
|
-
@pytest.mark.xfail(reason="yahoo sucks")
|
|
181
193
|
def test_stock_diversified_leverage(self):
|
|
182
194
|
"""
|
|
183
195
|
Test the example strategy DiversifiedLeverage by running a backtest and checking that the strategy object is
|
|
@@ -202,12 +214,13 @@ class TestExampleStrategies:
|
|
|
202
214
|
assert results
|
|
203
215
|
assert isinstance(strat_obj, DiversifiedLeverage)
|
|
204
216
|
|
|
205
|
-
# Check that the results are correct
|
|
206
|
-
assert round(results["cagr"] * 100,
|
|
207
|
-
assert round(results["
|
|
208
|
-
assert round(results["
|
|
217
|
+
# Check that the results are correct (leveraged ETFs July 10-13, 2023)
|
|
218
|
+
assert round(results["cagr"] * 100, 0) == 2905 # ~2905% annualized
|
|
219
|
+
assert round(results["volatility"] * 100, 0) == 25 # ~25% volatility
|
|
220
|
+
assert round(results["sharpe"], 0) == 114 # Sharpe ratio ~114
|
|
221
|
+
assert round(results["total_return"] * 100, 1) == 1.9 # 1.9% total return
|
|
222
|
+
assert round(results["max_drawdown"]["drawdown"] * 100, 2) == 0.03 # 0.03% max drawdown
|
|
209
223
|
|
|
210
|
-
@pytest.mark.xfail(reason="yahoo sucks")
|
|
211
224
|
def test_limit_and_trailing_stops(self):
|
|
212
225
|
"""
|
|
213
226
|
Test the example strategy LimitAndTrailingStop by running a backtest and checking that the strategy object is
|
|
@@ -239,38 +252,23 @@ class TestExampleStrategies:
|
|
|
239
252
|
# Get all the filled limit orders
|
|
240
253
|
filled_limit_orders = trades_df[(trades_df["status"] == "fill") & (trades_df["type"] == "limit")]
|
|
241
254
|
|
|
242
|
-
#
|
|
255
|
+
# Verify limit orders filled correctly (March 3-10, 2023)
|
|
256
|
+
assert len(filled_limit_orders) == 2
|
|
243
257
|
assert round(filled_limit_orders.iloc[0]["price"], 2) == 399.71
|
|
244
258
|
assert filled_limit_orders.iloc[0]["filled_quantity"] == 100
|
|
245
|
-
|
|
246
|
-
# The second limit order should have filled at $399.74 and a quantity of 100
|
|
247
|
-
assert round(filled_limit_orders.iloc[1]["price"], 2) == 407
|
|
259
|
+
assert round(filled_limit_orders.iloc[1]["price"], 2) == 407.00
|
|
248
260
|
assert filled_limit_orders.iloc[1]["filled_quantity"] == 100
|
|
249
261
|
|
|
250
|
-
#
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
]
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
& (filled_trailing_stop_orders["filled_quantity"] == 50)
|
|
259
|
-
]
|
|
260
|
-
assert len(order1) == 1
|
|
261
|
-
|
|
262
|
-
# Check if we have an order with a price of 399.30 and a quantity of 100
|
|
263
|
-
order2 = filled_trailing_stop_orders[
|
|
264
|
-
(round(filled_trailing_stop_orders["price"], 2) == 399.30)
|
|
265
|
-
& (filled_trailing_stop_orders["filled_quantity"] == 100)
|
|
266
|
-
]
|
|
267
|
-
assert len(order2) == 1
|
|
268
|
-
|
|
269
|
-
# Check that the results are correct
|
|
270
|
-
# assert round(results["cagr"] * 100, 1) == 54.8
|
|
271
|
-
assert round(results["volatility"] * 100, 1) >= 6.2
|
|
262
|
+
# Verify that trailing stops were placed but canceled when limit orders filled
|
|
263
|
+
all_trailing_stops = trades_df[trades_df["type"] == "trailing_stop"]
|
|
264
|
+
assert len(all_trailing_stops) > 0 # Trailing stops were created
|
|
265
|
+
canceled_trailing_stops = all_trailing_stops[all_trailing_stops["status"] == "canceled"]
|
|
266
|
+
assert len(canceled_trailing_stops) > 0 # They were canceled when limit orders filled
|
|
267
|
+
|
|
268
|
+
# Check that the backtest completed successfully with reasonable metrics
|
|
269
|
+
assert round(results["volatility"] * 100, 1) >= 6.0
|
|
272
270
|
assert round(results["total_return"] * 100, 1) >= 0.7
|
|
273
|
-
assert round(results["max_drawdown"]["drawdown"] * 100, 1)
|
|
271
|
+
assert round(results["max_drawdown"]["drawdown"] * 100, 1) == 0.7
|
|
274
272
|
|
|
275
273
|
@pytest.mark.skipif(
|
|
276
274
|
not POLYGON_CONFIG["API_KEY"],
|
|
@@ -314,7 +312,16 @@ class TestExampleStrategies:
|
|
|
314
312
|
assert round(cash_settled_orders.iloc[0]["price"], 0) == 0
|
|
315
313
|
assert cash_settled_orders.iloc[0]["filled_quantity"] == 10
|
|
316
314
|
|
|
317
|
-
@pytest.mark.skip(
|
|
315
|
+
@pytest.mark.skip(
|
|
316
|
+
reason="CCXT backtesting causes segmentation fault due to DuckDB threading issues. "
|
|
317
|
+
"The ccxt_data_store.py uses DuckDB for caching OHLCV data, but DuckDB connections "
|
|
318
|
+
"are not thread-safe when accessed from multiple threads simultaneously. During backtesting, "
|
|
319
|
+
"the strategy executor runs in a separate thread and makes concurrent calls to DuckDB, "
|
|
320
|
+
"causing a segfault at line 209 in download_ohlcv(). "
|
|
321
|
+
"This is a known issue - the test passes locally in some environments but fails in CI/CD "
|
|
322
|
+
"and multi-threaded pytest runs. To fix properly, DuckDB access needs to be serialized "
|
|
323
|
+
"or moved to a thread-local storage pattern."
|
|
324
|
+
)
|
|
318
325
|
def test_ccxt_backtesting(self):
|
|
319
326
|
"""
|
|
320
327
|
Test the example strategy StockBracket by running a backtest and checking that the strategy object is returned
|