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.
- 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 +145 -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/__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_polars_backtesting.py +194 -48
- 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.1.1.data/data/ThetaTerminal.jar +0 -0
- {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/METADATA +1 -2
- {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/RECORD +161 -44
- 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 +4 -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 +76 -90
- 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.23.dist-info → lumibot-4.1.1.dist-info}/LICENSE +0 -0
- {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/WHEEL +0 -0
- {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comprehensive but practical ThetaData tests.
|
|
3
|
+
Tests the essentials without going overboard.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import datetime
|
|
7
|
+
import os
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
import pytz
|
|
12
|
+
from dotenv import load_dotenv
|
|
13
|
+
from lumibot.entities import Asset
|
|
14
|
+
from lumibot.tools import thetadata_helper
|
|
15
|
+
from lumibot.tools.helpers import to_datetime_aware
|
|
16
|
+
from lumibot.backtesting import ThetaDataBacktesting, PolygonDataBacktesting
|
|
17
|
+
|
|
18
|
+
# Load environment variables from .env file
|
|
19
|
+
load_dotenv()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _require_theta_credentials() -> Tuple[str, str]:
|
|
23
|
+
"""Fetch ThetaData credentials or skip when unavailable."""
|
|
24
|
+
username = os.environ.get("THETADATA_USERNAME")
|
|
25
|
+
password = os.environ.get("THETADATA_PASSWORD")
|
|
26
|
+
|
|
27
|
+
if not username or username.lower() in {"", "uname"}:
|
|
28
|
+
pytest.skip("ThetaData username not configured")
|
|
29
|
+
if not password or password.lower() in {"", "pwd"}:
|
|
30
|
+
pytest.skip("ThetaData password not configured")
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
_, connected = thetadata_helper.check_connection(username=username, password=password)
|
|
34
|
+
except Exception as exc: # pragma: no cover - integration guard
|
|
35
|
+
pytest.skip(f"ThetaData service unavailable: {exc}")
|
|
36
|
+
|
|
37
|
+
if not connected:
|
|
38
|
+
pytest.skip("ThetaData connection could not be established")
|
|
39
|
+
|
|
40
|
+
return username, password
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.fixture(scope="module")
|
|
44
|
+
def theta_credentials():
|
|
45
|
+
"""Module-scoped credentials fixture that validates live connectivity."""
|
|
46
|
+
return _require_theta_credentials()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.mark.apitest
|
|
50
|
+
class TestThetaDataStocks:
|
|
51
|
+
"""Test stock data accuracy."""
|
|
52
|
+
|
|
53
|
+
def test_first_10_minutes_timestamps_and_prices(self):
|
|
54
|
+
"""
|
|
55
|
+
CRITICAL: Verify the +1 minute timestamp bug is fixed.
|
|
56
|
+
Test first 10 minutes to ensure market open spike is at 9:30, not 9:31.
|
|
57
|
+
"""
|
|
58
|
+
username = os.environ.get("THETADATA_USERNAME")
|
|
59
|
+
password = os.environ.get("THETADATA_PASSWORD")
|
|
60
|
+
|
|
61
|
+
asset = Asset("SPY", asset_type="stock")
|
|
62
|
+
|
|
63
|
+
# Get first 10 bars (9:30-9:40) directly from ThetaData
|
|
64
|
+
df = thetadata_helper.get_price_data(
|
|
65
|
+
username=username,
|
|
66
|
+
password=password,
|
|
67
|
+
asset=asset,
|
|
68
|
+
start=datetime.datetime(2024, 8, 1, 9, 30),
|
|
69
|
+
end=datetime.datetime(2024, 8, 1, 9, 40),
|
|
70
|
+
timespan="minute"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
assert df is not None and len(df) > 0, "No bars returned"
|
|
74
|
+
|
|
75
|
+
print(f"\nFirst 10 minutes of SPY:")
|
|
76
|
+
print(f"{'Time':<25} {'Open':<10} {'High':<10} {'Low':<10} {'Close':<10} {'Volume':<15}")
|
|
77
|
+
print("=" * 100)
|
|
78
|
+
|
|
79
|
+
for i in range(min(10, len(df))):
|
|
80
|
+
idx = df.index[i]
|
|
81
|
+
row = df.iloc[i]
|
|
82
|
+
print(f"{str(idx):<25} {row['open']:<10.2f} {row['high']:<10.2f} {row['low']:<10.2f} {row['close']:<10.2f} {row['volume']:<15,.0f}")
|
|
83
|
+
|
|
84
|
+
# Verify timestamps are 60 seconds apart
|
|
85
|
+
for i in range(1, min(10, len(df))):
|
|
86
|
+
time_diff = (df.index[i] - df.index[i-1]).total_seconds()
|
|
87
|
+
assert time_diff == 60, f"Bar {i} is {time_diff}s after bar {i-1}, expected 60s"
|
|
88
|
+
|
|
89
|
+
# Verify market open spike
|
|
90
|
+
# ThetaData has 1-minute offset (9:29 instead of 9:30), so check both first and second bar
|
|
91
|
+
first_bar = df.iloc[0]
|
|
92
|
+
second_bar = df.iloc[1]
|
|
93
|
+
|
|
94
|
+
# Find the bar with highest volume in first 3 bars (market open spike)
|
|
95
|
+
max_volume_idx = df.iloc[:3]['volume'].idxmax()
|
|
96
|
+
max_volume_bar = df.loc[max_volume_idx]
|
|
97
|
+
|
|
98
|
+
# Market open spike should have >100k volume
|
|
99
|
+
assert max_volume_bar['volume'] > 100000, \
|
|
100
|
+
f"Market open spike has low volume ({max_volume_bar['volume']:,.0f})"
|
|
101
|
+
|
|
102
|
+
print(f"\n✓ Timestamp verification PASSED")
|
|
103
|
+
print(f" - Market open spike at {max_volume_bar.name}: {max_volume_bar['volume']:,.0f} volume")
|
|
104
|
+
|
|
105
|
+
def test_noon_period_accuracy(self):
|
|
106
|
+
"""Test pricing accuracy at noon (different market conditions)."""
|
|
107
|
+
import os
|
|
108
|
+
POLYGON_API_KEY = os.environ.get("POLYGON_API_KEY")
|
|
109
|
+
|
|
110
|
+
if not POLYGON_API_KEY:
|
|
111
|
+
pytest.skip("Polygon API key not available")
|
|
112
|
+
|
|
113
|
+
# ThetaData
|
|
114
|
+
theta_ds = ThetaDataBacktesting(
|
|
115
|
+
datetime_start=datetime.datetime(2024, 8, 1),
|
|
116
|
+
datetime_end=datetime.datetime(2024, 8, 1, 12, 15),
|
|
117
|
+
username=os.environ.get("THETADATA_USERNAME"),
|
|
118
|
+
password=os.environ.get("THETADATA_PASSWORD"),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Polygon
|
|
122
|
+
polygon_ds = PolygonDataBacktesting(
|
|
123
|
+
datetime_start=datetime.datetime(2024, 8, 1),
|
|
124
|
+
datetime_end=datetime.datetime(2024, 8, 1, 12, 15),
|
|
125
|
+
api_key=POLYGON_API_KEY,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
asset = Asset("SPY", asset_type="stock")
|
|
129
|
+
|
|
130
|
+
# Get bars at noon
|
|
131
|
+
test_times = [
|
|
132
|
+
datetime.datetime(2024, 8, 1, 12, 0),
|
|
133
|
+
datetime.datetime(2024, 8, 1, 12, 5),
|
|
134
|
+
datetime.datetime(2024, 8, 1, 12, 10),
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
print(f"\nNoon period comparison for SPY:")
|
|
138
|
+
print(f"{'Time':<25} {'ThetaData':<12} {'Polygon':<12} {'Diff':<10} {'Status'}")
|
|
139
|
+
print("=" * 80)
|
|
140
|
+
|
|
141
|
+
for test_time in test_times:
|
|
142
|
+
# ThetaData
|
|
143
|
+
theta_ds._datetime = to_datetime_aware(test_time)
|
|
144
|
+
theta_bars = theta_ds.get_historical_prices(
|
|
145
|
+
asset=asset, length=1, timestep="minute"
|
|
146
|
+
)
|
|
147
|
+
theta_df = theta_bars.df if hasattr(theta_bars, 'df') else theta_bars
|
|
148
|
+
theta_price = theta_df.iloc[-1]['close'] if len(theta_df) > 0 else None
|
|
149
|
+
|
|
150
|
+
# Polygon
|
|
151
|
+
polygon_ds._datetime = to_datetime_aware(test_time)
|
|
152
|
+
polygon_bars = polygon_ds.get_historical_prices(
|
|
153
|
+
asset=asset, length=1, timestep="minute"
|
|
154
|
+
)
|
|
155
|
+
polygon_df = polygon_bars.df if hasattr(polygon_bars, 'df') else polygon_bars
|
|
156
|
+
polygon_price = polygon_df.iloc[-1]['close'] if len(polygon_df) > 0 else None
|
|
157
|
+
|
|
158
|
+
if theta_price and polygon_price:
|
|
159
|
+
diff = abs(theta_price - polygon_price)
|
|
160
|
+
status = "✓ PASS" if diff <= 0.01 else "✗ FAIL"
|
|
161
|
+
print(f"{str(test_time):<25} ${theta_price:<11.2f} ${polygon_price:<11.2f} ${diff:<9.4f} {status}")
|
|
162
|
+
|
|
163
|
+
assert diff <= 0.01, f"Price difference ${diff:.4f} exceeds 1¢ tolerance"
|
|
164
|
+
|
|
165
|
+
print(f"\n✓ Noon period accuracy PASSED")
|
|
166
|
+
|
|
167
|
+
def test_multiple_symbols(self):
|
|
168
|
+
"""Test 2-3 symbols with different price ranges."""
|
|
169
|
+
username = os.environ.get("THETADATA_USERNAME")
|
|
170
|
+
password = os.environ.get("THETADATA_PASSWORD")
|
|
171
|
+
|
|
172
|
+
theta = ThetaDataBacktesting(
|
|
173
|
+
datetime_start=datetime.datetime(2024, 8, 1),
|
|
174
|
+
datetime_end=datetime.datetime(2024, 8, 1, 10, 0),
|
|
175
|
+
username=username,
|
|
176
|
+
password=password,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Test different price ranges
|
|
180
|
+
symbols = [
|
|
181
|
+
("SPY", "ETF ~$550"),
|
|
182
|
+
("AMZN", "Stock ~$190"),
|
|
183
|
+
("AMD", "Stock ~$160"),
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
print(f"\nMultiple symbol test at market open:")
|
|
187
|
+
print(f"{'Symbol':<10} {'Description':<20} {'Open':<10} {'Close':<10} {'Volume':<15} {'Status'}")
|
|
188
|
+
print("=" * 90)
|
|
189
|
+
|
|
190
|
+
for symbol, description in symbols:
|
|
191
|
+
asset = Asset(symbol, asset_type="stock")
|
|
192
|
+
|
|
193
|
+
# Set datetime to market open
|
|
194
|
+
theta._datetime = to_datetime_aware(datetime.datetime(2024, 8, 1, 9, 30))
|
|
195
|
+
|
|
196
|
+
bars = theta.get_historical_prices(
|
|
197
|
+
asset=asset,
|
|
198
|
+
length=1,
|
|
199
|
+
timestep="minute"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
df = bars.df if hasattr(bars, 'df') else bars
|
|
203
|
+
assert df is not None and len(df) > 0, f"No data for {symbol}"
|
|
204
|
+
|
|
205
|
+
bar = df.iloc[0]
|
|
206
|
+
|
|
207
|
+
# Verify OHLC consistency
|
|
208
|
+
assert bar['high'] >= bar['open'], f"{symbol}: high < open"
|
|
209
|
+
assert bar['high'] >= bar['close'], f"{symbol}: high < close"
|
|
210
|
+
assert bar['low'] <= bar['open'], f"{symbol}: low > open"
|
|
211
|
+
assert bar['low'] <= bar['close'], f"{symbol}: low > close"
|
|
212
|
+
assert bar['volume'] > 0, f"{symbol}: zero volume"
|
|
213
|
+
|
|
214
|
+
status = "✓ PASS"
|
|
215
|
+
print(f"{symbol:<10} {description:<20} ${bar['open']:<9.2f} ${bar['close']:<9.2f} {bar['volume']:<15,.0f} {status}")
|
|
216
|
+
|
|
217
|
+
print(f"\n✓ Multiple symbols PASSED")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@pytest.mark.apitest
|
|
221
|
+
class TestThetaDataMethods:
|
|
222
|
+
"""Test key methods work correctly."""
|
|
223
|
+
|
|
224
|
+
def test_get_quote(self):
|
|
225
|
+
"""Test get_quote() returns correct data."""
|
|
226
|
+
username = os.environ.get("THETADATA_USERNAME")
|
|
227
|
+
password = os.environ.get("THETADATA_PASSWORD")
|
|
228
|
+
|
|
229
|
+
theta = ThetaDataBacktesting(
|
|
230
|
+
datetime_start=datetime.datetime(2024, 8, 1, 9, 30),
|
|
231
|
+
datetime_end=datetime.datetime(2024, 8, 1, 10, 0),
|
|
232
|
+
username=username,
|
|
233
|
+
password=password,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Simulate strategy getting quote
|
|
237
|
+
asset = Asset("SPY", asset_type="stock")
|
|
238
|
+
quote = theta.get_quote(asset, quote_asset=Asset("USD", asset_type="forex"))
|
|
239
|
+
|
|
240
|
+
print(f"\nget_quote() test for SPY:")
|
|
241
|
+
print(f" Price: ${quote.price:.2f}")
|
|
242
|
+
print(f" Bid: ${quote.bid:.2f}" if quote.bid else " Bid: None")
|
|
243
|
+
print(f" Ask: ${quote.ask:.2f}" if quote.ask else " Ask: None")
|
|
244
|
+
print(f" Volume: {quote.volume:,.0f}")
|
|
245
|
+
print(f" Timestamp: {quote.timestamp}")
|
|
246
|
+
|
|
247
|
+
assert quote is not None, "get_quote returned None"
|
|
248
|
+
assert quote.price > 0, "Quote price is zero or negative"
|
|
249
|
+
assert quote.volume > 0, "Quote volume is zero"
|
|
250
|
+
|
|
251
|
+
print(f"\n✓ get_quote() PASSED")
|
|
252
|
+
|
|
253
|
+
def test_get_chains(self):
|
|
254
|
+
"""Test get_chains() returns option chains."""
|
|
255
|
+
username = os.environ.get("THETADATA_USERNAME")
|
|
256
|
+
password = os.environ.get("THETADATA_PASSWORD")
|
|
257
|
+
|
|
258
|
+
theta = ThetaDataBacktesting(
|
|
259
|
+
datetime_start=datetime.datetime(2024, 8, 1, 9, 30),
|
|
260
|
+
datetime_end=datetime.datetime(2024, 8, 1, 10, 0),
|
|
261
|
+
username=username,
|
|
262
|
+
password=password,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
asset = Asset("SPY", asset_type="stock")
|
|
266
|
+
chains = theta.get_chains(asset)
|
|
267
|
+
|
|
268
|
+
print(f"\nget_chains() test for SPY:")
|
|
269
|
+
|
|
270
|
+
assert chains is not None, "get_chains returned None"
|
|
271
|
+
|
|
272
|
+
# Get expirations
|
|
273
|
+
if hasattr(chains, 'expirations'):
|
|
274
|
+
expirations = chains.expirations()
|
|
275
|
+
else:
|
|
276
|
+
expirations = chains.get("Chains", {}).get("CALL", {}).keys()
|
|
277
|
+
|
|
278
|
+
expirations_list = list(expirations)
|
|
279
|
+
print(f" Number of expirations: {len(expirations_list)}")
|
|
280
|
+
print(f" First 3 expirations: {expirations_list[:3]}")
|
|
281
|
+
|
|
282
|
+
# Get strikes for first expiration
|
|
283
|
+
first_exp = expirations_list[0]
|
|
284
|
+
if hasattr(chains, 'strikes'):
|
|
285
|
+
strikes = chains.strikes(first_exp, "CALL")
|
|
286
|
+
else:
|
|
287
|
+
strikes = chains.get("Chains", {}).get("CALL", {}).get(first_exp, [])
|
|
288
|
+
|
|
289
|
+
print(f" Strikes for {first_exp}: {len(strikes)} strikes")
|
|
290
|
+
print(f" Sample strikes: {sorted(strikes)[:5]} ... {sorted(strikes)[-5:]}")
|
|
291
|
+
|
|
292
|
+
assert len(expirations_list) > 0, "No expirations found"
|
|
293
|
+
assert len(strikes) > 0, "No strikes found"
|
|
294
|
+
|
|
295
|
+
print(f"\n✓ get_chains() PASSED")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@pytest.mark.apitest
|
|
299
|
+
class TestThetaDataOptions:
|
|
300
|
+
"""Test options pricing."""
|
|
301
|
+
|
|
302
|
+
def test_atm_call_and_put(self):
|
|
303
|
+
"""Test ATM call and put pricing."""
|
|
304
|
+
username = os.environ.get("THETADATA_USERNAME")
|
|
305
|
+
password = os.environ.get("THETADATA_PASSWORD")
|
|
306
|
+
|
|
307
|
+
theta = ThetaDataBacktesting(
|
|
308
|
+
datetime_start=datetime.datetime(2024, 8, 1, 9, 30),
|
|
309
|
+
datetime_end=datetime.datetime(2024, 8, 1, 10, 0),
|
|
310
|
+
username=username,
|
|
311
|
+
password=password,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Get underlying price
|
|
315
|
+
underlying = Asset("SPY", asset_type="stock")
|
|
316
|
+
underlying_price = theta.get_last_price(underlying)
|
|
317
|
+
|
|
318
|
+
print(f"\nOptions test for SPY:")
|
|
319
|
+
print(f" Underlying price: ${underlying_price:.2f}")
|
|
320
|
+
|
|
321
|
+
# Get chains
|
|
322
|
+
chains = theta.get_chains(underlying)
|
|
323
|
+
|
|
324
|
+
# Get first expiration
|
|
325
|
+
if hasattr(chains, 'expirations'):
|
|
326
|
+
expirations = list(chains.expirations())
|
|
327
|
+
else:
|
|
328
|
+
expirations = list(chains.get("Chains", {}).get("CALL", {}).keys())
|
|
329
|
+
|
|
330
|
+
first_exp = expirations[0]
|
|
331
|
+
expiration_date = datetime.datetime.strptime(first_exp, "%Y-%m-%d").date()
|
|
332
|
+
|
|
333
|
+
# Get ATM strike
|
|
334
|
+
if hasattr(chains, 'strikes'):
|
|
335
|
+
strikes = chains.strikes(first_exp, "CALL")
|
|
336
|
+
else:
|
|
337
|
+
strikes = chains.get("Chains", {}).get("CALL", {}).get(first_exp, [])
|
|
338
|
+
|
|
339
|
+
atm_strike = min(strikes, key=lambda x: abs(x - underlying_price))
|
|
340
|
+
|
|
341
|
+
print(f" Expiration: {first_exp}")
|
|
342
|
+
print(f" ATM strike: ${atm_strike:.2f}")
|
|
343
|
+
|
|
344
|
+
# Test ATM CALL
|
|
345
|
+
call_option = Asset(
|
|
346
|
+
"SPY",
|
|
347
|
+
asset_type="option",
|
|
348
|
+
expiration=expiration_date,
|
|
349
|
+
strike=atm_strike,
|
|
350
|
+
right="CALL"
|
|
351
|
+
)
|
|
352
|
+
call_price = theta.get_last_price(call_option)
|
|
353
|
+
|
|
354
|
+
# Test ATM PUT
|
|
355
|
+
put_option = Asset(
|
|
356
|
+
"SPY",
|
|
357
|
+
asset_type="option",
|
|
358
|
+
expiration=expiration_date,
|
|
359
|
+
strike=atm_strike,
|
|
360
|
+
right="PUT"
|
|
361
|
+
)
|
|
362
|
+
put_price = theta.get_last_price(put_option)
|
|
363
|
+
|
|
364
|
+
print(f" ATM Call price: ${call_price:.2f}")
|
|
365
|
+
print(f" ATM Put price: ${put_price:.2f}")
|
|
366
|
+
|
|
367
|
+
assert call_price > 0, "Call price is zero or negative"
|
|
368
|
+
assert put_price > 0, "Put price is zero or negative"
|
|
369
|
+
assert call_price > 0.05, "Call price suspiciously low (< $0.05)"
|
|
370
|
+
assert put_price > 0.05, "Put price suspiciously low (< $0.05)"
|
|
371
|
+
|
|
372
|
+
print(f"\n✓ Options pricing PASSED")
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
@pytest.mark.apitest
|
|
376
|
+
class TestThetaDataIndexes:
|
|
377
|
+
"""Test index data."""
|
|
378
|
+
|
|
379
|
+
def test_spx_pricing(self):
|
|
380
|
+
"""Test SPX index pricing."""
|
|
381
|
+
username = os.environ.get("THETADATA_USERNAME")
|
|
382
|
+
password = os.environ.get("THETADATA_PASSWORD")
|
|
383
|
+
|
|
384
|
+
theta = ThetaDataBacktesting(
|
|
385
|
+
datetime_start=datetime.datetime(2024, 8, 1, 9, 30),
|
|
386
|
+
datetime_end=datetime.datetime(2024, 8, 1, 12, 30),
|
|
387
|
+
username=username,
|
|
388
|
+
password=password,
|
|
389
|
+
use_quote_data=False, # Indices don't need bid/ask data
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
asset = Asset("SPX", asset_type="index")
|
|
393
|
+
|
|
394
|
+
# Test at market open
|
|
395
|
+
open_price = theta.get_last_price(asset, quote_asset=Asset("USD", asset_type="forex"))
|
|
396
|
+
|
|
397
|
+
print(f"\nSPX index test:")
|
|
398
|
+
print(f" Market open (9:30): ${open_price:.2f}")
|
|
399
|
+
|
|
400
|
+
assert open_price > 0, "SPX price is zero or negative"
|
|
401
|
+
assert 4000 < open_price < 7000, f"SPX price ${open_price:.2f} is outside reasonable range"
|
|
402
|
+
|
|
403
|
+
print(f"\n✓ Index pricing PASSED")
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@pytest.mark.apitest
|
|
407
|
+
class TestThetaDataExtendedHours:
|
|
408
|
+
"""Test pre-market and after-hours data."""
|
|
409
|
+
|
|
410
|
+
def test_premarket_data(self):
|
|
411
|
+
"""Test pre-market data (9:00-9:30)."""
|
|
412
|
+
username = os.environ.get("THETADATA_USERNAME")
|
|
413
|
+
password = os.environ.get("THETADATA_PASSWORD")
|
|
414
|
+
|
|
415
|
+
theta = ThetaDataBacktesting(
|
|
416
|
+
datetime_start=datetime.datetime(2024, 8, 1, 9, 0),
|
|
417
|
+
datetime_end=datetime.datetime(2024, 8, 1, 9, 30),
|
|
418
|
+
username=username,
|
|
419
|
+
password=password,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
asset = Asset("SPY", asset_type="stock")
|
|
423
|
+
|
|
424
|
+
# Set datetime to pre-market
|
|
425
|
+
theta._datetime = to_datetime_aware(datetime.datetime(2024, 8, 1, 9, 0))
|
|
426
|
+
|
|
427
|
+
bars = theta.get_historical_prices(
|
|
428
|
+
asset=asset,
|
|
429
|
+
length=5,
|
|
430
|
+
timestep="minute"
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
df = bars.df if hasattr(bars, 'df') else bars
|
|
434
|
+
|
|
435
|
+
print(f"\nPre-market data test for SPY:")
|
|
436
|
+
print(f" Bars from 9:00-9:05:")
|
|
437
|
+
for i in range(min(5, len(df))):
|
|
438
|
+
bar = df.iloc[i]
|
|
439
|
+
print(f" {df.index[i]}: Open=${bar['open']:.2f}, Volume={bar['volume']:,.0f}")
|
|
440
|
+
|
|
441
|
+
# Pre-market should have much lower volume than regular hours
|
|
442
|
+
if len(df) > 0:
|
|
443
|
+
avg_volume = df['volume'].mean()
|
|
444
|
+
print(f" Average pre-market volume: {avg_volume:,.0f}")
|
|
445
|
+
print(f" ✓ Pre-market data available")
|
|
446
|
+
else:
|
|
447
|
+
pytest.skip("Pre-market data not available")
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
@pytest.mark.apitest
|
|
451
|
+
class TestThetaDataQuoteContinuity:
|
|
452
|
+
"""Test that quote data is continuous across multiple days for options."""
|
|
453
|
+
|
|
454
|
+
def test_multi_day_option_quote_coverage(self):
|
|
455
|
+
"""
|
|
456
|
+
CRITICAL: Verify quote data covers the same date range as OHLC data.
|
|
457
|
+
This test ensures pagination is working correctly.
|
|
458
|
+
"""
|
|
459
|
+
username = os.environ.get("THETADATA_USERNAME")
|
|
460
|
+
password = os.environ.get("THETADATA_PASSWORD")
|
|
461
|
+
|
|
462
|
+
# Test a liquid option over 10+ trading days
|
|
463
|
+
asset = Asset(
|
|
464
|
+
symbol="SPY",
|
|
465
|
+
asset_type="option",
|
|
466
|
+
expiration=datetime.date(2024, 9, 20),
|
|
467
|
+
strike=550,
|
|
468
|
+
right="CALL"
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
start = datetime.datetime(2024, 8, 26, 9, 30)
|
|
472
|
+
end = datetime.datetime(2024, 9, 12, 16, 0)
|
|
473
|
+
|
|
474
|
+
# Get OHLC data
|
|
475
|
+
df_ohlc = thetadata_helper.get_price_data(
|
|
476
|
+
username=username,
|
|
477
|
+
password=password,
|
|
478
|
+
asset=asset,
|
|
479
|
+
start=start,
|
|
480
|
+
end=end,
|
|
481
|
+
timespan="minute",
|
|
482
|
+
datastyle="ohlc"
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# Get quote data
|
|
486
|
+
df_quote = thetadata_helper.get_price_data(
|
|
487
|
+
username=username,
|
|
488
|
+
password=password,
|
|
489
|
+
asset=asset,
|
|
490
|
+
start=start,
|
|
491
|
+
end=end,
|
|
492
|
+
timespan="minute",
|
|
493
|
+
datastyle="quote"
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
assert df_ohlc is not None and len(df_ohlc) > 0, "No OHLC data returned"
|
|
497
|
+
assert df_quote is not None and len(df_quote) > 0, "No quote data returned"
|
|
498
|
+
|
|
499
|
+
# Check date coverage
|
|
500
|
+
ohlc_dates = df_ohlc.index.date
|
|
501
|
+
quote_dates = df_quote.index.date
|
|
502
|
+
|
|
503
|
+
ohlc_unique_dates = sorted(set(ohlc_dates))
|
|
504
|
+
quote_unique_dates = sorted(set(quote_dates))
|
|
505
|
+
|
|
506
|
+
print(f"\nOHLC date coverage: {len(ohlc_unique_dates)} unique dates")
|
|
507
|
+
print(f"Quote date coverage: {len(quote_unique_dates)} unique dates")
|
|
508
|
+
print(f"OHLC rows: {len(df_ohlc)}")
|
|
509
|
+
print(f"Quote rows: {len(df_quote)}")
|
|
510
|
+
|
|
511
|
+
# Quote data should cover at least 80% of OHLC dates (allow some tolerance)
|
|
512
|
+
coverage_ratio = len(quote_unique_dates) / len(ohlc_unique_dates)
|
|
513
|
+
print(f"Quote coverage ratio: {coverage_ratio:.1%}")
|
|
514
|
+
|
|
515
|
+
assert coverage_ratio >= 0.8, f"Quote data only covers {coverage_ratio:.1%} of OHLC dates. Pagination may be broken."
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
@pytest.mark.apitest
|
|
519
|
+
class TestThetaDataHelperLive:
|
|
520
|
+
"""Live validation for thetadata_helper utilities."""
|
|
521
|
+
|
|
522
|
+
eastern = pytz.timezone("America/New_York")
|
|
523
|
+
|
|
524
|
+
def test_get_price_data_regular_vs_extended(self, theta_credentials):
|
|
525
|
+
username, password = theta_credentials
|
|
526
|
+
asset = Asset("SPY", asset_type="stock")
|
|
527
|
+
start = datetime.datetime(2024, 8, 1, 4, 0)
|
|
528
|
+
end = datetime.datetime(2024, 8, 1, 10, 0)
|
|
529
|
+
|
|
530
|
+
extended_df = thetadata_helper.get_historical_data(
|
|
531
|
+
asset=asset,
|
|
532
|
+
start_dt=start,
|
|
533
|
+
end_dt=end,
|
|
534
|
+
ivl=60000,
|
|
535
|
+
username=username,
|
|
536
|
+
password=password,
|
|
537
|
+
datastyle="ohlc",
|
|
538
|
+
include_after_hours=True,
|
|
539
|
+
)
|
|
540
|
+
assert extended_df is not None and not extended_df.empty, "ThetaData returned no extended-hours data for SPY"
|
|
541
|
+
|
|
542
|
+
rth_df = thetadata_helper.get_historical_data(
|
|
543
|
+
asset=asset,
|
|
544
|
+
start_dt=start,
|
|
545
|
+
end_dt=end,
|
|
546
|
+
ivl=60000,
|
|
547
|
+
username=username,
|
|
548
|
+
password=password,
|
|
549
|
+
datastyle="ohlc",
|
|
550
|
+
include_after_hours=False,
|
|
551
|
+
)
|
|
552
|
+
assert rth_df is not None and not rth_df.empty, "ThetaData returned no regular-hours data for SPY"
|
|
553
|
+
|
|
554
|
+
extended_local = extended_df.index.tz_convert(self.eastern)
|
|
555
|
+
rth_local = rth_df.index.tz_convert(self.eastern)
|
|
556
|
+
|
|
557
|
+
assert extended_local.min().time() <= datetime.time(4, 5), "Extended data missing premarket rows"
|
|
558
|
+
assert rth_local.min().time() >= datetime.time(9, 29), "Regular-hours data unexpectedly includes premarket rows"
|
|
559
|
+
|
|
560
|
+
def test_get_price_data_multi_chunk_fetch(self, theta_credentials):
|
|
561
|
+
username, password = theta_credentials
|
|
562
|
+
asset = Asset("SPY", asset_type="stock")
|
|
563
|
+
start = datetime.datetime(2025, 8, 1)
|
|
564
|
+
end = datetime.datetime(2025, 8, 20)
|
|
565
|
+
|
|
566
|
+
df = thetadata_helper.get_price_data(
|
|
567
|
+
username=username,
|
|
568
|
+
password=password,
|
|
569
|
+
asset=asset,
|
|
570
|
+
start=start,
|
|
571
|
+
end=end,
|
|
572
|
+
timespan="minute",
|
|
573
|
+
include_after_hours=False,
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
if df is None or df.empty:
|
|
577
|
+
pytest.skip("ThetaData returned no historical data for requested range")
|
|
578
|
+
|
|
579
|
+
assert df.index.min().date() <= start.date()
|
|
580
|
+
assert df.index.max().date() >= end.date()
|
|
581
|
+
assert df.index.is_monotonic_increasing
|
|
582
|
+
assert not df.index.has_duplicates
|
|
583
|
+
|
|
584
|
+
def test_get_historical_data_option_live(self, theta_credentials):
|
|
585
|
+
username, password = theta_credentials
|
|
586
|
+
asset = Asset(
|
|
587
|
+
symbol="SPY",
|
|
588
|
+
asset_type="option",
|
|
589
|
+
expiration=datetime.datetime(2024, 8, 16),
|
|
590
|
+
strike=450.0,
|
|
591
|
+
right="CALL",
|
|
592
|
+
)
|
|
593
|
+
start_dt = datetime.datetime(2024, 8, 1, 9, 30)
|
|
594
|
+
end_dt = datetime.datetime(2024, 8, 1, 16, 0)
|
|
595
|
+
|
|
596
|
+
df = thetadata_helper.get_historical_data(
|
|
597
|
+
asset=asset,
|
|
598
|
+
start_dt=start_dt,
|
|
599
|
+
end_dt=end_dt,
|
|
600
|
+
ivl=60000,
|
|
601
|
+
username=username,
|
|
602
|
+
password=password,
|
|
603
|
+
datastyle="ohlc",
|
|
604
|
+
include_after_hours=False,
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
if df is None or df.empty:
|
|
608
|
+
pytest.skip("ThetaData returned no option data for SPY call on 2024-08-01")
|
|
609
|
+
|
|
610
|
+
assert set(["open", "high", "low", "close", "volume", "count"]).issubset(df.columns)
|
|
611
|
+
assert df.index.tz.zone == "America/New_York"
|
|
612
|
+
assert (df[["open", "high", "low", "close"]] >= 0).all().all()
|
|
613
|
+
|
|
614
|
+
def test_get_historical_data_index_live(self, theta_credentials):
|
|
615
|
+
username, password = theta_credentials
|
|
616
|
+
asset = Asset("SPX", asset_type="index")
|
|
617
|
+
start_dt = datetime.datetime(2024, 8, 1, 9, 30)
|
|
618
|
+
end_dt = datetime.datetime(2024, 8, 1, 16, 0)
|
|
619
|
+
|
|
620
|
+
df = thetadata_helper.get_historical_data(
|
|
621
|
+
asset=asset,
|
|
622
|
+
start_dt=start_dt,
|
|
623
|
+
end_dt=end_dt,
|
|
624
|
+
ivl=60000,
|
|
625
|
+
username=username,
|
|
626
|
+
password=password,
|
|
627
|
+
datastyle="ohlc",
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
if df is None or df.empty:
|
|
631
|
+
pytest.skip("ThetaData returned no SPX index data for requested window")
|
|
632
|
+
|
|
633
|
+
assert df.index.tz.zone == "America/New_York"
|
|
634
|
+
assert "count" in df.columns
|
|
635
|
+
assert df.shape[0] > 0
|
|
636
|
+
|
|
637
|
+
def test_get_historical_data_quote_style(self, theta_credentials):
|
|
638
|
+
username, password = theta_credentials
|
|
639
|
+
asset = Asset("SPY", asset_type="stock")
|
|
640
|
+
start_dt = datetime.datetime(2024, 8, 1, 9, 30)
|
|
641
|
+
end_dt = datetime.datetime(2024, 8, 1, 10, 0)
|
|
642
|
+
|
|
643
|
+
df = thetadata_helper.get_historical_data(
|
|
644
|
+
asset=asset,
|
|
645
|
+
start_dt=start_dt,
|
|
646
|
+
end_dt=end_dt,
|
|
647
|
+
ivl=60000,
|
|
648
|
+
username=username,
|
|
649
|
+
password=password,
|
|
650
|
+
datastyle="quote",
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
if df is None or df.empty:
|
|
654
|
+
pytest.skip("ThetaData returned no quote data for SPY in requested window")
|
|
655
|
+
|
|
656
|
+
expected_columns = {"bid_size", "bid_condition", "bid", "ask_size", "ask_condition", "ask"}
|
|
657
|
+
assert expected_columns.issubset(df.columns)
|
|
658
|
+
assert df.index.tz.zone == "America/New_York"
|
|
659
|
+
|
|
660
|
+
def test_get_historical_data_no_data_returns_none(self, theta_credentials):
|
|
661
|
+
username, password = theta_credentials
|
|
662
|
+
asset = Asset("SPY", asset_type="stock")
|
|
663
|
+
start_dt = datetime.datetime(2024, 8, 3, 9, 30) # Saturday
|
|
664
|
+
end_dt = datetime.datetime(2024, 8, 3, 16, 0)
|
|
665
|
+
|
|
666
|
+
df = thetadata_helper.get_historical_data(
|
|
667
|
+
asset=asset,
|
|
668
|
+
start_dt=start_dt,
|
|
669
|
+
end_dt=end_dt,
|
|
670
|
+
ivl=60000,
|
|
671
|
+
username=username,
|
|
672
|
+
password=password,
|
|
673
|
+
datastyle="ohlc",
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
assert df is None
|
|
677
|
+
|
|
678
|
+
def test_get_expirations_and_strikes_live(self, theta_credentials):
|
|
679
|
+
username, password = theta_credentials
|
|
680
|
+
after_date = datetime.date(2024, 8, 1)
|
|
681
|
+
|
|
682
|
+
expirations = thetadata_helper.get_expirations(
|
|
683
|
+
username=username,
|
|
684
|
+
password=password,
|
|
685
|
+
ticker="AAPL",
|
|
686
|
+
after_date=after_date,
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
if not expirations:
|
|
690
|
+
pytest.skip("ThetaData returned no expirations for AAPL")
|
|
691
|
+
|
|
692
|
+
first_expiration = datetime.datetime.strptime(expirations[0], "%Y-%m-%d")
|
|
693
|
+
assert first_expiration.date() >= after_date
|
|
694
|
+
|
|
695
|
+
strikes = thetadata_helper.get_strikes(
|
|
696
|
+
username=username,
|
|
697
|
+
password=password,
|
|
698
|
+
ticker="AAPL",
|
|
699
|
+
expiration=first_expiration,
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
assert strikes
|
|
703
|
+
assert all(isinstance(value, float) for value in strikes)
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
@pytest.mark.apitest
|
|
707
|
+
class TestThetaDataPagination:
|
|
708
|
+
"""Test that pagination follows next_page header correctly."""
|
|
709
|
+
|
|
710
|
+
def test_pagination_with_mock(self):
|
|
711
|
+
"""
|
|
712
|
+
Test pagination logic by verifying the get_request function can handle
|
|
713
|
+
multiple pages. This is a basic test to ensure the code structure is correct.
|
|
714
|
+
"""
|
|
715
|
+
from lumibot.tools import thetadata_helper
|
|
716
|
+
|
|
717
|
+
# Just verify the function signature accepts the parameters and has pagination logic
|
|
718
|
+
import inspect
|
|
719
|
+
source = inspect.getsource(thetadata_helper.get_request)
|
|
720
|
+
|
|
721
|
+
# Check for pagination keywords in the source
|
|
722
|
+
assert "next_page" in source, "get_request should check for next_page header"
|
|
723
|
+
assert "all_responses" in source or "page" in source.lower(), "get_request should collect multiple pages"
|
|
724
|
+
|
|
725
|
+
print("\n✓ Pagination logic detected in get_request()")
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
if __name__ == "__main__":
|
|
729
|
+
pytest.main([__file__, "-v", "-s"])
|