import numpy as np import talib.abstract as ta import technical.indicators as ti import freqtrade.vendor.qtpylib.indicators as qtpylib import arrow from freqtrade.strategy.interface import IStrategy from freqtrade.strategy import merge_informative_pair from typing import Dict, List, Optional, Tuple from pandas import DataFrame, Series from functools import reduce from datetime import datetime from freqtrade.persistence import Trade from statistics import mean from cachetools import TTLCache from collections import namedtuple '\nLoosely based on:\nhttps://github.com/nicolay-zlobin/jesse-indicators/blob/main/strategies/BadStreak/__init__.py\n\nTODO:\n - Move custom indicators out to helper file or freqtrade/technical?\n\n' class Stinkfist(IStrategy): INTERFACE_VERSION = 3 '\n Strategy Configuration Items\n ' timeframe = '5m' inf_timeframe = '1h' entry_params = {'inf-pct-adr': 0.8, 'mp': 30} exit_params = {} minimal_roi = {'0': 0.05, '10': 0.025, '20': 0.015, '30': 0.01, '720': 0.005, '1440': 0} stoploss = -0.4 # Probably don't change these use_exit_signal = True exit_profit_only = False ignore_roi_if_entry_signal = True startup_candle_count: int = 72 # Custom Dicts for storing trade data and other custom things this strategy does custom_trade_info = {} custom_current_price_cache: TTLCache = TTLCache(maxsize=100, ttl=300) # 5 minutes '\n Informative Pair Definitions\n ' def informative_pairs(self): pairs = self.dp.current_whitelist() informative_pairs = [(pair, self.inf_timeframe) for pair in pairs] return informative_pairs '\n Indicator Definitions\n ' def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # Populate/update the trade data if there is any, set trades to false if not live/dry self.custom_trade_info[metadata['pair']] = self.populate_trades(metadata['pair']) # Set up primary indicators dataframe['rmi-slow'] = ti.RMI(dataframe, length=21, mom=5) dataframe['rmi-fast'] = ti.RMI(dataframe, length=8, mom=4) # MA Streak dataframe['mac'] = self.mac(dataframe, 20, 50) dataframe['streak'] = self.ma_streak(dataframe, period=4) streak = abs(int(dataframe['streak'].iloc[-1])) streak_back_close = dataframe['close'].shift(streak + 1) dataframe['streak-roc'] = 100 * (dataframe['close'] - streak_back_close) / streak_back_close # Percent Change Channel pcc = self.pcc(dataframe, period=20, mult=2) dataframe['pcc-lowerband'] = pcc.lowerband # Momentum Pinball dataframe['roc'] = ta.ROC(dataframe, timeperiod=1) dataframe['mp'] = ta.RSI(dataframe['roc'], timeperiod=3) # Trend Calculations dataframe['rmi-up'] = np.where(dataframe['rmi-slow'] >= dataframe['rmi-slow'].shift(), 1, 0) dataframe['rmi-dn'] = np.where(dataframe['rmi-slow'] <= dataframe['rmi-slow'].shift(), 1, 0) dataframe['rmi-up-trend'] = np.where(dataframe['rmi-up'].rolling(3, min_periods=1).sum() >= 2, 1, 0) dataframe['rmi-dn-trend'] = np.where(dataframe['rmi-dn'].rolling(3, min_periods=1).sum() >= 2, 1, 0) # Informative indicators for current pair on inf_timeframe informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=self.inf_timeframe) informative['1d_high'] = informative['close'].rolling(24).max() informative['3d_low'] = informative['close'].rolling(72).min() informative['adr'] = informative['1d_high'] - informative['3d_low'] dataframe = merge_informative_pair(dataframe, informative, self.timeframe, self.inf_timeframe, ffill=True) return dataframe '\n Buy Trigger Signals\n ' def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: params = self.entry_params trade_data = self.custom_trade_info[metadata['pair']] conditions = [] if trade_data['active_trade']: rmi_grow = self.linear_growth(30, 70, 180, 720, trade_data['open_minutes']) profit_factor = 1 - dataframe['rmi-slow'].iloc[-1] / 300 conditions.append(dataframe['rmi-up-trend'] == 1) conditions.append(trade_data['current_profit'] > trade_data['peak_profit'] * profit_factor) conditions.append(dataframe['rmi-slow'] >= rmi_grow) else: # Primary entry triggers # informative timeframe conditions # default timeframe conditions conditions.append((dataframe['close'] <= dataframe[f'3d_low_{self.inf_timeframe}'] + params['inf-pct-adr'] * dataframe[f'adr_{self.inf_timeframe}']) & (dataframe['mp'] < params['mp']) & (dataframe['streak-roc'] > dataframe['pcc-lowerband']) & (dataframe['mac'] == 1)) # Anything below here applies to persisting and new entry signal conditions.append(dataframe['volume'].gt(0)) if conditions: dataframe.loc[reduce(lambda x, y: x & y, conditions), 'entry'] = 1 return dataframe '\n Sell Trigger Signals\n ' def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: params = self.exit_params trade_data = self.custom_trade_info[metadata['pair']] conditions = [] if trade_data['active_trade']: loss_cutoff = self.linear_growth(-0.03, 0, 0, 300, trade_data['open_minutes']) conditions.append((trade_data['current_profit'] < loss_cutoff) & (trade_data['current_profit'] > self.stoploss) & (dataframe['rmi-dn-trend'] == 1) & dataframe['volume'].gt(0)) if trade_data['peak_profit'] > 0: conditions.append(dataframe['rmi-slow'] < 50) else: conditions.append(dataframe['rmi-slow'] < 10) if trade_data['other_trades']: if trade_data['free_slots'] > 0: hold_pct = trade_data['free_slots'] / 100 * -1 conditions.append(trade_data['avg_other_profit'] >= hold_pct) else: conditions.append(trade_data['biggest_loser'] == True) else: conditions.append(dataframe['volume'].lt(0)) if conditions: dataframe.loc[reduce(lambda x, y: x & y, conditions), 'exit'] = 1 return dataframe '\n Super Legit Custom Methods\n ' def populate_trades(self, pair: str) -> dict: if not pair in self.custom_trade_info: self.custom_trade_info[pair] = {} trade_data = {} trade_data['active_trade'] = trade_data['other_trades'] = trade_data['biggest_loser'] = False self.custom_trade_info['meta'] = {} if self.config['runmode'].value in ('live', 'dry_run'): active_trade = Trade.get_trades([Trade.pair == pair, Trade.is_open.is_(True)]).all() if active_trade: current_rate = self.get_current_price(pair, True) active_trade[0].adjust_min_max_rates(current_rate) present = arrow.utcnow() trade_start = arrow.get(active_trade[0].open_date) open_minutes = (present - trade_start).total_seconds() // 60 trade_data['active_trade'] = True trade_data['current_profit'] = active_trade[0].calc_profit_ratio(current_rate) trade_data['peak_profit'] = max(0, active_trade[0].calc_profit_ratio(active_trade[0].max_rate)) trade_data['open_minutes']: int = open_minutes trade_data['open_candles']: int = open_minutes // active_trade[0].timeframe else: trade_data['current_profit'] = trade_data['peak_profit'] = 0.0 trade_data['open_minutes'] = trade_data['open_candles'] = 0 other_trades = Trade.get_trades([Trade.pair != pair, Trade.is_open.is_(True)]).all() if other_trades: trade_data['other_trades'] = True other_profit = tuple((trade.calc_profit_ratio(self.get_current_price(trade.pair, False)) for trade in other_trades)) trade_data['avg_other_profit'] = mean(other_profit) if trade_data['current_profit'] < min(other_profit): trade_data['biggest_loser'] = True else: trade_data['avg_other_profit'] = 0 open_trades = len(Trade.get_open_trades()) trade_data['free_slots'] = max(0, self.config['max_open_trades'] - open_trades) return trade_data def get_current_price(self, pair: str, refresh: bool) -> float: if not refresh: rate = self.custom_current_price_cache.get(pair) if rate: return rate ask_strategy = self.config.get('ask_strategy', {}) if ask_strategy.get('use_order_book', False): ob = self.dp.orderbook(pair, 1) rate = ob[f"{ask_strategy['price_side']}s"][0][0] else: ticker = self.dp.ticker(pair) rate = ticker['last'] self.custom_current_price_cache[pair] = rate return rate def linear_growth(self, start: float, end: float, start_time: int, end_time: int, trade_time: int) -> float: time = max(0, trade_time - start_time) rate = (end - start) / (end_time - start_time) return min(end, start + rate * trade_time) '\n Custom Indicators\n ' '\n Moving Average Cross\n Port of: https://www.tradingview.com/script/PcWAuplI-Moving-Average-Cross/\n ' def mac(self, dataframe: DataFrame, fast: int=20, slow: int=50) -> Series: dataframe = dataframe.copy() # Fast MAs upper_fast = ta.EMA(dataframe['high'], timeperiod=fast) lower_fast = ta.EMA(dataframe['low'], timeperiod=fast) # Slow MAs upper_slow = ta.EMA(dataframe['high'], timeperiod=slow) lower_slow = ta.EMA(dataframe['low'], timeperiod=slow) # Crosses crosses_lf_us = qtpylib.crossed_above(lower_fast, upper_slow) | qtpylib.crossed_below(lower_fast, upper_slow) crosses_uf_ls = qtpylib.crossed_above(upper_fast, lower_slow) | qtpylib.crossed_below(upper_fast, lower_slow) dir_1 = np.where(crosses_lf_us, 1, np.nan) dir_2 = np.where(crosses_uf_ls, -1, np.nan) dir = np.where(dir_1 == 1, dir_1, np.nan) dir = np.where(dir_2 == -1, dir_2, dir_1) res = Series(dir).fillna(method='ffill').to_numpy() return res '\n MA Streak\n Port of: https://www.tradingview.com/script/Yq1z7cIv-MA-Streak-Can-Show-When-a-Run-Is-Getting-Long-in-the-Tooth/\n ' def ma_streak(self, dataframe: DataFrame, period: int=4, source_type='close') -> Series: dataframe = dataframe.copy() avgval = self.zlema(dataframe[source_type], period) arr = np.diff(avgval) pos = np.clip(arr, 0, 1).astype(bool).cumsum() neg = np.clip(arr, -1, 0).astype(bool).cumsum() streak = np.where(arr >= 0, pos - np.maximum.accumulate(np.where(arr <= 0, pos, 0)), -neg + np.maximum.accumulate(np.where(arr >= 0, neg, 0))) res = np.concatenate((np.full(dataframe.shape[0] - streak.shape[0], np.nan), streak)) return res '\n Percent Change Channel\n PCC is like KC unless it uses percentage changes in price to set channel distance.\n https://www.tradingview.com/script/6wwAWXA1-MA-Streak-Change-Channel/\n ' def pcc(self, dataframe: DataFrame, period: int=20, mult: int=2): PercentChangeChannel = namedtuple('PercentChangeChannel', ['upperband', 'middleband', 'lowerband']) dataframe = dataframe.copy() close = dataframe['close'] previous_close = close.shift() low = dataframe['low'] high = dataframe['high'] close_change = (close - previous_close) / previous_close * 100 high_change = (high - close) / close * 100 low_change = (low - close) / close * 100 mid = self.zlema(close_change, period) rangema = self.zlema(high_change - low_change, period) upper = mid + rangema * mult lower = mid - rangema * mult return PercentChangeChannel(upper, rangema, lower) def zlema(self, series: Series, period): ema1 = ta.EMA(series, period) ema2 = ta.EMA(ema1, period) d = ema1 - ema2 zlema = ema1 + d return zlema '\n Price protection on trade entry and timeouts, built-in Freqtrade functionality\n https://www.freqtrade.io/en/latest/strategy-advanced/\n ' def check_entry_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: bid_strategy = self.config.get('bid_strategy', {}) ob = self.dp.orderbook(pair, 1) current_price = ob[f"{bid_strategy['price_side']}s"][0][0] if current_price > order['price'] * 1.01: return True return False def check_exit_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: ask_strategy = self.config.get('ask_strategy', {}) ob = self.dp.orderbook(pair, 1) current_price = ob[f"{ask_strategy['price_side']}s"][0][0] if current_price < order['price'] * 0.99: return True return False def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, **kwargs) -> bool: bid_strategy = self.config.get('bid_strategy', {}) ob = self.dp.orderbook(pair, 1) current_price = ob[f"{bid_strategy['price_side']}s"][0][0] if current_price > rate * 1.01: return False return True '\nSub-strategy overrides\nAnything not explicity defined here will follow the settings in the base strategy\n' # Sub-strategy with parameters specific to BTC stake class Stinkfist_BTC(Stinkfist): timeframe = '5m' inf_timeframe = '1h' entry_params = {'inf-pct-adr': 0.91556, 'mp': 66} use_exit_signal = False # Sub-strategy with parameters specific to ETH stake class Stinkfist_ETH(Stinkfist): timeframe = '5m' inf_timeframe = '1h' entry_params = {'inf-pct-adr': 0.81628, 'mp': 40} trailing_stop = True trailing_stop_positive = 0.014 trailing_stop_positive_offset = 0.022 trailing_only_offset_is_reached = False use_exit_signal = False