Skip to content

Commit 2815fb9

Browse files
committed
[Trading] handle lazy markets loading exchanges trading
1 parent b5c7766 commit 2815fb9

12 files changed

Lines changed: 206 additions & 26 deletions

File tree

packages/tentacles/Trading/Mode/simple_market_making_trading_mode/api/core.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,25 @@ async def _fill_market_making_data_by_symbol(
107107
elif dependency.symbol not in dependency_symbol_alias_by_symbol:
108108
dependency_symbol_alias_by_symbol[dependency.symbol] = None
109109
available_symbols = set(exchange_manager.exchange.get_all_available_symbols(active_only=True))
110-
symbols_to_skip_ticker_fetch = {
111-
source.pair
112-
for source in price_sources
113-
if source.formula and source.pair not in available_symbols
114-
}
110+
lazy_load_markets = exchange_manager.exchange.get_option_value(
111+
octobot_trading.enums.ExchangeClientOptions.LAZY_LOAD_MARKETS
112+
)
113+
if lazy_load_markets:
114+
symbols_to_skip_ticker_fetch = {
115+
source.pair
116+
for source in price_sources
117+
if source.formula and source.pair not in available_symbols
118+
}
119+
else:
120+
symbols_to_skip_ticker_fetch = {
121+
symbol for symbol in (set(symbols) | set(dependency_symbol_alias_by_symbol.keys()))
122+
if symbol not in available_symbols
123+
}
124+
if symbols_to_skip_ticker_fetch:
125+
_get_logger().info(
126+
f"Ignored unavailable pairs on [{exchange_internal_name}]: "
127+
f"{sorted(symbols_to_skip_ticker_fetch)}"
128+
)
115129
symbols_to_fetch = (set(symbols) | set(dependency_symbol_alias_by_symbol.keys())) - symbols_to_skip_ticker_fetch
116130
tickers = {
117131
symbol: ticker
@@ -121,7 +135,11 @@ async def _fill_market_making_data_by_symbol(
121135
tickers, ticker_updater = await _fetch_tickers(
122136
exchange_manager, tickers, list(symbols_to_fetch)
123137
)
124-
if missing_tickers_to_fetch := [symbol for symbol in symbols_to_fetch if symbol not in tickers]:
138+
if missing_tickers_to_fetch := [
139+
symbol for symbol in symbols_to_fetch
140+
if symbol not in tickers
141+
and (lazy_load_markets or symbol in available_symbols)
142+
]:
125143
try:
126144
tickers.update(await ticker_updater.fetch_all_tickers(missing_tickers_to_fetch))
127145
except octobot_trading.errors.NotSupported as err:
@@ -694,6 +712,7 @@ async def _get_price_and_predicted_order_book(
694712
books_by_symbol = {}
695713
for pair, reference_price in reference_price_by_pair.items():
696714
if not reference_price or reference_price.is_nan():
715+
error_by_pair[pair] = _get_unsupported_pair_message(pair, mm_exchange)
697716
continue
698717
mm_data = mm_data_by_exchange[mm_exchange].get(pair)
699718
_adapt_volume_if_necessary(mm_data, reference_price)
@@ -742,6 +761,10 @@ def _get_missing_symbol_message(symbol: str, exchange: str) -> str:
742761
)
743762

744763

764+
def _get_unsupported_pair_message(pair: str, exchange: str) -> str:
765+
return f"{pair} is not supported on {exchange}"
766+
767+
745768
def _format_format_market_making_volume(volume: typing.Union[dict, None], error: typing.Union[str, None]):
746769
return {
747770
constants.VOLUME_KEY: volume,

packages/tentacles/Trading/Mode/simple_market_making_trading_mode/tests/api/test_core.py

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,7 @@ async def test_get_price_and_predicted_order_book_with_invalid_formula(profile_d
589589

590590

591591
async def test_get_price_and_predicted_order_book_with_error(profile_data_with_full_mm_config, mm_data_by_exchange):
592-
"""Test _get_price_and_predicted_order_book with error handling."""
592+
"""Empty reference price sources yield an unsupported-pair error."""
593593
profile_data_with_full_mm_config.tentacles[0].config[
594594
simple_market_making_trading.SimpleMarketMakingTradingMode.CONFIG_PAIR_SETTINGS
595595
][0][simple_market_making_trading.SimpleMarketMakingTradingMode.REFERENCE_PRICE] = []
@@ -602,7 +602,9 @@ async def test_get_price_and_predicted_order_book_with_error(profile_data_with_f
602602

603603
assert result == {
604604
"BTC/USDT": {
605-
market_making_constants.ERROR_KEY: "BTC/USDT reference price on binance can't be computed from the following price sources: {}"
605+
market_making_constants.ERROR_KEY: market_making_core._get_unsupported_pair_message(
606+
"BTC/USDT", "binance"
607+
),
606608
}
607609
}
608610

@@ -1059,21 +1061,42 @@ async def _mock_exchange_manager_context(exchange_manager):
10591061
yield exchange_manager
10601062

10611063

1064+
def _configure_lazy_load_markets(exchange_manager, lazy_load_markets: bool):
1065+
exchange_manager.exchange.get_option_value = mock.Mock(
1066+
side_effect=lambda option, **kwargs: (
1067+
lazy_load_markets
1068+
if option == trading_enums.ExchangeClientOptions.LAZY_LOAD_MARKETS
1069+
else False
1070+
)
1071+
)
1072+
1073+
1074+
def _get_symbols_passed_to_fetch_tickers(fetch_tickers_mock) -> set:
1075+
return {
1076+
symbol
1077+
for call_args in fetch_tickers_mock.call_args_list
1078+
for symbol in call_args.args[2]
1079+
}
1080+
1081+
10621082
async def _call_fill_market_making_data_by_symbol(
10631083
price_sources,
10641084
available_symbols,
10651085
formula_init_patches=None,
1086+
lazy_load_markets=True,
10661087
):
10671088
profile_data = _profile_data_for_market_making_fill()
10681089
mm_data_by_symbol_by_exchange = {}
10691090
exchange_manager = mock.Mock()
10701091
exchange_manager.exchange.get_all_available_symbols = mock.Mock(
10711092
return_value=available_symbols
10721093
)
1094+
_configure_lazy_load_markets(exchange_manager, lazy_load_markets)
10731095
ticker_updater = mock.Mock()
10741096
ticker_updater.fetch_all_tickers = mock.AsyncMock(return_value={})
10751097
ticker_cache = mock.Mock()
10761098
ticker_cache.get_all_tickers = mock.Mock(return_value={})
1099+
fetch_tickers_mock = mock.AsyncMock(return_value=({}, ticker_updater))
10771100

10781101
patches = [
10791102
mock.patch.object(
@@ -1099,7 +1122,7 @@ async def _call_fill_market_making_data_by_symbol(
10991122
mock.patch.object(
11001123
market_making_core,
11011124
"_fetch_tickers",
1102-
mock.AsyncMock(return_value=({}, ticker_updater)),
1125+
fetch_tickers_mock,
11031126
),
11041127
]
11051128
if formula_init_patches:
@@ -1118,7 +1141,10 @@ async def _call_fill_market_making_data_by_symbol(
11181141
auth=None,
11191142
)
11201143

1121-
return mm_data_by_symbol_by_exchange, ticker_updater.fetch_all_tickers
1144+
return mm_data_by_symbol_by_exchange, {
1145+
"fetch_tickers": fetch_tickers_mock,
1146+
"fetch_all_tickers": ticker_updater.fetch_all_tickers,
1147+
}
11221148

11231149

11241150
class TestFillMarketMakingDataBySymbol:
@@ -1141,12 +1167,13 @@ async def test_skips_ticker_fetch_for_unsupported_ref_price_symbol_with_formula(
11411167
mock.patch.object(exchange_operators, "create_ohlcv_operators", return_value=[]),
11421168
mock.patch.object(exchange_operators, "create_price_operators", return_value=[]),
11431169
]
1144-
mm_data_by_symbol_by_exchange, fetch_all_tickers_mock = await _call_fill_market_making_data_by_symbol(
1170+
mm_data_by_symbol_by_exchange, fetch_mocks = await _call_fill_market_making_data_by_symbol(
11451171
price_sources,
11461172
available_symbols={"BTC/ETH", "ETH/USDT"},
11471173
formula_init_patches=formula_init_patches,
11481174
)
11491175

1176+
fetch_all_tickers_mock = fetch_mocks["fetch_all_tickers"]
11501177
assert fetch_all_tickers_mock.call_count == 0 or all(
11511178
"BTC/USDT" not in call_args.args[0]
11521179
for call_args in fetch_all_tickers_mock.call_args_list
@@ -1189,12 +1216,13 @@ async def test_still_fetches_formula_dependency_symbols(self):
11891216
return_value=exchange_operators.create_price_operators(exchange_manager, "BTC/USDT"),
11901217
),
11911218
]
1192-
mm_data_by_symbol_by_exchange, fetch_all_tickers_mock = await _call_fill_market_making_data_by_symbol(
1219+
mm_data_by_symbol_by_exchange, fetch_mocks = await _call_fill_market_making_data_by_symbol(
11931220
price_sources,
11941221
available_symbols={"BTC/ETH", "ETH/USDT"},
11951222
formula_init_patches=formula_init_patches,
11961223
)
11971224

1225+
fetch_all_tickers_mock = fetch_mocks["fetch_all_tickers"]
11981226
assert fetch_all_tickers_mock.call_count == 1
11991227
fetched_symbols = fetch_all_tickers_mock.call_args.args[0]
12001228
assert "BTC/USDT" not in fetched_symbols
@@ -1203,6 +1231,7 @@ async def test_still_fetches_formula_dependency_symbols(self):
12031231
assert set(mm_data_by_symbol_by_exchange["binance"]) == {"BTC/USDT", "BTC/ETH", "ETH/USDT"}
12041232

12051233
async def test_fetches_ticker_for_unsupported_symbol_without_formula(self):
1234+
"""Lazy-load exchanges still fetch direct pairs not listed in available_symbols."""
12061235
price_sources = [
12071236
advanced_reference_price_import.AdvancedPriceSource(
12081237
exchange="binance",
@@ -1212,13 +1241,55 @@ async def test_fetches_ticker_for_unsupported_symbol_without_formula(self):
12121241
formula="",
12131242
)
12141243
]
1215-
mm_data_by_symbol_by_exchange, fetch_all_tickers_mock = await _call_fill_market_making_data_by_symbol(
1244+
mm_data_by_symbol_by_exchange, fetch_mocks = await _call_fill_market_making_data_by_symbol(
12161245
price_sources,
12171246
available_symbols={"BTC/ETH", "ETH/USDT"},
1247+
lazy_load_markets=True,
12181248
)
12191249

1220-
assert fetch_all_tickers_mock.call_count == 1
1221-
assert fetch_all_tickers_mock.call_args.args[0] == ["BTC/USDT"]
1250+
fetch_tickers_mock = fetch_mocks["fetch_tickers"]
1251+
assert fetch_tickers_mock.call_count == 1
1252+
assert fetch_tickers_mock.call_args.args[2] == ["BTC/USDT"]
1253+
1254+
async def test_skips_unavailable_direct_pair_on_cex(self):
1255+
price_sources = [
1256+
advanced_reference_price_import.AdvancedPriceSource(
1257+
exchange="binance",
1258+
pair="BTC/USDT",
1259+
time_frame=advanced_reference_price_import.DEFAULT_TIME_FRAME,
1260+
weight=decimal.Decimal("1.0"),
1261+
formula="",
1262+
)
1263+
]
1264+
mm_data_by_symbol_by_exchange, fetch_mocks = await _call_fill_market_making_data_by_symbol(
1265+
price_sources,
1266+
available_symbols={"BTC/ETH", "ETH/USDT"},
1267+
lazy_load_markets=False,
1268+
)
1269+
1270+
assert "BTC/USDT" not in _get_symbols_passed_to_fetch_tickers(fetch_mocks["fetch_tickers"])
1271+
btc_usdt_data = mm_data_by_symbol_by_exchange["binance"]["BTC/USDT"]
1272+
assert btc_usdt_data.price.is_nan()
1273+
1274+
async def test_still_fetches_unavailable_direct_pair_on_lazy_load_exchange(self):
1275+
price_sources = [
1276+
advanced_reference_price_import.AdvancedPriceSource(
1277+
exchange="binance",
1278+
pair="BTC/USDT",
1279+
time_frame=advanced_reference_price_import.DEFAULT_TIME_FRAME,
1280+
weight=decimal.Decimal("1.0"),
1281+
formula="",
1282+
)
1283+
]
1284+
_, fetch_mocks = await _call_fill_market_making_data_by_symbol(
1285+
price_sources,
1286+
available_symbols={"BTC/ETH", "ETH/USDT"},
1287+
lazy_load_markets=True,
1288+
)
1289+
1290+
fetch_tickers_mock = fetch_mocks["fetch_tickers"]
1291+
assert fetch_tickers_mock.call_count == 1
1292+
assert fetch_tickers_mock.call_args.args[2] == ["BTC/USDT"]
12221293

12231294

12241295
def _mock_create_price_operators(prices_by_symbol: dict[str, float]):

packages/trading/octobot_trading/exchanges/connectors/ccxt/ccxt_connector.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -651,12 +651,14 @@ async def get_price_ticker(self, symbol: str, **kwargs: dict) -> typing.Optional
651651
return self.adapter.adapt_ticker(
652652
await self.client.fetch_ticker(symbol, params=kwargs)
653653
)
654+
except ccxt.async_support.BadSymbol as err:
655+
raise octobot_trading.errors.UnSupportedSymbolError(str(err)) from err
654656
except ccxt.async_support.NotSupported:
655657
raise octobot_trading.errors.NotSupported
656658
except ccxt.async_support.BaseError as e:
657659
raise octobot_trading.errors.FailedRequest(
658660
f"Failed to get_price_ticker {html_util.get_html_summary_if_relevant(e)}"
659-
)
661+
) from e
660662

661663
@ccxt_client_util.converted_ccxt_common_errors
662664
async def get_all_currencies_price_ticker(

packages/trading/octobot_trading/exchanges/types/rest_exchange.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,21 @@ async def load_markets_for_symbols(self, symbols: list[str]) -> list[dict]:
582582
)
583583
return loaded_markets
584584

585+
def _is_lazy_market_loaded(self, symbol: str) -> bool:
586+
client_markets = self.connector.client.markets or {}
587+
return symbol in client_markets
588+
589+
async def ensure_lazy_market_loaded(self, symbol: str) -> None:
590+
if not self.lazy_load_markets():
591+
return
592+
if self._is_lazy_market_loaded(symbol):
593+
return
594+
await self.load_markets_for_symbols([symbol])
595+
596+
async def get_market_status_including_lazy_load(self, symbol, price_example=None, with_fixer=True):
597+
await self.ensure_lazy_market_loaded(symbol)
598+
return self.get_market_status(symbol, price_example=price_example, with_fixer=with_fixer)
599+
585600
def get_market_status(self, symbol, price_example=None, with_fixer=True):
586601
"""
587602
Override using get_fixed_market_status in exchange tentacle if the default market status is not as expected

packages/trading/octobot_trading/modes/modes_util.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ async def convert_with_market_or_limit_order(
195195

196196
# get order quantity
197197
quantity = _get_available_or_target_quantity(exchange_mgr, symbol, order_type, price, asset_amount)
198-
symbol_market = exchange_mgr.exchange.get_market_status(symbol, with_fixer=False)
198+
symbol_market = await exchange_mgr.exchange.get_market_status_including_lazy_load(symbol, with_fixer=False)
199199
created_orders = []
200200
for order_quantity, order_price in \
201201
trading_personal_data.decimal_check_and_adapt_order_details_if_necessary(

packages/trading/octobot_trading/modes/script_keywords/basic_keywords/price.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,5 @@ async def get_price_with_offset(context, offset_input, use_delta_type_as_flat_va
6363
f"1.2, -0.222, -0.222d, @65100, 5%, e5%, e500"
6464
)
6565

66-
symbol_market = context.exchange_manager.exchange.get_market_status(context.symbol, with_fixer=False)
66+
symbol_market = await context.exchange_manager.exchange.get_market_status_including_lazy_load(context.symbol, with_fixer=False)
6767
return personal_data.decimal_adapt_price(symbol_market, computed_price)

packages/trading/octobot_trading/personal_data/orders/order_factory.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ async def create_base_orders_and_associated_elements(
473473
current_price = await personal_data.get_up_to_date_price(
474474
self.exchange_manager, symbol=symbol, timeout=constants.ORDER_DATA_FETCHING_TIMEOUT
475475
)
476-
symbol_market = self.exchange_manager.exchange.get_market_status(symbol, with_fixer=False)
476+
symbol_market = await self.exchange_manager.exchange.get_market_status_including_lazy_load(symbol, with_fixer=False)
477477
ctx = script_keywords.get_base_context_from_exchange_manager(self.exchange_manager, symbol)
478478
# market orders have no price
479479
computed_price = current_price if price is None else await self._get_computed_price(ctx, price)

packages/trading/octobot_trading/personal_data/orders/order_util.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ async def get_pre_order_data(exchange_manager, symbol: str, timeout: int = None,
272272
portfolio_type=commons_constants.PORTFOLIO_AVAILABLE,
273273
target_price=None):
274274
price = target_price or await get_up_to_date_price(exchange_manager, symbol, timeout=timeout)
275-
symbol_market = exchange_manager.exchange.get_market_status(symbol, with_fixer=False)
275+
symbol_market = await exchange_manager.exchange.get_market_status_including_lazy_load(symbol, with_fixer=False)
276276
currency_available, market_available, market_quantity = get_portfolio_amounts(
277277
exchange_manager, symbol, price, portfolio_type=portfolio_type
278278
)

packages/trading/octobot_trading/personal_data/orders/types/limit/limit_order.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ async def update_price_if_outdated(self):
3434
try:
3535
current_price = await self.exchange_manager.exchange_symbols_data.get_exchange_symbol_data(self.symbol) \
3636
.prices_manager.get_mark_price(timeout=constants.CHAINED_ORDER_PRICE_FETCHING_TIMEOUT)
37-
self._update_limit_price_if_necessary(current_price)
37+
await self._update_limit_price_if_necessary(current_price)
3838
except asyncio.TimeoutError:
3939
# price can't be checked
4040
return
4141

42-
def _update_limit_price_if_necessary(self, current_price):
42+
async def _update_limit_price_if_necessary(self, current_price):
4343
updated_price = self.origin_price
4444
if self.side is enums.TradeOrderSide.BUY:
4545
highest_accepted_buy_price = (
@@ -58,7 +58,7 @@ def _update_limit_price_if_necessary(self, current_price):
5858
# => Increase it to the current price
5959
updated_price = lowest_accepted_sell_price
6060
if self.origin_price != updated_price:
61-
symbol_market = self.exchange_manager.exchange.get_market_status(self.symbol, with_fixer=False)
61+
symbol_market = await self.exchange_manager.exchange.get_market_status_including_lazy_load(self.symbol, with_fixer=False)
6262
self.origin_price = decimal_order_adapter.decimal_adapt_price(symbol_market, updated_price)
6363

6464
async def update_order_status(self, force_refresh=False):

0 commit comments

Comments
 (0)