import numpy as np import talib.abstract as ta 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, timedelta from freqtrade.persistence import Trade from technical.indicators import RMI from statistics import mean from cachetools import TTLCache class Schism5(IStrategy): INTERFACE_VERSION = 3 '\n Strategy Configuration Items\n ' timeframe = '5m' inf_timeframe = '1h' entry_params = {'bear-entry-rsi': 49, 'bull-entry-rsi': 39} exit_params = {'bear-exit-rsi': 86, 'bull-exit-rsi': 86} minimal_roi = {'0': 0.14025, '34': 0.08031, '86': 0.03995, '203': 0} stoploss = -0.3 use_custom_stoploss = True custom_stop_ramp_minutes = 110 custom_stop_trailing = 0.001 use_exit_signal = True exit_profit_only = True ignore_roi_if_entry_signal = True startup_candle_count: int = 72 custom_trade_info = {} custom_current_price_cache: TTLCache = TTLCache(maxsize=100, ttl=300) '\n Informative Pair Definitions\n ' def informative_pairs(self): # add existing pairs from whitelist on the inf_timeframe 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: self.custom_trade_info[metadata['pair']] = self.populate_trades(metadata['pair']) dataframe['rmi-slow'] = RMI(dataframe, length=21, mom=5) dataframe['rmi-fast'] = RMI(dataframe, length=8, mom=4) dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) 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 = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=self.inf_timeframe) informative['rsi'] = ta.RSI(informative, timeperiod=14) dataframe = merge_informative_pair(dataframe, informative, self.timeframe, self.inf_timeframe, ffill=True) dataframe['bull'] = dataframe[f'rsi_{self.inf_timeframe}'].gt(60).astype('int') * 20 return dataframe '\n Buy Trigger Signals\n ' def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: params = self.get_pair_params(metadata['pair'], 'entry') trade_data = self.custom_trade_info[metadata['pair']] conditions = [] if trade_data['active_trade']: profit_factor = 1 - dataframe['rmi-slow'].iloc[-1] / 400 rmi_grow = self.linear_growth(30, 70, 0, 240, trade_data['open_minutes']) 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: conditions.append((dataframe['bull'] > 0) & qtpylib.crossed_below(dataframe['rsi'], params['bull-entry-rsi']) | ~(dataframe['bull'] > 0) & qtpylib.crossed_below(dataframe['rsi'], params['bear-entry-rsi'])) 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.get_pair_params(metadata['pair'], 'exit') conditions = [] conditions.append((dataframe['bull'] > 0) & (dataframe['rsi'] > params['bull-exit-rsi']) | ~(dataframe['bull'] > 0) & (dataframe['rsi'] > params['bear-exit-rsi'])) conditions.append(dataframe['volume'].gt(0)) if conditions: dataframe.loc[reduce(lambda x, y: x & y, conditions), 'exit'] = 1 return dataframe def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: since_open = current_time - trade.open_date sl_pct = 1 - max(min(since_open / timedelta(minutes=self.custom_stop_ramp_minutes), 1), 0) ** 3 sl_ramp = self.stoploss * sl_pct return min(0, sl_ramp) - self.custom_stop_trailing 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 '\n 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 * time) def get_pair_params(self, pair: str, side: str) -> Dict: entry_params = self.entry_params exit_params = self.exit_params ### Stake: USD if pair in ('ABC/XYZ', 'DEF/XYZ'): entry_params = self.entry_params_GROUP1 exit_params = self.exit_params_GROUP1 elif pair in 'QRD/WTF': entry_params = self.entry_params_QRD exit_params = self.exit_params_QRD if side == 'exit': return exit_params return entry_params class Schism5_BTC(Schism5): timeframe = '1h' inf_timeframe = '4h' entry_params = {'inf-rsi': 64, 'mp': 55, 'rmi-fast': 31, 'rmi-slow': 16, 'xinf-stake-rmi': 67, 'xtf-fiat-rsi': 17, 'xtf-stake-rsi': 57} minimal_roi = {'0': 0.05, '240': 0.025, '1440': 0.01, '4320': 0} use_exit_signal = False class Schism5_ETH(Schism5): timeframe = '1h' inf_timeframe = '4h' entry_params = {'inf-rsi': 13, 'inf-stake-rmi': 69, 'mp': 40, 'rmi-fast': 42, 'rmi-slow': 17, 'tf-fiat-rsi': 15, 'tf-stake-rsi': 92} minimal_roi = {'0': 0.05, '240': 0.025, '1440': 0.01, '4320': 0} use_exit_signal = False