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.

Files changed (161) 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 +145 -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/__pycache__/__init__.cpython-312.pyc +0 -0
  37. lumibot/data_sources/__pycache__/alpaca_data.cpython-312.pyc +0 -0
  38. lumibot/data_sources/__pycache__/alpha_vantage_data.cpython-312.pyc +0 -0
  39. lumibot/data_sources/__pycache__/bitunix_data.cpython-312.pyc +0 -0
  40. lumibot/data_sources/__pycache__/ccxt_backtesting_data.cpython-312.pyc +0 -0
  41. lumibot/data_sources/__pycache__/ccxt_data.cpython-312.pyc +0 -0
  42. lumibot/data_sources/__pycache__/data_source.cpython-312.pyc +0 -0
  43. lumibot/data_sources/__pycache__/data_source_backtesting.cpython-312.pyc +0 -0
  44. lumibot/data_sources/__pycache__/databento_data_polars_backtesting.cpython-312.pyc +0 -0
  45. lumibot/data_sources/__pycache__/databento_data_polars_live.cpython-312.pyc +0 -0
  46. lumibot/data_sources/__pycache__/example_broker_data.cpython-312.pyc +0 -0
  47. lumibot/data_sources/__pycache__/exceptions.cpython-312.pyc +0 -0
  48. lumibot/data_sources/__pycache__/interactive_brokers_data.cpython-312.pyc +0 -0
  49. lumibot/data_sources/__pycache__/interactive_brokers_rest_data.cpython-312.pyc +0 -0
  50. lumibot/data_sources/__pycache__/pandas_data.cpython-312.pyc +0 -0
  51. lumibot/data_sources/__pycache__/polars_mixin.cpython-312.pyc +0 -0
  52. lumibot/data_sources/__pycache__/polygon_data_polars.cpython-312.pyc +0 -0
  53. lumibot/data_sources/__pycache__/projectx_data.cpython-312.pyc +0 -0
  54. lumibot/data_sources/__pycache__/schwab_data.cpython-312.pyc +0 -0
  55. lumibot/data_sources/__pycache__/tradier_data.cpython-312.pyc +0 -0
  56. lumibot/data_sources/__pycache__/tradovate_data.cpython-312.pyc +0 -0
  57. lumibot/data_sources/__pycache__/yahoo_data_polars.cpython-312.pyc +0 -0
  58. lumibot/data_sources/data_source_backtesting.py +3 -5
  59. lumibot/data_sources/databento_data_polars_backtesting.py +194 -48
  60. lumibot/data_sources/pandas_data.py +6 -3
  61. lumibot/data_sources/polars_mixin.py +126 -21
  62. lumibot/data_sources/tradeovate_data.py +80 -0
  63. lumibot/data_sources/tradier_data.py +2 -1
  64. lumibot/entities/__pycache__/__init__.cpython-312.pyc +0 -0
  65. lumibot/entities/__pycache__/asset.cpython-312.pyc +0 -0
  66. lumibot/entities/__pycache__/bar.cpython-312.pyc +0 -0
  67. lumibot/entities/__pycache__/bars.cpython-312.pyc +0 -0
  68. lumibot/entities/__pycache__/chains.cpython-312.pyc +0 -0
  69. lumibot/entities/__pycache__/data.cpython-312.pyc +0 -0
  70. lumibot/entities/__pycache__/dataline.cpython-312.pyc +0 -0
  71. lumibot/entities/__pycache__/order.cpython-312.pyc +0 -0
  72. lumibot/entities/__pycache__/position.cpython-312.pyc +0 -0
  73. lumibot/entities/__pycache__/quote.cpython-312.pyc +0 -0
  74. lumibot/entities/__pycache__/trading_fee.cpython-312.pyc +0 -0
  75. lumibot/entities/asset.py +8 -0
  76. lumibot/entities/order.py +1 -1
  77. lumibot/entities/quote.py +14 -0
  78. lumibot/example_strategies/__pycache__/__init__.cpython-312.pyc +0 -0
  79. lumibot/example_strategies/__pycache__/test_broker_functions.cpython-312-pytest-8.4.1.pyc +0 -0
  80. lumibot/strategies/__pycache__/__init__.cpython-312.pyc +0 -0
  81. lumibot/strategies/__pycache__/_strategy.cpython-312.pyc +0 -0
  82. lumibot/strategies/__pycache__/strategy.cpython-312.pyc +0 -0
  83. lumibot/strategies/__pycache__/strategy_executor.cpython-312.pyc +0 -0
  84. lumibot/strategies/_strategy.py +95 -27
  85. lumibot/strategies/strategy.py +5 -6
  86. lumibot/strategies/strategy_executor.py +2 -2
  87. lumibot/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  88. lumibot/tools/__pycache__/alpaca_helpers.cpython-312.pyc +0 -0
  89. lumibot/tools/__pycache__/bitunix_helpers.cpython-312.pyc +0 -0
  90. lumibot/tools/__pycache__/black_scholes.cpython-312.pyc +0 -0
  91. lumibot/tools/__pycache__/ccxt_data_store.cpython-312.pyc +0 -0
  92. lumibot/tools/__pycache__/databento_helper.cpython-312.pyc +0 -0
  93. lumibot/tools/__pycache__/databento_helper_polars.cpython-312.pyc +0 -0
  94. lumibot/tools/__pycache__/debugers.cpython-312.pyc +0 -0
  95. lumibot/tools/__pycache__/decorators.cpython-312.pyc +0 -0
  96. lumibot/tools/__pycache__/helpers.cpython-312.pyc +0 -0
  97. lumibot/tools/__pycache__/indicators.cpython-312.pyc +0 -0
  98. lumibot/tools/__pycache__/lumibot_logger.cpython-312.pyc +0 -0
  99. lumibot/tools/__pycache__/pandas.cpython-312.pyc +0 -0
  100. lumibot/tools/__pycache__/polygon_helper.cpython-312.pyc +0 -0
  101. lumibot/tools/__pycache__/polygon_helper_async.cpython-312.pyc +0 -0
  102. lumibot/tools/__pycache__/polygon_helper_polars_optimized.cpython-312.pyc +0 -0
  103. lumibot/tools/__pycache__/projectx_helpers.cpython-312.pyc +0 -0
  104. lumibot/tools/__pycache__/schwab_helper.cpython-312.pyc +0 -0
  105. lumibot/tools/__pycache__/thetadata_helper.cpython-312.pyc +0 -0
  106. lumibot/tools/__pycache__/types.cpython-312.pyc +0 -0
  107. lumibot/tools/__pycache__/yahoo_helper.cpython-312.pyc +0 -0
  108. lumibot/tools/__pycache__/yahoo_helper_polars_optimized.cpython-312.pyc +0 -0
  109. lumibot/tools/databento_helper.py +384 -133
  110. lumibot/tools/databento_helper_polars.py +218 -156
  111. lumibot/tools/databento_roll.py +216 -0
  112. lumibot/tools/lumibot_logger.py +32 -17
  113. lumibot/tools/polygon_helper.py +65 -0
  114. lumibot/tools/thetadata_helper.py +588 -70
  115. lumibot/traders/__pycache__/__init__.cpython-312.pyc +0 -0
  116. lumibot/traders/__pycache__/trader.cpython-312.pyc +0 -0
  117. lumibot/traders/trader.py +1 -1
  118. lumibot/trading_builtins/__pycache__/__init__.cpython-312.pyc +0 -0
  119. lumibot/trading_builtins/__pycache__/custom_stream.cpython-312.pyc +0 -0
  120. lumibot/trading_builtins/__pycache__/safe_list.cpython-312.pyc +0 -0
  121. lumibot-4.1.1.data/data/ThetaTerminal.jar +0 -0
  122. {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/METADATA +1 -2
  123. {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/RECORD +161 -44
  124. tests/backtest/check_timing_offset.py +198 -0
  125. tests/backtest/check_volume_spike.py +112 -0
  126. tests/backtest/comprehensive_comparison.py +166 -0
  127. tests/backtest/debug_comparison.py +91 -0
  128. tests/backtest/diagnose_price_difference.py +97 -0
  129. tests/backtest/direct_api_comparison.py +203 -0
  130. tests/backtest/profile_thetadata_vs_polygon.py +255 -0
  131. tests/backtest/root_cause_analysis.py +109 -0
  132. tests/backtest/test_accuracy_verification.py +244 -0
  133. tests/backtest/test_daily_data_timestamp_comparison.py +801 -0
  134. tests/backtest/test_databento.py +4 -0
  135. tests/backtest/test_databento_comprehensive_trading.py +564 -0
  136. tests/backtest/test_debug_avg_fill_price.py +112 -0
  137. tests/backtest/test_dividends.py +8 -3
  138. tests/backtest/test_example_strategies.py +54 -47
  139. tests/backtest/test_futures_edge_cases.py +451 -0
  140. tests/backtest/test_futures_single_trade.py +270 -0
  141. tests/backtest/test_futures_ultra_simple.py +191 -0
  142. tests/backtest/test_index_data_verification.py +348 -0
  143. tests/backtest/test_polygon.py +45 -24
  144. tests/backtest/test_thetadata.py +246 -60
  145. tests/backtest/test_thetadata_comprehensive.py +729 -0
  146. tests/backtest/test_thetadata_vs_polygon.py +557 -0
  147. tests/backtest/test_yahoo.py +1 -2
  148. tests/conftest.py +20 -0
  149. tests/test_backtesting_data_source_env.py +249 -0
  150. tests/test_backtesting_quiet_logs_complete.py +10 -11
  151. tests/test_databento_helper.py +76 -90
  152. tests/test_databento_timezone_fixes.py +21 -4
  153. tests/test_get_historical_prices.py +6 -6
  154. tests/test_options_helper.py +162 -40
  155. tests/test_polygon_helper.py +21 -13
  156. tests/test_quiet_logs_requirements.py +5 -5
  157. tests/test_thetadata_helper.py +487 -171
  158. tests/test_yahoo_data.py +125 -0
  159. {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/LICENSE +0 -0
  160. {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/WHEEL +0 -0
  161. {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,284 @@
1
+ import json
2
+ import datetime
3
+ from openai import OpenAI
4
+
5
+ class GrokNewsHelper:
6
+ """
7
+ GrokNewsHelper queries xAI's Grok API via the OpenAI-compatible client to fetch
8
+ financial/news data in a structured JSON format.
9
+
10
+ How it Works:
11
+ -------------
12
+ 1. Build a system prompt describing the desired JSON schema in detail.
13
+ 2. Use xAI's OpenAI-compatible client to create a chat completion with:
14
+ - A system message (detailed instructions).
15
+ - A user message (the actual user query).
16
+ 3. Parse the assistant's text content as JSON.
17
+ 4. Return a Python dictionary with fields:
18
+ {
19
+ "query": ...,
20
+ "timestamp_utc": ...,
21
+ "analysis_summary": ...,
22
+ "items": [...]
23
+ }
24
+
25
+ Example:
26
+ --------
27
+ from grok_news_helper import GrokNewsHelper
28
+
29
+ helper = GrokNewsHelper(api_key="YOUR_XAI_API_KEY")
30
+ result = helper.execute_query("What stocks are trending right now on Twitter?")
31
+ print(json.dumps(result, indent=2))
32
+ """
33
+
34
+ def __init__(self, api_key: str):
35
+ """
36
+ Initialize the GrokNewsHelper with your xAI API key.
37
+ Internally creates an xAI OpenAI-compatible client.
38
+
39
+ Parameters
40
+ ----------
41
+ api_key : str
42
+ The xAI API key (required).
43
+ """
44
+ if not api_key:
45
+ # Try to get the API key from the environment if not provided
46
+ import os
47
+
48
+ # Might be called GROK_API_KEY or XAI_API_KEY
49
+ api_key = os.getenv("GROK_API_KEY")
50
+ if not api_key:
51
+ api_key = os.getenv("XAI_API_KEY")
52
+
53
+ if not api_key:
54
+ raise ValueError("API key is required for GrokNewsHelper. Get one from x.ai and set it as GROK_API_KEY or XAI_API_KEY in your secrets, environment variables, .env file or directly in the code.")
55
+
56
+ self.api_key = api_key
57
+
58
+ # Create the xAI client (OpenAI-compatible)
59
+ self.client = OpenAI(
60
+ api_key=self.api_key,
61
+ base_url="https://api.x.ai/v1",
62
+ )
63
+
64
+ def build_prompt(self, user_query: str) -> str:
65
+ """
66
+ Constructs a system prompt describing the required JSON schema in detail
67
+ and instructing the model to avoid hallucinations and remain factual.
68
+
69
+ The schema includes fields like 'symbol', 'confidence', 'magnitude', etc.
70
+ Now also includes an optional 'price_targets' object with float fields.
71
+
72
+ Parameters
73
+ ----------
74
+ user_query : str
75
+ The question or prompt about financial/news data.
76
+
77
+ Returns
78
+ -------
79
+ str
80
+ The system message content with instructions for Grok.
81
+ """
82
+ system_prompt = f"""\
83
+ You are a financial news aggregator assistant with real-time access to Twitter, news feeds,
84
+ and other finance data. Remain factual and accurate in your response (avoid hallucinations).
85
+
86
+ Your task is to return ONLY valid JSON, following this exact schema:
87
+
88
+ JSON Schema:
89
+ {{
90
+ "query": "<string, required - echo the user's query>",
91
+ "timestamp_utc": "<string, required - current UTC time in ISO 8601 format>",
92
+ "analysis_summary": "<string, required - short summary of the findings>",
93
+ "items": [
94
+ {{
95
+ "symbol": "<string, required - e.g. 'AAPL', 'BTC'. Always neds to be a valid symbol>",
96
+ "asset_type": "<string, required - 'stock', 'crypto', 'index', 'commodity', 'forex', 'none'>",
97
+ "headline": "<string, required - short note on why it's trending>",
98
+ "confidence": "<integer, 0-10, required - reliability score>",
99
+ "sentiment_score": "<integer, -10 to 10, required - negative is bearish, positive is bullish>",
100
+ "popularity_metric": "<integer >= 0, required - measure of mentions>",
101
+ "volume_of_messages": "<integer, optional - if known>",
102
+ "magnitude": "<integer, 0-10, required - overall impact level>",
103
+ "type_of_news": "<string, optional - e.g. 'earnings', 'ipo', 'macro', 'ceo_tweet'>",
104
+ "price_targets": {{
105
+ "low": "<float, optional>",
106
+ "high": "<float, optional>",
107
+ "average": "<float, optional>"
108
+ }},
109
+ "additional_info": {{
110
+ "sector": "<string, optional>",
111
+ "recent_events": "<string, optional>",
112
+ "notable_executive_actions": "<string, optional>",
113
+ "macro_support": "<string, optional>",
114
+ "related_tickers": ["<string>", "..."],
115
+ "external_links": ["<string>", "..."]
116
+ }}
117
+ }}
118
+ ]
119
+ }}
120
+
121
+ Instructions:
122
+ 1) Output MUST be valid JSON (no extra text or markdown).
123
+ 2) If no data found, 'items' can be empty.
124
+ 3) The 'magnitude' field is required (0-10).
125
+ 4) Provide minimal text in 'analysis_summary' (1-3 sentences).
126
+ 5) The field 'query' must repeat the user's question exactly.
127
+
128
+ Now, the user's query is:
129
+ \"{user_query}\"
130
+
131
+ Return only valid JSON following the schema.
132
+ """
133
+ return system_prompt
134
+
135
+ def execute_query(self, user_query: str) -> dict:
136
+ """
137
+ Executes a query by creating a chat completion via the xAI OpenAI-compatible client.
138
+ We pass a system message (JSON schema instructions) + user message (user_query).
139
+
140
+ Steps:
141
+ ------
142
+ 1) Build the system prompt with `build_prompt(user_query)`.
143
+ 2) Call `client.chat.completions.create(...)` with:
144
+ - model = "grok-2-latest" (or any available model)
145
+ - messages (system + user).
146
+ - temperature=0 to reduce hallucinations.
147
+ 3) Parse the assistant's output text as JSON.
148
+ 4) Return a dictionary matching the schema. If parsing fails, return an error structure.
149
+
150
+ Parameters
151
+ ----------
152
+ user_query : str
153
+ The textual query about financial/news data (e.g. "Which stocks are trending now?").
154
+
155
+ Returns
156
+ -------
157
+ dict
158
+ A dictionary conforming to the described JSON schema:
159
+ {
160
+ "query": "...",
161
+ "timestamp_utc": "...",
162
+ "analysis_summary": "...",
163
+ "items": [...]
164
+ }
165
+ """
166
+ # 1) Build system prompt
167
+ system_msg = self.build_prompt(user_query)
168
+
169
+ # 2) Create the chat completion using the xAI client
170
+ try:
171
+ completion = self.client.chat.completions.create(
172
+ model="grok-2-latest",
173
+ messages=[
174
+ {"role": "system", "content": system_msg},
175
+ {"role": "user", "content": user_query}
176
+ ],
177
+ temperature=0,
178
+ stream=False
179
+ )
180
+ except Exception as e:
181
+ # If there's a network or API error, fallback with an error structure
182
+ return {
183
+ "query": user_query,
184
+ "timestamp_utc": datetime.datetime.now(datetime.UTC).isoformat()
185
+ if hasattr(datetime, 'UTC') else datetime.datetime.utcnow().isoformat(),
186
+ "analysis_summary": f"Error calling xAI API: {str(e)}",
187
+ "items": []
188
+ }
189
+
190
+ # 3) Extract the assistant's text (the content of the first choice)
191
+ try:
192
+ assistant_text = completion.choices[0].message.content
193
+ except (AttributeError, IndexError, KeyError) as e:
194
+ return {
195
+ "query": user_query,
196
+ "timestamp_utc": datetime.datetime.now(datetime.UTC).isoformat()
197
+ if hasattr(datetime, 'UTC') else datetime.datetime.utcnow().isoformat(),
198
+ "analysis_summary": f"Error: Unexpected response format from Grok API. {str(e)}",
199
+ "items": []
200
+ }
201
+
202
+ # 4) Parse the assistant's text as JSON
203
+ try:
204
+ data = json.loads(assistant_text)
205
+ except json.JSONDecodeError as e:
206
+ return {
207
+ "query": user_query,
208
+ "timestamp_utc": datetime.datetime.now(datetime.UTC).isoformat()
209
+ if hasattr(datetime, 'UTC') else datetime.datetime.utcnow().isoformat(),
210
+ "analysis_summary": f"Error: LLM output was not valid JSON. {str(e)}",
211
+ "items": []
212
+ }
213
+
214
+ # 5) Post-process to ensure numeric fields are integers
215
+ self._post_process_data(data)
216
+
217
+ return data
218
+
219
+ def _post_process_data(self, data: dict) -> None:
220
+ """
221
+ Enforces that certain fields are integers:
222
+ - confidence
223
+ - sentiment_score
224
+ - popularity_metric
225
+ - magnitude
226
+ - volume_of_messages (if present)
227
+
228
+ Also handles optional price_targets as floats if present.
229
+
230
+ Modifies the 'data' dictionary in-place.
231
+
232
+ Parameters
233
+ ----------
234
+ data : dict
235
+ The parsed JSON dictionary from the assistant.
236
+ """
237
+ items = data.get("items", [])
238
+ for item in items:
239
+ for int_field in ("confidence", "sentiment_score", "popularity_metric", "magnitude"):
240
+ if int_field in item:
241
+ try:
242
+ item[int_field] = int(item[int_field])
243
+ except (ValueError, TypeError):
244
+ item[int_field] = 0
245
+
246
+ if "volume_of_messages" in item:
247
+ try:
248
+ item["volume_of_messages"] = int(item["volume_of_messages"])
249
+ except (ValueError, TypeError):
250
+ item["volume_of_messages"] = 0
251
+
252
+ # Optional price_targets handling
253
+ if "price_targets" in item and isinstance(item["price_targets"], dict):
254
+ for float_field in ("low", "high", "average"):
255
+ if float_field in item["price_targets"]:
256
+ try:
257
+ item["price_targets"][float_field] = float(item["price_targets"][float_field])
258
+ except (ValueError, TypeError):
259
+ item["price_targets"][float_field] = None
260
+
261
+
262
+ # ------------------------------------------------------------------------------
263
+ # Example usage in a standalone script:
264
+ # ------------------------------------------------------------------------------
265
+ if __name__ == "__main__":
266
+ # Attempt to retrieve xAI API key from environment
267
+ import os
268
+ import dotenv
269
+
270
+ dotenv.load_dotenv()
271
+ xai_api_key = os.getenv("XAI_API_KEY")
272
+ if not xai_api_key or xai_api_key == "YOUR_XAI_API_KEY":
273
+ print("WARNING: No valid XAI_API_KEY found in environment!")
274
+ xai_api_key = "YOUR_XAI_API_KEY" # fallback
275
+
276
+ nq_helper = GrokNewsHelper(api_key=xai_api_key)
277
+
278
+ # Example user query
279
+ # user_query = "What drugs from small biotech companies are expected to get FDA approvals soon?"
280
+ user_query = "what is the twitter account @Banana3Stocks recommending to buy or sell right now and at what price targets? he usually says things like 'see you at XX price' or 'XX price soon' or 'run to XX' or 'pivot is XX' or 'XX later this year' or 'ready for $XX'"
281
+ result = nq_helper.execute_query(user_query)
282
+
283
+ # Print the structured response
284
+ print(json.dumps(result, indent=2))
@@ -66,8 +66,8 @@ class OptionsHelper:
66
66
  expiry: date, put_or_call: str = "call") -> Optional[Asset]:
67
67
  """
68
68
  Find a valid option with the given expiry and strike.
69
- If no option is available (e.g. due to holidays/weekends), increment the expiry day-by-day
70
- (up to 10 times) until a valid option is found.
69
+ First tries the requested strike, then searches nearby strikes from the option chain.
70
+ If no strikes work for this expiry, tries the next expiry date.
71
71
 
72
72
  Parameters
73
73
  ----------
@@ -86,55 +86,111 @@ class OptionsHelper:
86
86
  The valid option asset or None if not found.
87
87
  """
88
88
  self.strategy.log_message(f"Finding next valid option for {underlying_asset.symbol} at strike {rounded_underlying_price} and expiry {expiry}", color="blue")
89
- loop_counter = 0
90
- while True:
89
+
90
+ expiry_attempts = 0
91
+ while expiry_attempts < 10:
92
+ # Check if this expiry was previously marked as invalid
91
93
  for record in self.non_existing_expiry_dates:
92
94
  if (record["underlying_asset_symbol"] == underlying_asset.symbol and
93
95
  record["expiry"] == expiry):
94
96
  self.strategy.log_message(f"Expiry {expiry} previously invalid for {underlying_asset.symbol}; trying next day.", color="yellow")
95
97
  expiry += timedelta(days=1)
98
+ expiry_attempts += 1
96
99
  continue
97
100
 
98
- option = Asset(
99
- underlying_asset.symbol,
100
- asset_type="option",
101
- expiration=expiry,
102
- strike=rounded_underlying_price,
103
- right=put_or_call,
104
- underlying_asset=underlying_asset,
105
- )
106
-
107
- # First check if quote data exists (preferred)
101
+ # Try to get option chain to find available strikes
108
102
  try:
109
- quote = self.strategy.get_quote(option)
110
- has_valid_quote = quote and (quote.bid is not None or quote.ask is not None)
111
- if has_valid_quote:
112
- self.strategy.log_message(f"Found valid quote for option {option.symbol} at expiry {expiry}", color="blue")
113
- return option
114
- except Exception as e:
115
- self.strategy.log_message(f"Error getting quote for {option.symbol}: {e}", color="yellow")
103
+ chains = self.strategy.get_chains(underlying_asset)
104
+ self.strategy.log_message(f"Got chains for {underlying_asset.symbol}: {bool(chains)}", color="cyan")
105
+
106
+ if chains:
107
+ # Get available strikes for this expiry
108
+ available_strikes = chains.strikes(expiry, put_or_call.upper())
109
+ self.strategy.log_message(f"Available strikes for {expiry}: {len(available_strikes) if available_strikes else 0} strikes", color="cyan")
110
+
111
+ if not available_strikes:
112
+ self.strategy.log_message(f"No strikes available for {put_or_call.upper()} on {expiry}; trying next expiry.", color="yellow")
113
+ self.non_existing_expiry_dates.append({
114
+ "underlying_asset_symbol": underlying_asset.symbol,
115
+ "expiry": expiry,
116
+ })
117
+ expiry += timedelta(days=1)
118
+ expiry_attempts += 1
119
+ continue
120
+
121
+ # Find the closest strike to our target
122
+ closest_strike = min(available_strikes, key=lambda x: abs(x - rounded_underlying_price))
123
+ self.strategy.log_message(f"Target strike {rounded_underlying_price} -> Closest available strike: {closest_strike}", color="green")
124
+
125
+ # Create option with the closest available strike
126
+ option = Asset(
127
+ underlying_asset.symbol,
128
+ asset_type="option",
129
+ expiration=expiry,
130
+ strike=closest_strike,
131
+ right=put_or_call,
132
+ underlying_asset=underlying_asset,
133
+ )
134
+
135
+ # Verify this option has price data
136
+ try:
137
+ quote = self.strategy.get_quote(option)
138
+ has_valid_quote = quote and (quote.bid is not None or quote.ask is not None)
139
+ if has_valid_quote:
140
+ self.strategy.log_message(f"Found valid option: {option.symbol} {option.right} {option.strike} exp {option.expiration}", color="green")
141
+ return option
142
+ except Exception as e:
143
+ self.strategy.log_message(f"Error getting quote for {option.symbol}: {e}", color="yellow")
144
+
145
+ # Fallback to last price
146
+ try:
147
+ price = self.strategy.get_last_price(option)
148
+ if price is not None:
149
+ self.strategy.log_message(f"Found valid option (via last price): {option.symbol} {option.right} {option.strike} exp {option.expiration}", color="green")
150
+ return option
151
+ except Exception as e:
152
+ pass
153
+
154
+ # If closest strike didn't work, this expiry might be invalid
155
+ self.strategy.log_message(f"Could not get price data for strike {closest_strike} on {expiry}; trying next expiry.", color="yellow")
156
+ self.non_existing_expiry_dates.append({
157
+ "underlying_asset_symbol": underlying_asset.symbol,
158
+ "expiry": expiry,
159
+ })
160
+ expiry += timedelta(days=1)
161
+ expiry_attempts += 1
162
+ continue
116
163
 
117
- # Fallback to checking last price if no quote
118
- try:
119
- price = self.strategy.get_last_price(option)
120
- self.strategy.log_message(f"Price for option {option.symbol} at expiry {expiry} is {price}", color="blue")
121
164
  except Exception as e:
122
- self.strategy.log_message(f"Error getting price for {option.symbol}: {e}", color="red")
123
- price = None
165
+ self.strategy.log_message(f"Error getting chains for {underlying_asset.symbol}: {e}; falling back to direct strike attempt.", color="yellow")
166
+ # Fallback: Try the exact strike requested (old behavior)
167
+ option = Asset(
168
+ underlying_asset.symbol,
169
+ asset_type="option",
170
+ expiration=expiry,
171
+ strike=rounded_underlying_price,
172
+ right=put_or_call,
173
+ underlying_asset=underlying_asset,
174
+ )
124
175
 
125
- if price is not None:
126
- return option
176
+ try:
177
+ price = self.strategy.get_last_price(option)
178
+ if price is not None:
179
+ return option
180
+ except Exception:
181
+ pass
127
182
 
128
- self.strategy.log_message(f"No price found for option {option.symbol} at expiry {expiry}.", color="yellow")
183
+ # No valid option found for this expiry, try next day
184
+ self.strategy.log_message(f"No valid option found for expiry {expiry}; trying next expiry.", color="yellow")
129
185
  self.non_existing_expiry_dates.append({
130
186
  "underlying_asset_symbol": underlying_asset.symbol,
131
187
  "expiry": expiry,
132
188
  })
133
189
  expiry += timedelta(days=1)
134
- loop_counter += 1
135
- if loop_counter >= 10:
136
- self.strategy.log_message("Exceeded maximum attempts to find a valid option.", color="red")
137
- return None
190
+ expiry_attempts += 1
191
+
192
+ self.strategy.log_message("Exceeded maximum attempts to find a valid option.", color="red")
193
+ return None
138
194
 
139
195
  def get_strike_deltas(self, underlying_asset: Asset, expiry: date, strikes: List[float],
140
196
  right: str, stop_greater_than: Optional[float] = None,
lumibot/credentials.py CHANGED
@@ -78,6 +78,9 @@ BACKTESTING_END = None
78
78
  if backtesting_end:
79
79
  BACKTESTING_END = parser.parse(backtesting_end)
80
80
 
81
+ # Get the backtesting data source
82
+ BACKTESTING_DATA_SOURCE = os.environ.get("BACKTESTING_DATA_SOURCE", "ThetaData")
83
+
81
84
  # Check if we should hide trades
82
85
  hide_trades = os.environ.get("HIDE_TRADES")
83
86
  if not hide_trades or hide_trades.lower() == "false":
@@ -68,11 +68,9 @@ class DataSourceBacktesting(DataSource, ABC):
68
68
  # catch it here and ignore it in this class. Child classes that need it should error check it themselves.
69
69
  self._config = config
70
70
 
71
- # If false, we don't show the progress bar
72
- # Also disable progress bar if BACKTESTING_QUIET_LOGS is enabled
73
- import os
74
- quiet_logs_enabled = os.environ.get("BACKTESTING_QUIET_LOGS", "").lower() == "true"
75
- self._show_progress_bar = show_progress_bar and not quiet_logs_enabled
71
+ # Progress bar should always show when enabled, regardless of quiet_logs
72
+ # Quiet logs only affects INFO/WARNING/ERROR logging, not progress bar
73
+ self._show_progress_bar = show_progress_bar
76
74
 
77
75
  self._progress_csv_path = "logs/progress.csv"
78
76
  # Add initialization for the logging timer attribute