Skip to content
This repository has been archived by the owner on Oct 26, 2022. It is now read-only.

Add trading path parser #22

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 61 additions & 41 deletions xrpl_trading_bot/clients/methods.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from asyncio import run
from decimal import Decimal
from typing import Dict, List, cast

from websockets.exceptions import ConnectionClosedError
Expand Down Expand Up @@ -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],
Expand All @@ -93,29 +142,12 @@ 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(
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)

Expand All @@ -135,24 +167,12 @@ 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,
)
)
)
except ConnectionClosedError:
return None
if is_order_book(message=message):
continue
else:
parse_final_order_book(
all_order_books=all_order_books,
transaction=message,
to_xrp=True
)
return subscribe_books
37 changes: 32 additions & 5 deletions xrpl_trading_bot/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,42 @@
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

from xrpl_trading_bot.clients import subscribe_to_account_balances
from xrpl_trading_bot.globals import WALLET

if __name__ == "__main__":
balances_subscribtion = Thread(
balances_subscription = Thread(
target=subscribe_to_account_balances,
args=(WALLET,),
)
balances_subscribtion.start()
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_subscribtion.join()
balances_subscription.join()
8 changes: 7 additions & 1 deletion xrpl_trading_bot/order_books/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
OrderBook,
OrderBookNotFoundException,
OrderBooks,
build_subscibe_books,
)

__all__ = ["OrderBook", "OrderBooks", "OrderBookNotFoundException"]
__all__ = [
"OrderBook",
"OrderBooks",
"OrderBookNotFoundException",
"build_subscibe_books"
]
137 changes: 132 additions & 5 deletions xrpl_trading_bot/order_books/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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]:
"""
Expand Down
3 changes: 3 additions & 0 deletions xrpl_trading_bot/trading/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from xrpl_trading_bot.trading.main import trade

__all__ = ["trade"]
27 changes: 27 additions & 0 deletions xrpl_trading_bot/trading/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +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):
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)
5 changes: 5 additions & 0 deletions xrpl_trading_bot/trading/paths/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from xrpl_trading_bot.trading.paths.main import build_trading_paths

__all__ = [
"build_trading_paths"
]
Loading