removed python source
This commit is contained in:
parent
7b0228c014
commit
d67fc3b2df
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