From 40d5869336b3936f0fff521b98babe3fa85ae14f Mon Sep 17 00:00:00 2001 From: LimpidCrypto <97235361+LimpidCrypto@users.noreply.github.com> Date: Sat, 30 Apr 2022 19:43:28 +0200 Subject: [PATCH 1/3] initial commit --- xrpl_trading_bot/trading/__init__.py | 0 xrpl_trading_bot/trading/paths/__init__.py | 0 xrpl_trading_bot/trading/paths/main.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 xrpl_trading_bot/trading/__init__.py create mode 100644 xrpl_trading_bot/trading/paths/__init__.py create mode 100644 xrpl_trading_bot/trading/paths/main.py diff --git a/xrpl_trading_bot/trading/__init__.py b/xrpl_trading_bot/trading/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/xrpl_trading_bot/trading/paths/__init__.py b/xrpl_trading_bot/trading/paths/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/xrpl_trading_bot/trading/paths/main.py b/xrpl_trading_bot/trading/paths/main.py new file mode 100644 index 0000000..e69de29 From 66c72f7c8e9219c582eaca6b4b65f481aa4990b1 Mon Sep 17 00:00:00 2001 From: LimpidCrypto <97235361+LimpidCrypto@users.noreply.github.com> Date: Mon, 2 May 2022 18:31:44 +0200 Subject: [PATCH 2/3] Current state --- xrpl_trading_bot/clients/methods.py | 37 ++-- xrpl_trading_bot/main.py | 13 +- xrpl_trading_bot/trading/__init__.py | 3 + xrpl_trading_bot/trading/main.py | 7 + xrpl_trading_bot/trading/paths/__init__.py | 5 + xrpl_trading_bot/trading/paths/main.py | 21 ++ .../trading/paths/market_maker/__init__.py | 0 .../paths/spatial_arbitrage/__init__.py | 5 + .../trading/paths/spatial_arbitrage/main.py | 188 ++++++++++++++++++ .../paths/triangular_arbitrage/__init__.py | 0 xrpl_trading_bot/trading/paths/utils.py | 3 + 11 files changed, 257 insertions(+), 25 deletions(-) create mode 100644 xrpl_trading_bot/trading/main.py create mode 100644 xrpl_trading_bot/trading/paths/market_maker/__init__.py create mode 100644 xrpl_trading_bot/trading/paths/spatial_arbitrage/__init__.py create mode 100644 xrpl_trading_bot/trading/paths/spatial_arbitrage/main.py create mode 100644 xrpl_trading_bot/trading/paths/triangular_arbitrage/__init__.py create mode 100644 xrpl_trading_bot/trading/paths/utils.py diff --git a/xrpl_trading_bot/clients/methods.py b/xrpl_trading_bot/clients/methods.py index c1e1f8f..cc7a940 100644 --- a/xrpl_trading_bot/clients/methods.py +++ b/xrpl_trading_bot/clients/methods.py @@ -93,7 +93,7 @@ def subscribe_to_order_books( all_order_books: All order books. subscribe_books: - Max. 10 subscribe books. + Max. 10 SubscribeBook objects. """ assert len(subscribe_books) <= 10 responses = run( @@ -135,24 +135,21 @@ def subscribe_to_order_books( with WebsocketClient(url=NonFullHistoryNodes.LIMPIDCRYPTO) as client: client.send(Subscribe(books=subscribe_books)) for message in client: - try: - # This client should receive every transaction without snapshot. - if is_order_book(message=message): - continue - else: - for currency_pair in all_subscription_book_currency_pairs: - order_book = all_order_books.get_order_book( - currency_pair=currency_pair - ) - all_order_books.set_order_book( - order_book=OrderBook.from_parser_result( - result=parse_final_order_book( - asks=order_book.asks, - bids=order_book.bids, - transaction=cast(SubscriptionRawTxnType, message), - to_xrp=True, - ) + if is_order_book(message=message): + continue + else: + for currency_pair in all_subscription_book_currency_pairs: + order_book = all_order_books.get_order_book( + currency_pair=currency_pair + ) + all_order_books.set_order_book( + order_book=OrderBook.from_parser_result( + result=parse_final_order_book( + asks=order_book.asks, + bids=order_book.bids, + transaction=cast(SubscriptionRawTxnType, message), + to_xrp=True, ) ) - except ConnectionClosedError: - return None + ) + return None diff --git a/xrpl_trading_bot/main.py b/xrpl_trading_bot/main.py index bcee68c..0100330 100644 --- a/xrpl_trading_bot/main.py +++ b/xrpl_trading_bot/main.py @@ -4,14 +4,17 @@ from threading import Thread -from xrpl_trading_bot.clients import subscribe_to_account_balances -from xrpl_trading_bot.globals import WALLET +from xrpl_trading_bot.clients import ( + subscribe_to_account_balances, + subscribe_to_order_books +) +from xrpl_trading_bot.globals import WALLET, all_order_books if __name__ == "__main__": - balances_subscribtion = Thread( + balances_subscription = Thread( target=subscribe_to_account_balances, args=(WALLET,), ) - balances_subscribtion.start() + balances_subscription.start() - balances_subscribtion.join() + balances_subscription.join() diff --git a/xrpl_trading_bot/trading/__init__.py b/xrpl_trading_bot/trading/__init__.py index e69de29..3cd6ef7 100644 --- a/xrpl_trading_bot/trading/__init__.py +++ b/xrpl_trading_bot/trading/__init__.py @@ -0,0 +1,3 @@ +from xrpl_trading_bot.trading.main import trade + +__all__ = ["trade"] diff --git a/xrpl_trading_bot/trading/main.py b/xrpl_trading_bot/trading/main.py new file mode 100644 index 0000000..71141d4 --- /dev/null +++ b/xrpl_trading_bot/trading/main.py @@ -0,0 +1,7 @@ +from xrpl_trading_bot.order_books.main import OrderBooks +from xrpl_trading_bot.trading.paths import build_trading_paths +from xrpl_trading_bot.wallet.main import XRPWallet + + +def trade(wallet: XRPWallet, order_books: OrderBooks): + print(build_trading_paths(wallet=wallet, order_books=order_books)) diff --git a/xrpl_trading_bot/trading/paths/__init__.py b/xrpl_trading_bot/trading/paths/__init__.py index e69de29..a68fbf1 100644 --- a/xrpl_trading_bot/trading/paths/__init__.py +++ b/xrpl_trading_bot/trading/paths/__init__.py @@ -0,0 +1,5 @@ +from xrpl_trading_bot.trading.paths.main import build_trading_paths + +__all__ = [ + "build_trading_paths" +] diff --git a/xrpl_trading_bot/trading/paths/main.py b/xrpl_trading_bot/trading/paths/main.py index e69de29..4ecb8ed 100644 --- a/xrpl_trading_bot/trading/paths/main.py +++ b/xrpl_trading_bot/trading/paths/main.py @@ -0,0 +1,21 @@ +from xrpl_trading_bot.order_books.main import OrderBooks +from xrpl_trading_bot.trading.paths.spatial_arbitrage import ( + build_spatial_arbitrage_trading_paths +) +from xrpl_trading_bot.wallet.main import XRPWallet + + +def build_trading_paths(wallet: XRPWallet, order_books: OrderBooks): + liquid_order_books = order_books.get_liquid_order_books() + illiquid_order_books = order_books.get_illiquid_order_books() + + paths = set() + + paths.update( + build_spatial_arbitrage_trading_paths( + liquid_order_books=liquid_order_books, + wallet=wallet + ) + ) + + return paths diff --git a/xrpl_trading_bot/trading/paths/market_maker/__init__.py b/xrpl_trading_bot/trading/paths/market_maker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/xrpl_trading_bot/trading/paths/spatial_arbitrage/__init__.py b/xrpl_trading_bot/trading/paths/spatial_arbitrage/__init__.py new file mode 100644 index 0000000..2d2defe --- /dev/null +++ b/xrpl_trading_bot/trading/paths/spatial_arbitrage/__init__.py @@ -0,0 +1,5 @@ +from xrpl_trading_bot.trading.paths.spatial_arbitrage.main import ( + build_spatial_arbitrage_trading_paths +) + +__all__ = ["build_spatial_arbitrage_trading_paths"] diff --git a/xrpl_trading_bot/trading/paths/spatial_arbitrage/main.py b/xrpl_trading_bot/trading/paths/spatial_arbitrage/main.py new file mode 100644 index 0000000..cecd71c --- /dev/null +++ b/xrpl_trading_bot/trading/paths/spatial_arbitrage/main.py @@ -0,0 +1,188 @@ +"""Parse the trading paths for spatial arbitrage.""" + +from __future__ import annotations +from decimal import Decimal + +from typing import List + +from xrpl_trading_bot.order_books import OrderBook +from xrpl_trading_bot.txn_parser.utils.types import CURRENCY_AMOUNT_TYPE +from xrpl_trading_bot.wallet import XRPWallet + + +def parse_currency_amount(currency_amount: CURRENCY_AMOUNT_TYPE, value: Decimal): + if isinstance(currency_amount, dict): + currency_amount["value"] = str(value) + else: + currency_amount = str(value) + return currency_amount + + +def build_path( + first_step_offer, + second_step_offer, + first_step_counter: str, + second_step_counter: str, + wallet: XRPWallet +): + first_step_taker_gets = first_step_offer["TakerPays"] + first_step_taker_pays = first_step_offer["TakerGets"] + second_step_taker_gets = second_step_offer["TakerPays"] + second_step_taker_pays = second_step_offer["TakerGets"] + first_step_taker_gets_balance = Decimal( + wallet.balances[first_step_counter] + ) + second_step_taker_gets_balance = Decimal( + wallet.balances[second_step_counter] + ) + first_step_quality = Decimal( + first_step_offer["quality"] + ) + second_step_quality = Decimal( + second_step_offer["quality"] + ) + # adjust values + if first_step_taker_gets_balance <= second_step_taker_gets_balance: + # adjust all values based of `first_step_taker_gets_balance` + first_step_taker_gets_value = first_step_taker_gets_balance + first_step_taker_pays_value = ( + first_step_taker_gets_value / first_step_quality * 0.999 + ) + second_step_taker_gets_value = first_step_taker_pays_value + second_step_taker_pays_value = ( + second_step_taker_gets_value / second_step_quality * 0.999 + ) + else: + # adjust all values based of `second_step_taker_gets_balance` + second_step_taker_gets_value = second_step_taker_gets_balance + second_step_taker_pays_value = ( + second_step_taker_gets_value / second_step_quality * 0.999 + ) + first_step_taker_pays_value = ( + second_step_taker_gets_value * 0.999 + ) + first_step_taker_gets_value = ( + first_step_taker_pays_value * first_step_taker_pays_value + ) + first_step_taker_gets = parse_currency_amount( + currency_amount=first_step_taker_gets, + value=first_step_taker_gets_value + ) + first_step_taker_pays = parse_currency_amount( + currency_amount=first_step_taker_pays, + value=first_step_taker_pays_value + ) + second_step_taker_gets = parse_currency_amount( + currency_amount=second_step_taker_gets, + value=second_step_taker_gets_value + ) + second_step_taker_pays = parse_currency_amount( + currency_amount=second_step_taker_pays, + value=second_step_taker_pays_value + ) + return { + "first_taker_gets": first_step_taker_gets, + "first_taker_pays": first_step_taker_pays, + "second_taker_gets": second_step_taker_gets, + "second_taker_pays": second_step_taker_pays, + } + + +def build_spatial_arbitrage_trading_paths( + liquid_order_books: List[OrderBook], + wallet: XRPWallet +): + paths = set() + for first_path_step_order_book in liquid_order_books: + for second_path_step_order_book in liquid_order_books: + if ( + first_path_step_order_book.currency_pair + != second_path_step_order_book.currency_pair + ): + # derive all parts from currency pair + first_step_base, first_step_counter = ( + first_path_step_order_book.currency_pair.split("/") + ) + first_step_base_currency, first_step_base_issuer = ( + first_step_base.split(".") + ) + first_step_counter_currency, first_step_counter_issuer = ( + first_step_counter.split(".") + ) + second_step_base, second_step_counter = ( + second_path_step_order_book.currency_pair.split("/") + ) + second_step_base_currency, second_step_base_issuer = ( + second_step_base.split(".") + ) + second_step_counter_currency, second_step_counter_issuer = ( + second_step_counter.split(".") + ) + if ( + first_step_counter_currency == second_step_base_currency + and first_step_base_currency == second_step_counter_currency + ): + # Example: EUR/USD and USD/EUR + # two buy offers + # two ask offers need to be consumed + first_step_offer = first_path_step_order_book.asks[0] + second_step_offer = second_path_step_order_book.asks[0] + paths.add( + build_path( + first_step_offer=first_step_offer, + second_step_offer=second_step_offer, + first_step_counter=first_step_counter, + second_step_counter=second_step_counter, + wallet=wallet + ) + ) + # Example: USD/EUR EUR/USD + # two sell offers + # two bid offers need to be consumed + first_step_offer = first_path_step_order_book.bids[0] + second_step_offer = second_path_step_order_book.bids[0] + paths.add( + build_path( + first_step_offer=first_step_offer, + second_step_offer=second_step_offer, + first_step_counter=first_step_counter, + second_step_counter=second_step_counter, + wallet=wallet + ) + ) + elif ( + first_step_counter_currency == second_step_counter_currency + and first_step_base_currency == second_step_base_currency + ): + # Example: EUR/USD EUR/USD + # first one buy and second one sell offer + # one ask and one bid offer need to be consumed + first_step_offer = first_path_step_order_book.asks[0] + second_step_offer = second_path_step_order_book.bids[0] + paths.add( + build_path( + first_step_offer=first_step_offer, + second_step_offer=second_step_offer, + first_step_counter=first_step_counter, + second_step_counter=second_step_counter, + wallet=wallet + ) + ) + # Example: USD/EUR USD/EUR + # first one sell and second one buy offer + # one ask and one bid offer need to be consumed + first_step_offer = first_path_step_order_book.bids[0] + second_step_offer = second_path_step_order_book.asks[0] + paths.add( + build_path( + first_step_offer=first_step_offer, + second_step_offer=second_step_offer, + first_step_counter=first_step_counter, + second_step_counter=second_step_counter, + wallet=wallet + ) + ) + else: + pass + else: + pass diff --git a/xrpl_trading_bot/trading/paths/triangular_arbitrage/__init__.py b/xrpl_trading_bot/trading/paths/triangular_arbitrage/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/xrpl_trading_bot/trading/paths/utils.py b/xrpl_trading_bot/trading/paths/utils.py new file mode 100644 index 0000000..dbcb470 --- /dev/null +++ b/xrpl_trading_bot/trading/paths/utils.py @@ -0,0 +1,3 @@ +"""Utils for parsing trading paths.""" + +from __future__ import annotations From 088324ba23a83a7eb6697f2116622fa7e41874c8 Mon Sep 17 00:00:00 2001 From: LimpidCrypto <97235361+LimpidCrypto@users.noreply.github.com> Date: Wed, 18 May 2022 15:40:45 +0200 Subject: [PATCH 3/3] Fix order book subscription; spatial arbitrage paths --- xrpl_trading_bot/clients/methods.py | 91 +++-- xrpl_trading_bot/main.py | 24 ++ xrpl_trading_bot/order_books/__init__.py | 8 +- xrpl_trading_bot/order_books/main.py | 137 +++++++- xrpl_trading_bot/trading/main.py | 22 +- xrpl_trading_bot/trading/paths/main.py | 6 +- .../trading/paths/spatial_arbitrage/main.py | 310 +++++++++++------- .../txn_parser/order_book_changes.py | 16 +- .../utils/order_book_changes_utils.py | 141 ++++---- 9 files changed, 517 insertions(+), 238 deletions(-) diff --git a/xrpl_trading_bot/clients/methods.py b/xrpl_trading_bot/clients/methods.py index cc7a940..3705667 100644 --- a/xrpl_trading_bot/clients/methods.py +++ b/xrpl_trading_bot/clients/methods.py @@ -1,4 +1,5 @@ from asyncio import run +from decimal import Decimal from typing import Dict, List, cast from websockets.exceptions import ConnectionClosedError @@ -81,6 +82,54 @@ def subscribe_to_account_balances(wallet: XRPWallet) -> None: return None +def _get_snapshots_once( + subscribe_books: List[SubscribeBook] +): + responses = run( + xrp_request_async( + requests=[Subscribe(books=[book]) for book in subscribe_books], + uri=FullHistoryNodes.XRPLF, + ) + ) + assert len(responses) == len(subscribe_books) + assert all([response.is_successful() for response in responses]) + order_books = [ + OrderBook.from_response(response=response) + for response in responses if response.result["asks"] or response.result["bids"] + ] + successful_currency_pairs = set( + order_book.currency_pair for order_book in order_books + ) + subscribe_books_currency_pairs = [] + for book in subscribe_books: + base = f"{book.taker_pays.currency}.{book.taker_pays.issuer}" if ( + isinstance(book.taker_pays, IssuedCurrency) + ) else "XRP" + counter = f"{book.taker_gets.currency}.{book.taker_gets.issuer}" if ( + isinstance(book.taker_gets, IssuedCurrency) + ) else "XRP" + currency_pair = f"{base}/{counter}" + subscribe_books_currency_pairs.append(currency_pair) + currency_pairs_diff = set(subscribe_books_currency_pairs).difference( + successful_currency_pairs + ) + order_books.extend( + [ + OrderBook( + asks=[], + bids=[], + currency_pair=pair, + exchange_rate=Decimal(0), + spread=Decimal(0) + ) + for pair in currency_pairs_diff + ] + ) + assert len(order_books) == len(subscribe_books) + + return order_books + + def subscribe_to_order_books( all_order_books: OrderBooks, # future 'AllOrderBooks' class subscribe_books: List[SubscribeBook], @@ -96,26 +145,9 @@ def subscribe_to_order_books( Max. 10 SubscribeBook objects. """ assert len(subscribe_books) <= 10 - responses = run( - xrp_request_async( - requests=[Subscribe(books=[book]) for book in subscribe_books], - uri=FullHistoryNodes.XRPLF, - ) + order_books = _get_snapshots_once( + subscribe_books=subscribe_books, ) - if not all([response.is_successful() for response in responses]): - return None - final_order_books = [ - parse_final_order_book( - asks=response.result["asks"], - bids=response.result["bids"], - transaction=None, - to_xrp=True, - ) - for response in responses - ] - order_books = [ - OrderBook.from_parser_result(result=book) for book in final_order_books - ] for order_book in order_books: all_order_books.set_order_book(order_book=order_book) @@ -138,18 +170,9 @@ def subscribe_to_order_books( if is_order_book(message=message): continue else: - for currency_pair in all_subscription_book_currency_pairs: - order_book = all_order_books.get_order_book( - currency_pair=currency_pair - ) - all_order_books.set_order_book( - order_book=OrderBook.from_parser_result( - result=parse_final_order_book( - asks=order_book.asks, - bids=order_book.bids, - transaction=cast(SubscriptionRawTxnType, message), - to_xrp=True, - ) - ) - ) - return None + parse_final_order_book( + all_order_books=all_order_books, + transaction=message, + to_xrp=True + ) + return subscribe_books diff --git a/xrpl_trading_bot/main.py b/xrpl_trading_bot/main.py index 0100330..8ed86d4 100644 --- a/xrpl_trading_bot/main.py +++ b/xrpl_trading_bot/main.py @@ -3,12 +3,17 @@ from __future__ import annotations from threading import Thread +from time import sleep +from typing import List from xrpl_trading_bot.clients import ( subscribe_to_account_balances, subscribe_to_order_books ) from xrpl_trading_bot.globals import WALLET, all_order_books +from xrpl_trading_bot.order_books import build_subscibe_books +from xrpl_trading_bot.trading import trade + if __name__ == "__main__": balances_subscription = Thread( @@ -16,5 +21,24 @@ args=(WALLET,), ) balances_subscription.start() + sleep(3) + subscribe_books = build_subscibe_books(wallet=WALLET) + subscribe_book_threads: List[Thread] = [] + for chunk in subscribe_books: + subscribe_book_threads.append( + Thread(target=subscribe_to_order_books, args=(all_order_books, chunk,)) + ) + for num, thread in enumerate(subscribe_book_threads): + thread.start() + if num % 5 == 0: + sleep(10) + trade_thread = Thread( + target=trade, + args=(WALLET, all_order_books,) + ) + trade_thread.start() + trade_thread.join() + for thread in subscribe_book_threads: + thread.join() balances_subscription.join() diff --git a/xrpl_trading_bot/order_books/__init__.py b/xrpl_trading_bot/order_books/__init__.py index 222acad..1d7078b 100644 --- a/xrpl_trading_bot/order_books/__init__.py +++ b/xrpl_trading_bot/order_books/__init__.py @@ -2,6 +2,12 @@ OrderBook, OrderBookNotFoundException, OrderBooks, + build_subscibe_books, ) -__all__ = ["OrderBook", "OrderBooks", "OrderBookNotFoundException"] +__all__ = [ + "OrderBook", + "OrderBooks", + "OrderBookNotFoundException", + "build_subscibe_books" +] diff --git a/xrpl_trading_bot/order_books/main.py b/xrpl_trading_bot/order_books/main.py index aa189c7..6787882 100644 --- a/xrpl_trading_bot/order_books/main.py +++ b/xrpl_trading_bot/order_books/main.py @@ -3,13 +3,121 @@ from dataclasses import dataclass from decimal import Decimal from typing import Any, Dict, List, cast +from xrpl import XRPLException + +from xrpl.models import IssuedCurrency, XRP, Response +from xrpl.models.requests.subscribe import SubscribeBook from xrpl_trading_bot.txn_parser.utils.types import ORDER_BOOK_SIDE_TYPE +from xrpl_trading_bot.wallet.main import XRPWallet LIQUID_ORDER_BOOK_LIMIT = 1 -class OrderBookNotFoundException(BaseException): +def build_subscibe_books(wallet: XRPWallet) -> List[List[SubscribeBook]]: + def chunks(lst: List[SubscribeBook], n: int): + """Yield successive n-sized chunks from lst.""" + for i in range(0, len(lst), n): + yield lst[i:i + n] + + balances = wallet.balances + subscribe_books = [] + for taker_gets_token in balances: + for taker_pays_token in balances: + if taker_gets_token != taker_pays_token: + if taker_gets_token != "XRP": + taker_gets_currency, taker_gets_issuer = taker_gets_token.split(".") + else: + taker_gets_currency = taker_gets_token + if taker_pays_token != "XRP": + taker_pays_currency, taker_pays_issuer = taker_pays_token.split(".") + else: + taker_pays_currency = taker_pays_token + + subscribe_book = SubscribeBook( + taker_gets=IssuedCurrency( + currency=taker_gets_currency, + issuer=taker_gets_issuer + ) + if taker_gets_currency != "XRP" + else XRP(), + taker_pays=IssuedCurrency( + currency=taker_pays_currency, + issuer=taker_pays_issuer + ) + if taker_pays_currency != "XRP" + else XRP(), + taker=wallet.classic_address, + both=True, + snapshot=True + ) + + flipped_subscribe_book = SubscribeBook( + taker_pays=IssuedCurrency( + currency=taker_gets_currency, + issuer=taker_gets_issuer + ) + if taker_gets_currency != "XRP" + else XRP(), + taker_gets=IssuedCurrency( + currency=taker_pays_currency, + issuer=taker_pays_issuer + ) + if taker_pays_currency != "XRP" + else XRP(), + taker=wallet.classic_address, + both=True, + snapshot=True + ) + + if flipped_subscribe_book not in subscribe_books: + subscribe_books.append(subscribe_book) + + return list(chunks(subscribe_books, 10)) + + +def derive_currency_pair(asks: ORDER_BOOK_SIDE_TYPE, bids: ORDER_BOOK_SIDE_TYPE) -> str: + """ + Derives the currency pair from an order book. + + Args: + asks: Ask side of an order book. + bids: Bid side of an order book. + + Returns: + The order books currency pair. + """ + if bids: + bid = bids[0] + base = ( + f"{bid['TakerPays']['currency']}.{bid['TakerPays']['issuer']}" + if (isinstance(bid["TakerPays"], dict)) + else "XRP" + ) + counter = ( + f"{bid['TakerGets']['currency']}.{bid['TakerGets']['issuer']}" + if (isinstance(bid["TakerGets"], dict)) + else "XRP" + ) + return f"{base}/{counter}" + elif asks: + ask = asks[0] + base = ( + f"{ask['TakerGets']['currency']}.{ask['TakerGets']['issuer']}" + if (isinstance(ask["TakerGets"], dict)) + else "XRP" + ) + counter = ( + f"{ask['TakerPays']['currency']}.{ask['TakerPays']['issuer']}" + if (isinstance(ask["TakerPays"], dict)) + else "XRP" + ) + return f"{base}/{counter}" + else: + raise XRPLException("Cannot derive currency pair because order book is empty.") + + +class OrderBookNotFoundException(AttributeError): """Gets raised if a requested order book could not be found.""" pass @@ -36,7 +144,7 @@ def is_liquid(self: OrderBook) -> bool: Returns: If an order book is liquid or not. """ - return self.spread < LIQUID_ORDER_BOOK_LIMIT + return self.spread < LIQUID_ORDER_BOOK_LIMIT and self.spread > Decimal(0) @classmethod def from_parser_result(cls, result: Dict[str, Any]) -> OrderBook: @@ -48,6 +156,21 @@ def from_parser_result(cls, result: Dict[str, Any]) -> OrderBook: spread=result["spread"], ) + @classmethod + def from_response(cls, response: Response): + assert response.is_successful() + result = response.result + return cls( + asks=result["asks"], + bids=result["bids"], + currency_pair=derive_currency_pair( + asks=result["asks"], + bids=result["bids"] + ), + exchange_rate=Decimal(0), + spread=Decimal(0), + ) + class OrderBooks: def set_order_book(self: OrderBooks, order_book: OrderBook) -> None: @@ -87,9 +210,13 @@ def get_order_book(self: OrderBooks, currency_pair: str) -> OrderBook: try: return cast(OrderBook, self.__getattribute__(currency_pair)) except AttributeError: - raise OrderBookNotFoundException( - "The requested order book could not be found." - ) + try: + base, counter = currency_pair.split("/") + return cast(OrderBook, self.__getattribute__(f"{counter}/{base}")) + except AttributeError: + raise OrderBookNotFoundException( + "The requested order book could not be found." + ) def get_all_order_books(self: OrderBooks) -> List[OrderBook]: """ diff --git a/xrpl_trading_bot/trading/main.py b/xrpl_trading_bot/trading/main.py index 71141d4..4039ca8 100644 --- a/xrpl_trading_bot/trading/main.py +++ b/xrpl_trading_bot/trading/main.py @@ -1,7 +1,27 @@ +from decimal import Decimal +from time import sleep from xrpl_trading_bot.order_books.main import OrderBooks from xrpl_trading_bot.trading.paths import build_trading_paths from xrpl_trading_bot.wallet.main import XRPWallet def trade(wallet: XRPWallet, order_books: OrderBooks): - print(build_trading_paths(wallet=wallet, order_books=order_books)) + while True: + trade_paths = build_trading_paths(wallet=wallet, order_books=order_books) + profitable_paths = [] + for path in trade_paths: + first_taker_gets = path["first_taker_gets"] + second_taker_pays = path["second_taker_pays"] + first_taker_gets_value = Decimal( + first_taker_gets["value"] + if isinstance(first_taker_gets, dict) + else first_taker_gets + ) + second_taker_pays_value = Decimal( + second_taker_pays["value"] + if isinstance(second_taker_pays, dict) + else second_taker_pays + ) + if second_taker_pays_value > first_taker_gets_value: + profitable_paths.append(path) + sleep(3) diff --git a/xrpl_trading_bot/trading/paths/main.py b/xrpl_trading_bot/trading/paths/main.py index 4ecb8ed..7782149 100644 --- a/xrpl_trading_bot/trading/paths/main.py +++ b/xrpl_trading_bot/trading/paths/main.py @@ -9,13 +9,13 @@ def build_trading_paths(wallet: XRPWallet, order_books: OrderBooks): liquid_order_books = order_books.get_liquid_order_books() illiquid_order_books = order_books.get_illiquid_order_books() - paths = set() + paths = [] - paths.update( + paths.extend( build_spatial_arbitrage_trading_paths( liquid_order_books=liquid_order_books, wallet=wallet ) ) - return paths + return tuple(paths) diff --git a/xrpl_trading_bot/trading/paths/spatial_arbitrage/main.py b/xrpl_trading_bot/trading/paths/spatial_arbitrage/main.py index cecd71c..5c59b2d 100644 --- a/xrpl_trading_bot/trading/paths/spatial_arbitrage/main.py +++ b/xrpl_trading_bot/trading/paths/spatial_arbitrage/main.py @@ -2,15 +2,16 @@ from __future__ import annotations from decimal import Decimal +from itertools import combinations -from typing import List +from typing import List, Literal from xrpl_trading_bot.order_books import OrderBook from xrpl_trading_bot.txn_parser.utils.types import CURRENCY_AMOUNT_TYPE from xrpl_trading_bot.wallet import XRPWallet -def parse_currency_amount(currency_amount: CURRENCY_AMOUNT_TYPE, value: Decimal): +def set_currency_amount(currency_amount: CURRENCY_AMOUNT_TYPE, value: Decimal): if isinstance(currency_amount, dict): currency_amount["value"] = str(value) else: @@ -18,65 +19,123 @@ def parse_currency_amount(currency_amount: CURRENCY_AMOUNT_TYPE, value: Decimal) return currency_amount -def build_path( +def parse_currency_from_currency_amount(currency_amount: CURRENCY_AMOUNT_TYPE) -> str: + if isinstance(currency_amount, dict): + return f"{currency_amount['currency']}.{currency_amount['issuer']}" + else: + return "XRP" + + +def adjust_values( first_step_offer, second_step_offer, - first_step_counter: str, - second_step_counter: str, - wallet: XRPWallet + wallet: XRPWallet, + to_be_consumed_example_pair: Literal[ + "XRP/USD USD/XRP", + "USD/XRP XRP/USD", + "XRP/USD XRP/USD", + "USD/XRP USD/XRP" + ] ): + # to_be_consumed_example_pair is to arbitrage XRP first_step_taker_gets = first_step_offer["TakerPays"] first_step_taker_pays = first_step_offer["TakerGets"] second_step_taker_gets = second_step_offer["TakerPays"] second_step_taker_pays = second_step_offer["TakerGets"] - first_step_taker_gets_balance = Decimal( - wallet.balances[first_step_counter] - ) - second_step_taker_gets_balance = Decimal( - wallet.balances[second_step_counter] - ) first_step_quality = Decimal( first_step_offer["quality"] ) second_step_quality = Decimal( second_step_offer["quality"] ) - # adjust values - if first_step_taker_gets_balance <= second_step_taker_gets_balance: - # adjust all values based of `first_step_taker_gets_balance` - first_step_taker_gets_value = first_step_taker_gets_balance - first_step_taker_pays_value = ( - first_step_taker_gets_value / first_step_quality * 0.999 - ) - second_step_taker_gets_value = first_step_taker_pays_value - second_step_taker_pays_value = ( - second_step_taker_gets_value / second_step_quality * 0.999 - ) - else: - # adjust all values based of `second_step_taker_gets_balance` + first_step_taker_gets_balance = Decimal( + wallet.balances[ + parse_currency_from_currency_amount( + currency_amount=first_step_taker_gets + ) + ] + ) + second_step_taker_gets_balance = Decimal( + wallet.balances[ + parse_currency_from_currency_amount( + currency_amount=second_step_taker_gets + ) + ] + ) + first_step_taker_gets_value = None + first_step_taker_pays_value = None + second_step_taker_gets_value = None + second_step_taker_pays_value = None + if to_be_consumed_example_pair == "XRP/USD USD/XRP": + # two constructed bids + first_step_direction = "bid" + second_step_direction = "bid" + if to_be_consumed_example_pair == "USD/XRP XRP/USD": + # two constructed asks + first_step_direction = "ask" + second_step_direction = "ask" + if to_be_consumed_example_pair == "XRP/USD XRP/USD": + # first constructed bid, second constructed ask + first_step_direction = "bid" + second_step_direction = "ask" + if to_be_consumed_example_pair == "USD/XRP USD/XRP": + # first constructed ask, second constructed bid + first_step_direction = "ask" + second_step_direction = "bid" + first_step_taker_gets_value = first_step_taker_gets_balance + first_step_taker_pays_value = ( + first_step_taker_gets_value + * first_step_quality + * Decimal(0.9999) + ) if first_step_direction == "bid" else ( + first_step_taker_gets_value + / first_step_quality + * Decimal(0.9999) + ) + second_step_taker_gets_value = first_step_taker_pays_value + second_step_taker_pays_value = ( + second_step_taker_gets_value + * second_step_quality + * Decimal(0.9999) + ) if second_step_direction == "bid" else ( + second_step_taker_gets_value + / second_step_quality + * Decimal(0.9999) + ) + if second_step_taker_gets_value > second_step_taker_gets_balance: second_step_taker_gets_value = second_step_taker_gets_balance second_step_taker_pays_value = ( - second_step_taker_gets_value / second_step_quality * 0.999 - ) - first_step_taker_pays_value = ( - second_step_taker_gets_value * 0.999 + second_step_taker_gets_value + * second_step_quality + * Decimal(0.9999) + ) if second_step_direction == "bid" else ( + second_step_taker_gets_value + / second_step_quality + * Decimal(0.9999) ) + first_step_taker_pays_value = second_step_taker_gets_value first_step_taker_gets_value = ( - first_step_taker_pays_value * first_step_taker_pays_value + first_step_taker_pays_value + / first_step_quality + * Decimal(0.9999) + ) if first_step_direction == "bid" else ( + first_step_taker_pays_value + * first_step_quality + * Decimal(0.9999) ) - first_step_taker_gets = parse_currency_amount( + first_step_taker_gets = set_currency_amount( currency_amount=first_step_taker_gets, value=first_step_taker_gets_value ) - first_step_taker_pays = parse_currency_amount( + first_step_taker_pays = set_currency_amount( currency_amount=first_step_taker_pays, value=first_step_taker_pays_value ) - second_step_taker_gets = parse_currency_amount( + second_step_taker_gets = set_currency_amount( currency_amount=second_step_taker_gets, value=second_step_taker_gets_value ) - second_step_taker_pays = parse_currency_amount( + second_step_taker_pays = set_currency_amount( currency_amount=second_step_taker_pays, value=second_step_taker_pays_value ) @@ -92,97 +151,108 @@ def build_spatial_arbitrage_trading_paths( liquid_order_books: List[OrderBook], wallet: XRPWallet ): - paths = set() - for first_path_step_order_book in liquid_order_books: - for second_path_step_order_book in liquid_order_books: + paths = [] + possible_paths = combinations(liquid_order_books, 2) + for path in possible_paths: + first_path_step_order_book = path[0] + second_path_step_order_book = path[1] + if ( + first_path_step_order_book.currency_pair + != second_path_step_order_book.currency_pair + ): + # derive all parts from currency pair + first_step_base, first_step_counter = ( + first_path_step_order_book.currency_pair.split("/") + ) + first_step_base_currency = ( + first_step_base.split(".") + )[0] + first_step_counter_currency = ( + first_step_counter.split(".") + )[0] + second_step_base, second_step_counter = ( + second_path_step_order_book.currency_pair.split("/") + ) + second_step_base_currency = ( + second_step_base.split(".") + )[0] + second_step_counter_currency = ( + second_step_counter.split(".") + )[0] + # print( + # first_step_base_currency, + # first_step_counter_currency, + # second_step_base_currency, + # second_step_counter_currency, + # ) if ( - first_path_step_order_book.currency_pair - != second_path_step_order_book.currency_pair + first_step_counter_currency == second_step_base_currency + and first_step_base_currency == second_step_counter_currency ): - # derive all parts from currency pair - first_step_base, first_step_counter = ( - first_path_step_order_book.currency_pair.split("/") - ) - first_step_base_currency, first_step_base_issuer = ( - first_step_base.split(".") - ) - first_step_counter_currency, first_step_counter_issuer = ( - first_step_counter.split(".") - ) - second_step_base, second_step_counter = ( - second_path_step_order_book.currency_pair.split("/") - ) - second_step_base_currency, second_step_base_issuer = ( - second_step_base.split(".") - ) - second_step_counter_currency, second_step_counter_issuer = ( - second_step_counter.split(".") - ) - if ( - first_step_counter_currency == second_step_base_currency - and first_step_base_currency == second_step_counter_currency - ): - # Example: EUR/USD and USD/EUR - # two buy offers - # two ask offers need to be consumed - first_step_offer = first_path_step_order_book.asks[0] - second_step_offer = second_path_step_order_book.asks[0] - paths.add( - build_path( - first_step_offer=first_step_offer, - second_step_offer=second_step_offer, - first_step_counter=first_step_counter, - second_step_counter=second_step_counter, - wallet=wallet - ) + # This should nerver be the case, but if it somehow + # still be possible, the trade will be parsed correctly. + + # two ask offers need to be consumed + # two bid offers need to be constructed based on the two asks + first_step_offer = first_path_step_order_book.asks[0] + second_step_offer = second_path_step_order_book.asks[0] + paths.append( + adjust_values( + first_step_offer=first_step_offer, + second_step_offer=second_step_offer, + wallet=wallet, + to_be_consumed_example_pair="USD/XRP XRP/USD" ) - # Example: USD/EUR EUR/USD - # two sell offers - # two bid offers need to be consumed - first_step_offer = first_path_step_order_book.bids[0] - second_step_offer = second_path_step_order_book.bids[0] - paths.add( - build_path( - first_step_offer=first_step_offer, - second_step_offer=second_step_offer, - first_step_counter=first_step_counter, - second_step_counter=second_step_counter, - wallet=wallet - ) + ) + # two bid offers need to be consumed + # two ask offers need to be constructed based on the two bids + first_step_offer = first_path_step_order_book.bids[0] + second_step_offer = second_path_step_order_book.bids[0] + paths.append( + adjust_values( + first_step_offer=first_step_offer, + second_step_offer=second_step_offer, + wallet=wallet, + to_be_consumed_example_pair="XRP/USD USD/XRP" ) - elif ( - first_step_counter_currency == second_step_counter_currency - and first_step_base_currency == second_step_base_currency - ): - # Example: EUR/USD EUR/USD - # first one buy and second one sell offer - # one ask and one bid offer need to be consumed - first_step_offer = first_path_step_order_book.asks[0] - second_step_offer = second_path_step_order_book.bids[0] - paths.add( - build_path( - first_step_offer=first_step_offer, - second_step_offer=second_step_offer, - first_step_counter=first_step_counter, - second_step_counter=second_step_counter, - wallet=wallet - ) + ) + elif ( + first_step_counter_currency == second_step_counter_currency + and first_step_base_currency == second_step_base_currency + ): + # first one ask and second one bid offer need to be consumed + # first offer needs to be bid offer constructed + # based on the ask offer + # second offer needs to be a ask offer constructed + # based on the bid offer + first_step_offer = first_path_step_order_book.asks[0] + second_step_offer = second_path_step_order_book.bids[0] + paths.append( + adjust_values( + first_step_offer=first_step_offer, + second_step_offer=second_step_offer, + wallet=wallet, + to_be_consumed_example_pair="USD/XRP USD/XRP" ) - # Example: USD/EUR USD/EUR - # first one sell and second one buy offer - # one ask and one bid offer need to be consumed - first_step_offer = first_path_step_order_book.bids[0] - second_step_offer = second_path_step_order_book.asks[0] - paths.add( - build_path( - first_step_offer=first_step_offer, - second_step_offer=second_step_offer, - first_step_counter=first_step_counter, - second_step_counter=second_step_counter, - wallet=wallet - ) + ) + # first one bid and second one ask offer need to be consumed + # first offer needs to be a ask offer constructed + # based on the bid offer + # second offer needs to be a bid offer constructed + # based on the ask offer + first_step_offer = first_path_step_order_book.bids[0] + second_step_offer = second_path_step_order_book.asks[0] + paths.append( + adjust_values( + first_step_offer=first_step_offer, + second_step_offer=second_step_offer, + wallet=wallet, + to_be_consumed_example_pair="XRP/USD XRP/USD" ) - else: - pass + ) else: pass + else: + pass + + return tuple(paths) diff --git a/xrpl_trading_bot/txn_parser/order_book_changes.py b/xrpl_trading_bot/txn_parser/order_book_changes.py index 8ad21ce..113e6eb 100644 --- a/xrpl_trading_bot/txn_parser/order_book_changes.py +++ b/xrpl_trading_bot/txn_parser/order_book_changes.py @@ -50,8 +50,7 @@ def parse_order_book_changes( def parse_final_order_book( - asks: ORDER_BOOK_SIDE_TYPE, - bids: ORDER_BOOK_SIDE_TYPE, + all_order_books, transaction: Optional[Union[RawTxnType, SubscriptionRawTxnType]], to_xrp: bool = False, ) -> Dict[str, Union[ORDER_BOOK_SIDE_TYPE, str, Optional[Decimal]]]: @@ -74,16 +73,9 @@ def parse_final_order_book( if "transaction" in transaction: transaction = cast(SubscriptionRawTxnType, transaction) transaction = normalize_transaction(transaction_data=transaction) - asks, bids, pair, ex_rate, spread = compute_final_order_book( - asks=asks, - bids=bids, + compute_final_order_book( + all_order_books=all_order_books, transaction=cast(Optional[RawTxnType], transaction), to_xrp=to_xrp, ) - return { - "asks": asks, - "bids": bids, - "currency_pair": pair, - "exchange_rate": Decimal(ex_rate) if ex_rate is not None else ex_rate, - "spread": Decimal(spread) if spread is not None else spread, - } + # print(all_order_books.__dict__) diff --git a/xrpl_trading_bot/txn_parser/utils/order_book_changes_utils.py b/xrpl_trading_bot/txn_parser/utils/order_book_changes_utils.py index b36ba60..f7bf4c2 100644 --- a/xrpl_trading_bot/txn_parser/utils/order_book_changes_utils.py +++ b/xrpl_trading_bot/txn_parser/utils/order_book_changes_utils.py @@ -621,7 +621,6 @@ def _normalize_offer( ], new_prev_txn_id: str, new_prev_txn_lgr_seq: int, - pair: str, to_xrp: bool, owner_funds: Optional[str] = None, ) -> NormalizedOffer: @@ -646,13 +645,7 @@ def _normalize_offer( assert diff_type in ["CreatedNode", "ModifiedNode", "DeletedNode"] taker_gets = _derive_field(node=offer, field_name="TakerGets", to_xrp=to_xrp) taker_pays = _derive_field(node=offer, field_name="TakerPays", to_xrp=to_xrp) - quality = str( - _derive_quality( - taker_gets=taker_gets, - taker_pays=taker_pays, - pair=pair, - ) - ) + quality = "0" if to_xrp and owner_funds is not None: owner_funds = _format_drops_to_xrp(amount=owner_funds) # type: ignore taker_gets_funded, taker_pays_funded = ( @@ -685,7 +678,6 @@ def _normalize_offer( def _normalize_offers( transaction: Union[RawTxnType, SubscriptionRawTxnType], - currency_pair: str, to_xrp: bool, ) -> List[NormalizedOffer]: """ @@ -711,7 +703,6 @@ def _normalize_offers( offer=offer, new_prev_txn_id=hash, new_prev_txn_lgr_seq=ledger_index, - pair=currency_pair, to_xrp=to_xrp, owner_funds=transaction["owner_funds"] if "owner_funds" in transaction @@ -762,6 +753,18 @@ def derive_currency_pair(asks: ORDER_BOOK_SIDE_TYPE, bids: ORDER_BOOK_SIDE_TYPE) raise XRPLException("Cannot derive currency pair because order book is empty.") +def _derive_currency_pair(offer: NormalizedOffer): + taker_pays = offer.TakerPays + taker_gets = offer.TakerGets + base = f"{taker_pays['currency']}.{taker_pays['issuer']}" if ( + isinstance(taker_pays, dict) + ) else "XRP" + counter = f"{taker_gets['currency']}.{taker_gets['issuer']}" if ( + isinstance(taker_gets, dict) + ) else "XRP" + return f"{base}/{counter}" + + def _derive_offer_status_for_final_order_book( offer: NormalizedOffer, ) -> Literal["created", "partially-filled", "filled", "cancelled"]: @@ -882,6 +885,7 @@ def _parse_final_order_book( ) offer_currency_pair = f"{base_currency}/{counter_currency}" new_exchange_rate = None + # if the offer is an offer that affected the wanted order book. if base_currency in currency_pair and counter_currency in currency_pair: # if flipped currency pair if currency_pair != offer_currency_pair: @@ -920,8 +924,7 @@ def _calculate_spread( def compute_final_order_book( - asks: ORDER_BOOK_SIDE_TYPE, - bids: ORDER_BOOK_SIDE_TYPE, + all_order_books, transaction: Optional[RawTxnType], to_xrp: bool, ) -> Tuple[ @@ -939,70 +942,84 @@ def compute_final_order_book( Returns: The new order book, currency pair, exchange rate and spread. """ - pair = derive_currency_pair(asks=asks, bids=bids) - exchange_rate = None - quoted_spread = None if transaction is not None: normalized_offers = _normalize_offers( - transaction=transaction, currency_pair=pair, to_xrp=to_xrp + transaction=transaction, to_xrp=to_xrp ) for offer in normalized_offers: offer_status = _derive_offer_status_for_final_order_book(offer=offer) + currency_pair = _derive_currency_pair(offer=offer) + try: + order_book = all_order_books.get_order_book(currency_pair=currency_pair) + currency_pair = order_book.currency_pair + except AttributeError: + continue + asks = order_book.asks + bids = order_book.bids asks, bids, new_exchange_rate = _parse_final_order_book( asks=asks, bids=bids, offer=offer, status=offer_status, - currency_pair=pair, + currency_pair=currency_pair, ) if new_exchange_rate is not None: - exchange_rate = new_exchange_rate - for ask in asks: - if to_xrp: - ask["TakerGets"] = cast( - CURRENCY_AMOUNT_TYPE, - _format_drops_to_xrp( - amount=cast(CURRENCY_AMOUNT_TYPE, ask["TakerGets"]) - ), - ) - ask["TakerPays"] = cast( - CURRENCY_AMOUNT_TYPE, - _format_drops_to_xrp( - amount=cast(CURRENCY_AMOUNT_TYPE, ask["TakerPays"]) - ), + order_book.exchange_rate = new_exchange_rate + for book in all_order_books.get_all_order_books(): + for ask in book.asks: + if to_xrp: + ask["TakerGets"] = cast( + CURRENCY_AMOUNT_TYPE, + _format_drops_to_xrp( + amount=cast(CURRENCY_AMOUNT_TYPE, ask["TakerGets"]) + ), + ) + ask["TakerPays"] = cast( + CURRENCY_AMOUNT_TYPE, + _format_drops_to_xrp( + amount=cast(CURRENCY_AMOUNT_TYPE, ask["TakerPays"]) + ), + ) + ask["quality"] = _derive_quality( + taker_gets=cast(CURRENCY_AMOUNT_TYPE, ask["TakerGets"]), + taker_pays=cast(CURRENCY_AMOUNT_TYPE, ask["TakerPays"]), + pair=book.currency_pair, ) - ask["quality"] = _derive_quality( - taker_gets=cast(CURRENCY_AMOUNT_TYPE, ask["TakerGets"]), - taker_pays=cast(CURRENCY_AMOUNT_TYPE, ask["TakerPays"]), - pair=pair, - ) - for bid in bids: - if to_xrp: - bid["TakerGets"] = cast( - CURRENCY_AMOUNT_TYPE, - _format_drops_to_xrp( - amount=cast(CURRENCY_AMOUNT_TYPE, bid["TakerGets"]) - ), + for bid in book.bids: + if to_xrp: + bid["TakerGets"] = cast( + CURRENCY_AMOUNT_TYPE, + _format_drops_to_xrp( + amount=cast(CURRENCY_AMOUNT_TYPE, bid["TakerGets"]) + ), + ) + bid["TakerPays"] = cast( + CURRENCY_AMOUNT_TYPE, + _format_drops_to_xrp( + amount=cast(CURRENCY_AMOUNT_TYPE, bid["TakerPays"]) + ), + ) + bid["quality"] = _derive_quality( + taker_gets=cast(CURRENCY_AMOUNT_TYPE, bid["TakerGets"]), + taker_pays=cast(CURRENCY_AMOUNT_TYPE, bid["TakerPays"]), + pair=book.currency_pair, ) - bid["TakerPays"] = cast( - CURRENCY_AMOUNT_TYPE, - _format_drops_to_xrp( - amount=cast(CURRENCY_AMOUNT_TYPE, bid["TakerPays"]) - ), + book.asks = list( + sorted( + book.asks, + key=lambda ask: Decimal(cast(str, ask["quality"])), + reverse=False ) - bid["quality"] = _derive_quality( - taker_gets=cast(CURRENCY_AMOUNT_TYPE, bid["TakerGets"]), - taker_pays=cast(CURRENCY_AMOUNT_TYPE, bid["TakerPays"]), - pair=pair, ) - sorted_asks = list( - sorted(asks, key=lambda ask: Decimal(cast(str, ask["quality"])), reverse=False) - ) - sorted_bids = list( - sorted(bids, key=lambda bid: Decimal(cast(str, bid["quality"])), reverse=True) - ) - if sorted_asks and sorted_bids: - quoted_spread = _calculate_spread( - tip_ask=sorted_asks[0], tip_bid=sorted_bids[0] + book.bids = list( + sorted( + book.bids, + key=lambda bid: Decimal(cast(str, bid["quality"])), + reverse=True + ) ) - return (sorted_asks, sorted_bids, pair, exchange_rate, quoted_spread) + if book.asks and book.bids: + book.spread = Decimal(_calculate_spread( + tip_ask=book.asks[0], tip_bid=book.bids[0] + )) + # return (sorted_asks, sorted_bids, pair, exchange_rate, quoted_spread)