lumibot 4.0.23__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/__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.0.23.dist-info → lumibot-4.1.0.dist-info}/METADATA +1 -2
- {lumibot-4.0.23.dist-info → lumibot-4.1.0.dist-info}/RECORD +160 -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 +73 -86
- 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.0.dist-info}/LICENSE +0 -0
- {lumibot-4.0.23.dist-info → lumibot-4.1.0.dist-info}/WHEEL +0 -0
- {lumibot-4.0.23.dist-info → lumibot-4.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import requests
|
|
3
|
+
import json
|
|
4
|
+
from typing import Union
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from termcolor import colored
|
|
8
|
+
from lumibot.brokers import Broker
|
|
9
|
+
from lumibot.entities import Asset, Order, Position
|
|
10
|
+
from lumibot.data_sources import TradeovateData
|
|
11
|
+
|
|
12
|
+
class TradeovateAPIError(Exception):
|
|
13
|
+
"""Exception raised for errors in the Tradeovate API."""
|
|
14
|
+
def __init__(self, message, status_code=None, response_text=None, original_exception=None):
|
|
15
|
+
self.status_code = status_code
|
|
16
|
+
self.response_text = response_text
|
|
17
|
+
self.original_exception = original_exception
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
|
|
20
|
+
class Tradeovate(Broker):
|
|
21
|
+
"""
|
|
22
|
+
Tradeovate broker that implements connection to the Tradeovate API.
|
|
23
|
+
"""
|
|
24
|
+
NAME = "Tradeovate"
|
|
25
|
+
|
|
26
|
+
def __init__(self, config=None, data_source=None):
|
|
27
|
+
if config is None:
|
|
28
|
+
config = {}
|
|
29
|
+
|
|
30
|
+
is_paper = config.get("IS_PAPER", True)
|
|
31
|
+
self.trading_api_url = "https://demo.tradovateapi.com/v1" if is_paper else "https://live.tradovateapi.com/v1"
|
|
32
|
+
self.market_data_url = config.get("MD_URL", "https://md.tradovateapi.com/v1")
|
|
33
|
+
self.username = config.get("USERNAME")
|
|
34
|
+
self.password = config.get("DEDICATED_PASSWORD")
|
|
35
|
+
self.app_id = config.get("APP_ID", "Lumibot")
|
|
36
|
+
self.app_version = config.get("APP_VERSION", "1.0")
|
|
37
|
+
self.cid = config.get("CID")
|
|
38
|
+
self.sec = config.get("SECRET")
|
|
39
|
+
|
|
40
|
+
# Authenticate and get tokens before creating data_source
|
|
41
|
+
try:
|
|
42
|
+
tokens = self._get_tokens()
|
|
43
|
+
self.trading_token = tokens["accessToken"]
|
|
44
|
+
self.market_token = tokens["marketToken"]
|
|
45
|
+
self.has_market_data = tokens["hasMarketData"]
|
|
46
|
+
logging.info(colored("Successfully acquired tokens from Tradeovate.", "green"))
|
|
47
|
+
|
|
48
|
+
# Now create the data source with the tokens if it wasn't provided
|
|
49
|
+
if data_source is None:
|
|
50
|
+
# Update config with API URLs for consistency
|
|
51
|
+
config["TRADING_API_URL"] = self.trading_api_url
|
|
52
|
+
config["MD_URL"] = self.market_data_url
|
|
53
|
+
data_source = TradeovateData(
|
|
54
|
+
config=config,
|
|
55
|
+
trading_token=self.trading_token,
|
|
56
|
+
market_token=self.market_token
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
super().__init__(name=self.NAME, data_source=data_source, config=config)
|
|
60
|
+
|
|
61
|
+
account_info = self._get_account_info(self.trading_token)
|
|
62
|
+
self.account_spec = account_info["accountSpec"]
|
|
63
|
+
self.account_id = account_info["accountId"]
|
|
64
|
+
logging.info(colored(f"Account Info: {account_info}", "green"))
|
|
65
|
+
|
|
66
|
+
self.user_id = self._get_user_info(self.trading_token)
|
|
67
|
+
logging.info(colored(f"User ID: {self.user_id}", "green"))
|
|
68
|
+
|
|
69
|
+
except TradeovateAPIError as e:
|
|
70
|
+
logging.error(colored(f"Failed to connect to Tradeovate: {e}", "red"))
|
|
71
|
+
raise e
|
|
72
|
+
|
|
73
|
+
def _get_headers(self, with_auth=True, with_content_type=False):
|
|
74
|
+
"""
|
|
75
|
+
Create standard headers for API requests.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
with_auth : bool
|
|
80
|
+
Whether to include the Authorization header with the trading token
|
|
81
|
+
with_content_type : bool
|
|
82
|
+
Whether to include Content-Type header for JSON requests
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
dict
|
|
87
|
+
Dictionary of headers for API requests
|
|
88
|
+
"""
|
|
89
|
+
headers = {"Accept": "application/json"}
|
|
90
|
+
if with_auth:
|
|
91
|
+
headers["Authorization"] = f"Bearer {self.trading_token}"
|
|
92
|
+
if with_content_type:
|
|
93
|
+
headers["Content-Type"] = "application/json"
|
|
94
|
+
return headers
|
|
95
|
+
|
|
96
|
+
def _get_tokens(self):
|
|
97
|
+
"""
|
|
98
|
+
Authenticate with Tradeovate and obtain the access tokens.
|
|
99
|
+
"""
|
|
100
|
+
url = f"{self.trading_api_url}/auth/accesstokenrequest"
|
|
101
|
+
payload = {
|
|
102
|
+
"name": self.username,
|
|
103
|
+
"password": self.password,
|
|
104
|
+
"appId": self.app_id,
|
|
105
|
+
"appVersion": self.app_version,
|
|
106
|
+
"cid": self.cid,
|
|
107
|
+
"sec": self.sec
|
|
108
|
+
}
|
|
109
|
+
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
|
110
|
+
try:
|
|
111
|
+
response = requests.post(url, json=payload, headers=headers)
|
|
112
|
+
response.raise_for_status()
|
|
113
|
+
data = response.json()
|
|
114
|
+
access_token = data.get("accessToken")
|
|
115
|
+
market_token = data.get("mdAccessToken")
|
|
116
|
+
has_market_data = data.get("hasMarketData", False)
|
|
117
|
+
if not access_token or not market_token:
|
|
118
|
+
raise TradeovateAPIError("Authentication succeeded but tokens are missing.")
|
|
119
|
+
return {"accessToken": access_token, "marketToken": market_token, "hasMarketData": has_market_data}
|
|
120
|
+
except requests.exceptions.RequestException as e:
|
|
121
|
+
raise TradeovateAPIError(f"Authentication failed",
|
|
122
|
+
status_code=getattr(e.response, 'status_code', None),
|
|
123
|
+
response_text=getattr(e.response, 'text', None),
|
|
124
|
+
original_exception=e)
|
|
125
|
+
|
|
126
|
+
def _get_account_info(self, trading_token):
|
|
127
|
+
"""
|
|
128
|
+
Retrieve account information from Tradeovate.
|
|
129
|
+
"""
|
|
130
|
+
url = f"{self.trading_api_url}/account/list"
|
|
131
|
+
headers = self._get_headers()
|
|
132
|
+
try:
|
|
133
|
+
response = requests.get(url, headers=headers)
|
|
134
|
+
response.raise_for_status()
|
|
135
|
+
accounts = response.json()
|
|
136
|
+
if isinstance(accounts, list) and accounts:
|
|
137
|
+
account = accounts[0]
|
|
138
|
+
return {"accountSpec": account.get("name"), "accountId": account.get("id")}
|
|
139
|
+
else:
|
|
140
|
+
raise TradeovateAPIError("No accounts found in the account list response.")
|
|
141
|
+
except requests.exceptions.RequestException as e:
|
|
142
|
+
raise TradeovateAPIError(f"Failed to retrieve account list",
|
|
143
|
+
status_code=getattr(e.response, 'status_code', None),
|
|
144
|
+
response_text=getattr(e.response, 'text', None),
|
|
145
|
+
original_exception=e)
|
|
146
|
+
|
|
147
|
+
def _get_user_info(self, trading_token):
|
|
148
|
+
"""
|
|
149
|
+
Retrieve user information from Tradeovate.
|
|
150
|
+
"""
|
|
151
|
+
url = f"{self.trading_api_url}/user/list"
|
|
152
|
+
headers = self._get_headers()
|
|
153
|
+
try:
|
|
154
|
+
response = requests.get(url, headers=headers)
|
|
155
|
+
response.raise_for_status()
|
|
156
|
+
users = response.json()
|
|
157
|
+
if isinstance(users, list) and users:
|
|
158
|
+
user = users[0]
|
|
159
|
+
return user.get("id")
|
|
160
|
+
else:
|
|
161
|
+
raise TradeovateAPIError("No users found in the user list response.")
|
|
162
|
+
except requests.exceptions.RequestException as e:
|
|
163
|
+
raise TradeovateAPIError(f"Failed to retrieve user list",
|
|
164
|
+
status_code=getattr(e.response, 'status_code', None),
|
|
165
|
+
response_text=getattr(e.response, 'text', None),
|
|
166
|
+
original_exception=e)
|
|
167
|
+
|
|
168
|
+
def _get_contract_details(self, contract_id: int) -> dict:
|
|
169
|
+
"""
|
|
170
|
+
Retrieve contract details for a given contract id from Tradeovate using the /contract/item endpoint.
|
|
171
|
+
|
|
172
|
+
Endpoint: GET /contract/item?id=<contract_id>
|
|
173
|
+
Response Schema: { "id": int, "name": string, "contractMaturityId": int }
|
|
174
|
+
"""
|
|
175
|
+
url = f"{self.trading_api_url}/contract/item"
|
|
176
|
+
params = {"id": contract_id}
|
|
177
|
+
headers = self._get_headers()
|
|
178
|
+
try:
|
|
179
|
+
response = requests.get(url, params=params, headers=headers)
|
|
180
|
+
response.raise_for_status()
|
|
181
|
+
return response.json()
|
|
182
|
+
except requests.exceptions.RequestException as e:
|
|
183
|
+
raise TradeovateAPIError(f"Failed to retrieve contract details for contract {contract_id}",
|
|
184
|
+
status_code=getattr(e.response, 'status_code', None),
|
|
185
|
+
response_text=getattr(e.response, 'text', None),
|
|
186
|
+
original_exception=e)
|
|
187
|
+
|
|
188
|
+
def _get_balances_at_broker(self, quote_asset: Asset, strategy) -> tuple:
|
|
189
|
+
"""
|
|
190
|
+
Retrieve the account financial snapshot from Tradeovate and compute:
|
|
191
|
+
- Cash balance (totalCashValue)
|
|
192
|
+
- Positions value (netLiq - totalCashValue)
|
|
193
|
+
- Portfolio value (netLiq)
|
|
194
|
+
"""
|
|
195
|
+
url = f"{self.trading_api_url}/cashBalance/getcashbalancesnapshot"
|
|
196
|
+
headers = self._get_headers(with_content_type=True)
|
|
197
|
+
payload = {"accountId": self.account_id}
|
|
198
|
+
try:
|
|
199
|
+
response = requests.post(url, json=payload, headers=headers)
|
|
200
|
+
response.raise_for_status()
|
|
201
|
+
data = response.json()
|
|
202
|
+
cash_balance = data.get("totalCashValue")
|
|
203
|
+
net_liq = data.get("netLiq")
|
|
204
|
+
if cash_balance is None or net_liq is None:
|
|
205
|
+
raise TradeovateAPIError("Missing totalCashValue or netLiq in account financials response.")
|
|
206
|
+
positions_value = net_liq - cash_balance
|
|
207
|
+
portfolio_value = net_liq
|
|
208
|
+
return cash_balance, positions_value, portfolio_value
|
|
209
|
+
except requests.exceptions.RequestException as e:
|
|
210
|
+
raise TradeovateAPIError(f"Failed to retrieve account financials",
|
|
211
|
+
status_code=getattr(e.response, 'status_code', None),
|
|
212
|
+
response_text=getattr(e.response, 'text', None),
|
|
213
|
+
original_exception=e)
|
|
214
|
+
|
|
215
|
+
def _get_stream_object(self):
|
|
216
|
+
logging.info(colored("Method '_get_stream_object' is not yet implemented.", "yellow"))
|
|
217
|
+
return None # Return None as a placeholder
|
|
218
|
+
|
|
219
|
+
def _parse_broker_order(self, response: dict, strategy_name: str, strategy_object=None) -> Order:
|
|
220
|
+
"""
|
|
221
|
+
Convert a Tradeovate order dictionary into a Lumibot Order object.
|
|
222
|
+
|
|
223
|
+
Expected Tradeovate fields:
|
|
224
|
+
- id: order id
|
|
225
|
+
- contractId: used to get asset details (for futures, asset_type is "future")
|
|
226
|
+
- orderQty: the quantity
|
|
227
|
+
- action: "Buy" or "Sell" (will be normalized to lowercase)
|
|
228
|
+
- ordStatus: order status; possible values include "Working", "Filled", "PartialFill",
|
|
229
|
+
"Canceled", "Rejected", "Expired", "Submitted", etc.
|
|
230
|
+
- timestamp: an ISO timestamp string (with a trailing 'Z' for UTC)
|
|
231
|
+
- orderType, price, stopPrice: if provided
|
|
232
|
+
|
|
233
|
+
This function retrieves contract details (using _get_contract_details) to create an Asset,
|
|
234
|
+
maps raw statuses to Lumibot's expected statuses, converts the timestamp into a datetime object,
|
|
235
|
+
and creates the Order. The quote is set to USD.
|
|
236
|
+
"""
|
|
237
|
+
try:
|
|
238
|
+
order_id = response.get("id")
|
|
239
|
+
contract_id = response.get("contractId")
|
|
240
|
+
asset = None
|
|
241
|
+
if contract_id:
|
|
242
|
+
try:
|
|
243
|
+
contract_details = self._get_contract_details(contract_id)
|
|
244
|
+
# For Tradeovate futures, assume asset_type is "future" and use the contract's name as the symbol.
|
|
245
|
+
symbol = contract_details.get("name", "")
|
|
246
|
+
asset = Asset(symbol=symbol, asset_type=Asset.AssetType.FUTURE)
|
|
247
|
+
except TradeovateAPIError as e:
|
|
248
|
+
logging.error(colored(f"Failed to retrieve contract details for order {order_id}: {e}", "red"))
|
|
249
|
+
|
|
250
|
+
quantity = response.get("orderQty", 0)
|
|
251
|
+
action = response.get("action", "").lower()
|
|
252
|
+
order_type = response.get("orderType", "market").lower()
|
|
253
|
+
limit_price = response.get("price")
|
|
254
|
+
stop_price = response.get("stopPrice")
|
|
255
|
+
|
|
256
|
+
# Map raw status to Lumibot's order status using common aliases.
|
|
257
|
+
raw_status = response.get("ordStatus", "").lower()
|
|
258
|
+
if raw_status in ["working"]:
|
|
259
|
+
status = Order.OrderStatus.OPEN
|
|
260
|
+
elif raw_status in ["filled"]:
|
|
261
|
+
status = Order.OrderStatus.FILLED
|
|
262
|
+
elif raw_status in ["partialfill", "partial_fill", "partially_filled"]:
|
|
263
|
+
status = Order.OrderStatus.PARTIALLY_FILLED
|
|
264
|
+
elif raw_status in ["canceled", "cancelled", "cancel"]:
|
|
265
|
+
status = Order.OrderStatus.CANCELED
|
|
266
|
+
elif raw_status in ["rejected"]:
|
|
267
|
+
status = Order.OrderStatus.ERROR
|
|
268
|
+
elif raw_status in ["expired"]:
|
|
269
|
+
status = Order.OrderStatus.CANCELED
|
|
270
|
+
elif raw_status in ["submitted", "new", "pending"]:
|
|
271
|
+
status = Order.OrderStatus.NEW
|
|
272
|
+
else:
|
|
273
|
+
status = raw_status
|
|
274
|
+
|
|
275
|
+
timestamp_str = response.get("timestamp")
|
|
276
|
+
date_created = None
|
|
277
|
+
if timestamp_str:
|
|
278
|
+
# Replace the trailing 'Z' with '+00:00' to properly parse UTC time.
|
|
279
|
+
date_created = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))
|
|
280
|
+
|
|
281
|
+
# Create the Lumibot Order. For unknown fields, we simply leave them out.
|
|
282
|
+
order_obj = Order(
|
|
283
|
+
strategy=strategy_name,
|
|
284
|
+
asset=asset,
|
|
285
|
+
quantity=quantity,
|
|
286
|
+
side=action,
|
|
287
|
+
type=order_type,
|
|
288
|
+
identifier=order_id,
|
|
289
|
+
quote=Asset("USD", asset_type=Asset.AssetType.FOREX)
|
|
290
|
+
)
|
|
291
|
+
order_obj.status = status
|
|
292
|
+
return order_obj
|
|
293
|
+
except Exception as e:
|
|
294
|
+
logging.error(colored(f"Error parsing order: {e}", "red"))
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
def _pull_broker_all_orders(self) -> list:
|
|
298
|
+
"""
|
|
299
|
+
Retrieve all orders from Tradeovate via the /order/list endpoint.
|
|
300
|
+
Returns the raw JSON list of orders (dictionaries) without parsing.
|
|
301
|
+
"""
|
|
302
|
+
url = f"{self.trading_api_url}/order/list"
|
|
303
|
+
headers = self._get_headers()
|
|
304
|
+
try:
|
|
305
|
+
response = requests.get(url, headers=headers)
|
|
306
|
+
response.raise_for_status()
|
|
307
|
+
return response.json()
|
|
308
|
+
except requests.exceptions.RequestException as e:
|
|
309
|
+
raise TradeovateAPIError(f"Failed to retrieve orders",
|
|
310
|
+
status_code=getattr(e.response, 'status_code', None),
|
|
311
|
+
response_text=getattr(e.response, 'text', None),
|
|
312
|
+
original_exception=e)
|
|
313
|
+
|
|
314
|
+
def _pull_broker_order(self, identifier: str) -> Order:
|
|
315
|
+
"""
|
|
316
|
+
Retrieve a specific order by its order id using the /order/item endpoint.
|
|
317
|
+
"""
|
|
318
|
+
url = f"{self.trading_api_url}/order/item"
|
|
319
|
+
params = {"id": identifier}
|
|
320
|
+
headers = self._get_headers()
|
|
321
|
+
try:
|
|
322
|
+
response = requests.get(url, params=params, headers=headers)
|
|
323
|
+
response.raise_for_status()
|
|
324
|
+
order_data = response.json()
|
|
325
|
+
order_obj = self._parse_broker_order(order_data, strategy_name="") # set strategy as needed
|
|
326
|
+
return order_obj
|
|
327
|
+
except requests.exceptions.RequestException as e:
|
|
328
|
+
raise TradeovateAPIError(f"Failed to retrieve order {identifier}",
|
|
329
|
+
status_code=getattr(e.response, 'status_code', None),
|
|
330
|
+
response_text=getattr(e.response, 'text', None),
|
|
331
|
+
original_exception=e)
|
|
332
|
+
|
|
333
|
+
def _pull_position(self, strategy, asset: Asset) -> Position:
|
|
334
|
+
logging.error(colored(f"Method '_pull_position' for asset {asset} is not yet implemented.", "red"))
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
def _pull_positions(self, strategy) -> list[Position]:
|
|
338
|
+
"""
|
|
339
|
+
Retrieve all open positions from Tradeovate via the /position/list endpoint.
|
|
340
|
+
For each returned position, create a Position object.
|
|
341
|
+
Assumes that each position dict contains:
|
|
342
|
+
- 'contractId': the contract identifier to retrieve asset details,
|
|
343
|
+
- 'netPos': the position quantity,
|
|
344
|
+
- 'netPrice': the average fill price.
|
|
345
|
+
The asset is created using contract details retrieved from Tradeovate.
|
|
346
|
+
"""
|
|
347
|
+
url = f"{self.trading_api_url}/position/list"
|
|
348
|
+
headers = self._get_headers()
|
|
349
|
+
try:
|
|
350
|
+
response = requests.get(url, headers=headers)
|
|
351
|
+
response.raise_for_status()
|
|
352
|
+
positions_data = response.json()
|
|
353
|
+
positions = []
|
|
354
|
+
for pos in positions_data:
|
|
355
|
+
contract_id = pos.get("contractId")
|
|
356
|
+
if not contract_id:
|
|
357
|
+
logging.error("No contractId found in position data.")
|
|
358
|
+
continue
|
|
359
|
+
try:
|
|
360
|
+
contract_details = self._get_contract_details(contract_id)
|
|
361
|
+
except TradeovateAPIError as e:
|
|
362
|
+
logging.error(colored(f"Failed to retrieve contract details for contractId {contract_id}: {e}", "red"))
|
|
363
|
+
continue
|
|
364
|
+
# Extract asset details from the contract details.
|
|
365
|
+
# For Tradeovate futures, assume asset_type is "future" and use the contract name as the symbol.
|
|
366
|
+
symbol = contract_details.get("name", "")
|
|
367
|
+
expiration = None
|
|
368
|
+
multiplier = 1 # default multiplier
|
|
369
|
+
asset = Asset(symbol=symbol, asset_type=Asset.AssetType.FUTURE, expiration=expiration, multiplier=multiplier)
|
|
370
|
+
quantity = pos.get("netPos", 0)
|
|
371
|
+
net_price = pos.get("netPrice", 0)
|
|
372
|
+
hold = 0
|
|
373
|
+
available = 0
|
|
374
|
+
position_obj = Position(
|
|
375
|
+
strategy,
|
|
376
|
+
asset,
|
|
377
|
+
quantity,
|
|
378
|
+
orders=[],
|
|
379
|
+
hold=hold,
|
|
380
|
+
available=available,
|
|
381
|
+
avg_fill_price=net_price
|
|
382
|
+
)
|
|
383
|
+
positions.append(position_obj)
|
|
384
|
+
return positions
|
|
385
|
+
except requests.exceptions.RequestException as e:
|
|
386
|
+
raise TradeovateAPIError(f"Failed to retrieve positions",
|
|
387
|
+
status_code=getattr(e.response, 'status_code', None),
|
|
388
|
+
response_text=getattr(e.response, 'text', None),
|
|
389
|
+
original_exception=e)
|
|
390
|
+
|
|
391
|
+
def _register_stream_events(self):
|
|
392
|
+
logging.error(colored("Method '_register_stream_events' is not yet implemented.", "red"))
|
|
393
|
+
return None
|
|
394
|
+
|
|
395
|
+
def _run_stream(self):
|
|
396
|
+
logging.error(colored("Method '_run_stream' is not yet implemented.", "red"))
|
|
397
|
+
return None
|
|
398
|
+
|
|
399
|
+
def _submit_order(self, order: Order) -> Order:
|
|
400
|
+
"""
|
|
401
|
+
Submit an order to Tradeovate.
|
|
402
|
+
|
|
403
|
+
This method takes an Order object, extracts necessary details, builds the payload,
|
|
404
|
+
and sends it to the Tradeovate API to place the order. On success, the order status
|
|
405
|
+
is updated to 'submitted' and the raw response is attached to the order. Otherwise,
|
|
406
|
+
the order is marked with an error.
|
|
407
|
+
"""
|
|
408
|
+
# Determine the action based on the order side
|
|
409
|
+
action = "Buy" if order.is_buy_order() else "Sell"
|
|
410
|
+
|
|
411
|
+
# Extract symbol from the order's asset
|
|
412
|
+
symbol = order.asset.symbol
|
|
413
|
+
|
|
414
|
+
# Determine the order type string based on the order type.
|
|
415
|
+
if order.order_type == Order.OrderType.MARKET:
|
|
416
|
+
order_type = "Market"
|
|
417
|
+
elif order.order_type == Order.OrderType.LIMIT:
|
|
418
|
+
order_type = "Limit"
|
|
419
|
+
elif order.order_type == Order.OrderType.STOP:
|
|
420
|
+
order_type = "Stop"
|
|
421
|
+
elif order.order_type == Order.OrderType.STOP_LIMIT:
|
|
422
|
+
order_type = "StopLimit"
|
|
423
|
+
else:
|
|
424
|
+
logging.warning(
|
|
425
|
+
f"Order type '{order.order_type}' is not fully supported. Defaulting to Market order."
|
|
426
|
+
)
|
|
427
|
+
order_type = "Market"
|
|
428
|
+
|
|
429
|
+
# Build the payload with numeric values sent as numbers and booleans as True/False.
|
|
430
|
+
payload = {
|
|
431
|
+
"accountSpec": self.account_spec,
|
|
432
|
+
"accountId": self.account_id,
|
|
433
|
+
"action": action,
|
|
434
|
+
"symbol": symbol,
|
|
435
|
+
# Convert order.quantity to an integer rather than a float.
|
|
436
|
+
"orderQty": int(order.quantity),
|
|
437
|
+
"orderType": order_type,
|
|
438
|
+
"isAutomated": True
|
|
439
|
+
}
|
|
440
|
+
# If a limit price is specified for limit orders, include it.
|
|
441
|
+
if order.limit_price is not None:
|
|
442
|
+
payload["limitPrice"] = float(order.limit_price)
|
|
443
|
+
# Similarly, include stop price if specified.
|
|
444
|
+
if order.stop_price is not None:
|
|
445
|
+
payload["stopPrice"] = float(order.stop_price)
|
|
446
|
+
|
|
447
|
+
url = f"{self.trading_api_url}/order/placeorder"
|
|
448
|
+
headers = self._get_headers(with_content_type=True)
|
|
449
|
+
|
|
450
|
+
try:
|
|
451
|
+
response = requests.post(url, json=payload, headers=headers)
|
|
452
|
+
response.raise_for_status()
|
|
453
|
+
data = response.json()
|
|
454
|
+
logging.info(f"Order successfully submitted: {data}")
|
|
455
|
+
order.status = Order.OrderStatus.SUBMITTED
|
|
456
|
+
order.update_raw(data)
|
|
457
|
+
return order
|
|
458
|
+
except requests.exceptions.RequestException as e:
|
|
459
|
+
error_message = f"Failed to submit order: {getattr(e.response, 'status_code', None)}, {getattr(e.response, 'text', None)}"
|
|
460
|
+
logging.error(error_message)
|
|
461
|
+
order.set_error(error_message)
|
|
462
|
+
return order
|
|
463
|
+
|
|
464
|
+
def cancel_order(self, order_id) -> None:
|
|
465
|
+
logging.error(colored(f"Method 'cancel_order' for order_id {order_id} is not yet implemented.", "red"))
|
|
466
|
+
return None
|
|
467
|
+
|
|
468
|
+
def _modify_order(self, order: Order, limit_price: Union[float, None] = None,
|
|
469
|
+
stop_price: Union[float, None] = None):
|
|
470
|
+
logging.error(colored(f"Method '_modify_order' for order {order} is not yet implemented.", "red"))
|
|
471
|
+
return None
|
|
472
|
+
|
|
473
|
+
def get_historical_account_value(self) -> dict:
|
|
474
|
+
logging.error(colored("Method 'get_historical_account_value' is not yet implemented.", "red"))
|
|
475
|
+
return {}
|