refactored Symbol to TradingPair. Implemented Symbol class. Implemented Balance and BalanceGroup class along with methods to calculate balance at a specific point in time.
This commit is contained in:
parent
01419e5927
commit
f9e3ad500c
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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] = {}
|
||||
|
14
main.py
14
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)
|
||||
|
Loading…
Reference in New Issue
Block a user