Skip to content

Commit

Permalink
CryptofeedStrategy: fix cryptofeed open interest on wrong pair (not p…
Browse files Browse the repository at this point in the history
…erp) / complete strategy with atr computing / refacto
  • Loading branch information
AntoineLep committed May 10, 2022
1 parent b8df46e commit 6e5c5ef
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 59 deletions.
13 changes: 12 additions & 1 deletion strategies/best_strat_ever/best_strategy_ever.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
import time

from core.stock.crypto_pair_manager import CryptoPairManager
from core.strategy.strategy import Strategy
from core.ftx.ws.ftx_websocket_client import FtxWebsocketClient
from core.ftx.rest.ftx_rest_api import FtxRestApi
Expand All @@ -19,6 +20,9 @@ def __init__(self):
self.ftx_ws_client: FtxWebsocketClient = FtxWebsocketClient()
self.ftx_ws_client.connect()
self.ftx_rest_api: FtxRestApi = FtxRestApi()
self.doge_manager: CryptoPairManager = CryptoPairManager("DOGE-PERP", self.ftx_rest_api)
self.doge_manager.add_time_frame(60)
self.doge_manager.start_all_time_frame_acq()

def before_loop(self) -> None:
pass
Expand All @@ -44,9 +48,16 @@ def loop(self) -> None:

logging.info(wallets)

doge_stock_data_manager = self.doge_manager.get_time_frame(60).stock_data_manager
if len(doge_stock_data_manager.stock_data_list) > 20:
atr14 = doge_stock_data_manager.stock_indicators["atr_14"]
logging.info("atr_14")
logging.info(atr14.iloc[-1])

def after_loop(self) -> None:
time.sleep(1)
time.sleep(5)

def cleanup(self) -> None:
"""Clean strategy execution"""
logging.info("BestStratEver cleanup")
self.doge_manager.stop_all_time_frame_acq()
165 changes: 108 additions & 57 deletions strategies/cryptofeed_strategy/cryptofeed_strategy.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import logging
import math
import threading
import time
from typing import List
import pandas as pd
import stockstats

from core.enums.order_type_enum import OrderTypeEnum
from core.enums.position_state_enum import PositionStateEnum
from core.enums.side_enum import SideEnum
from core.enums.trigger_order_type_enum import TriggerOrderTypeEnum
from core.ftx.rest.ftx_rest_api import FtxRestApi
from core.models.candle import Candle
from core.models.opening_config_dict import OpeningConfigDict
from core.models.position_config_dict import PositionConfigDict
from core.models.trigger_order_config_dict import TriggerOrderConfigDict
from core.strategy.strategy import Strategy
from core.trading.position_driver import PositionDriver
from strategies.cryptofeed_strategy.cryptofeed_service import CryptofeedService
from strategies.cryptofeed_strategy.enums.cryptofeed_data_type_enum import CryptofeedDataTypeEnum
from strategies.cryptofeed_strategy.enums.cryptofeed_side_enum import CryptofeedSideEnum
from cryptofeed.types import Liquidation

from tools.utils import format_wallet_raw_data, format_ohlcv_raw_data

SLEEP_TIME_BETWEEN_LOOPS = 10
LIQUIDATION_HISTORY_RETENTION_TIME = 60 * 60 # 1 hour retention
from strategies.cryptofeed_strategy.stock_utils import StockUtils

PAIRS_TO_TRACK = [
"SOL", "LUNA", "WAVES", "GMT", "AXS", "AVAX", "ZIL", "RUNE", "NEAR", "AAVE", "APE", "ETC", "FIL", "ATOM", "LOOKS",
Expand All @@ -35,8 +35,14 @@
"JASMY", "BTC", "ETH", "DOGE"
]

TIMEFRAMES = [60, 60 * 5] # 1 min and 5 min
SLEEP_TIME_BETWEEN_LOOPS = 10
LIQUIDATION_HISTORY_RETENTION_TIME = 60 * 60 # 1 hour retention
STOP_LOSS_PERCENTAGE = 1
TAKE_PROFIT_PERCENTAGE_1 = 0.5
TIMEFRAMES = [60] # 1 min
EXCHANGES = ["FTX", "BINANCE_FUTURES"]
TRIGGER_LIQUIDATION_VALUE = 10000
LIQUIDATIONS_OI_RATIO_THRESHOLD = 500


class CryptofeedStrategy(Strategy):
Expand Down Expand Up @@ -81,6 +87,9 @@ def __init__(self):
for timeframe in TIMEFRAMES:
self.timeframes_close_ts[timeframe] = time.time() + timeframe

# Dict of running position drivers
self.position_drivers = {}

self._t: threading.Thread = threading.Thread(target=self.strategy_runner)

def before_loop(self) -> None:
Expand All @@ -99,10 +108,7 @@ def loop(self) -> None:
if time.time() > self.timeframes_close_ts[timeframe]:
# Set next timeframe end timestamp
self.timeframes_close_ts[timeframe] = time.time() + timeframe

# ignore 5 min for the moment
if timeframe == 60:
self.perform_data_analysis(timeframe)
self.perform_data_analysis(timeframe)

# Remove values older than LIQUIDATION_HISTORY_RETENTION_TIME
self.liquidations = list(filter(lambda data: data.timestamp > time.time() - LIQUIDATION_HISTORY_RETENTION_TIME,
Expand All @@ -123,55 +129,85 @@ def perform_data_analysis(self, timeframe: int):
sell_liquidation_sum = sum(
[self.computed_liquidations[exchange][timeframe][pair]['sell'] for exchange in EXCHANGES])

if buy_liquidation_sum > 0 or sell_liquidation_sum > 0:
if buy_liquidation_sum > 1000 or sell_liquidation_sum > 1000:
logging.info(f"Liquidations for pair: {pair:<10} - buy: ${buy_liquidation_sum:<12} - "
f"sell: ${sell_liquidation_sum:<12}")

# Check we got oi data for all listed exchanges
if all([pair in self.open_interest[exchange] for exchange in EXCHANGES]):
oi_sum = sum([self.open_interest[exchange][pair]["open_interest"] for exchange in EXCHANGES])
logging.info(f"Open interest for pair: {pair:<10} - ${oi_sum}")

if buy_liquidation_sum > 30000 or sell_liquidation_sum > 30000:
if sell_liquidation_sum * 500 < oi_sum < buy_liquidation_sum * 500:
# Check the liquidations (buy or sell) exceeds the TRIGGER_LIQUIDATION_VALUE
if buy_liquidation_sum > TRIGGER_LIQUIDATION_VALUE or \
sell_liquidation_sum > TRIGGER_LIQUIDATION_VALUE:

# Getting current price of pair
current_price = StockUtils.get_market_price(self.ftx_rest_api, pair + '-PERP')

# Sum the open interest usd value for listed exchanges
oi_sum_usd = 0
for exchange in EXCHANGES:
cur_exchange_oi_usd = round(
float(self.open_interest[exchange][pair]["open_interest"]) * current_price, 1)
oi_sum_usd += cur_exchange_oi_usd
logging.info(f'oi_sum_usd for {exchange} {pair} - ${cur_exchange_oi_usd:_}')

logging.info(f'oi_sum_usd for {pair} - ${oi_sum_usd:_}')

# Open position logic
if sell_liquidation_sum * LIQUIDATIONS_OI_RATIO_THRESHOLD < oi_sum_usd < \
buy_liquidation_sum * LIQUIDATIONS_OI_RATIO_THRESHOLD:
self.open_position(pair, SideEnum.BUY)
elif buy_liquidation_sum * 500 < oi_sum < sell_liquidation_sum * 500:
elif buy_liquidation_sum * LIQUIDATIONS_OI_RATIO_THRESHOLD < oi_sum_usd < \
sell_liquidation_sum * LIQUIDATIONS_OI_RATIO_THRESHOLD:
self.open_position(pair, SideEnum.SELL)

def open_position(self, pair: str, side: SideEnum) -> None:
# TODO: check a position on this coin is not yet opened
atr_14 = self.get_atr_14(pair)
logging.info(f"{pair} - atr_14:")
logging.info(atr_14)

def get_atr_14(self, pair):
# Retrieve 20 last candles
candles = self.ftx_rest_api.get(f"markets/{pair}-PERP/candles", {
"resolution": 60,
"limit": 20,
"start_time": math.floor(time.time() - 60 * 20)
})

candles = [format_ohlcv_raw_data(candle, 60) for candle in candles]
candles = [Candle(candle["id"], candle["time"], candle["open_price"], candle["high_price"], candle["low_price"],
candle["close_price"], candle["volume"]) for candle in candles]

stock_stat_candles = [{
"date": candle.time,
"open": candle.open_price,
"high": candle.high_price,
"low": candle.low_price,
"close": candle.close_price,
"volume": candle.volume
} for candle in candles]

stock_indicators = stockstats.StockDataFrame.retype(pd.DataFrame(stock_stat_candles))
return stock_indicators["atr_14"]

def available_without_borrow(self) -> float:
response = self.ftx_rest_api.get("wallet/balances")
wallets = [format_wallet_raw_data(wallet) for wallet in response if wallet["coin"] == 'USD']

return wallets[0]["available_without_borrow"]
# Don't reopen a position if there is a position driver already opened
if pair in self.position_drivers and self.position_drivers[pair].position_state == PositionStateEnum.OPENED:
return

atr_14 = StockUtils.get_atr_14(self.ftx_rest_api, pair)
logging.info(f"{pair} - atr_14: {atr_14.iloc[-1]}")

# Getting current price of pair
current_price = StockUtils.get_market_price(self.ftx_rest_api, pair + '-PERP')

available_balance_without_borrow = StockUtils.get_available_balance_without_borrow(self.ftx_rest_api,)
percent_balance_per_trade = (available_balance_without_borrow * 20) * 0.01
logging.info(f'available without borrow: ${available_balance_without_borrow}')
position_size = percent_balance_per_trade / current_price

openings: List[OpeningConfigDict] = [{
"price": None,
"size": position_size,
"type": OrderTypeEnum.MARKET
}]
sl: TriggerOrderConfigDict = {
"size": position_size,
"type": TriggerOrderTypeEnum.STOP,
"reduce_only": True,
"trigger_price": current_price - atr_14.iloc[-1] if
side == SideEnum.BUY else current_price + atr_14.iloc[-1],
"order_price": None,
"trail_value": None
}
tp1: TriggerOrderConfigDict = {
"size": position_size,
"type": TriggerOrderTypeEnum.TAKE_PROFIT,
"reduce_only": True,
"trigger_price": current_price + current_price * TAKE_PROFIT_PERCENTAGE_1 / 100 if
side == SideEnum.BUY else current_price - current_price * TAKE_PROFIT_PERCENTAGE_1 / 100,
"order_price": None,
"trail_value": None
}
position_config: PositionConfigDict = {
"openings": openings,
"trigger_orders": [sl, tp1],
"max_open_duration": 60 * 60 * 24
}

self.position_drivers[pair] = PositionDriver(self.ftx_rest_api, 60)
self.position_drivers[pair].open_position(pair + '-PERP', side, position_config)

def get_liquidations(self, exchanges: List[str] = None, symbols: List[str] = None, side: CryptofeedSideEnum = None,
max_age: int = -1):
Expand Down Expand Up @@ -206,6 +242,9 @@ def get_liquidations(self, exchanges: List[str] = None, symbols: List[str] = Non
return liquidations

def perform_liquidations(self) -> None:
"""
Compute a sum of all liquidations into the configured timeframes for listed exchanges
"""
for exchange in EXCHANGES:
for timeframe in TIMEFRAMES:
for pair in PAIRS_TO_TRACK:
Expand Down Expand Up @@ -261,10 +300,22 @@ def perform_new_open_interest(self) -> None:

for oi in new_oi:
symbol = next(filter(lambda sym: oi.symbol.startswith(sym + "-"), PAIRS_TO_TRACK), None)
self.open_interest[oi.exchange][symbol] = {
"open_interest": oi.open_interest,
"timestamp": oi.timestamp
}

# If the open interest data already exists
if symbol in self.open_interest[oi.exchange]:

# Check the new open interest value is not more than 3 times less than the last received value to
# prevent cryptofeed wrong data (eg. not perp future) from getting performed
if oi.open_interest * 3 > self.open_interest[oi.exchange][symbol]["open_interest"]:
self.open_interest[oi.exchange][symbol] = {
"open_interest": oi.open_interest,
"timestamp": oi.timestamp
}
else:
self.open_interest[oi.exchange][symbol] = {
"open_interest": oi.open_interest,
"timestamp": oi.timestamp
}

def run(self) -> None:
"""
Expand Down
71 changes: 71 additions & 0 deletions strategies/cryptofeed_strategy/stock_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import math
import time

import pandas as pd
import stockstats

from core.ftx.rest.ftx_rest_api import FtxRestApi
from core.models.candle import Candle
from core.models.market_data_dict import MarketDataDict
from tools.utils import format_wallet_raw_data, format_market_raw_data, format_ohlcv_raw_data


class StockUtils(object):

@staticmethod
def get_market_price(ftx_rest_api: FtxRestApi, pair: str) -> float:
"""
Retrieve the market price for a given pair
:param ftx_rest_api: a FTX rest api instance
:param pair: The pair to retrieve market price for
:return: The market price of the given pair
"""
response = ftx_rest_api.get(f"markets/{pair}")
market_data: MarketDataDict = format_market_raw_data(response)
return market_data.get("price")

@staticmethod
def get_atr_14(ftx_rest_api: FtxRestApi, pair: str) -> pd.DataFrame:
"""
Get the atr 14 stockstat indicator
:param ftx_rest_api: a FTX rest api instance
:param pair: The pair to get the atr 14 stockstat indicator for
:return: The atr 14 stockstat indicator
"""
# Retrieve 20 last candles
candles = ftx_rest_api.get(f"markets/{pair}-PERP/candles", {
"resolution": 60,
"limit": 20,
"start_time": math.floor(time.time() - 60 * 20)
})

candles = [format_ohlcv_raw_data(candle, 60) for candle in candles]
candles = [Candle(candle["id"], candle["time"], candle["open_price"], candle["high_price"], candle["low_price"],
candle["close_price"], candle["volume"]) for candle in candles]

stock_stat_candles = [{
"date": candle.time,
"open": candle.open_price,
"high": candle.high_price,
"low": candle.low_price,
"close": candle.close_price,
"volume": candle.volume
} for candle in candles]

stock_indicators = stockstats.StockDataFrame.retype(pd.DataFrame(stock_stat_candles))
return stock_indicators["atr_14"]

@staticmethod
def get_available_balance_without_borrow(ftx_rest_api: FtxRestApi) -> float:
"""
Retrieve the usd available balance without borrow
:param ftx_rest_api: a FTX rest api instance
:return: The usd available balance without borrow
"""
response = ftx_rest_api.get("wallet/balances")
wallets = [format_wallet_raw_data(wallet) for wallet in response if wallet["coin"] == 'USD']

return wallets[0]["available_without_borrow"]
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"BNT-PERP", "AMPL-PERP", "PROM-PERP", "KSOS-PERP", "BIT-PERP", "BOBA-PERP", "DAWN-PERP", "RAMP-PERP",
"YFII-PERP", "OXY-PERP", "SOS-PERP", "LEO-PERP", "ORBS-PERP", "MTA-PERP", "TRYB-PERP", "MCB-PERP", "EDEN-PERP",
"MNGO-PERP", "CONV-PERP", "BAO-PERP", "SECO-PERP", "CEL-PERP", "HOLY-PERP", "ROOK-PERP", "MER-PERP", "TULIP-PERP",
"ASD-PERP", "KIN-PERP", "MOB-PERP", "SRN-PERP", "BTT-PERP", "MEDIA-PERP", "IOST-PERP", "JASMY-PERP"
"ASD-PERP", "KIN-PERP", "MOB-PERP", "SRN-PERP", "BTT-PERP", "MEDIA-PERP", "IOST-PERP", "JASMY-PERP", "GAL-PERP",
"GST-PERP"
]

# FTX api rate limit is 10 requests per second
Expand Down

0 comments on commit 6e5c5ef

Please sign in to comment.