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.

Files changed (164) hide show
  1. lumibot/__pycache__/__init__.cpython-312.pyc +0 -0
  2. lumibot/__pycache__/constants.cpython-312.pyc +0 -0
  3. lumibot/__pycache__/credentials.cpython-312.pyc +0 -0
  4. lumibot/backtesting/__init__.py +6 -5
  5. lumibot/backtesting/__pycache__/__init__.cpython-312.pyc +0 -0
  6. lumibot/backtesting/__pycache__/alpaca_backtesting.cpython-312.pyc +0 -0
  7. lumibot/backtesting/__pycache__/alpha_vantage_backtesting.cpython-312.pyc +0 -0
  8. lumibot/backtesting/__pycache__/backtesting_broker.cpython-312.pyc +0 -0
  9. lumibot/backtesting/__pycache__/ccxt_backtesting.cpython-312.pyc +0 -0
  10. lumibot/backtesting/__pycache__/databento_backtesting.cpython-312.pyc +0 -0
  11. lumibot/backtesting/__pycache__/interactive_brokers_rest_backtesting.cpython-312.pyc +0 -0
  12. lumibot/backtesting/__pycache__/pandas_backtesting.cpython-312.pyc +0 -0
  13. lumibot/backtesting/__pycache__/polygon_backtesting.cpython-312.pyc +0 -0
  14. lumibot/backtesting/__pycache__/thetadata_backtesting.cpython-312.pyc +0 -0
  15. lumibot/backtesting/__pycache__/yahoo_backtesting.cpython-312.pyc +0 -0
  16. lumibot/backtesting/backtesting_broker.py +209 -9
  17. lumibot/backtesting/databento_backtesting.py +141 -24
  18. lumibot/backtesting/thetadata_backtesting.py +63 -42
  19. lumibot/brokers/__pycache__/__init__.cpython-312.pyc +0 -0
  20. lumibot/brokers/__pycache__/alpaca.cpython-312.pyc +0 -0
  21. lumibot/brokers/__pycache__/bitunix.cpython-312.pyc +0 -0
  22. lumibot/brokers/__pycache__/broker.cpython-312.pyc +0 -0
  23. lumibot/brokers/__pycache__/ccxt.cpython-312.pyc +0 -0
  24. lumibot/brokers/__pycache__/example_broker.cpython-312.pyc +0 -0
  25. lumibot/brokers/__pycache__/interactive_brokers.cpython-312.pyc +0 -0
  26. lumibot/brokers/__pycache__/interactive_brokers_rest.cpython-312.pyc +0 -0
  27. lumibot/brokers/__pycache__/projectx.cpython-312.pyc +0 -0
  28. lumibot/brokers/__pycache__/schwab.cpython-312.pyc +0 -0
  29. lumibot/brokers/__pycache__/tradier.cpython-312.pyc +0 -0
  30. lumibot/brokers/__pycache__/tradovate.cpython-312.pyc +0 -0
  31. lumibot/brokers/alpaca.py +11 -1
  32. lumibot/brokers/tradeovate.py +475 -0
  33. lumibot/components/grok_news_helper.py +284 -0
  34. lumibot/components/options_helper.py +90 -34
  35. lumibot/credentials.py +3 -0
  36. lumibot/data_sources/__init__.py +2 -1
  37. lumibot/data_sources/__pycache__/__init__.cpython-312.pyc +0 -0
  38. lumibot/data_sources/__pycache__/alpaca_data.cpython-312.pyc +0 -0
  39. lumibot/data_sources/__pycache__/alpha_vantage_data.cpython-312.pyc +0 -0
  40. lumibot/data_sources/__pycache__/bitunix_data.cpython-312.pyc +0 -0
  41. lumibot/data_sources/__pycache__/ccxt_backtesting_data.cpython-312.pyc +0 -0
  42. lumibot/data_sources/__pycache__/ccxt_data.cpython-312.pyc +0 -0
  43. lumibot/data_sources/__pycache__/data_source.cpython-312.pyc +0 -0
  44. lumibot/data_sources/__pycache__/data_source_backtesting.cpython-312.pyc +0 -0
  45. lumibot/data_sources/__pycache__/databento_data_polars_backtesting.cpython-312.pyc +0 -0
  46. lumibot/data_sources/__pycache__/databento_data_polars_live.cpython-312.pyc +0 -0
  47. lumibot/data_sources/__pycache__/example_broker_data.cpython-312.pyc +0 -0
  48. lumibot/data_sources/__pycache__/exceptions.cpython-312.pyc +0 -0
  49. lumibot/data_sources/__pycache__/interactive_brokers_data.cpython-312.pyc +0 -0
  50. lumibot/data_sources/__pycache__/interactive_brokers_rest_data.cpython-312.pyc +0 -0
  51. lumibot/data_sources/__pycache__/pandas_data.cpython-312.pyc +0 -0
  52. lumibot/data_sources/__pycache__/polars_mixin.cpython-312.pyc +0 -0
  53. lumibot/data_sources/__pycache__/polygon_data_polars.cpython-312.pyc +0 -0
  54. lumibot/data_sources/__pycache__/projectx_data.cpython-312.pyc +0 -0
  55. lumibot/data_sources/__pycache__/schwab_data.cpython-312.pyc +0 -0
  56. lumibot/data_sources/__pycache__/tradier_data.cpython-312.pyc +0 -0
  57. lumibot/data_sources/__pycache__/tradovate_data.cpython-312.pyc +0 -0
  58. lumibot/data_sources/__pycache__/yahoo_data_polars.cpython-312.pyc +0 -0
  59. lumibot/data_sources/data_source_backtesting.py +3 -5
  60. lumibot/data_sources/databento_data.py +5 -5
  61. lumibot/data_sources/databento_data_polars_backtesting.py +636 -0
  62. lumibot/data_sources/databento_data_polars_live.py +793 -0
  63. lumibot/data_sources/pandas_data.py +6 -3
  64. lumibot/data_sources/polars_mixin.py +126 -21
  65. lumibot/data_sources/tradeovate_data.py +80 -0
  66. lumibot/data_sources/tradier_data.py +2 -1
  67. lumibot/entities/__pycache__/__init__.cpython-312.pyc +0 -0
  68. lumibot/entities/__pycache__/asset.cpython-312.pyc +0 -0
  69. lumibot/entities/__pycache__/bar.cpython-312.pyc +0 -0
  70. lumibot/entities/__pycache__/bars.cpython-312.pyc +0 -0
  71. lumibot/entities/__pycache__/chains.cpython-312.pyc +0 -0
  72. lumibot/entities/__pycache__/data.cpython-312.pyc +0 -0
  73. lumibot/entities/__pycache__/dataline.cpython-312.pyc +0 -0
  74. lumibot/entities/__pycache__/order.cpython-312.pyc +0 -0
  75. lumibot/entities/__pycache__/position.cpython-312.pyc +0 -0
  76. lumibot/entities/__pycache__/quote.cpython-312.pyc +0 -0
  77. lumibot/entities/__pycache__/trading_fee.cpython-312.pyc +0 -0
  78. lumibot/entities/asset.py +8 -0
  79. lumibot/entities/order.py +1 -1
  80. lumibot/entities/quote.py +14 -0
  81. lumibot/example_strategies/__pycache__/__init__.cpython-312.pyc +0 -0
  82. lumibot/example_strategies/__pycache__/test_broker_functions.cpython-312-pytest-8.4.1.pyc +0 -0
  83. lumibot/strategies/__pycache__/__init__.cpython-312.pyc +0 -0
  84. lumibot/strategies/__pycache__/_strategy.cpython-312.pyc +0 -0
  85. lumibot/strategies/__pycache__/strategy.cpython-312.pyc +0 -0
  86. lumibot/strategies/__pycache__/strategy_executor.cpython-312.pyc +0 -0
  87. lumibot/strategies/_strategy.py +95 -27
  88. lumibot/strategies/strategy.py +5 -6
  89. lumibot/strategies/strategy_executor.py +2 -2
  90. lumibot/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  91. lumibot/tools/__pycache__/alpaca_helpers.cpython-312.pyc +0 -0
  92. lumibot/tools/__pycache__/bitunix_helpers.cpython-312.pyc +0 -0
  93. lumibot/tools/__pycache__/black_scholes.cpython-312.pyc +0 -0
  94. lumibot/tools/__pycache__/ccxt_data_store.cpython-312.pyc +0 -0
  95. lumibot/tools/__pycache__/databento_helper.cpython-312.pyc +0 -0
  96. lumibot/tools/__pycache__/databento_helper_polars.cpython-312.pyc +0 -0
  97. lumibot/tools/__pycache__/debugers.cpython-312.pyc +0 -0
  98. lumibot/tools/__pycache__/decorators.cpython-312.pyc +0 -0
  99. lumibot/tools/__pycache__/helpers.cpython-312.pyc +0 -0
  100. lumibot/tools/__pycache__/indicators.cpython-312.pyc +0 -0
  101. lumibot/tools/__pycache__/lumibot_logger.cpython-312.pyc +0 -0
  102. lumibot/tools/__pycache__/pandas.cpython-312.pyc +0 -0
  103. lumibot/tools/__pycache__/polygon_helper.cpython-312.pyc +0 -0
  104. lumibot/tools/__pycache__/polygon_helper_async.cpython-312.pyc +0 -0
  105. lumibot/tools/__pycache__/polygon_helper_polars_optimized.cpython-312.pyc +0 -0
  106. lumibot/tools/__pycache__/projectx_helpers.cpython-312.pyc +0 -0
  107. lumibot/tools/__pycache__/schwab_helper.cpython-312.pyc +0 -0
  108. lumibot/tools/__pycache__/thetadata_helper.cpython-312.pyc +0 -0
  109. lumibot/tools/__pycache__/types.cpython-312.pyc +0 -0
  110. lumibot/tools/__pycache__/yahoo_helper.cpython-312.pyc +0 -0
  111. lumibot/tools/__pycache__/yahoo_helper_polars_optimized.cpython-312.pyc +0 -0
  112. lumibot/tools/databento_helper.py +384 -133
  113. lumibot/tools/databento_helper_polars.py +218 -156
  114. lumibot/tools/databento_roll.py +216 -0
  115. lumibot/tools/lumibot_logger.py +32 -17
  116. lumibot/tools/polygon_helper.py +65 -0
  117. lumibot/tools/thetadata_helper.py +588 -70
  118. lumibot/traders/__pycache__/__init__.cpython-312.pyc +0 -0
  119. lumibot/traders/__pycache__/trader.cpython-312.pyc +0 -0
  120. lumibot/traders/trader.py +1 -1
  121. lumibot/trading_builtins/__pycache__/__init__.cpython-312.pyc +0 -0
  122. lumibot/trading_builtins/__pycache__/custom_stream.cpython-312.pyc +0 -0
  123. lumibot/trading_builtins/__pycache__/safe_list.cpython-312.pyc +0 -0
  124. {lumibot-4.0.22.dist-info → lumibot-4.1.0.dist-info}/METADATA +1 -2
  125. {lumibot-4.0.22.dist-info → lumibot-4.1.0.dist-info}/RECORD +164 -46
  126. tests/backtest/check_timing_offset.py +198 -0
  127. tests/backtest/check_volume_spike.py +112 -0
  128. tests/backtest/comprehensive_comparison.py +166 -0
  129. tests/backtest/debug_comparison.py +91 -0
  130. tests/backtest/diagnose_price_difference.py +97 -0
  131. tests/backtest/direct_api_comparison.py +203 -0
  132. tests/backtest/profile_thetadata_vs_polygon.py +255 -0
  133. tests/backtest/root_cause_analysis.py +109 -0
  134. tests/backtest/test_accuracy_verification.py +244 -0
  135. tests/backtest/test_daily_data_timestamp_comparison.py +801 -0
  136. tests/backtest/test_databento.py +57 -0
  137. tests/backtest/test_databento_comprehensive_trading.py +564 -0
  138. tests/backtest/test_debug_avg_fill_price.py +112 -0
  139. tests/backtest/test_dividends.py +8 -3
  140. tests/backtest/test_example_strategies.py +54 -47
  141. tests/backtest/test_futures_edge_cases.py +451 -0
  142. tests/backtest/test_futures_single_trade.py +270 -0
  143. tests/backtest/test_futures_ultra_simple.py +191 -0
  144. tests/backtest/test_index_data_verification.py +348 -0
  145. tests/backtest/test_polygon.py +45 -24
  146. tests/backtest/test_thetadata.py +246 -60
  147. tests/backtest/test_thetadata_comprehensive.py +729 -0
  148. tests/backtest/test_thetadata_vs_polygon.py +557 -0
  149. tests/backtest/test_yahoo.py +1 -2
  150. tests/conftest.py +20 -0
  151. tests/test_backtesting_data_source_env.py +249 -0
  152. tests/test_backtesting_quiet_logs_complete.py +10 -11
  153. tests/test_databento_helper.py +73 -86
  154. tests/test_databento_live.py +10 -10
  155. tests/test_databento_timezone_fixes.py +21 -4
  156. tests/test_get_historical_prices.py +6 -6
  157. tests/test_options_helper.py +162 -40
  158. tests/test_polygon_helper.py +21 -13
  159. tests/test_quiet_logs_requirements.py +5 -5
  160. tests/test_thetadata_helper.py +487 -171
  161. tests/test_yahoo_data.py +125 -0
  162. {lumibot-4.0.22.dist-info → lumibot-4.1.0.dist-info}/LICENSE +0 -0
  163. {lumibot-4.0.22.dist-info → lumibot-4.1.0.dist-info}/WHEEL +0 -0
  164. {lumibot-4.0.22.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 {}