trdr 0.0.2__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.
Files changed (64) hide show
  1. trdr/__init__.py +0 -0
  2. trdr/conftest.py +148 -0
  3. trdr/core/__init__.py +0 -0
  4. trdr/core/bar_provider/__init__.py +0 -0
  5. trdr/core/bar_provider/base_bar_provider.py +115 -0
  6. trdr/core/bar_provider/exceptions.py +40 -0
  7. trdr/core/bar_provider/models.py +38 -0
  8. trdr/core/bar_provider/yf_bar_provider/__init__.py +0 -0
  9. trdr/core/bar_provider/yf_bar_provider/test_yf_bar_provider.py +73 -0
  10. trdr/core/bar_provider/yf_bar_provider/yf_bar_provider.py +301 -0
  11. trdr/core/broker/__init__.py +0 -0
  12. trdr/core/broker/alpaca_broker/README.md +68 -0
  13. trdr/core/broker/alpaca_broker/__init__.py +0 -0
  14. trdr/core/broker/alpaca_broker/alpaca_broker.py +404 -0
  15. trdr/core/broker/base_broker.py +391 -0
  16. trdr/core/broker/exceptions.py +6 -0
  17. trdr/core/broker/mock_broker/__init__.py +0 -0
  18. trdr/core/broker/mock_broker/mock_broker.py +130 -0
  19. trdr/core/broker/mock_broker/test_mock_broker.py +394 -0
  20. trdr/core/broker/mock_broker/test_mock_broker_pdt_validation.py +277 -0
  21. trdr/core/broker/models.py +145 -0
  22. trdr/core/broker/pdt/README.md +82 -0
  23. trdr/core/broker/pdt/__init__.py +0 -0
  24. trdr/core/broker/pdt/base_pdt_strategy.py +32 -0
  25. trdr/core/broker/pdt/exceptions.py +16 -0
  26. trdr/core/broker/pdt/models.py +40 -0
  27. trdr/core/broker/pdt/nun_strategy.py +86 -0
  28. trdr/core/broker/pdt/test_pdt_evaluation.py +247 -0
  29. trdr/core/broker/pdt/wiggle_strategy.py +65 -0
  30. trdr/core/broker/pdt/yolo_strategy.py +48 -0
  31. trdr/core/broker/test_order_model.py +407 -0
  32. trdr/core/broker/test_position_model.py +638 -0
  33. trdr/core/security_provider/__init__.py +0 -0
  34. trdr/core/security_provider/base_security_provider.py +108 -0
  35. trdr/core/security_provider/models.py +197 -0
  36. trdr/core/security_provider/security_provider.py +43 -0
  37. trdr/core/security_provider/test_security_model.py +74 -0
  38. trdr/core/security_provider/test_security_provider.py +30 -0
  39. trdr/core/shared/__init__.py +0 -0
  40. trdr/core/shared/exceptions.py +2 -0
  41. trdr/core/shared/models.py +299 -0
  42. trdr/core/trading_context/__init__.py +0 -0
  43. trdr/core/trading_context/exceptions.py +2 -0
  44. trdr/core/trading_context/test_trading_context.py +481 -0
  45. trdr/core/trading_context/trading_context.py +318 -0
  46. trdr/core/trading_engine/__init__.py +0 -0
  47. trdr/core/trading_engine/trading_engine.py +222 -0
  48. trdr/dsl/__init__.py +0 -0
  49. trdr/dsl/dsl_ast.py +375 -0
  50. trdr/dsl/dsl_loader.py +99 -0
  51. trdr/dsl/lexer.py +202 -0
  52. trdr/dsl/parser.py +288 -0
  53. trdr/test_utils/__init__.py +0 -0
  54. trdr/test_utils/fake_yf_download.py +38 -0
  55. trdr/test_utils/order_generator.py +149 -0
  56. trdr/test_utils/position_generator.py +152 -0
  57. trdr/test_utils/security_generator.py +162 -0
  58. trdr/test_utils/strategies/always_buy.trdr +13 -0
  59. trdr/test_utils/strategies/always_sell.trdr +13 -0
  60. trdr/test_utils/strategies/test_strat.trdr +28 -0
  61. trdr/test_utils/test_symbols.py +505 -0
  62. trdr-0.0.2.dist-info/METADATA +14 -0
  63. trdr-0.0.2.dist-info/RECORD +64 -0
  64. trdr-0.0.2.dist-info/WHEEL +4 -0
@@ -0,0 +1,301 @@
1
+ from typing import List, Tuple, Optional
2
+ from datetime import timedelta, timezone
3
+ import yfinance as yf
4
+ from decimal import Decimal
5
+ import pandas as pd
6
+ from opentelemetry import trace
7
+ import logging
8
+
9
+ from ..exceptions import (
10
+ BarProviderException,
11
+ InsufficientBarsException,
12
+ NoBarsForSymbolException,
13
+ BarConversionException,
14
+ )
15
+ from ..base_bar_provider import BaseBarProvider
16
+ from ..models import Bar, TradingDateTime, Money
17
+ from ...shared.models import Timeframe
18
+
19
+ # Disable yfinance logging
20
+ logger = logging.getLogger("yfinance")
21
+ logger.disabled = True
22
+
23
+
24
+ class YFBarProvider(BaseBarProvider):
25
+ def __init__(
26
+ self,
27
+ *args,
28
+ **kwargs,
29
+ ):
30
+ """Disabled constructor - use YFBarProvider.create() instead."""
31
+ raise TypeError("Use YFBarProvider.create() instead to create a new bar provider")
32
+
33
+ async def _initialize(self, symbols: List[str]) -> None:
34
+ """
35
+ Initialize the YFBarProvider instance.
36
+
37
+ This function is called by the T.create() method implemented in the base class.
38
+ Use that method to create a new bar provider.
39
+ """
40
+ with self._tracer.start_as_current_span("YFBarProvider._initialize") as span:
41
+ self._no_data_errors = ["YFTzMissingError", "YFPricesMissingError", "JSONDecodeError", "YFRateLimitError"]
42
+ if not symbols:
43
+ span.set_status(trace.Status(trace.StatusCode.ERROR))
44
+ e = BarProviderException("Symbols must contain at least one symbol")
45
+ span.record_exception(e)
46
+ raise e
47
+ try:
48
+ await self._refresh_data(symbols)
49
+ except Exception as e:
50
+ span.add_event("refresh_data_error")
51
+ span.set_status(trace.Status(trace.StatusCode.ERROR))
52
+ raise
53
+ else:
54
+ span.set_status(trace.Status(trace.StatusCode.OK))
55
+
56
+ async def _refresh_data(self, symbols: List[str]) -> None:
57
+ """
58
+ Refresh the stock data for the symbols.
59
+
60
+ This function is called by the factory method in the base class upon instantiation
61
+ of a new provider.
62
+ """
63
+ with self._tracer.start_as_current_span("YFBarProvider._refresh_data") as span:
64
+ span.set_attribute("number_of_symbols_requested", len(symbols))
65
+ try:
66
+ symbols_with_data, data = await self._fetch_batch_stock_data(symbols.copy())
67
+ span.set_attribute("number_of_symbols_with_no_data", len(symbols) - len(symbols_with_data))
68
+ except Exception as e:
69
+ span.add_event("data_fetch_error")
70
+ span.set_status(trace.Status(trace.StatusCode.ERROR))
71
+ raise e
72
+ else:
73
+ for symbol in symbols_with_data:
74
+ try:
75
+ symbol_data = data.xs(symbol, level=0, axis=1)
76
+ bars = self._convert_df_to_bars(symbol, symbol_data)
77
+ self._data_cache[symbol] = bars
78
+ except BarConversionException as e:
79
+ continue
80
+ except Exception as e:
81
+ span.set_status(trace.Status(trace.StatusCode.ERROR))
82
+ raise e
83
+ span.set_attribute(
84
+ "number_of_symbols_with_data_that_failed_to_convert_to_bars",
85
+ len(symbols_with_data) - len(self._data_cache.keys()),
86
+ )
87
+ span.set_attribute("number_of_symbols_with_data", len(self._data_cache.keys()))
88
+ span.set_status(trace.Status(trace.StatusCode.OK))
89
+ span.add_event("refresh_complete")
90
+
91
+ async def _fetch_batch_stock_data(
92
+ self,
93
+ symbols: List[str],
94
+ ) -> Tuple[List[str], pd.DataFrame]:
95
+ """
96
+ Fetch batch stock data from Yahoo Finance.
97
+
98
+ Returns:
99
+ pd.DataFrame: DataFrame containing the stock data.
100
+ """
101
+ with self._tracer.start_as_current_span("YFBarProvider._fetch_batch_stock_data") as span:
102
+ end_datetime = TradingDateTime.now().timestamp
103
+ start_datetime = end_datetime - timedelta(days=300)
104
+ span.set_attribute("total_symbols_to_fetch_data_for", len(symbols))
105
+ span.set_attribute("start_datetime", str(start_datetime))
106
+ span.set_attribute("end_datetime", str(end_datetime))
107
+ span.set_attribute("date_range_days", (end_datetime - start_datetime).days)
108
+ span.add_event("begin_data_fetch")
109
+ data = yf.download(
110
+ symbols,
111
+ start=start_datetime,
112
+ end=end_datetime,
113
+ group_by="ticker",
114
+ interval=Timeframe.d1.to_yf_interval(),
115
+ )
116
+ span.add_event("data_fetch_complete")
117
+ if yf.shared._ERRORS:
118
+ """
119
+ If a symbol has one of the following errors associated with it in the yf.shared._ERRORS dictionary, it means that we received no data for that symbol. We should therefore remove it from self._symbols.
120
+ The JSONDecodeError is the manifestation of a rate limit.
121
+ """
122
+
123
+ symbols_with_no_data = [
124
+ symbol
125
+ for symbol, error in yf.shared._ERRORS.items()
126
+ if any(no_data_error in error for no_data_error in self._no_data_errors)
127
+ ]
128
+ for symbol in symbols_with_no_data:
129
+ if symbol in symbols:
130
+ symbols.remove(symbol)
131
+
132
+ # Gather any other errors.
133
+ other_errors = [
134
+ error
135
+ for error in yf.shared._ERRORS.values()
136
+ if all(no_data_error not in error for no_data_error in self._no_data_errors)
137
+ ]
138
+ if other_errors:
139
+ error_msg = "; ".join(other_errors)
140
+ span.set_status(trace.Status(trace.StatusCode.ERROR))
141
+ e = BarProviderException(f"YFinance errors: {error_msg}")
142
+ span.record_exception(e)
143
+ raise e
144
+ else:
145
+ span.set_status(trace.Status(trace.StatusCode.OK))
146
+ if symbols_with_no_data:
147
+ span.set_attribute("symbols_with_no_data", symbols_with_no_data)
148
+
149
+ """
150
+ Our yf object is long lived across the lifetime of the bar provider. Therefore, we should reset the errors dictionary. We need it clean as we will need to inspect errors produced by calls to yf.download()
151
+ """
152
+ yf.shared._ERRORS = {}
153
+
154
+ return symbols, data
155
+
156
+ def _convert_df_to_bars(self, symbol: str, df: pd.DataFrame) -> List[Bar]:
157
+ """
158
+ Convert a DataFrame to a list of Bar objects.
159
+
160
+ Args:
161
+ df (pd.DataFrame): DataFrame containing the stock data.
162
+
163
+ Returns:
164
+ List[Bar]: List of Bar objects.
165
+ """
166
+ with self._tracer.start_as_current_span("YFBarProvider._convert_df_to_bars") as span:
167
+ span.set_attribute("symbol", symbol)
168
+
169
+ bars = []
170
+ total_rows = len(df)
171
+ bar_creation_errors = 0
172
+
173
+ for date, row in df.iterrows():
174
+ try:
175
+ utc_timestamp = pd.Timestamp(date).to_pydatetime().replace(tzinfo=timezone.utc)
176
+ bar = Bar(
177
+ trading_datetime=TradingDateTime.from_utc(utc_timestamp),
178
+ open=Money(amount=Decimal(float(row["Open"]))),
179
+ high=Money(amount=Decimal(float(row["High"]))),
180
+ low=Money(amount=Decimal(float(row["Low"]))),
181
+ close=Money(amount=Decimal(float(row["Close"]))),
182
+ volume=int(row["Volume"]),
183
+ )
184
+ bars.append(bar)
185
+ except Exception as lower_e:
186
+ bar_creation_errors += 1
187
+ if bar_creation_errors / total_rows > 0.05:
188
+ span.set_status(trace.Status(trace.StatusCode.ERROR))
189
+ e = BarConversionException(
190
+ f"failed to convert {bar_creation_errors} out of {total_rows} rows to Bars"
191
+ )
192
+ span.set_attribute("exception.cause", lower_e)
193
+ span.record_exception(e)
194
+ raise e from lower_e
195
+ span.set_attribute("bars_created", len(bars))
196
+ span.set_attribute("bar_creation_errors", bar_creation_errors)
197
+ span.set_status(trace.Status(trace.StatusCode.OK))
198
+
199
+ return bars
200
+
201
+ def get_symbols(self) -> List[str]:
202
+ return list(self._data_cache.keys())
203
+
204
+ async def get_current_bar(self, symbol: str) -> Bar:
205
+ with self._tracer.start_as_current_span("YFBarProvider.get_current_bar") as span:
206
+ span.set_attribute("symbol", symbol)
207
+ span.add_event("begin_current_bar_data_fetch")
208
+ data = yf.download(
209
+ symbol,
210
+ period=Timeframe.d1.to_yf_interval(),
211
+ interval=Timeframe.m15.to_yf_interval(),
212
+ group_by="ticker",
213
+ )
214
+ span.add_event("current_bar_data_fetch_complete")
215
+ if yf.shared._ERRORS:
216
+ error = yf.shared._ERRORS.get(symbol, None)
217
+ if not error or not any(no_data_error in error for no_data_error in self._no_data_errors):
218
+ """
219
+ If we receive an error not associated with the symbol of interest, we should raise an exception.
220
+ """
221
+ # Gather any other errors.
222
+ errors = [error for error in yf.shared._ERRORS.values()]
223
+ error_msg = "; ".join(errors)
224
+ span.set_status(trace.Status(trace.StatusCode.ERROR))
225
+ e = BarProviderException(f"Received an error not related to no data errors: {error_msg}")
226
+ span.record_exception(e)
227
+ raise e
228
+ if any(no_data_error in error for no_data_error in self._no_data_errors):
229
+ """
230
+ If we didn't receive any data for the symbol of interest we can't construct the current bar.
231
+ """
232
+ span.set_status(trace.Status(trace.StatusCode.ERROR))
233
+ e = NoBarsForSymbolException(f"{symbol}")
234
+ span.record_exception(e)
235
+ raise e
236
+ """
237
+ Our yf object is long lived across the lifetime of the bar provider. Therefore, we should reset the errors dictionary. We need it clean as we will need to inspect errors produced by calls to yf.download()
238
+ """
239
+ yf.shared._ERRORS = {}
240
+
241
+ try:
242
+ symbol_data = data.xs(symbol, level=0, axis=1)
243
+ bars = self._convert_df_to_bars(symbol, symbol_data)
244
+ most_recent_bar = bars[-1]
245
+ most_recent_bar.trading_datetime = TradingDateTime.now()
246
+ except Exception as e:
247
+ span.set_status(trace.Status(trace.StatusCode.ERROR))
248
+ raise e
249
+ else:
250
+ span.set_status(trace.Status(trace.StatusCode.OK))
251
+ return most_recent_bar
252
+
253
+ async def get_bars(
254
+ self,
255
+ symbol: str,
256
+ lookback: Optional[int] = None,
257
+ ) -> List[Bar]:
258
+ """
259
+ Get the bars for a symbol with a specified lookback period.
260
+
261
+ Args:
262
+ symbol (str): The symbol to get the bars for.
263
+ lookback (int): The number of bars to look back.
264
+
265
+ Returns:
266
+ List[Bar]: List of bars for the symbol.
267
+
268
+ Raises:
269
+ NoBarsForSymbolException: If we didn't receive any data for the symbol of interest.
270
+ InsufficientBarsException: If the number of bars requested is greater than the number of bars available.
271
+ """
272
+ with self._tracer.start_as_current_span("YFBarProvider.get_bars") as span:
273
+ span.set_attribute("symbol", symbol)
274
+ if lookback is not None:
275
+ span.set_attribute("requested_lookback", lookback)
276
+ if not self._data_cache.get(symbol, None):
277
+ span.add_event("no_data_found_for_symbol")
278
+ span.set_status(trace.Status(trace.StatusCode.ERROR))
279
+ e = NoBarsForSymbolException(symbol)
280
+ span.record_exception(e)
281
+ raise e
282
+ if lookback is None:
283
+ lookback = len(self._data_cache[symbol])
284
+ if len(self._data_cache[symbol]) < lookback:
285
+ span.set_attribute("lookback_available_for_symbol", len(self._data_cache[symbol]))
286
+ span.add_event("lookback_too_large_for_symbol")
287
+ span.set_status(trace.Status(trace.StatusCode.ERROR))
288
+ e = InsufficientBarsException(
289
+ f"Only {len(self._data_cache[symbol])} bars available for symbol: {symbol}"
290
+ )
291
+ span.record_exception(e)
292
+ raise e
293
+ try:
294
+ bars = self._data_cache[symbol][-lookback:]
295
+ except Exception as e:
296
+ span.set_status(trace.Status(trace.StatusCode.ERROR))
297
+ span.record_exception(e)
298
+ raise
299
+ else:
300
+ span.set_status(trace.Status(trace.StatusCode.OK))
301
+ return bars
File without changes
@@ -0,0 +1,68 @@
1
+ # ALPACA Broker Implementation
2
+
3
+ The ALPACA broker implementation allows TRDR to connect with the Alpaca trading API for real and paper trading.
4
+
5
+ ## Requirements
6
+
7
+ To use the ALPACA broker, you must set the following environment variables:
8
+
9
+ - `ALPACA_API_KEY`: Your Alpaca API key
10
+ - `ALPACA_SECRET_KEY`: Your Alpaca secret key
11
+ - `ALPACA_BASE_URL`: The Alpaca API base URL
12
+ - For paper trading: `https://paper-api.alpaca.markets`
13
+ - For live trading: `https://api.alpaca.markets`
14
+
15
+ ## Important Notes
16
+
17
+ - The broker will automatically check if you're using a live trading URL and will display a warning if detected.
18
+ - Ensure your API keys have the appropriate permissions for the operations you intend to perform.
19
+ - The broker handles:
20
+ - Account information retrieval
21
+ - Position management
22
+ - Order placement and cancellation
23
+ - Pattern Day Trading (PDT) rule compliance through the PDT strategies
24
+
25
+ ## Example Usage
26
+
27
+ ```python
28
+ import asyncio
29
+ import os
30
+ from trdr.core.bar_provider.yf_bar_provider.yf_bar_provider import YFBarProvider
31
+ from trdr.core.security_provider.security_provider import SecurityProvider
32
+ from trdr.core.broker.alpaca_broker.alpaca_broker import AlpacaBroker
33
+ from trdr.core.trading_engine.trading_engine import TradingEngine
34
+ from trdr.core.trading_context.trading_context import TradingContext
35
+ from trdr.core.broker.pdt.nun_strategy import NunStrategy
36
+
37
+ # Set environment variables
38
+ os.environ["ALPACA_API_KEY"] = "your_api_key"
39
+ os.environ["ALPACA_SECRET_KEY"] = "your_secret_key"
40
+ os.environ["ALPACA_BASE_URL"] = "https://paper-api.alpaca.markets" # For paper trading
41
+
42
+ async def main():
43
+ try:
44
+ pdt_strategy = NunStrategy.create()
45
+ async with await AlpacaBroker.create(pdt_strategy=pdt_strategy) as broker:
46
+ bar_provider = await YFBarProvider.create(["TSLA"])
47
+ security_provider = await SecurityProvider.create(bar_provider)
48
+ context = await TradingContext.create(security_provider, broker)
49
+ engine = await TradingEngine.create("my-strategy", context)
50
+ await engine.execute()
51
+ except Exception as e:
52
+ print(e)
53
+
54
+ if __name__ == "__main__":
55
+ asyncio.run(main())
56
+ ```
57
+
58
+ ## PDT Rule Compliance
59
+
60
+ The ALPACA broker works with TRDR's Pattern Day Trading rule compliance strategies. You can specify which PDT strategy to use when creating the broker:
61
+
62
+ ```python
63
+ # Example with Nun Strategy
64
+ broker = await AlpacaBroker.create(pdt_strategy=NunStrategy.create())
65
+
66
+ # Example with YOLO Strategy
67
+ broker = await AlpacaBroker.create(pdt_strategy=YoloStrategy.create())
68
+ ```
File without changes