trdr 0.0.1__py2.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 +37 -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.1.dist-info/METADATA +13 -0
  63. trdr-0.0.1.dist-info/RECORD +64 -0
  64. trdr-0.0.1.dist-info/WHEEL +5 -0
trdr/__init__.py ADDED
File without changes
trdr/conftest.py ADDED
@@ -0,0 +1,148 @@
1
+ import asyncio
2
+ import pytest
3
+ import yfinance as yf
4
+ import random
5
+ import datetime
6
+
7
+ from .core.security_provider.security_provider import SecurityProvider
8
+ from .core.bar_provider.yf_bar_provider.yf_bar_provider import YFBarProvider
9
+ from .test_utils.fake_yf_download import fake_yf_download
10
+ from .test_utils.security_generator import SecurityGenerator, SecurityCriteria
11
+ from .test_utils.position_generator import PositionGenerator, PositionCriteria
12
+ from .test_utils.order_generator import OrderGenerator, OrderCriteria
13
+ from .core.broker.mock_broker.mock_broker import MockBroker
14
+ from .core.broker.pdt.nun_strategy import NunStrategy
15
+ from .core.broker.pdt.wiggle_strategy import WiggleStrategy
16
+ from .core.broker.pdt.yolo_strategy import YoloStrategy
17
+ from .core.trading_context.trading_context import TradingContext
18
+ from .core.shared.models import TradingDateTime
19
+
20
+
21
+ @pytest.fixture(scope="function")
22
+ def weekday_trading_datetime():
23
+ trading_now = TradingDateTime.now()
24
+ while trading_now.is_weekend:
25
+ trading_now = trading_now + datetime.timedelta(days=1)
26
+ return trading_now
27
+
28
+
29
+ @pytest.fixture(scope="function")
30
+ def weekend_trading_datetime():
31
+ trading_now = TradingDateTime.now()
32
+ while not trading_now.is_weekend:
33
+ trading_now = trading_now + datetime.timedelta(days=1)
34
+ return trading_now
35
+
36
+
37
+ @pytest.fixture(scope="function")
38
+ def yf_bar_provider_with_fake_data(monkeypatch):
39
+ monkeypatch.setattr(yf, "download", fake_yf_download)
40
+ monkeypatch.setattr(yf.shared, "_ERRORS", {"ABCDEFG": "YFTzMissingError()", "AMZN": "JSONDecodeError()"})
41
+ return asyncio.run(YFBarProvider.create(["AAPL", "MSFT", "ABCDEFG", "AMZN"]))
42
+
43
+
44
+ @pytest.fixture(scope="function")
45
+ def security_provider_with_fake_data(yf_bar_provider_with_fake_data):
46
+ return asyncio.run(SecurityProvider.create(yf_bar_provider_with_fake_data))
47
+
48
+
49
+ @pytest.fixture(scope="function")
50
+ def random_security(symbol="AAPL", bar_count=200):
51
+ """Create a random security with the given symbol and bar count.
52
+
53
+ Args:
54
+ symbol: The ticker symbol to use
55
+ count: The number of bars to generate
56
+
57
+ Returns:
58
+ A Security instance with randomly generated price and volume data
59
+ """
60
+ generator = SecurityGenerator(SecurityCriteria(bar_count=bar_count))
61
+ security = generator.find_suitable_security()
62
+ # Override the symbol if requested
63
+ if symbol != security.symbol:
64
+ security.symbol = symbol
65
+ return security
66
+
67
+
68
+ @pytest.fixture(scope="function")
69
+ def get_random_security():
70
+ """Legacy fixture name for backward compatibility."""
71
+ generator = SecurityGenerator(SecurityCriteria(bar_count=200))
72
+ return generator.find_suitable_security()
73
+
74
+
75
+ @pytest.fixture(scope="module")
76
+ def security_generator():
77
+ """Return a configured SecurityGenerator instance."""
78
+ return SecurityGenerator(SecurityCriteria(bar_count=200))
79
+
80
+
81
+ @pytest.fixture(scope="function")
82
+ def short_dummy_position():
83
+ """Create a test position with default values."""
84
+ position = PositionGenerator(criteria=PositionCriteria(bar_count=1, net_position_bias=0)).generate_positions()[0]
85
+ return position
86
+
87
+
88
+ @pytest.fixture(scope="function")
89
+ def long_dummy_position():
90
+ """Create a test position with default values."""
91
+ position = PositionGenerator(criteria=PositionCriteria(bar_count=1, net_position_bias=1)).generate_positions()[0]
92
+ return position
93
+
94
+
95
+ @pytest.fixture(scope="function")
96
+ def dummy_positions():
97
+ """Create a dictionary of test positions."""
98
+ num_positions = random.randint(1, 9)
99
+ positions = PositionGenerator(
100
+ criteria=PositionCriteria(bar_count=num_positions, net_position_bias=0.5)
101
+ ).generate_positions()
102
+ return positions
103
+
104
+
105
+ @pytest.fixture(scope="function")
106
+ def mock_broker_with_nun_strategy():
107
+ nun_strategy = NunStrategy.create()
108
+ broker = asyncio.run(MockBroker.create(pdt_strategy=nun_strategy))
109
+ yield broker
110
+ asyncio.run(broker._session.close())
111
+
112
+
113
+ @pytest.fixture(scope="function")
114
+ def mock_broker_with_wiggle_strategy():
115
+ pdt_strategy = WiggleStrategy.create()
116
+ pdt_strategy.wiggle_room = 2
117
+ broker = asyncio.run(MockBroker.create(pdt_strategy=pdt_strategy))
118
+ yield broker
119
+ asyncio.run(broker._session.close())
120
+
121
+
122
+ @pytest.fixture(scope="function")
123
+ def mock_broker_with_yolo_strategy():
124
+ pdt_strategy = YoloStrategy.create()
125
+ broker = asyncio.run(MockBroker.create(pdt_strategy=pdt_strategy))
126
+ yield broker
127
+ asyncio.run(broker._session.close())
128
+
129
+
130
+ @pytest.fixture(scope="function")
131
+ def mock_trading_context(security_provider_with_fake_data, mock_broker_with_nun_strategy):
132
+ return asyncio.run(TradingContext.create(security_provider_with_fake_data, mock_broker_with_nun_strategy))
133
+
134
+
135
+ @pytest.fixture(scope="function")
136
+ def create_order():
137
+
138
+ def _create_order(symbol: str, side=None):
139
+ return OrderGenerator(criteria=OrderCriteria(count=1, symbol=symbol, side=side)).generate_orders()[0]
140
+
141
+ return _create_order
142
+
143
+
144
+ @pytest.fixture(scope="function")
145
+ def prepared_trading_context(mock_trading_context: TradingContext):
146
+ """Prepare trading context with a valid symbol and security."""
147
+ asyncio.run(mock_trading_context.next_symbol())
148
+ return mock_trading_context
trdr/core/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,115 @@
1
+ from typing import List, Type, Optional, TypeVar
2
+ from abc import ABC, abstractmethod
3
+ from opentelemetry import trace
4
+
5
+ from .models import Bar
6
+
7
+ T = TypeVar("T", bound="BaseBarProvider")
8
+
9
+
10
+ class BaseBarProvider(ABC):
11
+ """
12
+ Abstract base class for retrieving and managing OHLCV bar data for securities.
13
+
14
+ This class defines the interface that all bar providers must implement, setting
15
+ a standardized way to retrieve historical and current price data. Bar providers
16
+ serve as the data foundation for trading strategies.
17
+
18
+ All implementations should maintain a data cache for efficient data retrieval
19
+ and support OpenTelemetry instrumentation for monitoring and tracing.
20
+
21
+ Attributes:
22
+ _data_cache: Internal cache for storing retrieved bar data
23
+ _tracer: OpenTelemetry tracer for instrumenting operations
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ tracer: trace.Tracer,
29
+ ):
30
+ self._data_cache = {}
31
+ self._tracer = tracer
32
+
33
+ @classmethod
34
+ async def create(cls: Type[T], symbols: List[str], tracer: Optional[trace.Tracer] = trace.NoOpTracer()) -> T:
35
+ """
36
+ Factory method to create and initialize a bar provider instance.
37
+
38
+ This async factory method pattern ensures proper initialization of resources
39
+ and error handling during the creation process. It invokes the implementation-specific
40
+ _initialize method which must be defined by concrete subclasses.
41
+
42
+ Args:
43
+ symbols: List of ticker symbols to initialize the provider with
44
+ tracer: OpenTelemetry tracer for instrumenting operations
45
+
46
+ Returns:
47
+ An initialized instance of the concrete bar provider
48
+
49
+ Raises:
50
+ Various exceptions depending on the implementation, typically related to
51
+ data availability or connectivity issues
52
+ """
53
+ self = cls.__new__(cls)
54
+ BaseBarProvider.__init__(self, tracer)
55
+ with self._tracer.start_as_current_span("BaseBarProvider.create") as span:
56
+ try:
57
+ await self._initialize(symbols)
58
+ except Exception as e:
59
+ span.record_exception(e)
60
+ span.set_status(trace.StatusCode.ERROR)
61
+ raise
62
+ else:
63
+ span.set_status(trace.StatusCode.OK)
64
+ return self
65
+
66
+ @abstractmethod
67
+ async def _initialize(self, symbols: List[str]) -> None:
68
+ """
69
+ This abstract method MUST be implemented by user defined data providers.
70
+ - This method SHOULD use the symbols argument to initialize the data cache.
71
+ - After the method has completed, the self._data_cache dictionary MUST be a dictionary where the keys are the symbols and the values are lists of Bars.
72
+ - The self._data_cache dictionary must only contain key,value pairs for symbols that have data available.
73
+ """
74
+ raise NotImplementedError("This method must be implemented by user defined data providers")
75
+
76
+ @abstractmethod
77
+ async def get_symbols(self) -> List[str]:
78
+ """
79
+ Get the list of symbols with data available.
80
+
81
+ Returns:
82
+ List[str]: List of symbols.
83
+ """
84
+ pass
85
+
86
+ @abstractmethod
87
+ async def get_bars(self, symbol: str, lookback: Optional[int] = None) -> List[Bar]:
88
+ """Get bars for a specific symbol.
89
+
90
+ Args:
91
+ symbol: The ticker symbol to get bars for
92
+ lookback: The number of bars to return
93
+
94
+ Raises:
95
+ NoBarsForSymbolException: If the symbol is not found in the data cache
96
+ InsufficientBarsException: If the number of bars requested is greater than the number of bars available
97
+ """
98
+ raise NotImplementedError("This method must be implemented by user defined data providers")
99
+
100
+ @abstractmethod
101
+ async def get_current_bar(self, symbol: str) -> Bar:
102
+ """
103
+ Get the current bar for a symbol.
104
+
105
+ Args:
106
+ symbol (str): The symbol to get the current bar for.
107
+
108
+ Returns:
109
+ Bar: The current bar for the symbol.
110
+
111
+ Raises:
112
+ BarProviderException: If we receive an error not associated with the symbol of interest or if we receive an error not related to a no data error.
113
+ NoBarsForSymbolException: If we didn't receive any data for the symbol of interest.
114
+ """
115
+ pass
@@ -0,0 +1,40 @@
1
+ class BarValidationException(Exception):
2
+ pass
3
+
4
+
5
+ class BarProviderException(Exception):
6
+ pass
7
+
8
+
9
+ class BarConversionException(BarProviderException):
10
+ pass
11
+
12
+
13
+ class DataSourceException(BarProviderException):
14
+ """
15
+ This exception represents the case where the data source returns an error other than a NoBarsForSymbolException.
16
+ """
17
+
18
+ pass
19
+
20
+
21
+ class NoBarsForSymbolException(DataSourceException):
22
+ """
23
+ This exception represents the case wehre Yahoo Fincance cannot return any data for a symbol.
24
+ """
25
+
26
+ symbol: str
27
+
28
+ def __init__(self, symbol: str):
29
+ self.symbol = symbol
30
+
31
+ def __str__(self):
32
+ return f"No bars found for symbol: {self.symbol}"
33
+
34
+
35
+ class TimeframeNotSupportedException(BarProviderException):
36
+ pass
37
+
38
+
39
+ class InsufficientBarsException(BarProviderException):
40
+ pass
@@ -0,0 +1,38 @@
1
+ from pydantic import BaseModel, model_validator
2
+
3
+ from ..shared.models import TradingDateTime, Money
4
+ from .exceptions import BarValidationException
5
+
6
+
7
+ class Bar(BaseModel):
8
+ trading_datetime: TradingDateTime
9
+ open: Money
10
+ high: Money
11
+ low: Money
12
+ close: Money
13
+ volume: int
14
+
15
+ @model_validator(mode="after")
16
+ def check_values(self) -> "Bar":
17
+ # Validate that the low price is less than or equal to high price.
18
+ if self.low.amount > self.high.amount:
19
+ raise BarValidationException("Low price must be less than or equal to high price")
20
+ # Validate that open price is between low and high.
21
+ if not (self.low.amount <= self.open.amount <= self.high.amount):
22
+ raise BarValidationException("Open price must be between low and high prices")
23
+ # Validate that close price is between low and high.
24
+ if not (self.low.amount <= self.close.amount <= self.high.amount):
25
+ raise BarValidationException("Close price must be between low and high prices")
26
+ # Validate that the volume is non-negative.
27
+ if self.volume < 0:
28
+ raise BarValidationException("Volume cannot be negative")
29
+ return self
30
+
31
+ def to_json(self) -> str:
32
+ return self.model_dump_json(indent=2)
33
+
34
+ def __str__(self) -> str:
35
+ return (
36
+ f"Bar(timestamp={self.trading_datetime}, open={self.open}, "
37
+ f"high={self.high}, low={self.low}, close={self.close}, volume={self.volume})"
38
+ )
File without changes
@@ -0,0 +1,73 @@
1
+ import pytest
2
+ import asyncio
3
+ import yfinance as yf
4
+
5
+ from .yf_bar_provider import YFBarProvider
6
+ from ..exceptions import NoBarsForSymbolException, BarProviderException, InsufficientBarsException
7
+ from ....test_utils.fake_yf_download import fake_yf_download
8
+
9
+
10
+ def test_that_only_symbols_with_data_are_in_the_data_cache(yf_bar_provider_with_fake_data):
11
+ data_cache_symbols = set(yf_bar_provider_with_fake_data._data_cache.keys())
12
+ assert data_cache_symbols == {"AAPL", "MSFT"}
13
+
14
+
15
+ def test_get_symbols_returns_a_list_of_symbols(yf_bar_provider_with_fake_data):
16
+ expected_symbols = {"AAPL", "MSFT"}
17
+ actual_symbols = yf_bar_provider_with_fake_data.get_symbols()
18
+ assert isinstance(actual_symbols, list)
19
+ # convert to set so order doesn't matter when we compare
20
+ actual_symbols = set(actual_symbols)
21
+ assert actual_symbols == expected_symbols
22
+
23
+
24
+ def test_get_symbols_returns_only_symbols_that_have_bars(yf_bar_provider_with_fake_data):
25
+ expected_symbols = {"AAPL", "MSFT"}
26
+ actual_symbols = set(yf_bar_provider_with_fake_data.get_symbols())
27
+ assert actual_symbols == expected_symbols
28
+
29
+
30
+ def test_gets_bars_throws_exception_when_no_bars_are_available_for_a_symbol(yf_bar_provider_with_fake_data):
31
+ with pytest.raises(NoBarsForSymbolException):
32
+ bars = asyncio.run(yf_bar_provider_with_fake_data.get_bars("ABCDEFG"))
33
+
34
+
35
+ def test_get_bars_throws_exception_when_symbol_is_not_in_data_cache(yf_bar_provider_with_fake_data):
36
+ with pytest.raises(NoBarsForSymbolException):
37
+ bars = asyncio.run(yf_bar_provider_with_fake_data.get_bars("ABCDEFG"))
38
+
39
+
40
+ def test_provider_throws_exception_when_data_source_returns_error(monkeypatch):
41
+ # ensure we raise an exception when the data source returns an error that is not a missing symbol error
42
+ with pytest.raises(BarProviderException):
43
+ monkeypatch.setattr(yf, "download", fake_yf_download)
44
+ monkeypatch.setattr(yf.shared, "_ERRORS", {"ABCDEFG": "RandomYFError()"})
45
+ bars = asyncio.run(YFBarProvider.create(["ABCDEFG"]))
46
+
47
+
48
+ def test_get_bars_throws_exception_when_lookback_is_greater_than_the_number_of_bars_available(
49
+ yf_bar_provider_with_fake_data,
50
+ ):
51
+ with pytest.raises(InsufficientBarsException):
52
+ bars = asyncio.run(yf_bar_provider_with_fake_data.get_bars("AAPL", 7))
53
+
54
+
55
+ def test_get_current_bar_throws_exception_when_no_bars_are_available_for_a_symbol(monkeypatch):
56
+ with pytest.raises(NoBarsForSymbolException):
57
+ """
58
+ Any time yf.download() is called, we clear the yf.shared._ERRORS dictionary after we are done inspecting it. Therefore, when we call yf.download() in the get_current_bar() method, we need to set the yf.shared._ERRORS dictionary again to the expected value of the test.
59
+ """
60
+ monkeypatch.setattr(yf, "download", fake_yf_download)
61
+ monkeypatch.setattr(yf.shared, "_ERRORS", {"ABCDEFG": "YFTzMissingError()"})
62
+ yf_bar_provider = asyncio.run(YFBarProvider.create(["ABCDEFG"]))
63
+ # yf.shared._ERRORS is currently {} as we reset it after the yf.download() call that occurs during initialization of the bar provider.
64
+ monkeypatch.setattr(yf.shared, "_ERRORS", {"ABCDEFG": "YFTzMissingError()"})
65
+ bar = asyncio.run(yf_bar_provider.get_current_bar("ABCDEFG"))
66
+
67
+
68
+ def test_get_current_bar_throws_exception_when_data_source_returns_error(monkeypatch):
69
+ with pytest.raises(BarProviderException):
70
+ monkeypatch.setattr(yf, "download", fake_yf_download)
71
+ yf_bar_provider = asyncio.run(YFBarProvider.create(["ABCDEFG"]))
72
+ monkeypatch.setattr(yf.shared, "_ERRORS", {"ABCDEFG": "RandomYFError()"})
73
+ bar = asyncio.run(yf_bar_provider.get_current_bar("ABCDEFG"))