11from investing_algorithm_framework import PortfolioConfiguration , \
2- MarketCredential , OrderSide
2+ MarketCredential , OrderSide , DataSource
33from investing_algorithm_framework .domain import OperationalException
44from tests .resources import TestBase
55from tests .resources .strategies_for_testing import StrategyOne
88class TestValidateSymbol (TestBase ):
99 """Tests for opt-in symbol validation on order creation (issue #247).
1010
11- Validates that target_symbol is not the same as trading_symbol,
12- which would result in a nonsensical order (e.g. EUR/EUR).
11+ Validates:
12+ 1. target_symbol is not the same as trading_symbol (e.g. EUR/EUR)
13+ 2. A data source exists for the target_symbol/trading_symbol pair
1314 """
1415 portfolio_configurations = [
1516 PortfolioConfiguration (
@@ -28,8 +29,21 @@ class TestValidateSymbol(TestBase):
2829 "EUR" : 1000
2930 }
3031
31- def test_default_allows_trading_symbol_as_target (self ):
32- """Default behavior: no validation, even trading_symbol is accepted."""
32+ def _register_data_source (self , symbol ):
33+ """Register a data source symbol in the data provider index."""
34+ data_source = DataSource (
35+ identifier = f"{ symbol } _ohlcv" ,
36+ symbol = symbol ,
37+ data_type = "OHLCV" ,
38+ time_frame = "1d" ,
39+ market = "BITVAVO" ,
40+ )
41+ data_provider_service = self .app .container .data_provider_service ()
42+ data_provider_service .data_provider_index \
43+ .data_providers_lookup [data_source ] = None
44+
45+ def test_default_allows_any_symbol (self ):
46+ """Default behavior: no validation, any symbol is accepted."""
3347 self .app .add_strategy (StrategyOne )
3448 self .app .context .create_limit_order (
3549 target_symbol = "EUR" ,
@@ -41,8 +55,8 @@ def test_default_allows_trading_symbol_as_target(self):
4155 order = order_repository .find ({"target_symbol" : "EUR" })
4256 self .assertIsNotNone (order )
4357
44- def test_validate_symbol_false_allows_trading_symbol_as_target (self ):
45- """Explicit validate_symbol=False allows trading_symbol as target ."""
58+ def test_validate_symbol_false_allows_any_symbol (self ):
59+ """Explicit validate_symbol=False skips all validation ."""
4660 self .app .add_strategy (StrategyOne )
4761 self .app .context .create_limit_order (
4862 target_symbol = "EUR" ,
@@ -55,9 +69,8 @@ def test_validate_symbol_false_allows_trading_symbol_as_target(self):
5569 order = order_repository .find ({"target_symbol" : "EUR" })
5670 self .assertIsNotNone (order )
5771
58- def test_validate_symbol_true_rejects_trading_symbol_as_target (self ):
59- """validate_symbol=True raises when target_symbol equals
60- trading_symbol (e.g. EUR/EUR)."""
72+ def test_rejects_target_equals_trading_symbol (self ):
73+ """Rejects orders where target_symbol == trading_symbol (EUR/EUR)."""
6174 self .app .add_strategy (StrategyOne )
6275
6376 with self .assertRaises (OperationalException ) as cm :
@@ -70,12 +83,11 @@ def test_validate_symbol_true_rejects_trading_symbol_as_target(self):
7083 )
7184
7285 error_msg = str (cm .exception )
73- self .assertIn ("EUR" , error_msg )
74- self .assertIn ("trading_symbol" , error_msg )
7586 self .assertIn ("EUR/EUR" , error_msg )
87+ self .assertIn ("trading_symbol" , error_msg )
7688
77- def test_validate_symbol_true_case_insensitive (self ):
78- """validate_symbol=True catches trading_symbol regardless of case ."""
89+ def test_rejects_target_equals_trading_symbol_case_insensitive (self ):
90+ """Case-insensitive check for target == trading_symbol ."""
7991 self .app .add_strategy (StrategyOne )
8092
8193 with self .assertRaises (OperationalException ):
@@ -87,10 +99,27 @@ def test_validate_symbol_true_case_insensitive(self):
8799 validate_symbol = True ,
88100 )
89101
90- def test_validate_symbol_true_accepts_valid_target (self ):
91- """validate_symbol=True passes for a target that differs
92- from trading_symbol."""
102+ def test_rejects_missing_data_source (self ):
103+ """Rejects when no data source is registered for the pair."""
104+ self .app .add_strategy (StrategyOne )
105+
106+ with self .assertRaises (OperationalException ) as cm :
107+ self .app .context .create_limit_order (
108+ target_symbol = "BTC" ,
109+ amount = 1 ,
110+ price = 10 ,
111+ order_side = OrderSide .BUY ,
112+ validate_symbol = True ,
113+ )
114+
115+ error_msg = str (cm .exception )
116+ self .assertIn ("BTC/EUR" , error_msg )
117+ self .assertIn ("No data source registered" , error_msg )
118+
119+ def test_accepts_with_matching_data_source (self ):
120+ """Passes when a data source exists for target/trading pair."""
93121 self .app .add_strategy (StrategyOne )
122+ self ._register_data_source ("BTC/EUR" )
94123
95124 self .app .context .create_limit_order (
96125 target_symbol = "BTC" ,
@@ -103,19 +132,42 @@ def test_validate_symbol_true_accepts_valid_target(self):
103132 order = order_repository .find ({"target_symbol" : "BTC" })
104133 self .assertIsNotNone (order )
105134
106- def test_validate_symbol_error_message (self ):
107- """Error message explains the EUR/EUR problem and how to skip ."""
135+ def test_rejects_wrong_data_source (self ):
136+ """Rejects when only a different pair is registered ."""
108137 self .app .add_strategy (StrategyOne )
138+ self ._register_data_source ("ETH/EUR" )
109139
110140 with self .assertRaises (OperationalException ) as cm :
111141 self .app .context .create_limit_order (
112- target_symbol = "EUR " ,
142+ target_symbol = "BTC " ,
113143 amount = 1 ,
114144 price = 10 ,
115145 order_side = OrderSide .BUY ,
116146 validate_symbol = True ,
117147 )
118148
119149 error_msg = str (cm .exception )
120- self .assertIn ("EUR/EUR" , error_msg )
150+ self .assertIn ("BTC/EUR" , error_msg )
151+ self .assertIn ("ETH/EUR" , error_msg )
152+
153+ def test_error_message_lists_registered_symbols (self ):
154+ """Error message includes registered data source symbols."""
155+ self .app .add_strategy (StrategyOne )
156+ self ._register_data_source ("BTC/EUR" )
157+ self ._register_data_source ("ETH/EUR" )
158+
159+ with self .assertRaises (OperationalException ) as cm :
160+ self .app .context .create_limit_order (
161+ target_symbol = "SOL" ,
162+ amount = 1 ,
163+ price = 10 ,
164+ order_side = OrderSide .BUY ,
165+ validate_symbol = True ,
166+ )
167+
168+ error_msg = str (cm .exception )
169+ self .assertIn ("SOL/EUR" , error_msg )
170+ self .assertIn ("BTC/EUR" , error_msg )
171+ self .assertIn ("ETH/EUR" , error_msg )
172+ self .assertIn ("validate_symbol=False" , error_msg )
121173 self .assertIn ("validate_symbol=False" , error_msg )
0 commit comments