From f9e3ad500c804233ea1b1e7b4fcec845255bfd1d Mon Sep 17 00:00:00 2001 From: Giulio De Pasquale Date: Wed, 23 Dec 2020 17:17:10 +0000 Subject: [PATCH] refactored Symbol to TradingPair. Implemented Symbol class. Implemented Balance and BalanceGroup class along with methods to calculate balance at a specific point in time. --- bfxbot/bfxbot.py | 20 +++--- bfxbot/bfxwrapper.py | 150 ++++++++++++++++++++++++++++++-------- bfxbot/currency.py | 167 +++++++++++++++++++++++++++++++++---------- bfxbot/models.py | 4 +- main.py | 14 ++-- 5 files changed, 269 insertions(+), 86 deletions(-) diff --git a/bfxbot/bfxbot.py b/bfxbot/bfxbot.py index 7d8c0cf..67f25a4 100644 --- a/bfxbot/bfxbot.py +++ b/bfxbot/bfxbot.py @@ -4,12 +4,13 @@ from typing import Dict, List, Optional, Tuple from bfxapi import Order from bfxbot.bfxwrapper import BfxWrapper -from bfxbot.currency import Symbol +from bfxbot.currency import TradingPair, Symbol from bfxbot.models import SymbolStatus, Ticker, EventHandler, Strategy, Event, EventKind, OFFER_PERC, PositionWrapper class BfxBot: - def __init__(self, api_key: str, api_secret: str, symbols: List[Symbol], tick_duration: int = 1, ): + def __init__(self, api_key: str, api_secret: str, symbols: List[TradingPair], quote: Symbol, + tick_duration: int = 1): if api_key is None: print("API_KEY is not set!") raise ValueError @@ -20,12 +21,13 @@ class BfxBot: self.__bfx: BfxWrapper = BfxWrapper(api_key, api_secret) self.__ticker: Ticker = Ticker(tick_duration) - self.__status: Dict[Symbol, SymbolStatus] = {} + self.__status: Dict[TradingPair, SymbolStatus] = {} + self.__quote: Symbol = quote - if isinstance(symbols, Symbol): + if isinstance(symbols, TradingPair): symbols = [symbols] - self.symbols: List[Symbol] = symbols + self.symbols: List[TradingPair] = symbols # init symbol statuses for s in self.symbols: @@ -55,7 +57,7 @@ class BfxBot: # updating positions symbol_positions = [x for x in active_positions if x.symbol == str(symbol)] for p in symbol_positions: - await self.__status[Symbol.from_str(p.symbol)].add_position(p) + await self.__status[TradingPair.from_str(p.symbol)].add_position(p) # updating orders active_orders = await self.__bfx.get_active_orders(symbol) @@ -87,7 +89,7 @@ class BfxBot: return closing_price - def close_order(self, symbol: Symbol, order_id: int): + def close_order(self, symbol: TradingPair, order_id: int): print(f"I would have closed order {order_id} for {symbol}") async def close_position(self, position_id: int): @@ -108,7 +110,7 @@ class BfxBot: await ss.add_event(Event(EventKind.ORDER_SUBMITTED, ss.current_tick)) async def get_balances(self): - return await self.__bfx.get_balances() + return await self.__bfx.get_current_balances(self.__quote) def set_strategy(self, symbol, strategy: Strategy): if symbol in self.__status: @@ -125,7 +127,7 @@ class BfxBot: return self.__status[symbol].eh - def symbol_status(self, symbol: Symbol) -> Optional[SymbolStatus]: + def symbol_status(self, symbol: TradingPair) -> Optional[SymbolStatus]: if symbol not in self.__status: return None diff --git a/bfxbot/bfxwrapper.py b/bfxbot/bfxwrapper.py index 153e31d..32a23ee 100644 --- a/bfxbot/bfxwrapper.py +++ b/bfxbot/bfxwrapper.py @@ -1,12 +1,14 @@ -from typing import List - from bfxapi.rest.bfx_rest import BfxRest from retrying_async import retry -from bfxbot.currency import Symbol, Balance, BalanceKind, OrderType, Direction +from bfxbot.currency import TradingPair, Balance, WalletKind, OrderType, Direction, Currency, BalanceGroup, Symbol +from bfxbot.utils import average class BfxWrapper(BfxRest): + # default timeframe (in milliseconds) when retrieving old prices + DEFAULT_TIME_DELTA = 5 * 60 * 1000 + def __init__(self, api_key: str, api_secret: str): super().__init__(API_KEY=api_key, API_SECRET=api_secret) @@ -16,7 +18,7 @@ class BfxWrapper(BfxRest): @retry() async def get_public_ticker(self, symbol): - if isinstance(symbol, Symbol): + if isinstance(symbol, TradingPair): symbol = str(symbol) return await super().get_public_ticker(symbol) @@ -27,14 +29,14 @@ class BfxWrapper(BfxRest): @retry() async def get_active_orders(self, symbol): - if isinstance(symbol, Symbol): + if isinstance(symbol, TradingPair): symbol = str(symbol) return await super().get_active_orders(symbol) @retry() async def get_trades(self, symbol, start, end): - if isinstance(symbol, Symbol): + if isinstance(symbol, TradingPair): symbol = str(symbol) return await super().get_trades(symbol, start, end) @@ -49,6 +51,58 @@ class BfxWrapper(BfxRest): # NEW METHODS ################################ + async def account_movements_between(self, start: int, end: int, ledger, quote: Symbol) -> BalanceGroup: + movements = BalanceGroup(quote) + + # TODO: Parallelize this + for entry in filter(lambda x: start <= x[3] <= end, ledger): + description: str = entry[8] + currency = entry[1] + amount = entry[5] + time = entry[3] + + if not description.lower().startswith("deposit"): + continue + + trading_pair = f"t{currency}{quote}" + start_time = time - self.DEFAULT_TIME_DELTA + end_time = time + self.DEFAULT_TIME_DELTA + + trades = await self.get_public_trades(symbol=trading_pair, start=start_time, end=end_time) + currency_price = average(list(map(lambda x: x[3], trades))) + + c = Currency(currency, amount, currency_price) + b = Balance(c, quote) + + movements.add_balance(b) + + return movements + + async def balance_at(self, time: int, ledger, quote: Symbol): + bg = BalanceGroup(quote) + + # TODO: Parallelize this + for entry in filter(lambda x: x[3] <= time, ledger): + currency = entry[1] + amount = entry[6] + + if currency in bg.currency_names(): + continue + + trading_pair = f"t{currency}{quote}" + start_time = time - self.DEFAULT_TIME_DELTA + end_time = time + self.DEFAULT_TIME_DELTA + + trades = await self.get_public_trades(symbol=trading_pair, start=start_time, end=end_time) + currency_price = average(list(map(lambda x: x[3], trades))) + + c = Currency(currency, amount, currency_price) + b = Balance(c, quote) + + bg.add_balance(b) + + return bg + # Calculate the average execution price for Trading or rate for Margin funding. async def calculate_execution_price(self, pair: str, amount: float): api_path = "/calc/trade/avg" @@ -60,8 +114,32 @@ class BfxWrapper(BfxRest): return res[0] - async def get_current_prices(self, symbol) -> (float, float, float): - if isinstance(symbol, Symbol): + async def get_account_information(self): + api_path = "auth/r/info/user" + + return await self.post(api_path) + + async def get_current_balances(self, quote: Symbol) -> BalanceGroup: + bg: BalanceGroup = BalanceGroup(quote) + + wallets = await self.get_wallets() + + for w in wallets: + kind = WalletKind.from_str(w.type) + + if not kind: + continue + + execution_price = await self.calculate_execution_price(f"t{w.currency}{quote}", w.balance) + c = Currency(w.currency, w.balance, execution_price) + b = Balance(c, quote, kind) + + bg.add_balance(b) + + return bg + + async def get_current_prices(self, symbol: TradingPair) -> (float, float, float): + if isinstance(symbol, TradingPair): symbol = str(symbol) tickers = await self.get_public_ticker(symbol) @@ -72,40 +150,52 @@ class BfxWrapper(BfxRest): return bid_price, ask_price, ticker_price - async def get_usd_balance(self): - balance = 0.0 + async def ledger_history(self, start, end): + def chunks(lst): + for i in range(len(lst) - 1): + yield lst[i:i + 2] - wallets = await self.get_wallets() + def get_timeframes(start, end, increments=10): + start = int(start) + end = int(end) - for w in wallets: - if w.currency == "USD": - balance += w.balance - else: - current_price = await self.get_current_prices(f"t{w.currency}USD") - balance += current_price * w.balance + delta = int((end - start) / increments) - return balance + return [x for x in range(start, end, delta)] - async def get_balances(self, quote: str = "USD") -> List[Balance]: - balances = [] - wallets = await self.get_wallets() + api_path = "auth/r/ledgers/hist" - for w in wallets: - kind = BalanceKind.from_str(w.type) + history = [] - if not kind: - continue + # TODO: Parallelize this + for c in chunks(get_timeframes(start, end)): + history.extend(await self.post(api_path, {'start': c[0], 'end': c[1], 'limit': 2500})) - execution_price = await self.calculate_execution_price(f"t{w.currency}{quote}", w.balance) - quote_equivalent = execution_price * w.balance - balances.append(Balance(w.currency, w.balance, kind, quote, quote_equivalent)) + history.sort(key=lambda ledger_entry: ledger_entry[3], reverse=True) - return balances + return history - async def maximum_order_amount(self, symbol: Symbol, direction: Direction, + async def maximum_order_amount(self, symbol: TradingPair, direction: Direction, order_type: OrderType = OrderType.EXCHANGE, rate: int = 1): api_path = "auth/calc/order/avail" return await self.post(api_path, {'symbol': str(symbol), 'type': order_type.value, "dir": direction.value, "rate": rate}) + + async def profit_loss(self, start: int, end: int, ledger, quote: Currency): + if start > end: + raise ValueError + + start_bg = await self.balance_at(start, ledger, quote) + end_bg = await self.balance_at(end, ledger, quote) + movements_bg = await self.account_movements_between(start, end, ledger, quote) + + start_quote = start_bg.quote_equivalent() + end_quote = end_bg.quote_equivalent() + movements_quote = movements_bg.quote_equivalent() + + profit_loss = end_quote - (start_quote + movements_quote) + profit_loss_percentage = profit_loss / (start_quote + movements_quote) * 100 + + return profit_loss, profit_loss_percentage diff --git a/bfxbot/currency.py b/bfxbot/currency.py index 0072171..a9c5866 100644 --- a/bfxbot/currency.py +++ b/bfxbot/currency.py @@ -1,46 +1,28 @@ import re from enum import Enum - - -class BalanceKind(Enum): - EXCHANGE = "exchange", - MARGIN = "margin" - - @staticmethod - def from_str(string: str): - string = string.lower() - - if "margin" in string: - return BalanceKind.MARGIN - if "exchange" in string: - return BalanceKind.EXCHANGE - - return None - - -class Balance: - def __init__(self, currency: str, amount: int, kind: BalanceKind, quote: str, quote_equivalent: float): - self.currency: str = currency - self.amount: int = amount - self.kind: BalanceKind = kind - self.quote: str = quote - self.quote_equivalent: float = quote_equivalent - - -class Direction(Enum): - UP = 1, - DOWN = -1 - - -class OrderType(Enum): - EXCHANGE = "EXCHANGE", - MARGIN = "MARGIN" +from typing import Optional, List class Symbol(Enum): XMR = "XMR" BTC = "BTC" ETH = "ETH" + USD = "USD" + + def __repr__(self): + return self.__str__() + + def __str__(self): + return self.value + + def __eq__(self, other): + return self.value == other.value + + +class TradingPair(Enum): + XMR = "XMR" + BTC = "BTC" + ETH = "ETH" def __repr__(self): return f"t{self.value}USD" @@ -48,6 +30,12 @@ class Symbol(Enum): def __str__(self): return self.__repr__() + def __eq__(self, other): + return self.value == other.value + + def __hash__(self): + return hash(self.__repr__()) + @staticmethod def from_str(string: str): match = re.compile("t([a-zA-Z]+)USD").match(string) @@ -58,10 +46,113 @@ class Symbol(Enum): currency = match.group(1).lower() if currency in ("xmr"): - return Symbol.XMR + return TradingPair.XMR elif currency in ("btc"): - return Symbol.BTC + return TradingPair.BTC elif currency in ("eth"): - return Symbol.ETH + return TradingPair.ETH else: return NotImplementedError + + +class Currency: + def __init__(self, name: str, amount: float, price: float = None): + self.__name: str = name + self.__amount: float = amount + self.__price: float = price + + def __str__(self): + return f"{self.__name} {self.__amount} @ {self.__price}" + + def __repr__(self): + return self.__str__() + + def amount(self) -> float: + return self.__amount + + def name(self) -> str: + return self.__name + + def price(self) -> Optional[float]: + return self.__price + + +class WalletKind(Enum): + EXCHANGE = "exchange", + MARGIN = "margin" + + @staticmethod + def from_str(string: str): + string = string.lower() + + if "margin" in string: + return WalletKind.MARGIN + if "exchange" in string: + return WalletKind.EXCHANGE + + return None + + +class Balance: + def __init__(self, currency: Currency, quote: Symbol, wallet: Optional[WalletKind] = None): + self.__currency: Currency = currency + self.__quote: Symbol = quote + self.__quote_equivalent: float = 0.0 + self.__wallet: Optional[WalletKind] = wallet + + if currency.name == quote.value: + self.__quote_equivalent = currency.amount() + else: + self.__quote_equivalent = currency.amount() * currency.price() + + def currency(self) -> Currency: + return self.__currency + + def quote(self) -> Symbol: + return self.__quote + + def quote_equivalent(self) -> float: + return self.__quote_equivalent + + def wallet(self) -> Optional[WalletKind]: + return self.__wallet + + +class BalanceGroup: + def __init__(self, quote: Symbol, balances: Optional[List[Balance]] = None): + if balances is None: + balances = [] + + self.__quote: Symbol = quote + self.__balances: Optional[List[Balance]] = balances + self.__quote_equivalent: float = 0.0 + + def __iter__(self): + return self.__balances.__iter__() + + def add_balance(self, balance: Balance): + self.__balances.append(balance) + + self.__quote_equivalent += balance.quote_equivalent() + + def balances(self) -> Optional[List[Balance]]: + return self.__balances + + def currency_names(self) -> List[str]: + return list(map(lambda x: x.name, self.balances())) + + def quote(self) -> Symbol: + return self.__quote + + def quote_equivalent(self) -> float: + return self.__quote_equivalent + + +class Direction(Enum): + UP = 1, + DOWN = -1 + + +class OrderType(Enum): + EXCHANGE = "EXCHANGE", + MARGIN = "MARGIN" diff --git a/bfxbot/models.py b/bfxbot/models.py index ff6cc94..607b207 100644 --- a/bfxbot/models.py +++ b/bfxbot/models.py @@ -5,7 +5,7 @@ from typing import List, Dict, Tuple, Optional from bfxapi import Order, Position -from bfxbot.currency import Symbol +from bfxbot.currency import TradingPair OFFER_PERC = 0.008 TAKER_FEE = 0.2 @@ -111,7 +111,7 @@ class PositionWrapper: class SymbolStatus: - def __init__(self, symbol: Symbol, strategy=None): + def __init__(self, symbol: TradingPair, strategy=None): self.symbol = symbol self.eh = EventHandler() self.prices: Dict[int, float] = {} diff --git a/main.py b/main.py index 98b0fac..45bfd33 100755 --- a/main.py +++ b/main.py @@ -12,7 +12,7 @@ from flask_socketio import SocketIO from bfxbot import BfxBot from bfxbot.bfxwrapper import Balance -from bfxbot.currency import Symbol +from bfxbot.currency import TradingPair, Symbol from bfxbot.models import PositionWrapper, SymbolStatus, Event, EventKind from bfxbot.utils import pw_to_posprop, balance_to_json from strategy import TrailingStopStrategy @@ -35,10 +35,10 @@ API_SECRET = os.getenv("API_SECRET") app = Flask(__name__) socketio = SocketIO(app, async_mode="threading") bot = BfxBot(api_key=API_KEY, api_secret=API_SECRET, - symbols=[Symbol.BTC], tick_duration=20) + symbols=[TradingPair.BTC], quote=Symbol.USD, tick_duration=20) strategy = TrailingStopStrategy() -bot.set_strategy(Symbol.BTC, strategy) -btc_eh = bot.symbol_event_handler(Symbol.BTC) +bot.set_strategy(TradingPair.BTC, strategy) +btc_eh = bot.symbol_event_handler(TradingPair.BTC) # initializing and starting bot on other thread threading.Thread(target=lambda: asyncio.run(bot_loop())).start() @@ -71,9 +71,9 @@ def on_connect(): while not ticks or not prices: try: - ticks = bot.symbol_status(Symbol.BTC).all_ticks() - prices = bot.symbol_status(Symbol.BTC).all_prices() - positions = bot.symbol_status(Symbol.BTC).current_positions() + ticks = bot.symbol_status(TradingPair.BTC).all_ticks() + prices = bot.symbol_status(TradingPair.BTC).all_prices() + positions = bot.symbol_status(TradingPair.BTC).current_positions() balances = loop.run_until_complete(bot.get_balances()) except KeyError: sleep(1)