diff --git a/.gitignore b/.gitignore index e5ac932f8..5334e7769 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ config*.json .hyperopt logfile.txt hyperopt_trials.pickle -user_data/ freqtrade-plot.html # Byte-compiled / optimized / DLL files diff --git a/config.json.example b/config.json.example index d3dbeb52e..e00542159 100644 --- a/config.json.example +++ b/config.json.example @@ -5,6 +5,9 @@ "fiat_display_currency": "USD", "ticker_interval" : "5m", "dry_run": false, + "trailing_stop": { + "positive" : 0.005 + }, "unfilledtimeout": 600, "bid_strategy": { "ask_last_balance": 0.0 diff --git a/config_full.json.example b/config_full.json.example index 77ef0faa0..570276e77 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -5,6 +5,7 @@ "fiat_display_currency": "USD", "dry_run": false, "ticker_interval": "5m", + "trailing_stop": true, "minimal_roi": { "40": 0.0, "30": 0.01, diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index dcb5376ce..2934d28dd 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -14,7 +14,6 @@ from freqtrade.exchange import get_ticker_history from freqtrade.persistence import Trade from freqtrade.strategy.resolver import StrategyResolver - logger = logging.getLogger(__name__) @@ -31,6 +30,7 @@ class Analyze(object): Analyze class contains everything the bot need to determine if the situation is good for buying or selling. """ + def __init__(self, config: dict) -> None: """ Init Analyze @@ -195,10 +195,41 @@ class Analyze(object): :return True if bot should sell at current rate """ current_profit = trade.calc_profit_percent(current_rate) - if self.strategy.stoploss is not None and current_profit < self.strategy.stoploss: + + if trade.stop_loss is None: + # initially adjust the stop loss to the base value + trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss) + + # evaluate if the stoploss was hit + if self.strategy.stoploss is not None and trade.stop_loss >= current_rate: + + if 'trailing_stop' in self.config and self.config['trailing_stop']: + logger.warning( + "HIT STOP: current price at {:.6f}, stop loss is {:.6f}, " + "initial stop loss was at {:.6f}, trade opened at {:.6f}".format( + current_rate, trade.stop_loss, trade.initial_stop_loss, trade.open_rate)) + logger.debug("trailing stop saved us: {:.6f}" + .format(trade.stop_loss - trade.initial_stop_loss)) + logger.debug('Stop loss hit.') return True + # update the stop loss afterwards, after all by definition it's supposed to be hanging + if 'trailing_stop' in self.config and self.config['trailing_stop']: + + # check if we have a special stop loss for positive condition + # and if profit is positive + stop_loss_value = self.strategy.stoploss + if isinstance(self.config['trailing_stop'], dict) and \ + 'positive' in self.config['trailing_stop'] and \ + current_profit > 0: + + logger.debug("using positive stop loss mode: {} since we have profit {}".format( + self.config['trailing_stop']['positive'], current_profit)) + stop_loss_value = self.config['trailing_stop']['positive'] + + trade.adjust_stop_loss(current_rate, stop_loss_value) + # Check if time matches and current rate is above threshold time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60 for duration, threshold in self.strategy.minimal_roi.items(): diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 2d497662e..fd83d73e2 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -148,6 +148,12 @@ class Trade(_DECL_BASE): open_date = Column(DateTime, nullable=False, default=datetime.utcnow) close_date = Column(DateTime) open_order_id = Column(String) + # absolute value of the stop loss + stop_loss = Column(Float, nullable=True, default=0.0) + # absolute value of the initial stop loss + initial_stop_loss = Column(Float, nullable=True, default=0.0) + # absolute value of the highest reached price + max_rate = Column(Float, nullable=True, default=0.0) def __repr__(self): return 'Trade(id={}, pair={}, amount={:.8f}, open_rate={:.8f}, open_since={})'.format( @@ -158,6 +164,50 @@ class Trade(_DECL_BASE): arrow.get(self.open_date).humanize() if self.is_open else 'closed' ) + def adjust_stop_loss(self, current_price, stoploss): + """ + + this adjusts the stop loss to it's most recently observed + setting + :param current_price: + :param stoploss: + :return: + """ + + new_loss = Decimal(current_price * (1 - abs(stoploss))) + + # keeping track of the highest observed rate for this trade + if self.max_rate is None: + self.max_rate = current_price + else: + if current_price > self.max_rate: + self.max_rate = current_price + + # no stop loss assigned yet + if self.stop_loss is None or self.stop_loss == 0: + logger.debug("assigning new stop loss") + self.stop_loss = new_loss + self.initial_stop_loss = new_loss + + # evaluate if the stop loss needs to be updated + else: + if new_loss > self.stop_loss: # stop losses only walk up, never down! + self.stop_loss = new_loss + logger.debug("adjusted stop loss") + else: + logger.debug("keeping current stop loss") + + logger.debug( + "{} - current price {:.8f}, bought at {:.8f} and calculated " + "stop loss is at: {:.8f} initial stop at {:.8f}. trailing stop loss saved us: {:.8f} " + "and max observed rate was {:.8f}".format( + self.pair, current_price, self.open_rate, + self.initial_stop_loss, + self.stop_loss, float(self.stop_loss) - float(self.initial_stop_loss), + self.max_rate + + )) + def update(self, order: Dict) -> None: """ Updates this entity with amount and actual open/close rates. diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index f972e64cc..51a3111fb 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -98,10 +98,11 @@ class RPC(object): trade.id, trade.pair, shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)), - '{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate)) + '{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate)), + '{:.6f}'.format(trade.amount * current_rate) ]) - columns = ['ID', 'Pair', 'Since', 'Profit'] + columns = ['ID', 'Pair', 'Since', 'Profit', 'Value'] df_statuses = DataFrame.from_records(trades_list, columns=columns) df_statuses = df_statuses.set_index(columns[0]) # The style used throughout is to return a tuple diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index f17a0115e..026d4b62f 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -84,6 +84,7 @@ def load_data_test(what): def simple_backtest(config, contour, num_results, mocker) -> None: mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) + backtesting = Backtesting(config) data = load_data_test(contour) @@ -97,6 +98,7 @@ def simple_backtest(config, contour, num_results, mocker) -> None: 'realistic': True } ) + # results :: assert len(results) == num_results diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index 1cf374b6b..729f12215 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -106,6 +106,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert 'just now' in result['Since'].all() assert 'ETH/BTC' in result['Pair'].all() assert '-0.59%' in result['Profit'].all() + assert 'Value' in result def test_rpc_daily_profit(default_conf, update, ticker, fee, diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 244910790..f8c271a30 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -29,10 +29,18 @@ def test_load_strategy(result): def test_load_strategy_custom_directory(result): resolver = StrategyResolver() extra_dir = os.path.join('some', 'path') - with pytest.raises( - FileNotFoundError, - match=r".*No such file or directory: '{}'".format(extra_dir)): - resolver._load_strategy('TestStrategy', extra_dir) + + if os.name == 'nt': + with pytest.raises( + FileNotFoundError, + match="FileNotFoundError: [WinError 3] The system cannot find the " + "path specified: '{}'".format(extra_dir)): + resolver._load_strategy('TestStrategy', extra_dir) + else: + with pytest.raises( + FileNotFoundError, + match=r".*No such file or directory: '{}'".format(extra_dir)): + resolver._load_strategy('TestStrategy', extra_dir) assert hasattr(resolver.strategy, 'populate_indicators') assert 'adx' in resolver.strategy.populate_indicators(result) diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 3e0f50fbb..3a47c2d56 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -444,6 +444,8 @@ def test_migrate_new(default_conf, fee): close_profit FLOAT, stake_amount FLOAT NOT NULL, amount FLOAT, + initial_stop_loss FLOAT, + max_rate FLOAT, open_date DATETIME NOT NULL, close_date DATETIME, open_order_id VARCHAR, diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 108c0b609..8cfcb2915 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -159,6 +159,15 @@ def plot_analyzed_dataframe(args: Namespace) -> None: fillcolor="rgba(0,176,246,0.2)", line={'color': "transparent"}, ) + bb_middle = go.Scatter( + x=data.date, + y=data.bb_middleband, + name='BB middle', + fill="tonexty", + fillcolor="rgba(0,176,246,0.2)", + line={'color': "red"}, + ) + macd = go.Scattergl(x=data['date'], y=data['macd'], name='MACD') macdsignal = go.Scattergl(x=data['date'], y=data['macdsignal'], name='MACD signal') volume = go.Bar(x=data['date'], y=data['volume'], name='Volume') @@ -173,7 +182,9 @@ def plot_analyzed_dataframe(args: Namespace) -> None: fig.append_trace(candles, 1, 1) fig.append_trace(bb_lower, 1, 1) + fig.append_trace(bb_middle, 1, 1) fig.append_trace(bb_upper, 1, 1) + fig.append_trace(buys, 1, 1) fig.append_trace(sells, 1, 1) fig.append_trace(volume, 2, 1) diff --git a/user_data/strategies/Long.py b/user_data/strategies/Long.py new file mode 100644 index 000000000..321218430 --- /dev/null +++ b/user_data/strategies/Long.py @@ -0,0 +1,94 @@ + +# --- Do not remove these libs --- +from freqtrade.strategy.interface import IStrategy +from typing import Dict, List +from hyperopt import hp +from functools import reduce +from pandas import DataFrame +# -------------------------------- + +import talib.abstract as ta +import freqtrade.vendor.qtpylib.indicators as qtpylib +import numpy # noqa + + +class Long(IStrategy): + """ + + author@: Gert Wohlgemuth + + """ + + # Minimal ROI designed for the strategy. + # This attribute will be overridden if the config file contains "minimal_roi" + minimal_roi = { + "60": 0.05, + "30": 0.06, + "20": 0.07, + "0": 0.08 + } + + # Optimal stoploss designed for the strategy + # This attribute will be overridden if the config file contains "stoploss" + stoploss = -0.15 + + # Optimal ticker interval for the strategy + ticker_interval = 60 + + def populate_indicators(self, dataframe: DataFrame) -> DataFrame: + + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + dataframe['cci'] = ta.CCI(dataframe) + dataframe['tema'] = ta.TEMA(dataframe, timeperiod=50) + + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_middleband'] = bollinger['mid'] + dataframe['bb_upperband'] = bollinger['upper'] + + # RSI + dataframe['rsi'] = ta.RSI(dataframe) + + # Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy) + rsi = 0.1 * (dataframe['rsi'] - 50) + dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1) + + # SAR Parabol + dataframe['sar'] = ta.SAR(dataframe) + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the buy signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + (dataframe['macd'] > dataframe['macdsignal']) & + (dataframe['macd'] > 0) & + (dataframe['cci'] <= 0.0) + ), + 'buy'] = 1 + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + """ + dataframe.loc[ + ( +# (dataframe['tema'] < dataframe['close']) + + (dataframe['sar'] > dataframe['close']) & + (dataframe['fisher_rsi'] > 0.3) + ), + 'sell'] = 1 + return dataframe diff --git a/user_data/strategies/Quickie.py b/user_data/strategies/Quickie.py new file mode 100644 index 000000000..cb60bd652 --- /dev/null +++ b/user_data/strategies/Quickie.py @@ -0,0 +1,75 @@ +# --- Do not remove these libs --- +from freqtrade.strategy.interface import IStrategy +from typing import Dict, List +from hyperopt import hp +from functools import reduce +from pandas import DataFrame +# -------------------------------- + +import talib.abstract as ta +import freqtrade.vendor.qtpylib.indicators as qtpylib + + +class Quickie(IStrategy): + """ + + author@: Gert Wohlgemuth + + idea: + momentum based strategie. The main idea is that it closes trades very quickly, while avoiding excessive losses. Hence a rather moderate stop loss in this case + """ + + # Minimal ROI designed for the strategy. + # This attribute will be overridden if the config file contains "minimal_roi" + minimal_roi = { + "60": 0.005, + "10": 0.01, + } + + # Optimal stoploss designed for the strategy + # This attribute will be overridden if the config file contains "stoploss" + stoploss = -0.25 + + # Optimal ticker interval for the strategy + ticker_interval = 5 + + def populate_indicators(self, dataframe: DataFrame) -> DataFrame: + dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) + dataframe['adx'] = ta.ADX(dataframe) + + dataframe['sma_200'] = ta.SMA(dataframe, timeperiod=200) + dataframe['sma_50'] = ta.SMA(dataframe, timeperiod=50) + + + # required for graphing + bollinger = qtpylib.bollinger_bands(dataframe['close'], window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_middleband'] = bollinger['mid'] + dataframe['bb_upperband'] = bollinger['upper'] + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + dataframe.loc[ + ( + ( + (dataframe['adx'] > 30) & + (dataframe['tema'] < dataframe['bb_middleband']) & + (dataframe['tema'] > dataframe['tema'].shift(1)) & + (dataframe['sma_200'] > dataframe['close']) + ) + ), + 'buy'] = 1 + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: + dataframe.loc[ + ( + ( + (dataframe['adx'] > 70) & + (dataframe['tema'] > dataframe['bb_middleband']) & + (dataframe['tema'] < dataframe['tema'].shift(1)) + ) + ), + 'sell'] = 1 + return dataframe diff --git a/user_data/strategies/Simple.py b/user_data/strategies/Simple.py new file mode 100644 index 000000000..a279dcb19 --- /dev/null +++ b/user_data/strategies/Simple.py @@ -0,0 +1,76 @@ +# --- Do not remove these libs --- +from freqtrade.strategy.interface import IStrategy +from typing import Dict, List +from hyperopt import hp +from functools import reduce +from pandas import DataFrame +# -------------------------------- + +import talib.abstract as ta +import freqtrade.vendor.qtpylib.indicators as qtpylib + + +class Simple(IStrategy): + """ + + author@: Gert Wohlgemuth + + idea: + this strategy is based on the book, 'The Simple Strategy' and can be found in detail here: + + https://www.amazon.com/Simple-Strategy-Powerful-Trading-Futures-ebook/dp/B00E66QPCG/ref=sr_1_1?ie=UTF8&qid=1525202675&sr=8-1&keywords=the+simple+strategy + """ + + # Minimal ROI designed for the strategy. + # since this strategy is planned around 5 minutes, we assume any time we have a 5% profit we should call it a day + # This attribute will be overridden if the config file contains "minimal_roi" + minimal_roi = { + "0": 0.01 + } + + # Optimal stoploss designed for the strategy + # This attribute will be overridden if the config file contains "stoploss" + stoploss = -0.25 + + # Optimal ticker interval for the strategy + ticker_interval = 5 + + def populate_indicators(self, dataframe: DataFrame) -> DataFrame: + # MACD + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + # RSI + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=7) + + # required for graphing + bollinger = qtpylib.bollinger_bands(dataframe['close'], window=12, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_upperband'] = bollinger['upper'] + dataframe['bb_middleband'] = bollinger['mid'] + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + dataframe.loc[ + ( + ( + (dataframe['macd'] > 0) # over 0 + & (dataframe['macd'] > dataframe['macdsignal']) # over signal + & (dataframe['bb_upperband'] > dataframe['bb_upperband'].shift(1)) # pointed up + & (dataframe['rsi'] > 70) # optional filter, need to investigate + ) + ), + 'buy'] = 1 + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: + # different strategy used for sell points, due to be able to duplicate it to 100% + dataframe.loc[ + ( + (dataframe['rsi'] > 80) + ), + 'sell'] = 1 + return dataframe diff --git a/user_data/strategies/ZLC.py b/user_data/strategies/ZLC.py new file mode 100644 index 000000000..d32421b3f --- /dev/null +++ b/user_data/strategies/ZLC.py @@ -0,0 +1,90 @@ +# --- Do not remove these libs --- +from freqtrade.strategy.interface import IStrategy +from typing import Dict, List +from hyperopt import hp +from functools import reduce +from pandas import DataFrame +# -------------------------------- + +import talib.abstract as ta +import freqtrade.vendor.qtpylib.indicators as qtpylib + + +class ZLC(IStrategy): + """ + + author@: Gert Wohlgemuth + """ + + # Minimal ROI designed for the strategy. + # This attribute will be overridden if the config file contains "minimal_roi" + minimal_roi = { + "60": 0.01, + "30": 0.03, + "20": 0.04, + "0": 0.01 + } + + # Optimal stoploss designed for the strategy + # This attribute will be overridden if the config file contains "stoploss" + stoploss = -0.3 + + # Optimal ticker interval for the strategy + ticker_interval = 5 + + def populate_indicators(self, dataframe: DataFrame) -> DataFrame: + dataframe['cci-slow'] = ta.CCI(dataframe, timeperiod=25) + dataframe['cci-fast'] = ta.CCI(dataframe, timeperiod=50) + dataframe['expo'] = ta.EMA(dataframe, timeperiod=35) + + # required for graphing + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_middleband'] = bollinger['mid'] + dataframe['bb_upperband'] = bollinger['upper'] + + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the buy signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + #don't buy on peak tops + (dataframe['close'] < dataframe['bb_middleband']) + # this is the main concept of evaluating buys + & (dataframe['cci-fast'] > 0) + & (dataframe['cci-slow'] > 0) + & (dataframe['close'] > dataframe['expo']) + + ) + , + 'buy'] = 1 + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + """ + dataframe.loc[ + (dataframe['close'] >= dataframe['bb_upperband']) | + ( + (dataframe['cci-fast'] < 0) + & (dataframe['cci-slow'] < 0) + & (dataframe['close'] < dataframe['expo']) + + ) + , + 'sell'] = 0 + return dataframe