rust #10
12
bfxbot.iml
12
bfxbot.iml
@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PackageRequirementsSettings">
|
||||
<option name="removeUnused" value="true" />
|
||||
</component>
|
||||
</module>
|
@ -1 +0,0 @@
|
||||
from .bfxbot import BfxBot
|
151
bfxbot/bfxbot.py
151
bfxbot/bfxbot.py
@ -1,151 +0,0 @@
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from bfxapi import Order
|
||||
|
||||
from bfxbot.bfxwrapper import BfxWrapper
|
||||
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[TradingPair], quote: Symbol,
|
||||
tick_duration: int = 1):
|
||||
if api_key is None:
|
||||
print("API_KEY is not set!")
|
||||
raise ValueError
|
||||
|
||||
if api_secret is None:
|
||||
print("API_SECRET is not set!")
|
||||
raise ValueError
|
||||
|
||||
self.__bfx: BfxWrapper = BfxWrapper(api_key, api_secret)
|
||||
self.__ticker: Ticker = Ticker(tick_duration)
|
||||
self.__status: Dict[TradingPair, SymbolStatus] = {}
|
||||
self.__quote: Symbol = quote
|
||||
self.__account_info = None
|
||||
self.__ledger = None
|
||||
|
||||
if isinstance(symbols, TradingPair):
|
||||
symbols = [symbols]
|
||||
|
||||
self.symbols: List[TradingPair] = symbols
|
||||
|
||||
# init symbol statuses
|
||||
for s in self.symbols:
|
||||
self.__status[s] = SymbolStatus(s)
|
||||
|
||||
def __position_wrapper_from_id(self, position_id) -> Tuple[Optional[PositionWrapper], Optional[SymbolStatus]]:
|
||||
for s in self.__status.values():
|
||||
pw = s.active_position_wrapper_from_id(position_id)
|
||||
|
||||
if pw:
|
||||
return pw, s
|
||||
return None, None
|
||||
|
||||
async def __update_status__(self):
|
||||
active_positions = await self.__bfx.get_active_position()
|
||||
|
||||
for symbol in self.__status:
|
||||
# updating tick
|
||||
self.__status[symbol].__init_tick__(self.__ticker.current_tick)
|
||||
|
||||
# updating last price
|
||||
last_price = await self.__bfx.get_current_prices(symbol)
|
||||
last_price = last_price[0]
|
||||
|
||||
self.__status[symbol].set_tick_price(self.__ticker.current_tick, last_price)
|
||||
|
||||
# updating positions
|
||||
symbol_positions = [x for x in active_positions if x.symbol == str(symbol)]
|
||||
for p in symbol_positions:
|
||||
await self.__status[TradingPair.from_str(p.symbol)].add_position(p)
|
||||
|
||||
# updating orders
|
||||
active_orders = await self.__bfx.get_active_orders(symbol)
|
||||
|
||||
for o in active_orders:
|
||||
self.__status[symbol].add_order(o)
|
||||
|
||||
# emitting new tick event
|
||||
# TODO: handle _on_new_tick() from Strategy
|
||||
await self.__status[symbol].add_event(Event(EventKind.NEW_TICK, self.__ticker.current_tick))
|
||||
|
||||
async def best_position_closing_price(self, position_id: int) -> Optional[float]:
|
||||
pw, _ = self.__position_wrapper_from_id(position_id)
|
||||
|
||||
if not pw:
|
||||
return None
|
||||
|
||||
is_long_pos = pw.position.amount < 0
|
||||
|
||||
pub_tick = await self.__bfx.get_public_ticker(pw.position.symbol)
|
||||
|
||||
bid_price = pub_tick[0]
|
||||
ask_price = pub_tick[2]
|
||||
|
||||
if is_long_pos:
|
||||
closing_price = bid_price * (1 - OFFER_PERC / 100)
|
||||
else:
|
||||
closing_price = ask_price * (1 + OFFER_PERC / 100)
|
||||
|
||||
return closing_price
|
||||
|
||||
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):
|
||||
pw, ss = self.__position_wrapper_from_id(position_id)
|
||||
|
||||
if not pw:
|
||||
print("Could not find open position!")
|
||||
return
|
||||
|
||||
closing_price = await self.best_position_closing_price(pw.position.id)
|
||||
|
||||
amount = pw.position.amount * -1
|
||||
|
||||
open_orders = await self.__bfx.get_active_orders(pw.position.symbol)
|
||||
|
||||
if not open_orders:
|
||||
await self.__bfx.submit_order(pw.position.symbol, closing_price, amount, Order.Type.LIMIT)
|
||||
await ss.add_event(Event(EventKind.ORDER_SUBMITTED, ss.current_tick))
|
||||
|
||||
async def get_balances(self):
|
||||
return await self.__bfx.get_current_balances(self.__quote)
|
||||
|
||||
async def get_profit_loss(self, start: int, end: int):
|
||||
return await self.__bfx.profit_loss(start, end, self.__ledger, self.__quote)
|
||||
|
||||
def set_strategy(self, symbol, strategy: Strategy):
|
||||
if symbol in self.__status:
|
||||
self.__status[symbol].strategy = strategy
|
||||
else:
|
||||
self.__status[symbol] = SymbolStatus(symbol, strategy)
|
||||
|
||||
async def start(self):
|
||||
self.__account_info = await self.__bfx.get_account_information()
|
||||
self.__ledger = await self.__bfx.ledger_history(0, time.time() * 1000)
|
||||
|
||||
await self.__update_status__()
|
||||
|
||||
def symbol_event_handler(self, symbol) -> Optional[EventHandler]:
|
||||
if symbol not in self.__status:
|
||||
return None
|
||||
|
||||
return self.__status[symbol].eh
|
||||
|
||||
def symbol_status(self, symbol: TradingPair) -> Optional[SymbolStatus]:
|
||||
if symbol not in self.__status:
|
||||
return None
|
||||
|
||||
return self.__status[symbol]
|
||||
|
||||
async def update(self):
|
||||
await asyncio.sleep(self.__ticker.seconds)
|
||||
self.__ticker.inc()
|
||||
await self.__update_status__()
|
||||
|
||||
async def __update_ledger(self):
|
||||
self.__ledger = await self.__bfx.ledger_history(0, time.time() * 1000)
|
@ -1,212 +0,0 @@
|
||||
from bfxapi.rest.bfx_rest import BfxRest
|
||||
from retrying_async import retry
|
||||
|
||||
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)
|
||||
|
||||
#######################################
|
||||
# OVERRIDDEN METHODS TO IMPLEMENT RETRY
|
||||
#######################################
|
||||
|
||||
@retry()
|
||||
async def get_public_ticker(self, symbol):
|
||||
if isinstance(symbol, TradingPair):
|
||||
symbol = str(symbol)
|
||||
|
||||
return await super().get_public_ticker(symbol)
|
||||
|
||||
@retry()
|
||||
async def get_active_position(self):
|
||||
return await super().get_active_position()
|
||||
|
||||
@retry()
|
||||
async def get_active_orders(self, 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, TradingPair):
|
||||
symbol = str(symbol)
|
||||
|
||||
return await super().get_trades(symbol, start, end)
|
||||
|
||||
@retry()
|
||||
async def post(self, endpoint: str, data=None, params=""):
|
||||
if data is None:
|
||||
data = {}
|
||||
return await super().post(endpoint, data, params)
|
||||
|
||||
################################
|
||||
# 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
|
||||
|
||||
if currency != str(quote):
|
||||
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)
|
||||
else:
|
||||
c = Currency(currency, amount)
|
||||
|
||||
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
|
||||
|
||||
|
||||
if currency != str(quote):
|
||||
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)
|
||||
else:
|
||||
c = Currency(currency, amount)
|
||||
|
||||
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"
|
||||
|
||||
res = await self.post(api_path, {
|
||||
'symbol': pair,
|
||||
'amount': amount
|
||||
})
|
||||
|
||||
return res[0]
|
||||
|
||||
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)
|
||||
|
||||
bid_price = tickers[0]
|
||||
ask_price = tickers[2]
|
||||
ticker_price = tickers[6]
|
||||
|
||||
return bid_price, ask_price, ticker_price
|
||||
|
||||
async def ledger_history(self, start, end):
|
||||
def chunks(lst):
|
||||
for i in range(len(lst) - 1):
|
||||
yield lst[i:i + 2]
|
||||
|
||||
def get_timeframes(start, end, increments=10):
|
||||
start = int(start)
|
||||
end = int(end)
|
||||
|
||||
delta = int((end - start) / increments)
|
||||
|
||||
return [x for x in range(start, end, delta)]
|
||||
|
||||
api_path = "auth/r/ledgers/hist"
|
||||
|
||||
history = []
|
||||
|
||||
# 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}))
|
||||
|
||||
history.sort(key=lambda ledger_entry: ledger_entry[3], reverse=True)
|
||||
|
||||
return history
|
||||
|
||||
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: Symbol):
|
||||
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,161 +0,0 @@
|
||||
import re
|
||||
from enum import Enum
|
||||
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"
|
||||
|
||||
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)
|
||||
|
||||
if not match:
|
||||
raise ValueError
|
||||
|
||||
currency = match.group(1).lower()
|
||||
|
||||
if currency in ("xmr"):
|
||||
return TradingPair.XMR
|
||||
elif currency in ("btc"):
|
||||
return TradingPair.BTC
|
||||
elif currency in ("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: Optional[float] = price
|
||||
|
||||
def __str__(self):
|
||||
if self.__price:
|
||||
return f"{self.__name} {self.__amount} @ {self.__price}"
|
||||
else:
|
||||
return f"{self.__name} {self.__amount}"
|
||||
|
||||
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() == str(quote):
|
||||
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.currency().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"
|
295
bfxbot/models.py
295
bfxbot/models.py
@ -1,295 +0,0 @@
|
||||
import inspect
|
||||
import time
|
||||
from enum import Enum
|
||||
from typing import List, Dict, Tuple, Optional
|
||||
|
||||
from bfxapi import Order, Position
|
||||
|
||||
from bfxbot.currency import TradingPair
|
||||
|
||||
OFFER_PERC = 0.008
|
||||
TAKER_FEE = 0.2
|
||||
MAKER_FEE = 0.1
|
||||
|
||||
|
||||
def __add_to_dict_list__(dictionary: Dict[int, List], k, v) -> Dict[int, List]:
|
||||
if k not in dictionary:
|
||||
dictionary[k] = [v]
|
||||
else:
|
||||
dictionary[k].append(v)
|
||||
|
||||
return dictionary
|
||||
|
||||
|
||||
class EventKind(Enum):
|
||||
NEW_MINIMUM = 1,
|
||||
NEW_MAXIMUM = 2,
|
||||
REACHED_LOSS = 3,
|
||||
REACHED_BREAK_EVEN = 4,
|
||||
REACHED_MIN_PROFIT = 5,
|
||||
REACHED_GOOD_PROFIT = 6,
|
||||
REACHED_MAX_LOSS = 7,
|
||||
CLOSE_POSITION = 8,
|
||||
TRAILING_STOP_SET = 9,
|
||||
TRAILING_STOP_MOVED = 10,
|
||||
ORDER_SUBMITTED = 11,
|
||||
NEW_TICK = 12
|
||||
|
||||
|
||||
class EventMetadata:
|
||||
def __init__(self, position_id: int = None, order_id: int = None):
|
||||
self.position_id: int = position_id
|
||||
self.order_id: int = order_id
|
||||
|
||||
|
||||
class PositionState(Enum):
|
||||
CRITICAL = -1,
|
||||
LOSS = 0,
|
||||
BREAK_EVEN = 1,
|
||||
MINIMUM_PROFIT = 2,
|
||||
PROFIT = 3,
|
||||
UNDEFINED = 4
|
||||
|
||||
def color(self) -> str:
|
||||
if self == self.LOSS or self == self.CRITICAL:
|
||||
return "red"
|
||||
elif self == self.BREAK_EVEN:
|
||||
return "yellow"
|
||||
else:
|
||||
return "green"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name}"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
|
||||
class Ticker:
|
||||
def __init__(self, sec) -> None:
|
||||
self.seconds: int = sec
|
||||
self.start_time = time.time()
|
||||
self.current_tick: int = 1
|
||||
|
||||
def inc(self):
|
||||
self.current_tick += 1
|
||||
|
||||
|
||||
class Event:
|
||||
def __init__(self, kind: EventKind, tick: int, metadata: EventMetadata = None) -> None:
|
||||
self.kind: EventKind = kind
|
||||
self.tick: int = tick
|
||||
self.metadata: EventMetadata = metadata
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.kind.name} @ Tick {self.tick}"
|
||||
|
||||
def has_metadata(self) -> bool:
|
||||
return self.metadata is not None
|
||||
|
||||
|
||||
class PositionWrapper:
|
||||
def __init__(self, position: Position, state: PositionState = PositionState.UNDEFINED,
|
||||
net_profit_loss: float = None,
|
||||
net_profit_loss_percentage: float = None):
|
||||
self.position: Position = position
|
||||
self.__net_profit_loss: float = net_profit_loss
|
||||
self.__net_profit_loss_percentage: float = net_profit_loss_percentage
|
||||
self.__state: PositionState = state
|
||||
|
||||
def net_profit_loss(self) -> float:
|
||||
return self.__net_profit_loss
|
||||
|
||||
def net_profit_loss_percentage(self) -> float:
|
||||
return self.__net_profit_loss_percentage
|
||||
|
||||
def set_state(self, state: PositionState):
|
||||
self.__state = state
|
||||
|
||||
def state(self) -> PositionState:
|
||||
return self.__state
|
||||
|
||||
|
||||
class SymbolStatus:
|
||||
def __init__(self, symbol: TradingPair, strategy=None):
|
||||
self.symbol = symbol
|
||||
self.eh = EventHandler()
|
||||
self.prices: Dict[int, float] = {}
|
||||
self.events: List[Event] = []
|
||||
self.orders: Dict[int, List[Order]] = {}
|
||||
self.positions: Dict[int, List[PositionWrapper]] = {}
|
||||
self.current_tick: int = 1
|
||||
self.strategy: Strategy = strategy
|
||||
|
||||
def __init_tick__(self, tick: int):
|
||||
self.current_tick = tick
|
||||
self.prices[self.current_tick] = None
|
||||
self.orders[self.current_tick] = []
|
||||
self.positions[self.current_tick] = []
|
||||
|
||||
async def add_event(self, event: Event):
|
||||
self.events.append(event)
|
||||
await self.eh.call_event(self, event)
|
||||
|
||||
def add_order(self, order: Order):
|
||||
if self.strategy:
|
||||
self.strategy.order_on_new_tick(order, self)
|
||||
self.orders = __add_to_dict_list__(self.orders, self.current_tick, order)
|
||||
|
||||
# Applies strategy and adds position to list
|
||||
async def add_position(self, position: Position):
|
||||
events = []
|
||||
|
||||
# if a strategy is defined then the strategy takes care of creating a PW for us
|
||||
if not self.strategy:
|
||||
pw = PositionWrapper(position)
|
||||
else:
|
||||
pw, events = await self.__apply_strategy_to_position__(position)
|
||||
self.positions = __add_to_dict_list__(self.positions, self.current_tick, pw)
|
||||
|
||||
# triggering state callbacks
|
||||
await self.__trigger_position_state_callbacks__(pw)
|
||||
|
||||
# triggering events callbacks
|
||||
for e in events:
|
||||
if not isinstance(e, Event):
|
||||
raise ValueError
|
||||
await self.add_event(e)
|
||||
|
||||
def all_prices(self) -> List[float]:
|
||||
return list(map(lambda x: self.prices[x], range(1, self.current_tick + 1)))
|
||||
|
||||
def all_ticks(self) -> List[int]:
|
||||
return [x for x in range(1, self.current_tick + 1)]
|
||||
|
||||
def current_positions(self) -> List[PositionWrapper]:
|
||||
return self.positions[self.current_tick]
|
||||
|
||||
def current_price(self):
|
||||
return self.prices[self.current_tick]
|
||||
|
||||
def previous_pw(self, pid: int) -> Optional[PositionWrapper]:
|
||||
if self.current_tick == 1:
|
||||
return None
|
||||
|
||||
if not self.positions[self.current_tick - 1]:
|
||||
return None
|
||||
|
||||
return next(filter(lambda x: x.position.id == pid, self.positions[self.current_tick - 1]))
|
||||
|
||||
def active_position_wrapper_from_id(self, position_id: int) -> Optional[PositionWrapper]:
|
||||
if self.current_tick in self.positions:
|
||||
for pw in self.positions[self.current_tick]:
|
||||
if pw.position.id == position_id:
|
||||
return pw
|
||||
return None
|
||||
|
||||
def set_tick_price(self, tick, price):
|
||||
self.prices[tick] = price
|
||||
|
||||
async def __apply_strategy_to_position__(self, position: Position) -> Tuple[PositionWrapper, List[Event]]:
|
||||
pw, events = self.strategy.position_on_new_tick(position, self)
|
||||
|
||||
if not isinstance(pw, PositionWrapper):
|
||||
raise ValueError
|
||||
|
||||
if not isinstance(events, list):
|
||||
raise ValueError
|
||||
|
||||
return pw, events
|
||||
|
||||
async def __trigger_position_state_callbacks__(self, pw: PositionWrapper):
|
||||
await self.eh.call_position_state(self, pw)
|
||||
|
||||
|
||||
class Strategy:
|
||||
"""
|
||||
Defines new position state and events after tick.
|
||||
"""
|
||||
|
||||
def position_on_new_tick(self, position: Position, ss: SymbolStatus) -> Tuple[PositionWrapper, List[Event]]:
|
||||
pass
|
||||
|
||||
"""
|
||||
Defines new order state and events after tick.
|
||||
"""
|
||||
|
||||
def order_on_new_tick(self, order: Order, ss: SymbolStatus):
|
||||
pass
|
||||
|
||||
|
||||
class EventHandler:
|
||||
def __init__(self):
|
||||
self.event_handlers = {}
|
||||
self.state_handlers = {}
|
||||
self.any_events = []
|
||||
self.any_state = []
|
||||
|
||||
async def call_event(self, status: SymbolStatus, event: Event):
|
||||
value = event.kind.value
|
||||
|
||||
# print("CALLING EVENT: {}".format(event))
|
||||
if value in self.event_handlers:
|
||||
for h in self.event_handlers[value]:
|
||||
if inspect.iscoroutinefunction(h):
|
||||
await h(event, status)
|
||||
else:
|
||||
h(event, status)
|
||||
|
||||
for h in self.any_events:
|
||||
if inspect.iscoroutinefunction(h):
|
||||
await h(event, status)
|
||||
else:
|
||||
h(event, status)
|
||||
|
||||
async def call_position_state(self, status: SymbolStatus, pw: PositionWrapper):
|
||||
state = pw.state()
|
||||
|
||||
if state in self.state_handlers:
|
||||
for h in self.state_handlers[state]:
|
||||
if inspect.iscoroutinefunction(h):
|
||||
await h(pw, status)
|
||||
else:
|
||||
h(pw, status)
|
||||
|
||||
for h in self.any_state:
|
||||
if inspect.iscoroutinefunction(h):
|
||||
await h(pw, status)
|
||||
else:
|
||||
h(pw, status)
|
||||
|
||||
def on_event(self, kind: EventKind):
|
||||
value = kind.value
|
||||
|
||||
def registerhandler(handler):
|
||||
if value in self.event_handlers:
|
||||
self.event_handlers[value].append(handler)
|
||||
else:
|
||||
self.event_handlers[value] = [handler]
|
||||
return handler
|
||||
|
||||
return registerhandler
|
||||
|
||||
def on_position_state(self, state: PositionState):
|
||||
def registerhandler(handler):
|
||||
if state in self.state_handlers:
|
||||
self.state_handlers[state].append(handler)
|
||||
else:
|
||||
self.state_handlers[state] = [handler]
|
||||
return handler
|
||||
|
||||
return registerhandler
|
||||
|
||||
def on_any_event(self):
|
||||
def registerhandle(handler):
|
||||
self.any_events.append(handler)
|
||||
return handler
|
||||
|
||||
return registerhandle
|
||||
|
||||
def on_any_position_state(self):
|
||||
def registerhandle(handler):
|
||||
self.any_state.append(handler)
|
||||
return handler
|
||||
|
||||
return registerhandle
|
@ -1,59 +0,0 @@
|
||||
import re
|
||||
|
||||
from bfxbot.bfxwrapper import Balance
|
||||
from bfxbot.models import PositionWrapper
|
||||
|
||||
|
||||
class CurrencyPair:
|
||||
def __init__(self, base: str, quote: str):
|
||||
self.base: str = base
|
||||
self.quote: str = quote
|
||||
|
||||
@staticmethod
|
||||
def from_str(string: str):
|
||||
symbol_regex = re.compile("t(?P<base>[a-zA-Z]{3})(?P<quote>[a-zA-Z]{3})")
|
||||
|
||||
match = symbol_regex.match(string)
|
||||
|
||||
if not match:
|
||||
return None
|
||||
|
||||
return CurrencyPair(match.group("base"), match.group("quote"))
|
||||
|
||||
|
||||
def average(a):
|
||||
return sum(a) / len(a)
|
||||
|
||||
|
||||
def balance_to_json(balance: Balance):
|
||||
return {
|
||||
'currency': balance.currency().name(),
|
||||
'amount': balance.currency().amount(),
|
||||
'kind': balance.wallet().value,
|
||||
'quote': balance.quote().value,
|
||||
'quote_equivalent': balance.quote_equivalent()
|
||||
}
|
||||
|
||||
|
||||
def net_pl_percentage(perc: float, reference_fee_perc: float):
|
||||
return perc - reference_fee_perc
|
||||
|
||||
|
||||
def pw_to_posprop(pw: PositionWrapper):
|
||||
pair = CurrencyPair.from_str(pw.position.symbol)
|
||||
|
||||
if not pair:
|
||||
raise ValueError
|
||||
|
||||
return {
|
||||
"id": pw.position.id,
|
||||
"amount": pw.position.amount,
|
||||
"base_price": pw.position.base_price,
|
||||
"state": str(pw.state()),
|
||||
"pair": {
|
||||
"base": pair.base,
|
||||
"quote": pair.quote
|
||||
},
|
||||
"profit_loss": pw.net_profit_loss(),
|
||||
"profit_loss_percentage": pw.net_profit_loss_percentage()
|
||||
}
|
140
main.py
140
main.py
@ -1,140 +0,0 @@
|
||||
# #!/usr/bin/env python
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import threading
|
||||
from time import sleep
|
||||
from typing import List
|
||||
|
||||
import dotenv
|
||||
from flask import Flask, render_template
|
||||
from flask_socketio import SocketIO
|
||||
|
||||
from bfxbot import BfxBot
|
||||
from bfxbot.bfxwrapper import Balance
|
||||
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
|
||||
|
||||
|
||||
async def bot_loop():
|
||||
await bot.start()
|
||||
|
||||
while True:
|
||||
await bot.update()
|
||||
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
API_KEY = os.getenv("API_KEY")
|
||||
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=[TradingPair.BTC], quote=Symbol.USD, tick_duration=20)
|
||||
strategy = TrailingStopStrategy()
|
||||
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()
|
||||
|
||||
|
||||
###################################
|
||||
# Flask callbacks
|
||||
###################################
|
||||
|
||||
@app.route('/')
|
||||
def entry():
|
||||
return render_template('index.html')
|
||||
|
||||
|
||||
###################################
|
||||
# Socker.IO callbacks
|
||||
###################################
|
||||
|
||||
@socketio.on("close_position")
|
||||
def on_close_position(message: dict):
|
||||
position_id = message['position_id']
|
||||
|
||||
loop.run_until_complete(bot.close_position(position_id))
|
||||
|
||||
|
||||
@socketio.on('connect')
|
||||
def on_connect():
|
||||
# sleeping on exception to avoid race condition
|
||||
ticks, prices, positions, balances = [], [], [], []
|
||||
|
||||
while not ticks or not prices:
|
||||
try:
|
||||
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)
|
||||
|
||||
socketio.emit("first_connect",
|
||||
{
|
||||
"ticks": ticks,
|
||||
"prices": prices,
|
||||
"positions": list(map(pw_to_posprop, positions)),
|
||||
"balances": list(map(balance_to_json, balances))
|
||||
})
|
||||
|
||||
|
||||
@socketio.on('get_profit_loss')
|
||||
def on_get_profit_loss(message):
|
||||
start = message['start']
|
||||
end = message['end']
|
||||
|
||||
profit_loss = loop.run_until_complete(bot.get_profit_loss(start, end))
|
||||
|
||||
socketio.emit("put_profit_loss", {
|
||||
"pl": profit_loss[0],
|
||||
"pl_perc": profit_loss[1]
|
||||
})
|
||||
|
||||
###################################
|
||||
# Bot callbacks
|
||||
###################################
|
||||
|
||||
@btc_eh.on_event(EventKind.CLOSE_POSITION)
|
||||
async def on_close_position(event: Event, _):
|
||||
print("CLOSING!")
|
||||
await bot.close_position(event.metadata.position_id)
|
||||
|
||||
|
||||
@btc_eh.on_any_position_state()
|
||||
async def on_any_state(pw: PositionWrapper, ss: SymbolStatus):
|
||||
await strategy.update_stop_percentage(pw, ss)
|
||||
|
||||
|
||||
@btc_eh.on_event(EventKind.NEW_TICK)
|
||||
async def on_new_tick(event: Event, status: SymbolStatus):
|
||||
tick = event.tick
|
||||
price = status.prices[event.tick]
|
||||
|
||||
balances: List[Balance] = await bot.get_balances()
|
||||
positions: List[PositionWrapper] = status.positions[event.tick] if event.tick in status.positions else []
|
||||
|
||||
socketio.emit("new_tick", {"tick": tick,
|
||||
"price": price,
|
||||
"positions": list(map(pw_to_posprop, positions)),
|
||||
"balances": list(map(balance_to_json, balances))})
|
||||
|
||||
|
||||
@btc_eh.on_any_event()
|
||||
def on_any_event(event: Event, _):
|
||||
socketio.emit("new_event", {
|
||||
"tick": event.tick,
|
||||
"kind": event.kind.name
|
||||
})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
socketio.run(app)
|
@ -1,36 +0,0 @@
|
||||
aiohttp==3.7.3
|
||||
astroid==2.4.2
|
||||
async-timeout==3.0.1
|
||||
asyncio==3.4.3
|
||||
attrs==20.3.0
|
||||
bidict==0.21.2
|
||||
bitfinex-api-py==1.1.8
|
||||
chardet==3.0.4
|
||||
click==7.1.2
|
||||
eventemitter==0.2.0
|
||||
Flask==1.1.2
|
||||
Flask-SocketIO==5.0.1
|
||||
idna==2.10
|
||||
isort==5.6.4
|
||||
itsdangerous==1.1.0
|
||||
Jinja2==2.11.2
|
||||
lazy-object-proxy==1.4.3
|
||||
MarkupSafe==1.1.1
|
||||
mccabe==0.6.1
|
||||
mpmath==1.1.0
|
||||
multidict==5.1.0
|
||||
pyee==8.1.0
|
||||
pylint==2.6.0
|
||||
python-dotenv==0.15.0
|
||||
python-engineio==4.0.0
|
||||
python-socketio==5.0.3
|
||||
retrying-async==1.2.0
|
||||
six==1.15.0
|
||||
sympy==1.7.1
|
||||
toml==0.10.2
|
||||
typing-extensions==3.7.4.3
|
||||
websockets==8.1
|
||||
Werkzeug==1.0.1
|
||||
wrapt==1.12.1
|
||||
yarl==1.6.3
|
||||
|
BIN
sounds/1up.mp3
BIN
sounds/1up.mp3
Binary file not shown.
BIN
sounds/coin.mp3
BIN
sounds/coin.mp3
Binary file not shown.
Binary file not shown.
BIN
sounds/goal.wav
BIN
sounds/goal.wav
Binary file not shown.
3
static/.gitignore
vendored
3
static/.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
*
|
||||
!.gitignore
|
||||
|
123
strategy.py
123
strategy.py
@ -1,123 +0,0 @@
|
||||
from typing import List, Dict
|
||||
|
||||
import sympy.abc
|
||||
from bfxapi import Position
|
||||
from sympy import Point, solve
|
||||
|
||||
from bfxbot.models import Strategy, PositionState, SymbolStatus, Event, EventKind, EventMetadata, PositionWrapper, \
|
||||
TAKER_FEE
|
||||
from bfxbot.utils import net_pl_percentage
|
||||
|
||||
|
||||
class SquaredTrailingStop:
|
||||
def __init__(self, p_min: Point, p_max: Point):
|
||||
a = sympy.abc.a
|
||||
b = sympy.abc.b
|
||||
c = sympy.abc.c
|
||||
|
||||
self.p_min = p_min
|
||||
self.p_max = p_max
|
||||
|
||||
e1 = 2 * a * (p_max.x + b)
|
||||
e2 = a * (p_min.x + b) ** 2 + c - p_min.y
|
||||
e3 = a * (p_max.x + b) ** 2 + c - p_max.y
|
||||
|
||||
s = solve([e1, e2, e3])[0]
|
||||
|
||||
self.a, self.b, self.c = s[a], s[b], s[c]
|
||||
|
||||
def y(self, x):
|
||||
def inter_y(x):
|
||||
return self.a * (x + self.b) ** 2 + self.c
|
||||
|
||||
if x < self.p_min.x:
|
||||
return self.p_min.y
|
||||
elif x > self.p_max.x:
|
||||
return self.p_max.y
|
||||
else:
|
||||
return inter_y(x)
|
||||
|
||||
def profit(self, x):
|
||||
if x < self.p_min.x:
|
||||
return 0
|
||||
return x - self.y(x)
|
||||
|
||||
|
||||
class TrailingStopStrategy(Strategy):
|
||||
BREAK_EVEN_PERC = TAKER_FEE
|
||||
MIN_PROFIT_PERC = BREAK_EVEN_PERC + 0.3
|
||||
GOOD_PROFIT_PERC = MIN_PROFIT_PERC * 2.5
|
||||
MAX_LOSS_PERC = -4.0
|
||||
|
||||
TRAILING_STOP = SquaredTrailingStop(Point(MIN_PROFIT_PERC, MIN_PROFIT_PERC / 3 * 2), Point(GOOD_PROFIT_PERC, 0.1))
|
||||
|
||||
def __init__(self):
|
||||
# position_id : stop percentage
|
||||
self.stop_percentage: Dict[int, float] = {}
|
||||
|
||||
def position_on_new_tick(self, current_position: Position, ss: SymbolStatus) -> (PositionState, List[Event]):
|
||||
events = []
|
||||
|
||||
pl_perc = net_pl_percentage(current_position.profit_loss_percentage, TAKER_FEE)
|
||||
prev = ss.previous_pw(current_position.id)
|
||||
event_metadata = EventMetadata(position_id=current_position.id)
|
||||
|
||||
if pl_perc > self.GOOD_PROFIT_PERC:
|
||||
state = PositionState.PROFIT
|
||||
elif self.MIN_PROFIT_PERC <= pl_perc < self.GOOD_PROFIT_PERC:
|
||||
state = PositionState.MINIMUM_PROFIT
|
||||
elif 0.0 <= pl_perc < self.MIN_PROFIT_PERC:
|
||||
state = PositionState.BREAK_EVEN
|
||||
elif self.MAX_LOSS_PERC < pl_perc < 0.0:
|
||||
state = PositionState.LOSS
|
||||
else:
|
||||
events.append(Event(EventKind.CLOSE_POSITION, ss.current_tick, event_metadata))
|
||||
state = PositionState.CRITICAL
|
||||
|
||||
pw = PositionWrapper(current_position, state=state, net_profit_loss=current_position.profit_loss,
|
||||
net_profit_loss_percentage=pl_perc)
|
||||
|
||||
if not prev or prev.state() == state:
|
||||
return pw, events
|
||||
|
||||
if state == PositionState.PROFIT:
|
||||
events.append(Event(EventKind.REACHED_GOOD_PROFIT, ss.current_tick, event_metadata))
|
||||
elif state == PositionState.MINIMUM_PROFIT:
|
||||
events.append(Event(EventKind.REACHED_MIN_PROFIT, ss.current_tick, event_metadata))
|
||||
elif state == PositionState.BREAK_EVEN:
|
||||
events.append(Event(EventKind.REACHED_BREAK_EVEN, ss.current_tick, event_metadata))
|
||||
elif state == PositionState.LOSS:
|
||||
events.append(Event(EventKind.REACHED_LOSS, ss.current_tick, event_metadata))
|
||||
else:
|
||||
events.append(Event(EventKind.REACHED_MAX_LOSS, ss.current_tick, event_metadata))
|
||||
events.append(Event(EventKind.CLOSE_POSITION, ss.current_tick, event_metadata))
|
||||
|
||||
return pw, events
|
||||
|
||||
async def update_stop_percentage(self, pw: PositionWrapper, ss: SymbolStatus):
|
||||
current_pl_perc = pw.net_profit_loss_percentage()
|
||||
pid = pw.position.id
|
||||
event_metadata = EventMetadata(position_id=pw.position.id)
|
||||
|
||||
# if trailing stop not set for this position and state is not profit (we should not set it)
|
||||
if pid not in self.stop_percentage and pw.state() not in [PositionState.MINIMUM_PROFIT,
|
||||
PositionState.PROFIT]:
|
||||
return
|
||||
|
||||
# set stop percentage for first time only if in profit
|
||||
if pid not in self.stop_percentage:
|
||||
await ss.add_event(Event(EventKind.TRAILING_STOP_SET, ss.current_tick, event_metadata))
|
||||
|
||||
self.stop_percentage[pid] = current_pl_perc - self.TRAILING_STOP.y(current_pl_perc)
|
||||
return
|
||||
|
||||
# moving trailing stop
|
||||
if current_pl_perc - self.TRAILING_STOP.y(current_pl_perc) > self.stop_percentage[pid]:
|
||||
await ss.add_event(Event(EventKind.TRAILING_STOP_MOVED, ss.current_tick, event_metadata))
|
||||
self.stop_percentage[pid] = current_pl_perc - self.TRAILING_STOP.y(current_pl_perc)
|
||||
|
||||
# close position if current P/L below stop percentage
|
||||
if current_pl_perc < self.stop_percentage[pid]:
|
||||
await ss.add_event(Event(EventKind.CLOSE_POSITION, ss.current_tick, event_metadata))
|
||||
|
||||
return
|
@ -1,16 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='index.css') }}">
|
||||
</head>
|
||||
|
||||
<title>Rustico</title>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="{{ url_for('static', filename='index.js') }}"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user