From c3f3bdaa2ae03ebffba27ba71cf1b7276fa77e76 Mon Sep 17 00:00:00 2001 From: incrementby1 Date: Tue, 26 Oct 2021 00:04:40 +0200 Subject: [PATCH 01/14] Add "allow_position_stacking" value to config, which allows rebuys of a pair Add function unlock_reason(str: pair) which removes all PairLocks with reason Provide demo strategy that allows buying the same pair multiple times --- StackingConfig.json | 89 +++ StackingDemo.py | 593 +++++++++++++++++++ freqtrade/configuration/configuration.py | 6 + freqtrade/freqtradebot.py | 17 +- freqtrade/persistence/pairlock_middleware.py | 18 + freqtrade/strategy/interface.py | 9 + 6 files changed, 727 insertions(+), 5 deletions(-) create mode 100644 StackingConfig.json create mode 100644 StackingDemo.py diff --git a/StackingConfig.json b/StackingConfig.json new file mode 100644 index 000000000..750ef92c6 --- /dev/null +++ b/StackingConfig.json @@ -0,0 +1,89 @@ + +{ + "max_open_trades": 12, + "stake_currency": "USDT", + "stake_amount": 100, + "tradable_balance_ratio": 0.99, + "fiat_display_currency": "USD", + "timeframe": "5m", + "dry_run": true, + "cancel_open_orders_on_exit": false, + "allow_position_stacking": true, + "unfilledtimeout": { + "buy": 10, + "sell": 30, + "unit": "minutes" + }, + "bid_strategy": { + "price_side": "ask", + "ask_last_balance": 0.0, + "use_order_book": true, + "order_book_top": 1, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "ask_strategy": { + "price_side": "bid", + "use_order_book": true, + "order_book_top": 1 + }, + "exchange": { + "name": "binance", + "key": "", + "secret": "", + "ccxt_config": {}, + "ccxt_async_config": {}, + "pair_whitelist": [ + ], + "pair_blacklist": [ + "BNB/.*" + ] + }, + "pairlists": [ + { + "method": "VolumePairList", + "number_assets": 80, + "sort_key": "quoteVolume", + "min_value": 0, + "refresh_period": 1800 + } + ], + "edge": { + "enabled": false, + "process_throttle_secs": 3600, + "calculate_since_number_of_days": 7, + "allowed_risk": 0.01, + "stoploss_range_min": -0.01, + "stoploss_range_max": -0.1, + "stoploss_range_step": -0.01, + "minimum_winrate": 0.60, + "minimum_expectancy": 0.20, + "min_trade_number": 10, + "max_trade_duration_minute": 1440, + "remove_pumps": false + }, + "telegram": { + "enabled": false, + "token": "", + "chat_id": "" + }, + "api_server": { + "enabled": true, + "listen_ip_address": "127.0.0.1", + "listen_port": 8080, + "verbosity": "error", + "enable_openapi": false, + "jwt_secret_key": "908cd4469c824f3838bfe56e4120d3a3dbda5294ef583ffc62c82f54d2c1bf58", + "CORS_origins": [], + "username": "user", + "password": "pass" + }, + "bot_name": "freqtrade", + "initial_state": "running", + "forcebuy_enable": false, + "internals": { + "process_throttle_secs": 5 + } +} diff --git a/StackingDemo.py b/StackingDemo.py new file mode 100644 index 000000000..739e847b7 --- /dev/null +++ b/StackingDemo.py @@ -0,0 +1,593 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +# flake8: noqa: F401 + +# --- Do not remove these libs --- +import numpy as np # noqa +import pandas as pd # noqa +from pandas import DataFrame + +from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter, + IStrategy, IntParameter) + +# -------------------------------- +# Add your lib to import here +import talib.abstract as ta +import freqtrade.vendor.qtpylib.indicators as qtpylib + +from freqtrade.persistence import Trade +from datetime import datetime,timezone,timedelta + +""" + Warning: +This is still work in progress, so there is no warranty that everything works as intended, +it is possible that this strategy results in huge losses or doesn't even work at all. +Make sure to only run this in dry_mode so you don't lose any money. + +""" + +class StackingDemo(IStrategy): + """ + This is the default strategy template with added functions for trade stacking / buying the same positions multiple times. + It should function like this: + Find good buys using indicators. + When a new buy occurs the strategy will enable rebuys of the pair like this: + self.custom_info[metadata["pair"]]["rebuy"] = 1 + Then, if the price should drop after the last buy within the timerange of rebuy_time_limit_hours, + the same pair will be purchased again. This is intended to help with reducing possible losses. + If the price only goes up after the first buy, the strategy won't buy this pair again, and after the time limit is over, + look for other pairs to buy. + For selling there is this flag: + self.custom_info[metadata["pair"]]["resell"] = 1 + which should simply sell all trades of this pair until none are left. + + You can set how many pairs you want to trade and how many trades you want to allow for a pair, + but you must make sure to set max_open_trades to the produce of max_open_pairs and max_open_trades in your configuration file. + Also allow_position_stacking has to be set to true in the configuration file. + + For backtesting make sure to provide --enable-position-stacking as an argument in the command line. + Backtesting will be slow. + Hyperopt was not tested. + + # run the bot: + freqtrade trade -c StackingConfig.json -s StackingDemo --db-url sqlite:///tradesv3_StackingDemo_dry-run.sqlite --dry-run + """ + # Strategy interface version - allow new iterations of the strategy interface. + # Check the documentation or the Sample strategy to get the latest version. + INTERFACE_VERSION = 2 + + # how many pairs to trade / trades per pair if allow_position_stacking is enabled + max_open_pairs, max_trades_per_pair = 4, 3 + # make sure to have this value in your config file + max_open_trades = max_open_pairs * max_trades_per_pair + + # debugging + print_trades = True + + # specify for how long to want to allow rebuys of this pair + rebuy_time_limit_hours = 2 + + # store additional information needed for this strategy: + custom_info = {} + custom_num_open_pairs = {} + + # 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.02, + "0": 0.001 + } + + # Optimal stoploss designed for the strategy. + # This attribute will be overridden if the config file contains "stoploss". + stoploss = -0.10 + + # Trailing stoploss + trailing_stop = False + # trailing_only_offset_is_reached = False + # trailing_stop_positive = 0.01 + # trailing_stop_positive_offset = 0.0 # Disabled / not configured + + # Optimal timeframe for the strategy. + timeframe = '5m' + + # Run "populate_indicators()" only for new candle. + process_only_new_candles = False + + # These values can be overridden in the "ask_strategy" section in the config. + use_sell_signal = True + sell_profit_only = False + ignore_roi_if_buy_signal = False + + # Number of candles the strategy requires before producing valid signals + startup_candle_count: int = 30 + + # Optional order type mapping. + order_types = { + 'buy': 'market', + 'sell': 'market', + 'stoploss': 'market', + 'stoploss_on_exchange': False + } + + # Optional order time in force. + order_time_in_force = { + 'buy': 'gtc', + 'sell': 'gtc' + } + + plot_config = { + # Main plot indicators (Moving averages, ...) + 'main_plot': { + 'tema': {}, + 'sar': {'color': 'white'}, + }, + 'subplots': { + # Subplots - each dict defines one additional plot + "MACD": { + 'macd': {'color': 'blue'}, + 'macdsignal': {'color': 'orange'}, + }, + "RSI": { + 'rsi': {'color': 'red'}, + } + } + } + def informative_pairs(self): + """ + Define additional, informative pair/interval combinations to be cached from the exchange. + These pair/interval combinations are non-tradeable, unless they are part + of the whitelist as well. + For more information, please consult the documentation + :return: List of tuples in the format (pair, interval) + Sample: return [("ETH/USDT", "5m"), + ("BTC/USDT", "15m"), + ] + """ + return [] + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Adds several different TA indicators to the given DataFrame + + Performance Note: For the best performance be frugal on the number of indicators + you are using. Let uncomment only the indicator you are using in your strategies + or your hyperopt configuration, otherwise you will waste your memory and CPU usage. + :param dataframe: Dataframe with data from the exchange + :param metadata: Additional information, like the currently traded pair + :return: a Dataframe with all mandatory indicators for the strategies + """ + + # STACKING STUFF + + # confirm config + self.max_trades_per_pair = self.config['max_open_trades'] / self.max_open_pairs + if not self.config["allow_position_stacking"]: + self.max_trades_per_pair = 1 + + # store number of open pairs + self.custom_num_open_pairs = {"num_open_pairs": 0} + + # Store custom information for this pair: + if not metadata["pair"] in self.custom_info: + self.custom_info[metadata["pair"]] = {} + + if not "rebuy" in self.custom_info[metadata["pair"]]: + # number of trades for this pair + self.custom_info[metadata["pair"]]["num_trades"] = 0 + # use rebuy/resell as buy-/sell- indicators + self.custom_info[metadata["pair"]]["rebuy"] = 0 + self.custom_info[metadata["pair"]]["resell"] = 0 + # store latest open_date for this pair + self.custom_info[metadata["pair"]]["last_open_date"] = datetime.now(timezone.utc) - timedelta(days=100) + # stare the value of the latest open price for this pair + self.custom_info[metadata["pair"]]["latest_open_rate"] = 0 + + # INDICATORS + + # Momentum Indicators + # ------------------------------------ + + # ADX + dataframe['adx'] = ta.ADX(dataframe) + + # # Plus Directional Indicator / Movement + # dataframe['plus_dm'] = ta.PLUS_DM(dataframe) + # dataframe['plus_di'] = ta.PLUS_DI(dataframe) + + # # Minus Directional Indicator / Movement + # dataframe['minus_dm'] = ta.MINUS_DM(dataframe) + # dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # # Aroon, Aroon Oscillator + # aroon = ta.AROON(dataframe) + # dataframe['aroonup'] = aroon['aroonup'] + # dataframe['aroondown'] = aroon['aroondown'] + # dataframe['aroonosc'] = ta.AROONOSC(dataframe) + + # # Awesome Oscillator + # dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) + + # # Keltner Channel + # keltner = qtpylib.keltner_channel(dataframe) + # dataframe["kc_upperband"] = keltner["upper"] + # dataframe["kc_lowerband"] = keltner["lower"] + # dataframe["kc_middleband"] = keltner["mid"] + # dataframe["kc_percent"] = ( + # (dataframe["close"] - dataframe["kc_lowerband"]) / + # (dataframe["kc_upperband"] - dataframe["kc_lowerband"]) + # ) + # dataframe["kc_width"] = ( + # (dataframe["kc_upperband"] - dataframe["kc_lowerband"]) / dataframe["kc_middleband"] + # ) + + # # Ultimate Oscillator + # dataframe['uo'] = ta.ULTOSC(dataframe) + + # # Commodity Channel Index: values [Oversold:-100, Overbought:100] + # dataframe['cci'] = ta.CCI(dataframe) + + # 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'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1) + + # # Inverse Fisher transform on RSI normalized: values [0.0, 100.0] (https://goo.gl/2JGGoy) + # dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) + + # # Stochastic Slow + # stoch = ta.STOCH(dataframe) + # dataframe['slowd'] = stoch['slowd'] + # dataframe['slowk'] = stoch['slowk'] + + # Stochastic Fast + stoch_fast = ta.STOCHF(dataframe) + dataframe['fastd'] = stoch_fast['fastd'] + dataframe['fastk'] = stoch_fast['fastk'] + + # # Stochastic RSI + # Please read https://github.com/freqtrade/freqtrade/issues/2961 before using this. + # STOCHRSI is NOT aligned with tradingview, which may result in non-expected results. + # stoch_rsi = ta.STOCHRSI(dataframe) + # dataframe['fastd_rsi'] = stoch_rsi['fastd'] + # dataframe['fastk_rsi'] = stoch_rsi['fastk'] + + # MACD + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + # MFI + dataframe['mfi'] = ta.MFI(dataframe) + + # # ROC + # dataframe['roc'] = ta.ROC(dataframe) + + # Overlap Studies + # ------------------------------------ + + # Bollinger Bands + 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'] + dataframe["bb_percent"] = ( + (dataframe["close"] - dataframe["bb_lowerband"]) / + (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) + ) + dataframe["bb_width"] = ( + (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) / dataframe["bb_middleband"] + ) + + # Bollinger Bands - Weighted (EMA based instead of SMA) + # weighted_bollinger = qtpylib.weighted_bollinger_bands( + # qtpylib.typical_price(dataframe), window=20, stds=2 + # ) + # dataframe["wbb_upperband"] = weighted_bollinger["upper"] + # dataframe["wbb_lowerband"] = weighted_bollinger["lower"] + # dataframe["wbb_middleband"] = weighted_bollinger["mid"] + # dataframe["wbb_percent"] = ( + # (dataframe["close"] - dataframe["wbb_lowerband"]) / + # (dataframe["wbb_upperband"] - dataframe["wbb_lowerband"]) + # ) + # dataframe["wbb_width"] = ( + # (dataframe["wbb_upperband"] - dataframe["wbb_lowerband"]) / dataframe["wbb_middleband"] + # ) + + # # EMA - Exponential Moving Average + # dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) + # dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) + # dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) + # dataframe['ema21'] = ta.EMA(dataframe, timeperiod=21) + # dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) + # dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) + + # # SMA - Simple Moving Average + # dataframe['sma3'] = ta.SMA(dataframe, timeperiod=3) + # dataframe['sma5'] = ta.SMA(dataframe, timeperiod=5) + # dataframe['sma10'] = ta.SMA(dataframe, timeperiod=10) + # dataframe['sma21'] = ta.SMA(dataframe, timeperiod=21) + # dataframe['sma50'] = ta.SMA(dataframe, timeperiod=50) + # dataframe['sma100'] = ta.SMA(dataframe, timeperiod=100) + + # Parabolic SAR + dataframe['sar'] = ta.SAR(dataframe) + + # TEMA - Triple Exponential Moving Average + dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) + + # Cycle Indicator + # ------------------------------------ + # Hilbert Transform Indicator - SineWave + hilbert = ta.HT_SINE(dataframe) + dataframe['htsine'] = hilbert['sine'] + dataframe['htleadsine'] = hilbert['leadsine'] + + # Pattern Recognition - Bullish candlestick patterns + # ------------------------------------ + # # Hammer: values [0, 100] + # dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) + # # Inverted Hammer: values [0, 100] + # dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) + # # Dragonfly Doji: values [0, 100] + # dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) + # # Piercing Line: values [0, 100] + # dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] + # # Morningstar: values [0, 100] + # dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] + # # Three White Soldiers: values [0, 100] + # dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] + + # Pattern Recognition - Bearish candlestick patterns + # ------------------------------------ + # # Hanging Man: values [0, 100] + # dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) + # # Shooting Star: values [0, 100] + # dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) + # # Gravestone Doji: values [0, 100] + # dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) + # # Dark Cloud Cover: values [0, 100] + # dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) + # # Evening Doji Star: values [0, 100] + # dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) + # # Evening Star: values [0, 100] + # dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) + + # Pattern Recognition - Bullish/Bearish candlestick patterns + # ------------------------------------ + # # Three Line Strike: values [0, -100, 100] + # dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) + # # Spinning Top: values [0, -100, 100] + # dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] + # # Engulfing: values [0, -100, 100] + # dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] + # # Harami: values [0, -100, 100] + # dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] + # # Three Outside Up/Down: values [0, -100, 100] + # dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] + # # Three Inside Up/Down: values [0, -100, 100] + # dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] + + # # Chart type + # # ------------------------------------ + # # Heikin Ashi Strategy + # heikinashi = qtpylib.heikinashi(dataframe) + # dataframe['ha_open'] = heikinashi['open'] + # dataframe['ha_close'] = heikinashi['close'] + # dataframe['ha_high'] = heikinashi['high'] + # dataframe['ha_low'] = heikinashi['low'] + + # Retrieve best bid and best ask from the orderbook + # ------------------------------------ + """ + # first check if dataprovider is available + if self.dp: + if self.dp.runmode.value in ('live', 'dry_run'): + ob = self.dp.orderbook(metadata['pair'], 1) + dataframe['best_bid'] = ob['bids'][0][0] + dataframe['best_ask'] = ob['asks'][0][0] + """ + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the buy signal for the given dataframe + :param dataframe: DataFrame populated with indicators + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + ( +# (qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 +# (dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle +# (dataframe['tema'] > dataframe['tema'].shift(1)) | # Guard: tema is raising + (dataframe['close'] < dataframe['close'].shift(1)) | + # use either buy signal or rebuy flag to trigger a buy + (self.custom_info[metadata["pair"]]["rebuy"] == 1) + ) & + (dataframe['volume'] > 0) # Make sure Volume is not 0 + ), + 'buy'] = 1 + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame populated with indicators + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + ( +# (qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 +# (dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle +# (dataframe['tema'] < dataframe['tema'].shift(1)) | # Guard: tema is falling + # use either sell signal or resell flag to trigger a sell + (dataframe['close'] > dataframe['close'].shift(1)) | + (self.custom_info[metadata["pair"]]["resell"] == 1) + ) & + (dataframe['volume'] > 0) # Make sure Volume is not 0 + ), + 'sell'] = 1 + return dataframe + + # use_custom_sell = True + + def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, + current_profit: float, **kwargs) -> 'Optional[Union[str, bool]]': + """ + Custom sell signal logic indicating that specified position should be sold. Returning a + string or True from this method is equal to setting sell signal on a candle at specified + time. This method is not called when sell signal is set. + + This method should be overridden to create sell signals that depend on trade parameters. For + example you could implement a sell relative to the candle when the trade was opened, + or a custom 1:2 risk-reward ROI. + + Custom sell reason max length is 64. Exceeding characters will be removed. + + :param pair: Pair that's currently analyzed + :param trade: trade object. + :param current_time: datetime object, containing the current datetime + :param current_rate: Rate, calculated based on pricing settings in ask_strategy. + :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return: To execute sell, return a string with custom sell reason or True. Otherwise return + None or False. + """ + # if self.custom_info[pair]["resell"] == 1: + # return 'resell' + return None + + def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, + time_in_force: str, current_time: 'datetime', **kwargs) -> bool: + return_statement = True + + if self.config['allow_position_stacking']: + return_statement = self.check_open_trades(pair, rate, current_time) + + # debugging + if return_statement and self.print_trades: + # use str.join() for speed + out = (current_time.strftime("%c"), " Bought: ", pair, ", rate: ", str(rate), ", rebuy: ", str(self.custom_info[pair]["rebuy"]), ", trades: ", str(self.custom_info[pair]["num_trades"])) + print("".join(out)) + + return return_statement + + def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float, + rate: float, time_in_force: str, sell_reason: str, + current_time: 'datetime', **kwargs) -> bool: + + if self.config["allow_position_stacking"]: + + # unlock open pairs limit after every sell + self.unlock_reason('Open pairs limit') + + # unlock open pairs limit after last item is sold + if self.custom_info[pair]["num_trades"] == 1: + # decrement open_pairs_count by 1 if last item is sold + self.custom_num_open_pairs["num_open_pairs"]-=1 + self.custom_info[pair]["resell"] = 0 + # reset rate + self.custom_info[pair]["latest_open_rate"] = 0.0 + self.unlock_reason('Trades per pair limit') + + # change dataframe to produce sell signal after a sell + if self.custom_info[pair]["num_trades"] >= self.max_trades_per_pair: + self.custom_info[pair]["resell"] = 1 + + # decrement number of trades by 1: + self.custom_info[pair]["num_trades"]-=1 + + # debugging stuff + if self.print_trades: + # use str.join() for speed + out = (current_time.strftime("%c"), " Sold: ", pair, ", rate: ", str(rate),", profit: ", str(trade.calc_profit_ratio(rate)), ", resell: ", str(self.custom_info[pair]["resell"]), ", trades: ", str(self.custom_info[pair]["num_trades"])) + print("".join(out)) + + return True + + def check_open_trades(self, pair: str, rate: float, current_time: datetime): + + # retrieve information about current open pairs + tr_info = self.get_trade_information(pair) + + # update number of open trades for the pair + self.custom_info[pair]["num_trades"] = tr_info[1] + self.custom_num_open_pairs["num_open_pairs"] = len(tr_info[0]) + # update value of the last open price + self.custom_info[pair]["latest_open_rate"] = tr_info[2] + + # don't buy if we have enough trades for this pair + if self.custom_info[pair]["num_trades"] >= self.max_trades_per_pair: + # lock if we already have enough pairs open, will be unlocked after last item of a pair is sold + self.lock_pair(pair, until=datetime.now(timezone.utc) + timedelta(days=100), reason='Trades per pair limit') + self.custom_info[pair]["rebuy"] = 0 + return False + + # don't buy if we have enough pairs + if self.custom_num_open_pairs["num_open_pairs"] >= self.max_open_pairs: + if not pair in tr_info[0]: + # lock if this pair is not in our list, will be unlocked after the next sell + self.lock_pair(pair, until=datetime.now(timezone.utc) + timedelta(days=100), reason='Open pairs limit') + self.custom_info[pair]["rebuy"] = 0 + return False + + # don't buy at a higher price, try until time limit is exceeded; skips if it's the first trade' + if rate > self.custom_info[pair]["latest_open_rate"] and self.custom_info[pair]["latest_open_rate"] != 0.0: + # how long do we want to try buying cheaper before we look for other pairs? + if (current_time - self.custom_info[pair]['last_open_date']).seconds/3600 > self.rebuy_time_limit_hours: + self.custom_info[pair]["rebuy"] = 0 + self.unlock_reason('Open pairs limit') + return False + + # set rebuy flag if num_trades < limit-1 + if self.custom_info[pair]["num_trades"] < self.max_trades_per_pair-1: + self.custom_info[pair]["rebuy"] = 1 + else: + self.custom_info[pair]["rebuy"] = 0 + + # update rate + self.custom_info[pair]["latest_open_rate"] = rate + + #update date open + self.custom_info[pair]["last_open_date"] = current_time + + # increment trade count by 1 + self.custom_info[pair]["num_trades"]+=1 + + return True + + # custom function to help with the strategy + def get_trade_information(self, pair:str): + + latest_open_rate, trade_count = 0, 0.0 + # store all open pairs + open_pairs = [] + + ### start nested function + def compare_trade(trade: Trade): + nonlocal trade_count, latest_open_rate, pair + if trade.pair == pair: + # update latest_rate + latest_open_rate = trade.open_rate + trade_count+=1 + return trade.pair + ### end nested function + + # replaced for loop with map for speed + open_pairs = map(compare_trade, Trade.get_open_trades()) + # remove duplicates + open_pairs = (list(dict.fromkeys(open_pairs))) + + #print(*open_pairs, sep="\n") + + # put this all together to reduce the amount of loops + return open_pairs, trade_count, latest_open_rate diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 822577916..d4cf09821 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -137,6 +137,12 @@ class Configuration: setup_logging(config) def _process_trading_options(self, config: Dict[str, Any]) -> None: + + # Allow_position_stacking defaults to False + if not config.get('allow_position_stacking'): + config['allow_position_stacking'] = False + logger.info('Allow_position_stacking is set to ' + str(config['allow_position_stacking'])) + if config['runmode'] not in TRADING_MODES: return diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index bf4742fdc..850cd1700 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -4,7 +4,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade() import copy import logging import traceback -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from math import isclose from threading import Lock from typing import Any, Dict, List, Optional @@ -359,10 +359,12 @@ class FreqtradeBot(LoggingMixin): logger.info("Active pair whitelist is empty.") return trades_created # Remove pairs for currently opened trades from the whitelist - for trade in Trade.get_open_trades(): - if trade.pair in whitelist: - whitelist.remove(trade.pair) - logger.debug('Ignoring %s in pair whitelist', trade.pair) + # Allow rebuying of the same pair if allow_position_stacking is set to True + if not self.config['allow_position_stacking']: + for trade in Trade.get_open_trades(): + if trade.pair in whitelist: + whitelist.remove(trade.pair) + logger.debug('Ignoring %s in pair whitelist', trade.pair) if not whitelist: logger.info("No currency pair in active pair whitelist, " @@ -592,6 +594,11 @@ class FreqtradeBot(LoggingMixin): self._notify_enter(trade, order_type) + # Lock pair for 1 timeframe duration to prevent immediate rebuys + if self.config['allow_position_stacking']: + self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc) + timedelta(minutes=timeframe_to_minutes(self.config['timeframe'])), + reason='Prevent immediate rebuys') + return True def _notify_enter(self, trade: Trade, order_type: str) -> None: diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index 8662fc36d..6e0164182 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -103,6 +103,24 @@ class PairLocks(): if PairLocks.use_db: PairLock.query.session.commit() + @staticmethod + def unlock_reason(reason: str, now: Optional[datetime] = None) -> None: + """ + Release all locks for this reason. + :param reason: Which reason to unlock + :param now: Datetime object (generated via datetime.now(timezone.utc)). + defaults to datetime.now(timezone.utc) + """ + if not now: + now = datetime.now(timezone.utc) + logger.info(f"Releasing all locks with reason \'{reason}\'.") + locks = PairLocks.get_all_locks() + for lock in locks: + if lock.reason == reason: + lock.active = False + if PairLocks.use_db: + PairLock.query.session.commit() + @staticmethod def is_global_lock(now: Optional[datetime] = None) -> bool: """ diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 7420bd9fd..547d9313f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -443,6 +443,15 @@ class IStrategy(ABC, HyperStrategyMixin): """ PairLocks.unlock_pair(pair, datetime.now(timezone.utc)) + def unlock_reason(self, reason: str) -> None: + """ + Unlocks all pairs previously locked using lock_pair with specified reason. + Not used by freqtrade itself, but intended to be used if users lock pairs + manually from within the strategy, to allow an easy way to unlock pairs. + :param reason: Unlock pairs to allow trading again + """ + PairLocks.unlock_reason(reason, datetime.now(timezone.utc)) + def is_pair_locked(self, pair: str, candle_date: datetime = None) -> bool: """ Checks if a pair is currently locked From ae068996941bf10f95786da39942402f77944116 Mon Sep 17 00:00:00 2001 From: incrementby1 Date: Tue, 26 Oct 2021 00:29:11 +0200 Subject: [PATCH 02/14] removed commenting --- StackingDemo.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/StackingDemo.py b/StackingDemo.py index 739e847b7..2500f86f0 100644 --- a/StackingDemo.py +++ b/StackingDemo.py @@ -403,10 +403,9 @@ class StackingDemo(IStrategy): dataframe.loc[ ( ( -# (qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 -# (dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle -# (dataframe['tema'] > dataframe['tema'].shift(1)) | # Guard: tema is raising - (dataframe['close'] < dataframe['close'].shift(1)) | + (qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 + (dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle + (dataframe['tema'] > dataframe['tema'].shift(1)) | # Guard: tema is raising # use either buy signal or rebuy flag to trigger a buy (self.custom_info[metadata["pair"]]["rebuy"] == 1) ) & @@ -426,11 +425,10 @@ class StackingDemo(IStrategy): dataframe.loc[ ( ( -# (qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 -# (dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle -# (dataframe['tema'] < dataframe['tema'].shift(1)) | # Guard: tema is falling + (qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 + (dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle + (dataframe['tema'] < dataframe['tema'].shift(1)) | # Guard: tema is falling # use either sell signal or resell flag to trigger a sell - (dataframe['close'] > dataframe['close'].shift(1)) | (self.custom_info[metadata["pair"]]["resell"] == 1) ) & (dataframe['volume'] > 0) # Make sure Volume is not 0 From 9f6e4c6c0e3c6e3fc4d2ad80cc4bfb3385b13152 Mon Sep 17 00:00:00 2001 From: incrementby1 Date: Tue, 26 Oct 2021 00:31:17 +0200 Subject: [PATCH 03/14] uncomment --- StackingDemo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/StackingDemo.py b/StackingDemo.py index 2500f86f0..b88248fac 100644 --- a/StackingDemo.py +++ b/StackingDemo.py @@ -73,8 +73,8 @@ class StackingDemo(IStrategy): # 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.02, + "60": 0.01, + "30": 0.02, "0": 0.001 } From 9c6cbc025aa6886bf551080869d1368e7d480591 Mon Sep 17 00:00:00 2001 From: incrementby1 <91958753+incrementby1@users.noreply.github.com> Date: Tue, 26 Oct 2021 00:34:01 +0200 Subject: [PATCH 04/14] Update StackingDemo.py --- StackingDemo.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/StackingDemo.py b/StackingDemo.py index 739e847b7..b88248fac 100644 --- a/StackingDemo.py +++ b/StackingDemo.py @@ -73,8 +73,8 @@ class StackingDemo(IStrategy): # 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.02, + "60": 0.01, + "30": 0.02, "0": 0.001 } @@ -403,10 +403,9 @@ class StackingDemo(IStrategy): dataframe.loc[ ( ( -# (qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 -# (dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle -# (dataframe['tema'] > dataframe['tema'].shift(1)) | # Guard: tema is raising - (dataframe['close'] < dataframe['close'].shift(1)) | + (qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 + (dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle + (dataframe['tema'] > dataframe['tema'].shift(1)) | # Guard: tema is raising # use either buy signal or rebuy flag to trigger a buy (self.custom_info[metadata["pair"]]["rebuy"] == 1) ) & @@ -426,11 +425,10 @@ class StackingDemo(IStrategy): dataframe.loc[ ( ( -# (qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 -# (dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle -# (dataframe['tema'] < dataframe['tema'].shift(1)) | # Guard: tema is falling + (qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 + (dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle + (dataframe['tema'] < dataframe['tema'].shift(1)) | # Guard: tema is falling # use either sell signal or resell flag to trigger a sell - (dataframe['close'] > dataframe['close'].shift(1)) | (self.custom_info[metadata["pair"]]["resell"] == 1) ) & (dataframe['volume'] > 0) # Make sure Volume is not 0 From 51c925f9f3737e59bcd61b7ee698afce9491784e Mon Sep 17 00:00:00 2001 From: incrementby1 <91958753+incrementby1@users.noreply.github.com> Date: Wed, 27 Oct 2021 12:40:26 +0200 Subject: [PATCH 05/14] Delete StackingConfig.json --- StackingConfig.json | 89 --------------------------------------------- 1 file changed, 89 deletions(-) delete mode 100644 StackingConfig.json diff --git a/StackingConfig.json b/StackingConfig.json deleted file mode 100644 index 750ef92c6..000000000 --- a/StackingConfig.json +++ /dev/null @@ -1,89 +0,0 @@ - -{ - "max_open_trades": 12, - "stake_currency": "USDT", - "stake_amount": 100, - "tradable_balance_ratio": 0.99, - "fiat_display_currency": "USD", - "timeframe": "5m", - "dry_run": true, - "cancel_open_orders_on_exit": false, - "allow_position_stacking": true, - "unfilledtimeout": { - "buy": 10, - "sell": 30, - "unit": "minutes" - }, - "bid_strategy": { - "price_side": "ask", - "ask_last_balance": 0.0, - "use_order_book": true, - "order_book_top": 1, - "check_depth_of_market": { - "enabled": false, - "bids_to_ask_delta": 1 - } - }, - "ask_strategy": { - "price_side": "bid", - "use_order_book": true, - "order_book_top": 1 - }, - "exchange": { - "name": "binance", - "key": "", - "secret": "", - "ccxt_config": {}, - "ccxt_async_config": {}, - "pair_whitelist": [ - ], - "pair_blacklist": [ - "BNB/.*" - ] - }, - "pairlists": [ - { - "method": "VolumePairList", - "number_assets": 80, - "sort_key": "quoteVolume", - "min_value": 0, - "refresh_period": 1800 - } - ], - "edge": { - "enabled": false, - "process_throttle_secs": 3600, - "calculate_since_number_of_days": 7, - "allowed_risk": 0.01, - "stoploss_range_min": -0.01, - "stoploss_range_max": -0.1, - "stoploss_range_step": -0.01, - "minimum_winrate": 0.60, - "minimum_expectancy": 0.20, - "min_trade_number": 10, - "max_trade_duration_minute": 1440, - "remove_pumps": false - }, - "telegram": { - "enabled": false, - "token": "", - "chat_id": "" - }, - "api_server": { - "enabled": true, - "listen_ip_address": "127.0.0.1", - "listen_port": 8080, - "verbosity": "error", - "enable_openapi": false, - "jwt_secret_key": "908cd4469c824f3838bfe56e4120d3a3dbda5294ef583ffc62c82f54d2c1bf58", - "CORS_origins": [], - "username": "user", - "password": "pass" - }, - "bot_name": "freqtrade", - "initial_state": "running", - "forcebuy_enable": false, - "internals": { - "process_throttle_secs": 5 - } -} From 6b17094c6ffee7ee81cc975a72859ffad7460163 Mon Sep 17 00:00:00 2001 From: incrementby1 <91958753+incrementby1@users.noreply.github.com> Date: Wed, 27 Oct 2021 12:41:49 +0200 Subject: [PATCH 06/14] Delete configuration.py --- freqtrade/configuration/configuration.py | 506 ----------------------- 1 file changed, 506 deletions(-) delete mode 100644 freqtrade/configuration/configuration.py diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py deleted file mode 100644 index d4cf09821..000000000 --- a/freqtrade/configuration/configuration.py +++ /dev/null @@ -1,506 +0,0 @@ -""" -This module contains the configuration class -""" -import logging -import warnings -from copy import deepcopy -from pathlib import Path -from typing import Any, Callable, Dict, List, Optional - -from freqtrade import constants -from freqtrade.configuration.check_exchange import check_exchange -from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings -from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir -from freqtrade.configuration.environment_vars import enironment_vars_to_dict -from freqtrade.configuration.load_config import load_config_file, load_file -from freqtrade.enums import NON_UTIL_MODES, TRADING_MODES, RunMode -from freqtrade.exceptions import OperationalException -from freqtrade.loggers import setup_logging -from freqtrade.misc import deep_merge_dicts, parse_db_uri_for_logging - - -logger = logging.getLogger(__name__) - - -class Configuration: - """ - Class to read and init the bot configuration - Reuse this class for the bot, backtesting, hyperopt and every script that required configuration - """ - - def __init__(self, args: Dict[str, Any], runmode: RunMode = None) -> None: - self.args = args - self.config: Optional[Dict[str, Any]] = None - self.runmode = runmode - - def get_config(self) -> Dict[str, Any]: - """ - Return the config. Use this method to get the bot config - :return: Dict: Bot config - """ - if self.config is None: - self.config = self.load_config() - - return self.config - - @staticmethod - def from_files(files: List[str]) -> Dict[str, Any]: - """ - Iterate through the config files passed in, loading all of them - and merging their contents. - Files are loaded in sequence, parameters in later configuration files - override the same parameter from an earlier file (last definition wins). - Runs through the whole Configuration initialization, so all expected config entries - are available to interactive environments. - :param files: List of file paths - :return: configuration dictionary - """ - c = Configuration({'config': files}, RunMode.OTHER) - return c.get_config() - - def load_from_files(self, files: List[str]) -> Dict[str, Any]: - - # Keep this method as staticmethod, so it can be used from interactive environments - config: Dict[str, Any] = {} - - if not files: - return deepcopy(constants.MINIMAL_CONFIG) - - # We expect here a list of config filenames - for path in files: - logger.info(f'Using config: {path} ...') - - # Merge config options, overwriting old values - config = deep_merge_dicts(load_config_file(path), config) - - # Load environment variables - env_data = enironment_vars_to_dict() - config = deep_merge_dicts(env_data, config) - - config['config_files'] = files - # Normalize config - if 'internals' not in config: - config['internals'] = {} - if 'ask_strategy' not in config: - config['ask_strategy'] = {} - - if 'pairlists' not in config: - config['pairlists'] = [] - - return config - - def load_config(self) -> Dict[str, Any]: - """ - Extract information for sys.argv and load the bot configuration - :return: Configuration dictionary - """ - # Load all configs - config: Dict[str, Any] = self.load_from_files(self.args.get("config", [])) - - # Keep a copy of the original configuration file - config['original_config'] = deepcopy(config) - - self._process_logging_options(config) - - self._process_runmode(config) - - self._process_common_options(config) - - self._process_trading_options(config) - - self._process_optimize_options(config) - - self._process_plot_options(config) - - self._process_data_options(config) - - # Check if the exchange set by the user is supported - check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True)) - - self._resolve_pairs_list(config) - - process_temporary_deprecated_settings(config) - - return config - - def _process_logging_options(self, config: Dict[str, Any]) -> None: - """ - Extract information for sys.argv and load logging configuration: - the -v/--verbose, --logfile options - """ - # Log level - config.update({'verbosity': self.args.get('verbosity', 0)}) - - if 'logfile' in self.args and self.args['logfile']: - config.update({'logfile': self.args['logfile']}) - - setup_logging(config) - - def _process_trading_options(self, config: Dict[str, Any]) -> None: - - # Allow_position_stacking defaults to False - if not config.get('allow_position_stacking'): - config['allow_position_stacking'] = False - logger.info('Allow_position_stacking is set to ' + str(config['allow_position_stacking'])) - - if config['runmode'] not in TRADING_MODES: - return - - if config.get('dry_run', False): - logger.info('Dry run is enabled') - if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]: - # Default to in-memory db for dry_run if not specified - config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL - else: - if not config.get('db_url', None): - config['db_url'] = constants.DEFAULT_DB_PROD_URL - logger.info('Dry run is disabled') - - logger.info(f'Using DB: "{parse_db_uri_for_logging(config["db_url"])}"') - - def _process_common_options(self, config: Dict[str, Any]) -> None: - - # Set strategy if not specified in config and or if it's non default - if self.args.get('strategy') or not config.get('strategy'): - config.update({'strategy': self.args.get('strategy')}) - - self._args_to_config(config, argname='strategy_path', - logstring='Using additional Strategy lookup path: {}') - - if ('db_url' in self.args and self.args['db_url'] and - self.args['db_url'] != constants.DEFAULT_DB_PROD_URL): - config.update({'db_url': self.args['db_url']}) - logger.info('Parameter --db-url detected ...') - - if config.get('forcebuy_enable', False): - logger.warning('`forcebuy` RPC message enabled.') - - # Support for sd_notify - if 'sd_notify' in self.args and self.args['sd_notify']: - config['internals'].update({'sd_notify': True}) - - def _process_datadir_options(self, config: Dict[str, Any]) -> None: - """ - Extract information for sys.argv and load directory configurations - --user-data, --datadir - """ - # Check exchange parameter here - otherwise `datadir` might be wrong. - if 'exchange' in self.args and self.args['exchange']: - config['exchange']['name'] = self.args['exchange'] - logger.info(f"Using exchange {config['exchange']['name']}") - - if 'pair_whitelist' not in config['exchange']: - config['exchange']['pair_whitelist'] = [] - - if 'user_data_dir' in self.args and self.args['user_data_dir']: - config.update({'user_data_dir': self.args['user_data_dir']}) - elif 'user_data_dir' not in config: - # Default to cwd/user_data (legacy option ...) - config.update({'user_data_dir': str(Path.cwd() / 'user_data')}) - - # reset to user_data_dir so this contains the absolute path. - config['user_data_dir'] = create_userdata_dir(config['user_data_dir'], create_dir=False) - logger.info('Using user-data directory: %s ...', config['user_data_dir']) - - config.update({'datadir': create_datadir(config, self.args.get('datadir', None))}) - logger.info('Using data directory: %s ...', config.get('datadir')) - - if self.args.get('exportfilename'): - self._args_to_config(config, argname='exportfilename', - logstring='Storing backtest results to {} ...') - config['exportfilename'] = Path(config['exportfilename']) - else: - config['exportfilename'] = (config['user_data_dir'] - / 'backtest_results') - - def _process_optimize_options(self, config: Dict[str, Any]) -> None: - - # This will override the strategy configuration - self._args_to_config(config, argname='timeframe', - logstring='Parameter -i/--timeframe detected ... ' - 'Using timeframe: {} ...') - - self._args_to_config(config, argname='position_stacking', - logstring='Parameter --enable-position-stacking detected ...') - - self._args_to_config( - config, argname='enable_protections', - logstring='Parameter --enable-protections detected, enabling Protections. ...') - - if 'use_max_market_positions' in self.args and not self.args["use_max_market_positions"]: - config.update({'use_max_market_positions': False}) - logger.info('Parameter --disable-max-market-positions detected ...') - logger.info('max_open_trades set to unlimited ...') - elif 'max_open_trades' in self.args and self.args['max_open_trades']: - config.update({'max_open_trades': self.args['max_open_trades']}) - logger.info('Parameter --max-open-trades detected, ' - 'overriding max_open_trades to: %s ...', config.get('max_open_trades')) - elif config['runmode'] in NON_UTIL_MODES: - logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) - # Setting max_open_trades to infinite if -1 - if config.get('max_open_trades') == -1: - config['max_open_trades'] = float('inf') - - if self.args.get('stake_amount', None): - # Convert explicitly to float to support CLI argument for both unlimited and value - try: - self.args['stake_amount'] = float(self.args['stake_amount']) - except ValueError: - pass - - self._args_to_config(config, argname='timeframe_detail', - logstring='Parameter --timeframe-detail detected, ' - 'using {} for intra-candle backtesting ...') - self._args_to_config(config, argname='stake_amount', - logstring='Parameter --stake-amount detected, ' - 'overriding stake_amount to: {} ...') - self._args_to_config(config, argname='dry_run_wallet', - logstring='Parameter --dry-run-wallet detected, ' - 'overriding dry_run_wallet to: {} ...') - self._args_to_config(config, argname='fee', - logstring='Parameter --fee detected, ' - 'setting fee to: {} ...') - - self._args_to_config(config, argname='timerange', - logstring='Parameter --timerange detected: {} ...') - - self._process_datadir_options(config) - - self._args_to_config(config, argname='strategy_list', - logstring='Using strategy list of {} strategies', logfun=len) - - self._args_to_config(config, argname='timeframe', - logstring='Overriding timeframe with Command line argument') - - self._args_to_config(config, argname='export', - logstring='Parameter --export detected: {} ...') - - self._args_to_config(config, argname='backtest_breakdown', - logstring='Parameter --breakdown detected ...') - - self._args_to_config(config, argname='disableparamexport', - logstring='Parameter --disableparamexport detected: {} ...') - - # Edge section: - if 'stoploss_range' in self.args and self.args["stoploss_range"]: - txt_range = eval(self.args["stoploss_range"]) - config['edge'].update({'stoploss_range_min': txt_range[0]}) - config['edge'].update({'stoploss_range_max': txt_range[1]}) - config['edge'].update({'stoploss_range_step': txt_range[2]}) - logger.info('Parameter --stoplosses detected: %s ...', self.args["stoploss_range"]) - - # Hyperopt section - self._args_to_config(config, argname='hyperopt', - logstring='Using Hyperopt class name: {}') - - self._args_to_config(config, argname='hyperopt_path', - logstring='Using additional Hyperopt lookup path: {}') - - self._args_to_config(config, argname='hyperoptexportfilename', - logstring='Using hyperopt file: {}') - - self._args_to_config(config, argname='epochs', - logstring='Parameter --epochs detected ... ' - 'Will run Hyperopt with for {} epochs ...' - ) - - self._args_to_config(config, argname='spaces', - logstring='Parameter -s/--spaces detected: {}') - - self._args_to_config(config, argname='print_all', - logstring='Parameter --print-all detected ...') - - if 'print_colorized' in self.args and not self.args["print_colorized"]: - logger.info('Parameter --no-color detected ...') - config.update({'print_colorized': False}) - else: - config.update({'print_colorized': True}) - - self._args_to_config(config, argname='print_json', - logstring='Parameter --print-json detected ...') - - self._args_to_config(config, argname='export_csv', - logstring='Parameter --export-csv detected: {}') - - self._args_to_config(config, argname='hyperopt_jobs', - logstring='Parameter -j/--job-workers detected: {}') - - self._args_to_config(config, argname='hyperopt_random_state', - logstring='Parameter --random-state detected: {}') - - self._args_to_config(config, argname='hyperopt_min_trades', - logstring='Parameter --min-trades detected: {}') - - self._args_to_config(config, argname='hyperopt_loss', - logstring='Using Hyperopt loss class name: {}') - - self._args_to_config(config, argname='hyperopt_show_index', - logstring='Parameter -n/--index detected: {}') - - self._args_to_config(config, argname='hyperopt_list_best', - logstring='Parameter --best detected: {}') - - self._args_to_config(config, argname='hyperopt_list_profitable', - logstring='Parameter --profitable detected: {}') - - self._args_to_config(config, argname='hyperopt_list_min_trades', - logstring='Parameter --min-trades detected: {}') - - self._args_to_config(config, argname='hyperopt_list_max_trades', - logstring='Parameter --max-trades detected: {}') - - self._args_to_config(config, argname='hyperopt_list_min_avg_time', - logstring='Parameter --min-avg-time detected: {}') - - self._args_to_config(config, argname='hyperopt_list_max_avg_time', - logstring='Parameter --max-avg-time detected: {}') - - self._args_to_config(config, argname='hyperopt_list_min_avg_profit', - logstring='Parameter --min-avg-profit detected: {}') - - self._args_to_config(config, argname='hyperopt_list_max_avg_profit', - logstring='Parameter --max-avg-profit detected: {}') - - self._args_to_config(config, argname='hyperopt_list_min_total_profit', - logstring='Parameter --min-total-profit detected: {}') - - self._args_to_config(config, argname='hyperopt_list_max_total_profit', - logstring='Parameter --max-total-profit detected: {}') - - self._args_to_config(config, argname='hyperopt_list_min_objective', - logstring='Parameter --min-objective detected: {}') - - self._args_to_config(config, argname='hyperopt_list_max_objective', - logstring='Parameter --max-objective detected: {}') - - self._args_to_config(config, argname='hyperopt_list_no_details', - logstring='Parameter --no-details detected: {}') - - self._args_to_config(config, argname='hyperopt_show_no_header', - logstring='Parameter --no-header detected: {}') - - self._args_to_config(config, argname="hyperopt_ignore_missing_space", - logstring="Paramter --ignore-missing-space detected: {}") - - def _process_plot_options(self, config: Dict[str, Any]) -> None: - - self._args_to_config(config, argname='pairs', - logstring='Using pairs {}') - - self._args_to_config(config, argname='indicators1', - logstring='Using indicators1: {}') - - self._args_to_config(config, argname='indicators2', - logstring='Using indicators2: {}') - - self._args_to_config(config, argname='trade_ids', - logstring='Filtering on trade_ids: {}') - - self._args_to_config(config, argname='plot_limit', - logstring='Limiting plot to: {}') - - self._args_to_config(config, argname='plot_auto_open', - logstring='Parameter --auto-open detected.') - - self._args_to_config(config, argname='trade_source', - logstring='Using trades from: {}') - - self._args_to_config(config, argname='erase', - logstring='Erase detected. Deleting existing data.') - - self._args_to_config(config, argname='no_trades', - logstring='Parameter --no-trades detected.') - - self._args_to_config(config, argname='timeframes', - logstring='timeframes --timeframes: {}') - - self._args_to_config(config, argname='days', - logstring='Detected --days: {}') - - self._args_to_config(config, argname='include_inactive', - logstring='Detected --include-inactive-pairs: {}') - - self._args_to_config(config, argname='download_trades', - logstring='Detected --dl-trades: {}') - - self._args_to_config(config, argname='dataformat_ohlcv', - logstring='Using "{}" to store OHLCV data.') - - self._args_to_config(config, argname='dataformat_trades', - logstring='Using "{}" to store trades data.') - - def _process_data_options(self, config: Dict[str, Any]) -> None: - - self._args_to_config(config, argname='new_pairs_days', - logstring='Detected --new-pairs-days: {}') - - def _process_runmode(self, config: Dict[str, Any]) -> None: - - self._args_to_config(config, argname='dry_run', - logstring='Parameter --dry-run detected, ' - 'overriding dry_run to: {} ...') - - if not self.runmode: - # Handle real mode, infer dry/live from config - self.runmode = RunMode.DRY_RUN if config.get('dry_run', True) else RunMode.LIVE - logger.info(f"Runmode set to {self.runmode.value}.") - - config.update({'runmode': self.runmode}) - - def _args_to_config(self, config: Dict[str, Any], argname: str, - logstring: str, logfun: Optional[Callable] = None, - deprecated_msg: Optional[str] = None) -> None: - """ - :param config: Configuration dictionary - :param argname: Argumentname in self.args - will be copied to config dict. - :param logstring: Logging String - :param logfun: logfun is applied to the configuration entry before passing - that entry to the log string using .format(). - sample: logfun=len (prints the length of the found - configuration instead of the content) - """ - if (argname in self.args and self.args[argname] is not None - and self.args[argname] is not False): - - config.update({argname: self.args[argname]}) - if logfun: - logger.info(logstring.format(logfun(config[argname]))) - else: - logger.info(logstring.format(config[argname])) - if deprecated_msg: - warnings.warn(f"DEPRECATED: {deprecated_msg}", DeprecationWarning) - - def _resolve_pairs_list(self, config: Dict[str, Any]) -> None: - """ - Helper for download script. - Takes first found: - * -p (pairs argument) - * --pairs-file - * whitelist from config - """ - - if "pairs" in config: - config['exchange']['pair_whitelist'] = config['pairs'] - return - - if "pairs_file" in self.args and self.args["pairs_file"]: - pairs_file = Path(self.args["pairs_file"]) - logger.info(f'Reading pairs file "{pairs_file}".') - # Download pairs from the pairs file if no config is specified - # or if pairs file is specified explicitly - if not pairs_file.exists(): - raise OperationalException(f'No pairs file found with path "{pairs_file}".') - config['pairs'] = load_file(pairs_file) - config['pairs'].sort() - return - - if 'config' in self.args and self.args['config']: - logger.info("Using pairlist from configuration.") - config['pairs'] = config.get('exchange', {}).get('pair_whitelist') - else: - # Fall back to /dl_path/pairs.json - pairs_file = config['datadir'] / 'pairs.json' - if pairs_file.exists(): - config['pairs'] = load_file(pairs_file) - if 'pairs' in config: - config['pairs'].sort() From c1b5dcd756eaace8c274dfdcbca701bb7197d6bd Mon Sep 17 00:00:00 2001 From: incrementby1 <91958753+incrementby1@users.noreply.github.com> Date: Wed, 27 Oct 2021 12:42:18 +0200 Subject: [PATCH 07/14] Delete freqtradebot.py --- freqtrade/freqtradebot.py | 1438 ------------------------------------- 1 file changed, 1438 deletions(-) delete mode 100644 freqtrade/freqtradebot.py diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py deleted file mode 100644 index 850cd1700..000000000 --- a/freqtrade/freqtradebot.py +++ /dev/null @@ -1,1438 +0,0 @@ -""" -Freqtrade is the main module of this bot. It contains the class Freqtrade() -""" -import copy -import logging -import traceback -from datetime import datetime, timedelta, timezone -from math import isclose -from threading import Lock -from typing import Any, Dict, List, Optional - -import arrow - -from freqtrade import __version__, constants -from freqtrade.configuration import validate_config_consistency -from freqtrade.data.converter import order_book_to_dataframe -from freqtrade.data.dataprovider import DataProvider -from freqtrade.edge import Edge -from freqtrade.enums import RPCMessageType, SellType, State -from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, - InvalidOrderException, PricingError) -from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds -from freqtrade.misc import safe_value_fallback, safe_value_fallback2 -from freqtrade.mixins import LoggingMixin -from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db -from freqtrade.plugins.pairlistmanager import PairListManager -from freqtrade.plugins.protectionmanager import ProtectionManager -from freqtrade.resolvers import ExchangeResolver, StrategyResolver -from freqtrade.rpc import RPCManager -from freqtrade.strategy.interface import IStrategy, SellCheckTuple -from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper -from freqtrade.wallets import Wallets - - -logger = logging.getLogger(__name__) - - -class FreqtradeBot(LoggingMixin): - """ - Freqtrade is the main class of the bot. - This is from here the bot start its logic. - """ - - def __init__(self, config: Dict[str, Any]) -> None: - """ - Init all variables and objects the bot needs to work - :param config: configuration dict, you can use Configuration.get_config() - to get the config dict. - """ - self.active_pair_whitelist: List[str] = [] - - logger.info('Starting freqtrade %s', __version__) - - # Init bot state - self.state = State.STOPPED - - # Init objects - self.config = config - - self.strategy: IStrategy = StrategyResolver.load_strategy(self.config) - - # Check config consistency here since strategies can set certain options - validate_config_consistency(config) - - self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) - - init_db(self.config.get('db_url', None), clean_open_orders=self.config['dry_run']) - - self.wallets = Wallets(self.config, self.exchange) - - PairLocks.timeframe = self.config['timeframe'] - - self.protections = ProtectionManager(self.config, self.strategy.protections) - - # RPC runs in separate threads, can start handling external commands just after - # initialization, even before Freqtradebot has a chance to start its throttling, - # so anything in the Freqtradebot instance should be ready (initialized), including - # the initial state of the bot. - # Keep this at the end of this initialization method. - self.rpc: RPCManager = RPCManager(self) - - self.pairlists = PairListManager(self.exchange, self.config) - - self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists) - - # Attach Dataprovider to strategy instance - self.strategy.dp = self.dataprovider - # Attach Wallets to strategy instance - self.strategy.wallets = self.wallets - - # Initializing Edge only if enabled - self.edge = Edge(self.config, self.exchange, self.strategy) if \ - self.config.get('edge', {}).get('enabled', False) else None - - self.active_pair_whitelist = self._refresh_active_whitelist() - - # Set initial bot state from config - initial_state = self.config.get('initial_state') - self.state = State[initial_state.upper()] if initial_state else State.STOPPED - - # Protect sell-logic from forcesell and vice versa - self._exit_lock = Lock() - LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) - - def notify_status(self, msg: str) -> None: - """ - Public method for users of this class (worker, etc.) to send notifications - via RPC about changes in the bot status. - """ - self.rpc.send_msg({ - 'type': RPCMessageType.STATUS, - 'status': msg - }) - - def cleanup(self) -> None: - """ - Cleanup pending resources on an already stopped bot - :return: None - """ - logger.info('Cleaning up modules ...') - - if self.config['cancel_open_orders_on_exit']: - self.cancel_all_open_orders() - - self.check_for_open_trades() - - self.rpc.cleanup() - cleanup_db() - - def startup(self) -> None: - """ - Called on startup and after reloading the bot - triggers notifications and - performs startup tasks - """ - self.rpc.startup_messages(self.config, self.pairlists, self.protections) - if not self.edge: - # Adjust stoploss if it was changed - Trade.stoploss_reinitialization(self.strategy.stoploss) - - # Only update open orders on startup - # This will update the database after the initial migration - self.startup_update_open_orders() - - def process(self) -> None: - """ - Queries the persistence layer for open trades and handles them, - otherwise a new trade is created. - :return: True if one or more trades has been created or closed, False otherwise - """ - - # Check whether markets have to be reloaded and reload them when it's needed - self.exchange.reload_markets() - - self.update_closed_trades_without_assigned_fees() - - # Query trades from persistence layer - trades = Trade.get_open_trades() - - self.active_pair_whitelist = self._refresh_active_whitelist(trades) - - # Refreshing candles - self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist), - self.strategy.gather_informative_pairs()) - - strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)() - - self.strategy.analyze(self.active_pair_whitelist) - - with self._exit_lock: - # Check and handle any timed out open orders - self.check_handle_timedout() - - # Protect from collisions with forcesell. - # Without this, freqtrade my try to recreate stoploss_on_exchange orders - # while selling is in process, since telegram messages arrive in an different thread. - with self._exit_lock: - trades = Trade.get_open_trades() - # First process current opened trades (positions) - self.exit_positions(trades) - - # Then looking for buy opportunities - if self.get_free_open_trades(): - self.enter_positions() - - Trade.commit() - - def process_stopped(self) -> None: - """ - Close all orders that were left open - """ - if self.config['cancel_open_orders_on_exit']: - self.cancel_all_open_orders() - - def check_for_open_trades(self): - """ - Notify the user when the bot is stopped - and there are still open trades active. - """ - open_trades = Trade.get_trades([Trade.is_open.is_(True)]).all() - - if len(open_trades) != 0: - msg = { - 'type': RPCMessageType.WARNING, - 'status': f"{len(open_trades)} open trades active.\n\n" - f"Handle these trades manually on {self.exchange.name}, " - f"or '/start' the bot again and use '/stopbuy' " - f"to handle open trades gracefully. \n" - f"{'Trades are simulated.' if self.config['dry_run'] else ''}", - } - self.rpc.send_msg(msg) - - def _refresh_active_whitelist(self, trades: List[Trade] = []) -> List[str]: - """ - Refresh active whitelist from pairlist or edge and extend it with - pairs that have open trades. - """ - # Refresh whitelist - self.pairlists.refresh_pairlist() - _whitelist = self.pairlists.whitelist - - # Calculating Edge positioning - if self.edge: - self.edge.calculate(_whitelist) - _whitelist = self.edge.adjust(_whitelist) - - if trades: - # Extend active-pair whitelist with pairs of open trades - # It ensures that candle (OHLCV) data are downloaded for open trades as well - _whitelist.extend([trade.pair for trade in trades if trade.pair not in _whitelist]) - return _whitelist - - def get_free_open_trades(self) -> int: - """ - Return the number of free open trades slots or 0 if - max number of open trades reached - """ - open_trades = len(Trade.get_open_trades()) - return max(0, self.config['max_open_trades'] - open_trades) - - def startup_update_open_orders(self): - """ - Updates open orders based on order list kept in the database. - Mainly updates the state of orders - but may also close trades - """ - if self.config['dry_run'] or self.config['exchange'].get('skip_open_order_update', False): - # Updating open orders in dry-run does not make sense and will fail. - return - - orders = Order.get_open_orders() - logger.info(f"Updating {len(orders)} open orders.") - for order in orders: - try: - fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, - order.ft_order_side == 'stoploss') - - self.update_trade_state(order.trade, order.order_id, fo) - - except ExchangeError as e: - - logger.warning(f"Error updating Order {order.order_id} due to {e}") - - def update_closed_trades_without_assigned_fees(self): - """ - Update closed trades without close fees assigned. - Only acts when Orders are in the database, otherwise the last order-id is unknown. - """ - if self.config['dry_run']: - # Updating open orders in dry-run does not make sense and will fail. - return - - trades: List[Trade] = Trade.get_sold_trades_without_assigned_fees() - for trade in trades: - - if not trade.is_open and not trade.fee_updated('sell'): - # Get sell fee - order = trade.select_order('sell', False) - if order: - logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.") - self.update_trade_state(trade, order.order_id, - stoploss_order=order.ft_order_side == 'stoploss') - - trades: List[Trade] = Trade.get_open_trades_without_assigned_fees() - for trade in trades: - if trade.is_open and not trade.fee_updated('buy'): - order = trade.select_order('buy', False) - if order: - logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") - self.update_trade_state(trade, order.order_id) - - def handle_insufficient_funds(self, trade: Trade): - """ - Determine if we ever opened a sell order for this trade. - If not, try update buy fees - otherwise "refind" the open order we obviously lost. - """ - sell_order = trade.select_order('sell', None) - if sell_order: - self.refind_lost_order(trade) - else: - self.reupdate_enter_order_fees(trade) - - def reupdate_enter_order_fees(self, trade: Trade): - """ - Get buy order from database, and try to reupdate. - Handles trades where the initial fee-update did not work. - """ - logger.info(f"Trying to reupdate buy fees for {trade}") - order = trade.select_order('buy', False) - if order: - logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") - self.update_trade_state(trade, order.order_id) - - def refind_lost_order(self, trade): - """ - Try refinding a lost trade. - Only used when InsufficientFunds appears on sell orders (stoploss or sell). - Tries to walk the stored orders and sell them off eventually. - """ - logger.info(f"Trying to refind lost order for {trade}") - for order in trade.orders: - logger.info(f"Trying to refind {order}") - fo = None - if not order.ft_is_open: - logger.debug(f"Order {order} is no longer open.") - continue - if order.ft_order_side == 'buy': - # Skip buy side - this is handled by reupdate_buy_order_fees - continue - try: - fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, - order.ft_order_side == 'stoploss') - if order.ft_order_side == 'stoploss': - if fo and fo['status'] == 'open': - # Assume this as the open stoploss order - trade.stoploss_order_id = order.order_id - elif order.ft_order_side == 'sell': - if fo and fo['status'] == 'open': - # Assume this as the open order - trade.open_order_id = order.order_id - if fo: - logger.info(f"Found {order} for trade {trade}.") - self.update_trade_state(trade, order.order_id, fo, - stoploss_order=order.ft_order_side == 'stoploss') - - except ExchangeError: - logger.warning(f"Error updating {order.order_id}.") - -# -# BUY / enter positions / open trades logic and methods -# - - def enter_positions(self) -> int: - """ - Tries to execute buy orders for new trades (positions) - """ - trades_created = 0 - - whitelist = copy.deepcopy(self.active_pair_whitelist) - if not whitelist: - logger.info("Active pair whitelist is empty.") - return trades_created - # Remove pairs for currently opened trades from the whitelist - # Allow rebuying of the same pair if allow_position_stacking is set to True - if not self.config['allow_position_stacking']: - for trade in Trade.get_open_trades(): - if trade.pair in whitelist: - whitelist.remove(trade.pair) - logger.debug('Ignoring %s in pair whitelist', trade.pair) - - if not whitelist: - logger.info("No currency pair in active pair whitelist, " - "but checking to sell open trades.") - return trades_created - if PairLocks.is_global_lock(): - lock = PairLocks.get_pair_longest_lock('*') - if lock: - self.log_once(f"Global pairlock active until " - f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)}. " - f"Not creating new trades, reason: {lock.reason}.", logger.info) - else: - self.log_once("Global pairlock active. Not creating new trades.", logger.info) - return trades_created - # Create entity and execute trade for each pair from whitelist - for pair in whitelist: - try: - trades_created += self.create_trade(pair) - except DependencyException as exception: - logger.warning('Unable to create trade for %s: %s', pair, exception) - - if not trades_created: - logger.debug("Found no buy signals for whitelisted currencies. Trying again...") - - return trades_created - - def create_trade(self, pair: str) -> bool: - """ - Check the implemented trading strategy for buy signals. - - If the pair triggers the buy signal a new trade record gets created - and the buy-order opening the trade gets issued towards the exchange. - - :return: True if a trade has been created. - """ - logger.debug(f"create_trade for pair {pair}") - - analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(pair, self.strategy.timeframe) - nowtime = analyzed_df.iloc[-1]['date'] if len(analyzed_df) > 0 else None - if self.strategy.is_pair_locked(pair, nowtime): - lock = PairLocks.get_pair_longest_lock(pair, nowtime) - if lock: - self.log_once(f"Pair {pair} is still locked until " - f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)} " - f"due to {lock.reason}.", - logger.info) - else: - self.log_once(f"Pair {pair} is still locked.", logger.info) - return False - - # get_free_open_trades is checked before create_trade is called - # but it is still used here to prevent opening too many trades within one iteration - if not self.get_free_open_trades(): - logger.debug(f"Can't open a new trade for {pair}: max number of trades is reached.") - return False - - # running get_signal on historical data fetched - (buy, sell, buy_tag) = self.strategy.get_signal( - pair, - self.strategy.timeframe, - analyzed_df - ) - - if buy and not sell: - stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge) - - bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {}) - if ((bid_check_dom.get('enabled', False)) and - (bid_check_dom.get('bids_to_ask_delta', 0) > 0)): - if self._check_depth_of_market_buy(pair, bid_check_dom): - return self.execute_entry(pair, stake_amount, buy_tag=buy_tag) - else: - return False - - return self.execute_entry(pair, stake_amount, buy_tag=buy_tag) - else: - return False - - def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool: - """ - Checks depth of market before executing a buy - """ - conf_bids_to_ask_delta = conf.get('bids_to_ask_delta', 0) - logger.info(f"Checking depth of market for {pair} ...") - order_book = self.exchange.fetch_l2_order_book(pair, 1000) - order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks']) - order_book_bids = order_book_data_frame['b_size'].sum() - order_book_asks = order_book_data_frame['a_size'].sum() - bids_ask_delta = order_book_bids / order_book_asks - logger.info( - f"Bids: {order_book_bids}, Asks: {order_book_asks}, Delta: {bids_ask_delta}, " - f"Bid Price: {order_book['bids'][0][0]}, Ask Price: {order_book['asks'][0][0]}, " - f"Immediate Bid Quantity: {order_book['bids'][0][1]}, " - f"Immediate Ask Quantity: {order_book['asks'][0][1]}." - ) - if bids_ask_delta >= conf_bids_to_ask_delta: - logger.info(f"Bids to asks delta for {pair} DOES satisfy condition.") - return True - else: - logger.info(f"Bids to asks delta for {pair} does not satisfy condition.") - return False - - def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None, - forcebuy: bool = False, buy_tag: Optional[str] = None) -> bool: - """ - Executes a limit buy for the given pair - :param pair: pair for which we want to create a LIMIT_BUY - :param stake_amount: amount of stake-currency for the pair - :return: True if a buy order is created, false if it fails. - """ - time_in_force = self.strategy.order_time_in_force['buy'] - - if price: - enter_limit_requested = price - else: - # Calculate price - proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side="buy") - custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, - default_retval=proposed_enter_rate)( - pair=pair, current_time=datetime.now(timezone.utc), - proposed_rate=proposed_enter_rate) - - enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate) - - if not enter_limit_requested: - raise PricingError('Could not determine buy price.') - - min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, enter_limit_requested, - self.strategy.stoploss) - - if not self.edge: - max_stake_amount = self.wallets.get_available_stake_amount() - stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, - default_retval=stake_amount)( - pair=pair, current_time=datetime.now(timezone.utc), - current_rate=enter_limit_requested, proposed_stake=stake_amount, - min_stake=min_stake_amount, max_stake=max_stake_amount) - stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount) - - if not stake_amount: - return False - - logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: " - f"{stake_amount} ...") - - amount = stake_amount / enter_limit_requested - order_type = self.strategy.order_types['buy'] - if forcebuy: - # Forcebuy can define a different ordertype - order_type = self.strategy.order_types.get('forcebuy', order_type) - - if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( - pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested, - time_in_force=time_in_force, current_time=datetime.now(timezone.utc)): - logger.info(f"User requested abortion of buying {pair}") - return False - amount = self.exchange.amount_to_precision(pair, amount) - order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy", - amount=amount, rate=enter_limit_requested, - time_in_force=time_in_force) - order_obj = Order.parse_from_ccxt_object(order, pair, 'buy') - order_id = order['id'] - order_status = order.get('status', None) - - # we assume the order is executed at the price requested - enter_limit_filled_price = enter_limit_requested - amount_requested = amount - - if order_status == 'expired' or order_status == 'rejected': - order_tif = self.strategy.order_time_in_force['buy'] - - # return false if the order is not filled - if float(order['filled']) == 0: - logger.warning('Buy %s order with time in force %s for %s is %s by %s.' - ' zero amount is fulfilled.', - order_tif, order_type, pair, order_status, self.exchange.name) - return False - else: - # the order is partially fulfilled - # in case of IOC orders we can check immediately - # if the order is fulfilled fully or partially - logger.warning('Buy %s order with time in force %s for %s is %s by %s.' - ' %s amount fulfilled out of %s (%s remaining which is canceled).', - order_tif, order_type, pair, order_status, self.exchange.name, - order['filled'], order['amount'], order['remaining'] - ) - stake_amount = order['cost'] - amount = safe_value_fallback(order, 'filled', 'amount') - enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') - - # in case of FOK the order may be filled immediately and fully - elif order_status == 'closed': - stake_amount = order['cost'] - amount = safe_value_fallback(order, 'filled', 'amount') - enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') - - # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL - fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') - trade = Trade( - pair=pair, - stake_amount=stake_amount, - amount=amount, - is_open=True, - amount_requested=amount_requested, - fee_open=fee, - fee_close=fee, - open_rate=enter_limit_filled_price, - open_rate_requested=enter_limit_requested, - open_date=datetime.utcnow(), - exchange=self.exchange.id, - open_order_id=order_id, - strategy=self.strategy.get_strategy_name(), - buy_tag=buy_tag, - timeframe=timeframe_to_minutes(self.config['timeframe']) - ) - trade.orders.append(order_obj) - - # Update fees if order is closed - if order_status == 'closed': - self.update_trade_state(trade, order_id, order) - - Trade.query.session.add(trade) - Trade.commit() - - # Updating wallets - self.wallets.update() - - self._notify_enter(trade, order_type) - - # Lock pair for 1 timeframe duration to prevent immediate rebuys - if self.config['allow_position_stacking']: - self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc) + timedelta(minutes=timeframe_to_minutes(self.config['timeframe'])), - reason='Prevent immediate rebuys') - - return True - - def _notify_enter(self, trade: Trade, order_type: str) -> None: - """ - Sends rpc notification when a buy occurred. - """ - msg = { - 'trade_id': trade.id, - 'type': RPCMessageType.BUY, - 'buy_tag': trade.buy_tag, - 'exchange': self.exchange.name.capitalize(), - 'pair': trade.pair, - 'limit': trade.open_rate, - 'order_type': order_type, - 'stake_amount': trade.stake_amount, - 'stake_currency': self.config['stake_currency'], - 'fiat_currency': self.config.get('fiat_display_currency', None), - 'amount': trade.amount, - 'open_date': trade.open_date or datetime.utcnow(), - 'current_rate': trade.open_rate_requested, - } - - # Send the message - self.rpc.send_msg(msg) - - def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str) -> None: - """ - Sends rpc notification when a buy cancel occurred. - """ - current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="buy") - - msg = { - 'trade_id': trade.id, - 'type': RPCMessageType.BUY_CANCEL, - 'buy_tag': trade.buy_tag, - 'exchange': self.exchange.name.capitalize(), - 'pair': trade.pair, - 'limit': trade.open_rate, - 'order_type': order_type, - 'stake_amount': trade.stake_amount, - 'stake_currency': self.config['stake_currency'], - 'fiat_currency': self.config.get('fiat_display_currency', None), - 'amount': trade.amount, - 'open_date': trade.open_date, - 'current_rate': current_rate, - 'reason': reason, - } - - # Send the message - self.rpc.send_msg(msg) - - def _notify_enter_fill(self, trade: Trade) -> None: - msg = { - 'trade_id': trade.id, - 'type': RPCMessageType.BUY_FILL, - 'buy_tag': trade.buy_tag, - 'exchange': self.exchange.name.capitalize(), - 'pair': trade.pair, - 'open_rate': trade.open_rate, - 'stake_amount': trade.stake_amount, - 'stake_currency': self.config['stake_currency'], - 'fiat_currency': self.config.get('fiat_display_currency', None), - 'amount': trade.amount, - 'open_date': trade.open_date, - } - self.rpc.send_msg(msg) - -# -# SELL / exit positions / close trades logic and methods -# - - def exit_positions(self, trades: List[Any]) -> int: - """ - Tries to execute sell orders for open trades (positions) - """ - trades_closed = 0 - for trade in trades: - try: - - if (self.strategy.order_types.get('stoploss_on_exchange') and - self.handle_stoploss_on_exchange(trade)): - trades_closed += 1 - Trade.commit() - continue - # Check if we can sell our current pair - if trade.open_order_id is None and trade.is_open and self.handle_trade(trade): - trades_closed += 1 - - except DependencyException as exception: - logger.warning('Unable to sell trade %s: %s', trade.pair, exception) - - # Updating wallets if any trade occurred - if trades_closed: - self.wallets.update() - - return trades_closed - - def handle_trade(self, trade: Trade) -> bool: - """ - Sells the current pair if the threshold is reached and updates the trade record. - :return: True if trade has been sold, False otherwise - """ - if not trade.is_open: - raise DependencyException(f'Attempt to handle closed trade: {trade}') - - logger.debug('Handling %s ...', trade) - - (buy, sell) = (False, False) - - if (self.config.get('use_sell_signal', True) or - self.config.get('ignore_roi_if_buy_signal', False)): - analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, - self.strategy.timeframe) - - (buy, sell, _) = self.strategy.get_signal( - trade.pair, - self.strategy.timeframe, - analyzed_df - ) - - logger.debug('checking sell') - exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") - if self._check_and_execute_exit(trade, exit_rate, buy, sell): - return True - - logger.debug('Found no sell signal for %s.', trade) - return False - - def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool: - """ - Abstracts creating stoploss orders from the logic. - Handles errors and updates the trade database object. - Force-sells the pair (using EmergencySell reason) in case of Problems creating the order. - :return: True if the order succeeded, and False in case of problems. - """ - try: - stoploss_order = self.exchange.stoploss(pair=trade.pair, amount=trade.amount, - stop_price=stop_price, - order_types=self.strategy.order_types) - - order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss') - trade.orders.append(order_obj) - trade.stoploss_order_id = str(stoploss_order['id']) - return True - except InsufficientFundsError as e: - logger.warning(f"Unable to place stoploss order {e}.") - # Try to figure out what went wrong - self.handle_insufficient_funds(trade) - - except InvalidOrderException as e: - trade.stoploss_order_id = None - logger.error(f'Unable to place a stoploss order on exchange. {e}') - logger.warning('Exiting the trade forcefully') - self.execute_trade_exit(trade, trade.stop_loss, sell_reason=SellCheckTuple( - sell_type=SellType.EMERGENCY_SELL)) - - except ExchangeError: - trade.stoploss_order_id = None - logger.exception('Unable to place a stoploss order on exchange.') - return False - - def handle_stoploss_on_exchange(self, trade: Trade) -> bool: - """ - Check if trade is fulfilled in which case the stoploss - on exchange should be added immediately if stoploss on exchange - is enabled. - """ - - logger.debug('Handling stoploss on exchange %s ...', trade) - - stoploss_order = None - - try: - # First we check if there is already a stoploss on exchange - stoploss_order = self.exchange.fetch_stoploss_order( - trade.stoploss_order_id, trade.pair) if trade.stoploss_order_id else None - except InvalidOrderException as exception: - logger.warning('Unable to fetch stoploss order: %s', exception) - - if stoploss_order: - trade.update_order(stoploss_order) - - # We check if stoploss order is fulfilled - if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): - trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value - self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, - stoploss_order=True) - # Lock pair for one candle to prevent immediate rebuys - self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), - reason='Auto lock') - self._notify_exit(trade, "stoploss") - return True - - if trade.open_order_id or not trade.is_open: - # Trade has an open Buy or Sell order, Stoploss-handling can't happen in this case - # as the Amount on the exchange is tied up in another trade. - # The trade can be closed already (sell-order fill confirmation came in this iteration) - return False - - # If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange - if not stoploss_order: - stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss - stop_price = trade.open_rate * (1 + stoploss) - - if self.create_stoploss_order(trade=trade, stop_price=stop_price): - trade.stoploss_last_update = datetime.utcnow() - return False - - # If stoploss order is canceled for some reason we add it - if stoploss_order and stoploss_order['status'] in ('canceled', 'cancelled'): - if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss): - return False - else: - trade.stoploss_order_id = None - logger.warning('Stoploss order was cancelled, but unable to recreate one.') - - # Finally we check if stoploss on exchange should be moved up because of trailing. - # Triggered Orders are now real orders - so don't replace stoploss anymore - if ( - stoploss_order - and stoploss_order.get('status_stop') != 'triggered' - and (self.config.get('trailing_stop', False) - or self.config.get('use_custom_stoploss', False)) - ): - # if trailing stoploss is enabled we check if stoploss value has changed - # in which case we cancel stoploss order and put another one with new - # value immediately - self.handle_trailing_stoploss_on_exchange(trade, stoploss_order) - - return False - - def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> None: - """ - Check to see if stoploss on exchange should be updated - in case of trailing stoploss on exchange - :param trade: Corresponding Trade - :param order: Current on exchange stoploss order - :return: None - """ - if self.exchange.stoploss_adjust(trade.stop_loss, order): - # we check if the update is necessary - update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) - if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat: - # cancelling the current stoploss on exchange first - logger.info(f"Cancelling current stoploss on exchange for pair {trade.pair} " - f"(orderid:{order['id']}) in order to add another one ...") - try: - co = self.exchange.cancel_stoploss_order_with_result(order['id'], trade.pair, - trade.amount) - trade.update_order(co) - except InvalidOrderException: - logger.exception(f"Could not cancel stoploss order {order['id']} " - f"for pair {trade.pair}") - - # Create new stoploss order - if not self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss): - logger.warning(f"Could not create trailing stoploss order " - f"for pair {trade.pair}.") - - def _check_and_execute_exit(self, trade: Trade, exit_rate: float, - buy: bool, sell: bool) -> bool: - """ - Check and execute exit - """ - should_sell = self.strategy.should_sell( - trade, exit_rate, datetime.now(timezone.utc), buy, sell, - force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 - ) - - if should_sell.sell_flag: - logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}') - self.execute_trade_exit(trade, exit_rate, should_sell) - return True - return False - - def _check_timed_out(self, side: str, order: dict) -> bool: - """ - Check if timeout is active, and if the order is still open and timed out - """ - timeout = self.config.get('unfilledtimeout', {}).get(side) - ordertime = arrow.get(order['datetime']).datetime - if timeout is not None: - timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes') - timeout_kwargs = {timeout_unit: -timeout} - timeout_threshold = arrow.utcnow().shift(**timeout_kwargs).datetime - return (order['status'] == 'open' and order['side'] == side - and ordertime < timeout_threshold) - return False - - def check_handle_timedout(self) -> None: - """ - Check if any orders are timed out and cancel if necessary - :param timeoutvalue: Number of minutes until order is considered timed out - :return: None - """ - - for trade in Trade.get_open_order_trades(): - try: - if not trade.open_order_id: - continue - order = self.exchange.fetch_order(trade.open_order_id, trade.pair) - except (ExchangeError): - logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) - continue - - fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order) - - if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and ( - fully_cancelled - or self._check_timed_out('buy', order) - or strategy_safe_wrapper(self.strategy.check_buy_timeout, - default_retval=False)(pair=trade.pair, - trade=trade, - order=order))): - self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) - - elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and ( - fully_cancelled - or self._check_timed_out('sell', order) - or strategy_safe_wrapper(self.strategy.check_sell_timeout, - default_retval=False)(pair=trade.pair, - trade=trade, - order=order))): - self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) - - def cancel_all_open_orders(self) -> None: - """ - Cancel all orders that are currently open - :return: None - """ - - for trade in Trade.get_open_order_trades(): - try: - order = self.exchange.fetch_order(trade.open_order_id, trade.pair) - except (ExchangeError): - logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) - continue - - if order['side'] == 'buy': - self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) - - elif order['side'] == 'sell': - self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) - Trade.commit() - - def handle_cancel_enter(self, trade: Trade, order: Dict, reason: str) -> bool: - """ - Buy cancel - cancel order - :return: True if order was fully cancelled - """ - was_trade_fully_canceled = False - - # Cancelled orders may have the status of 'canceled' or 'closed' - if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES: - filled_val = order.get('filled', 0.0) or 0.0 - filled_stake = filled_val * trade.open_rate - minstake = self.exchange.get_min_pair_stake_amount( - trade.pair, trade.open_rate, self.strategy.stoploss) - - if filled_val > 0 and filled_stake < minstake: - logger.warning( - f"Order {trade.open_order_id} for {trade.pair} not cancelled, " - f"as the filled amount of {filled_val} would result in an unsellable trade.") - return False - corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, - trade.amount) - # Avoid race condition where the order could not be cancelled coz its already filled. - # Simply bailing here is the only safe way - as this order will then be - # handled in the next iteration. - if corder.get('status') not in constants.NON_OPEN_EXCHANGE_STATES: - logger.warning(f"Order {trade.open_order_id} for {trade.pair} not cancelled.") - return False - else: - # Order was cancelled already, so we can reuse the existing dict - corder = order - reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] - - logger.info('Buy order %s for %s.', reason, trade) - - # Using filled to determine the filled amount - filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled') - if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC): - logger.info('Buy order fully cancelled. Removing %s from database.', trade) - # if trade is not partially completed, just delete the trade - trade.delete() - was_trade_fully_canceled = True - reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}" - else: - # if trade is partially complete, edit the stake details for the trade - # and close the order - # cancel_order may not contain the full order dict, so we need to fallback - # to the order dict acquired before cancelling. - # we need to fall back to the values from order if corder does not contain these keys. - trade.amount = filled_amount - trade.stake_amount = trade.amount * trade.open_rate - self.update_trade_state(trade, trade.open_order_id, corder) - - trade.open_order_id = None - logger.info('Partial buy order timeout for %s.', trade) - reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}" - - self.wallets.update() - self._notify_enter_cancel(trade, order_type=self.strategy.order_types['buy'], - reason=reason) - return was_trade_fully_canceled - - def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> str: - """ - Sell cancel - cancel order and update trade - :return: Reason for cancel - """ - # if trade is not partially completed, just cancel the order - if order['remaining'] == order['amount'] or order.get('filled') == 0.0: - if not self.exchange.check_order_canceled_empty(order): - try: - # if trade is not partially completed, just delete the order - co = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, - trade.amount) - trade.update_order(co) - except InvalidOrderException: - logger.exception(f"Could not cancel sell order {trade.open_order_id}") - return 'error cancelling order' - logger.info('Sell order %s for %s.', reason, trade) - else: - reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] - logger.info('Sell order %s for %s.', reason, trade) - trade.update_order(order) - - trade.close_rate = None - trade.close_rate_requested = None - trade.close_profit = None - trade.close_profit_abs = None - trade.close_date = None - trade.is_open = True - trade.open_order_id = None - else: - # TODO: figure out how to handle partially complete sell orders - reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] - - self.wallets.update() - self._notify_exit_cancel( - trade, - order_type=self.strategy.order_types['sell'], - reason=reason - ) - return reason - - def _safe_exit_amount(self, pair: str, amount: float) -> float: - """ - Get sellable amount. - Should be trade.amount - but will fall back to the available amount if necessary. - This should cover cases where get_real_amount() was not able to update the amount - for whatever reason. - :param pair: Pair we're trying to sell - :param amount: amount we expect to be available - :return: amount to sell - :raise: DependencyException: if available balance is not within 2% of the available amount. - """ - # Update wallets to ensure amounts tied up in a stoploss is now free! - self.wallets.update() - trade_base_currency = self.exchange.get_pair_base_currency(pair) - wallet_amount = self.wallets.get_free(trade_base_currency) - logger.debug(f"{pair} - Wallet: {wallet_amount} - Trade-amount: {amount}") - if wallet_amount >= amount: - return amount - elif wallet_amount > amount * 0.98: - logger.info(f"{pair} - Falling back to wallet-amount {wallet_amount} -> {amount}.") - return wallet_amount - else: - raise DependencyException( - f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}") - - def execute_trade_exit(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool: - """ - Executes a trade exit for the given trade and limit - :param trade: Trade instance - :param limit: limit rate for the sell order - :param sell_reason: Reason the sell was triggered - :return: True if it succeeds (supported) False (not supported) - """ - sell_type = 'sell' - if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): - sell_type = 'stoploss' - - # if stoploss is on exchange and we are on dry_run mode, - # we consider the sell price stop price - if self.config['dry_run'] and sell_type == 'stoploss' \ - and self.strategy.order_types['stoploss_on_exchange']: - limit = trade.stop_loss - - # set custom_exit_price if available - proposed_limit_rate = limit - current_profit = trade.calc_profit_ratio(limit) - custom_exit_price = strategy_safe_wrapper(self.strategy.custom_exit_price, - default_retval=proposed_limit_rate)( - pair=trade.pair, trade=trade, - current_time=datetime.now(timezone.utc), - proposed_rate=proposed_limit_rate, current_profit=current_profit) - - limit = self.get_valid_price(custom_exit_price, proposed_limit_rate) - - # First cancelling stoploss on exchange ... - if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id: - try: - co = self.exchange.cancel_stoploss_order_with_result(trade.stoploss_order_id, - trade.pair, trade.amount) - trade.update_order(co) - except InvalidOrderException: - logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") - - order_type = self.strategy.order_types[sell_type] - if sell_reason.sell_type == SellType.EMERGENCY_SELL: - # Emergency sells (default to market!) - order_type = self.strategy.order_types.get("emergencysell", "market") - if sell_reason.sell_type == SellType.FORCE_SELL: - # Force sells (default to the sell_type defined in the strategy, - # but we allow this value to be changed) - order_type = self.strategy.order_types.get("forcesell", order_type) - - amount = self._safe_exit_amount(trade.pair, trade.amount) - time_in_force = self.strategy.order_time_in_force['sell'] - - if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( - pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, - time_in_force=time_in_force, sell_reason=sell_reason.sell_reason, - current_time=datetime.now(timezone.utc)): - logger.info(f"User requested abortion of selling {trade.pair}") - return False - - try: - # Execute sell and update trade record - order = self.exchange.create_order(pair=trade.pair, - ordertype=order_type, side="sell", - amount=amount, rate=limit, - time_in_force=time_in_force - ) - except InsufficientFundsError as e: - logger.warning(f"Unable to place order {e}.") - # Try to figure out what went wrong - self.handle_insufficient_funds(trade) - return False - - order_obj = Order.parse_from_ccxt_object(order, trade.pair, 'sell') - trade.orders.append(order_obj) - - trade.open_order_id = order['id'] - trade.sell_order_status = '' - trade.close_rate_requested = limit - trade.sell_reason = sell_reason.sell_reason - # In case of market sell orders the order can be closed immediately - if order.get('status', 'unknown') in ('closed', 'expired'): - self.update_trade_state(trade, trade.open_order_id, order) - Trade.commit() - - # Lock pair for one candle to prevent immediate re-buys - self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), - reason='Auto lock') - - self._notify_exit(trade, order_type) - - return True - - def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False) -> None: - """ - Sends rpc notification when a sell occurred. - """ - profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested - profit_trade = trade.calc_profit(rate=profit_rate) - # Use cached rates here - it was updated seconds ago. - current_rate = self.exchange.get_rate( - trade.pair, refresh=False, side="sell") if not fill else None - profit_ratio = trade.calc_profit_ratio(profit_rate) - gain = "profit" if profit_ratio > 0 else "loss" - - msg = { - 'type': (RPCMessageType.SELL_FILL if fill - else RPCMessageType.SELL), - 'trade_id': trade.id, - 'exchange': trade.exchange.capitalize(), - 'pair': trade.pair, - 'gain': gain, - 'limit': profit_rate, - 'order_type': order_type, - 'amount': trade.amount, - 'open_rate': trade.open_rate, - 'close_rate': trade.close_rate, - 'current_rate': current_rate, - 'profit_amount': profit_trade, - 'profit_ratio': profit_ratio, - 'sell_reason': trade.sell_reason, - 'open_date': trade.open_date, - 'close_date': trade.close_date or datetime.utcnow(), - 'stake_currency': self.config['stake_currency'], - 'fiat_currency': self.config.get('fiat_display_currency', None), - } - - if 'fiat_display_currency' in self.config: - msg.update({ - 'fiat_currency': self.config['fiat_display_currency'], - }) - - # Send the message - self.rpc.send_msg(msg) - - def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str) -> None: - """ - Sends rpc notification when a sell cancel occurred. - """ - if trade.sell_order_status == reason: - return - else: - trade.sell_order_status = reason - - profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested - profit_trade = trade.calc_profit(rate=profit_rate) - current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="sell") - profit_ratio = trade.calc_profit_ratio(profit_rate) - gain = "profit" if profit_ratio > 0 else "loss" - - msg = { - 'type': RPCMessageType.SELL_CANCEL, - 'trade_id': trade.id, - 'exchange': trade.exchange.capitalize(), - 'pair': trade.pair, - 'gain': gain, - 'limit': profit_rate or 0, - 'order_type': order_type, - 'amount': trade.amount, - 'open_rate': trade.open_rate, - 'current_rate': current_rate, - 'profit_amount': profit_trade, - 'profit_ratio': profit_ratio, - 'sell_reason': trade.sell_reason, - 'open_date': trade.open_date, - 'close_date': trade.close_date or datetime.now(timezone.utc), - 'stake_currency': self.config['stake_currency'], - 'fiat_currency': self.config.get('fiat_display_currency', None), - 'reason': reason, - } - - if 'fiat_display_currency' in self.config: - msg.update({ - 'fiat_currency': self.config['fiat_display_currency'], - }) - - # Send the message - self.rpc.send_msg(msg) - -# -# Common update trade state methods -# - - def update_trade_state(self, trade: Trade, order_id: str, action_order: Dict[str, Any] = None, - stoploss_order: bool = False) -> bool: - """ - Checks trades with open orders and updates the amount if necessary - Handles closing both buy and sell orders. - :param trade: Trade object of the trade we're analyzing - :param order_id: Order-id of the order we're analyzing - :param action_order: Already acquired order object - :return: True if order has been cancelled without being filled partially, False otherwise - """ - if not order_id: - logger.warning(f'Orderid for trade {trade} is empty.') - return False - - # Update trade with order values - logger.info('Found open order for %s', trade) - try: - order = action_order or self.exchange.fetch_order_or_stoploss_order(order_id, - trade.pair, - stoploss_order) - except InvalidOrderException as exception: - logger.warning('Unable to fetch order %s: %s', order_id, exception) - return False - - trade.update_order(order) - - # Try update amount (binance-fix) - try: - new_amount = self.get_real_amount(trade, order) - if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount, - abs_tol=constants.MATH_CLOSE_PREC): - order['amount'] = new_amount - order.pop('filled', None) - trade.recalc_open_trade_value() - except DependencyException as exception: - logger.warning("Could not update trade amount: %s", exception) - - if self.exchange.check_order_canceled_empty(order): - # Trade has been cancelled on exchange - # Handling of this will happen in check_handle_timeout. - return True - trade.update(order) - Trade.commit() - - # Updating wallets when order is closed - if not trade.is_open: - if not stoploss_order and not trade.open_order_id: - self._notify_exit(trade, '', True) - self.handle_protections(trade.pair) - self.wallets.update() - elif not trade.open_order_id: - # Buy fill - self._notify_enter_fill(trade) - - return False - - def handle_protections(self, pair: str) -> None: - prot_trig = self.protections.stop_per_pair(pair) - if prot_trig: - msg = {'type': RPCMessageType.PROTECTION_TRIGGER, } - msg.update(prot_trig.to_json()) - self.rpc.send_msg(msg) - - prot_trig_glb = self.protections.global_stop() - if prot_trig_glb: - msg = {'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL, } - msg.update(prot_trig_glb.to_json()) - self.rpc.send_msg(msg) - - def apply_fee_conditional(self, trade: Trade, trade_base_currency: str, - amount: float, fee_abs: float) -> float: - """ - Applies the fee to amount (either from Order or from Trades). - Can eat into dust if more than the required asset is available. - """ - self.wallets.update() - if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount: - # Eat into dust if we own more than base currency - logger.info(f"Fee amount for {trade} was in base currency - " - f"Eating Fee {fee_abs} into dust.") - elif fee_abs != 0: - real_amount = self.exchange.amount_to_precision(trade.pair, amount - fee_abs) - logger.info(f"Applying fee on amount for {trade} " - f"(from {amount} to {real_amount}).") - return real_amount - return amount - - def get_real_amount(self, trade: Trade, order: Dict) -> float: - """ - Detect and update trade fee. - Calls trade.update_fee() upon correct detection. - Returns modified amount if the fee was taken from the destination currency. - Necessary for exchanges which charge fees in base currency (e.g. binance) - :return: identical (or new) amount for the trade - """ - # Init variables - order_amount = safe_value_fallback(order, 'filled', 'amount') - # Only run for closed orders - if trade.fee_updated(order.get('side', '')) or order['status'] == 'open': - return order_amount - - trade_base_currency = self.exchange.get_pair_base_currency(trade.pair) - # use fee from order-dict if possible - if self.exchange.order_has_fee(order): - fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(order) - logger.info(f"Fee for Trade {trade} [{order.get('side')}]: " - f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}") - if fee_rate is None or fee_rate < 0.02: - # Reject all fees that report as > 2%. - # These are most likely caused by a parsing bug in ccxt - # due to multiple trades (https://github.com/ccxt/ccxt/issues/8025) - trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', '')) - if trade_base_currency == fee_currency: - # Apply fee to amount - return self.apply_fee_conditional(trade, trade_base_currency, - amount=order_amount, fee_abs=fee_cost) - return order_amount - return self.fee_detection_from_trades(trade, order, order_amount) - - def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float) -> float: - """ - fee-detection fallback to Trades. Parses result of fetch_my_trades to get correct fee. - """ - trades = self.exchange.get_trades_for_order(self.exchange.get_order_id_conditional(order), - trade.pair, trade.open_date) - - if len(trades) == 0: - logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade) - return order_amount - fee_currency = None - amount = 0 - fee_abs = 0.0 - fee_cost = 0.0 - trade_base_currency = self.exchange.get_pair_base_currency(trade.pair) - fee_rate_array: List[float] = [] - for exectrade in trades: - amount += exectrade['amount'] - if self.exchange.order_has_fee(exectrade): - fee_cost_, fee_currency, fee_rate_ = self.exchange.extract_cost_curr_rate(exectrade) - fee_cost += fee_cost_ - if fee_rate_ is not None: - fee_rate_array.append(fee_rate_) - # only applies if fee is in quote currency! - if trade_base_currency == fee_currency: - fee_abs += fee_cost_ - # Ensure at least one trade was found: - if fee_currency: - # fee_rate should use mean - fee_rate = sum(fee_rate_array) / float(len(fee_rate_array)) if fee_rate_array else None - if fee_rate is not None and fee_rate < 0.02: - # Only update if fee-rate is < 2% - trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', '')) - - if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC): - logger.warning(f"Amount {amount} does not match amount {trade.amount}") - raise DependencyException("Half bought? Amounts don't match") - - if fee_abs != 0: - return self.apply_fee_conditional(trade, trade_base_currency, - amount=amount, fee_abs=fee_abs) - else: - return amount - - def get_valid_price(self, custom_price: float, proposed_price: float) -> float: - """ - Return the valid price. - Check if the custom price is of the good type if not return proposed_price - :return: valid price for the order - """ - if custom_price: - try: - valid_custom_price = float(custom_price) - except ValueError: - valid_custom_price = proposed_price - else: - valid_custom_price = proposed_price - - cust_p_max_dist_r = self.config.get('custom_price_max_distance_ratio', 0.02) - min_custom_price_allowed = proposed_price - (proposed_price * cust_p_max_dist_r) - max_custom_price_allowed = proposed_price + (proposed_price * cust_p_max_dist_r) - - # Bracket between min_custom_price_allowed and max_custom_price_allowed - return max( - min(valid_custom_price, max_custom_price_allowed), - min_custom_price_allowed) From 91b9e5ce6872f9cf5f679ee51d7794cfbf9a9519 Mon Sep 17 00:00:00 2001 From: incrementby1 <91958753+incrementby1@users.noreply.github.com> Date: Wed, 27 Oct 2021 12:43:00 +0200 Subject: [PATCH 08/14] Delete StackingDemo.py --- StackingDemo.py | 591 ------------------------------------------------ 1 file changed, 591 deletions(-) delete mode 100644 StackingDemo.py diff --git a/StackingDemo.py b/StackingDemo.py deleted file mode 100644 index b88248fac..000000000 --- a/StackingDemo.py +++ /dev/null @@ -1,591 +0,0 @@ -# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement -# flake8: noqa: F401 - -# --- Do not remove these libs --- -import numpy as np # noqa -import pandas as pd # noqa -from pandas import DataFrame - -from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter, - IStrategy, IntParameter) - -# -------------------------------- -# Add your lib to import here -import talib.abstract as ta -import freqtrade.vendor.qtpylib.indicators as qtpylib - -from freqtrade.persistence import Trade -from datetime import datetime,timezone,timedelta - -""" - Warning: -This is still work in progress, so there is no warranty that everything works as intended, -it is possible that this strategy results in huge losses or doesn't even work at all. -Make sure to only run this in dry_mode so you don't lose any money. - -""" - -class StackingDemo(IStrategy): - """ - This is the default strategy template with added functions for trade stacking / buying the same positions multiple times. - It should function like this: - Find good buys using indicators. - When a new buy occurs the strategy will enable rebuys of the pair like this: - self.custom_info[metadata["pair"]]["rebuy"] = 1 - Then, if the price should drop after the last buy within the timerange of rebuy_time_limit_hours, - the same pair will be purchased again. This is intended to help with reducing possible losses. - If the price only goes up after the first buy, the strategy won't buy this pair again, and after the time limit is over, - look for other pairs to buy. - For selling there is this flag: - self.custom_info[metadata["pair"]]["resell"] = 1 - which should simply sell all trades of this pair until none are left. - - You can set how many pairs you want to trade and how many trades you want to allow for a pair, - but you must make sure to set max_open_trades to the produce of max_open_pairs and max_open_trades in your configuration file. - Also allow_position_stacking has to be set to true in the configuration file. - - For backtesting make sure to provide --enable-position-stacking as an argument in the command line. - Backtesting will be slow. - Hyperopt was not tested. - - # run the bot: - freqtrade trade -c StackingConfig.json -s StackingDemo --db-url sqlite:///tradesv3_StackingDemo_dry-run.sqlite --dry-run - """ - # Strategy interface version - allow new iterations of the strategy interface. - # Check the documentation or the Sample strategy to get the latest version. - INTERFACE_VERSION = 2 - - # how many pairs to trade / trades per pair if allow_position_stacking is enabled - max_open_pairs, max_trades_per_pair = 4, 3 - # make sure to have this value in your config file - max_open_trades = max_open_pairs * max_trades_per_pair - - # debugging - print_trades = True - - # specify for how long to want to allow rebuys of this pair - rebuy_time_limit_hours = 2 - - # store additional information needed for this strategy: - custom_info = {} - custom_num_open_pairs = {} - - # 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.02, - "0": 0.001 - } - - # Optimal stoploss designed for the strategy. - # This attribute will be overridden if the config file contains "stoploss". - stoploss = -0.10 - - # Trailing stoploss - trailing_stop = False - # trailing_only_offset_is_reached = False - # trailing_stop_positive = 0.01 - # trailing_stop_positive_offset = 0.0 # Disabled / not configured - - # Optimal timeframe for the strategy. - timeframe = '5m' - - # Run "populate_indicators()" only for new candle. - process_only_new_candles = False - - # These values can be overridden in the "ask_strategy" section in the config. - use_sell_signal = True - sell_profit_only = False - ignore_roi_if_buy_signal = False - - # Number of candles the strategy requires before producing valid signals - startup_candle_count: int = 30 - - # Optional order type mapping. - order_types = { - 'buy': 'market', - 'sell': 'market', - 'stoploss': 'market', - 'stoploss_on_exchange': False - } - - # Optional order time in force. - order_time_in_force = { - 'buy': 'gtc', - 'sell': 'gtc' - } - - plot_config = { - # Main plot indicators (Moving averages, ...) - 'main_plot': { - 'tema': {}, - 'sar': {'color': 'white'}, - }, - 'subplots': { - # Subplots - each dict defines one additional plot - "MACD": { - 'macd': {'color': 'blue'}, - 'macdsignal': {'color': 'orange'}, - }, - "RSI": { - 'rsi': {'color': 'red'}, - } - } - } - def informative_pairs(self): - """ - Define additional, informative pair/interval combinations to be cached from the exchange. - These pair/interval combinations are non-tradeable, unless they are part - of the whitelist as well. - For more information, please consult the documentation - :return: List of tuples in the format (pair, interval) - Sample: return [("ETH/USDT", "5m"), - ("BTC/USDT", "15m"), - ] - """ - return [] - - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Adds several different TA indicators to the given DataFrame - - Performance Note: For the best performance be frugal on the number of indicators - you are using. Let uncomment only the indicator you are using in your strategies - or your hyperopt configuration, otherwise you will waste your memory and CPU usage. - :param dataframe: Dataframe with data from the exchange - :param metadata: Additional information, like the currently traded pair - :return: a Dataframe with all mandatory indicators for the strategies - """ - - # STACKING STUFF - - # confirm config - self.max_trades_per_pair = self.config['max_open_trades'] / self.max_open_pairs - if not self.config["allow_position_stacking"]: - self.max_trades_per_pair = 1 - - # store number of open pairs - self.custom_num_open_pairs = {"num_open_pairs": 0} - - # Store custom information for this pair: - if not metadata["pair"] in self.custom_info: - self.custom_info[metadata["pair"]] = {} - - if not "rebuy" in self.custom_info[metadata["pair"]]: - # number of trades for this pair - self.custom_info[metadata["pair"]]["num_trades"] = 0 - # use rebuy/resell as buy-/sell- indicators - self.custom_info[metadata["pair"]]["rebuy"] = 0 - self.custom_info[metadata["pair"]]["resell"] = 0 - # store latest open_date for this pair - self.custom_info[metadata["pair"]]["last_open_date"] = datetime.now(timezone.utc) - timedelta(days=100) - # stare the value of the latest open price for this pair - self.custom_info[metadata["pair"]]["latest_open_rate"] = 0 - - # INDICATORS - - # Momentum Indicators - # ------------------------------------ - - # ADX - dataframe['adx'] = ta.ADX(dataframe) - - # # Plus Directional Indicator / Movement - # dataframe['plus_dm'] = ta.PLUS_DM(dataframe) - # dataframe['plus_di'] = ta.PLUS_DI(dataframe) - - # # Minus Directional Indicator / Movement - # dataframe['minus_dm'] = ta.MINUS_DM(dataframe) - # dataframe['minus_di'] = ta.MINUS_DI(dataframe) - - # # Aroon, Aroon Oscillator - # aroon = ta.AROON(dataframe) - # dataframe['aroonup'] = aroon['aroonup'] - # dataframe['aroondown'] = aroon['aroondown'] - # dataframe['aroonosc'] = ta.AROONOSC(dataframe) - - # # Awesome Oscillator - # dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) - - # # Keltner Channel - # keltner = qtpylib.keltner_channel(dataframe) - # dataframe["kc_upperband"] = keltner["upper"] - # dataframe["kc_lowerband"] = keltner["lower"] - # dataframe["kc_middleband"] = keltner["mid"] - # dataframe["kc_percent"] = ( - # (dataframe["close"] - dataframe["kc_lowerband"]) / - # (dataframe["kc_upperband"] - dataframe["kc_lowerband"]) - # ) - # dataframe["kc_width"] = ( - # (dataframe["kc_upperband"] - dataframe["kc_lowerband"]) / dataframe["kc_middleband"] - # ) - - # # Ultimate Oscillator - # dataframe['uo'] = ta.ULTOSC(dataframe) - - # # Commodity Channel Index: values [Oversold:-100, Overbought:100] - # dataframe['cci'] = ta.CCI(dataframe) - - # 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'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1) - - # # Inverse Fisher transform on RSI normalized: values [0.0, 100.0] (https://goo.gl/2JGGoy) - # dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) - - # # Stochastic Slow - # stoch = ta.STOCH(dataframe) - # dataframe['slowd'] = stoch['slowd'] - # dataframe['slowk'] = stoch['slowk'] - - # Stochastic Fast - stoch_fast = ta.STOCHF(dataframe) - dataframe['fastd'] = stoch_fast['fastd'] - dataframe['fastk'] = stoch_fast['fastk'] - - # # Stochastic RSI - # Please read https://github.com/freqtrade/freqtrade/issues/2961 before using this. - # STOCHRSI is NOT aligned with tradingview, which may result in non-expected results. - # stoch_rsi = ta.STOCHRSI(dataframe) - # dataframe['fastd_rsi'] = stoch_rsi['fastd'] - # dataframe['fastk_rsi'] = stoch_rsi['fastk'] - - # MACD - macd = ta.MACD(dataframe) - dataframe['macd'] = macd['macd'] - dataframe['macdsignal'] = macd['macdsignal'] - dataframe['macdhist'] = macd['macdhist'] - - # MFI - dataframe['mfi'] = ta.MFI(dataframe) - - # # ROC - # dataframe['roc'] = ta.ROC(dataframe) - - # Overlap Studies - # ------------------------------------ - - # Bollinger Bands - 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'] - dataframe["bb_percent"] = ( - (dataframe["close"] - dataframe["bb_lowerband"]) / - (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) - ) - dataframe["bb_width"] = ( - (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) / dataframe["bb_middleband"] - ) - - # Bollinger Bands - Weighted (EMA based instead of SMA) - # weighted_bollinger = qtpylib.weighted_bollinger_bands( - # qtpylib.typical_price(dataframe), window=20, stds=2 - # ) - # dataframe["wbb_upperband"] = weighted_bollinger["upper"] - # dataframe["wbb_lowerband"] = weighted_bollinger["lower"] - # dataframe["wbb_middleband"] = weighted_bollinger["mid"] - # dataframe["wbb_percent"] = ( - # (dataframe["close"] - dataframe["wbb_lowerband"]) / - # (dataframe["wbb_upperband"] - dataframe["wbb_lowerband"]) - # ) - # dataframe["wbb_width"] = ( - # (dataframe["wbb_upperband"] - dataframe["wbb_lowerband"]) / dataframe["wbb_middleband"] - # ) - - # # EMA - Exponential Moving Average - # dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) - # dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) - # dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) - # dataframe['ema21'] = ta.EMA(dataframe, timeperiod=21) - # dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) - # dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) - - # # SMA - Simple Moving Average - # dataframe['sma3'] = ta.SMA(dataframe, timeperiod=3) - # dataframe['sma5'] = ta.SMA(dataframe, timeperiod=5) - # dataframe['sma10'] = ta.SMA(dataframe, timeperiod=10) - # dataframe['sma21'] = ta.SMA(dataframe, timeperiod=21) - # dataframe['sma50'] = ta.SMA(dataframe, timeperiod=50) - # dataframe['sma100'] = ta.SMA(dataframe, timeperiod=100) - - # Parabolic SAR - dataframe['sar'] = ta.SAR(dataframe) - - # TEMA - Triple Exponential Moving Average - dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) - - # Cycle Indicator - # ------------------------------------ - # Hilbert Transform Indicator - SineWave - hilbert = ta.HT_SINE(dataframe) - dataframe['htsine'] = hilbert['sine'] - dataframe['htleadsine'] = hilbert['leadsine'] - - # Pattern Recognition - Bullish candlestick patterns - # ------------------------------------ - # # Hammer: values [0, 100] - # dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) - # # Inverted Hammer: values [0, 100] - # dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) - # # Dragonfly Doji: values [0, 100] - # dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) - # # Piercing Line: values [0, 100] - # dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] - # # Morningstar: values [0, 100] - # dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] - # # Three White Soldiers: values [0, 100] - # dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] - - # Pattern Recognition - Bearish candlestick patterns - # ------------------------------------ - # # Hanging Man: values [0, 100] - # dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) - # # Shooting Star: values [0, 100] - # dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) - # # Gravestone Doji: values [0, 100] - # dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) - # # Dark Cloud Cover: values [0, 100] - # dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) - # # Evening Doji Star: values [0, 100] - # dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) - # # Evening Star: values [0, 100] - # dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) - - # Pattern Recognition - Bullish/Bearish candlestick patterns - # ------------------------------------ - # # Three Line Strike: values [0, -100, 100] - # dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) - # # Spinning Top: values [0, -100, 100] - # dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] - # # Engulfing: values [0, -100, 100] - # dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] - # # Harami: values [0, -100, 100] - # dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] - # # Three Outside Up/Down: values [0, -100, 100] - # dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] - # # Three Inside Up/Down: values [0, -100, 100] - # dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] - - # # Chart type - # # ------------------------------------ - # # Heikin Ashi Strategy - # heikinashi = qtpylib.heikinashi(dataframe) - # dataframe['ha_open'] = heikinashi['open'] - # dataframe['ha_close'] = heikinashi['close'] - # dataframe['ha_high'] = heikinashi['high'] - # dataframe['ha_low'] = heikinashi['low'] - - # Retrieve best bid and best ask from the orderbook - # ------------------------------------ - """ - # first check if dataprovider is available - if self.dp: - if self.dp.runmode.value in ('live', 'dry_run'): - ob = self.dp.orderbook(metadata['pair'], 1) - dataframe['best_bid'] = ob['bids'][0][0] - dataframe['best_ask'] = ob['asks'][0][0] - """ - - return dataframe - - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Based on TA indicators, populates the buy signal for the given dataframe - :param dataframe: DataFrame populated with indicators - :param metadata: Additional information, like the currently traded pair - :return: DataFrame with buy column - """ - dataframe.loc[ - ( - ( - (qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 - (dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle - (dataframe['tema'] > dataframe['tema'].shift(1)) | # Guard: tema is raising - # use either buy signal or rebuy flag to trigger a buy - (self.custom_info[metadata["pair"]]["rebuy"] == 1) - ) & - (dataframe['volume'] > 0) # Make sure Volume is not 0 - ), - 'buy'] = 1 - - return dataframe - - def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Based on TA indicators, populates the sell signal for the given dataframe - :param dataframe: DataFrame populated with indicators - :param metadata: Additional information, like the currently traded pair - :return: DataFrame with buy column - """ - dataframe.loc[ - ( - ( - (qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 - (dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle - (dataframe['tema'] < dataframe['tema'].shift(1)) | # Guard: tema is falling - # use either sell signal or resell flag to trigger a sell - (self.custom_info[metadata["pair"]]["resell"] == 1) - ) & - (dataframe['volume'] > 0) # Make sure Volume is not 0 - ), - 'sell'] = 1 - return dataframe - - # use_custom_sell = True - - def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, - current_profit: float, **kwargs) -> 'Optional[Union[str, bool]]': - """ - Custom sell signal logic indicating that specified position should be sold. Returning a - string or True from this method is equal to setting sell signal on a candle at specified - time. This method is not called when sell signal is set. - - This method should be overridden to create sell signals that depend on trade parameters. For - example you could implement a sell relative to the candle when the trade was opened, - or a custom 1:2 risk-reward ROI. - - Custom sell reason max length is 64. Exceeding characters will be removed. - - :param pair: Pair that's currently analyzed - :param trade: trade object. - :param current_time: datetime object, containing the current datetime - :param current_rate: Rate, calculated based on pricing settings in ask_strategy. - :param current_profit: Current profit (as ratio), calculated based on current_rate. - :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return: To execute sell, return a string with custom sell reason or True. Otherwise return - None or False. - """ - # if self.custom_info[pair]["resell"] == 1: - # return 'resell' - return None - - def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, - time_in_force: str, current_time: 'datetime', **kwargs) -> bool: - return_statement = True - - if self.config['allow_position_stacking']: - return_statement = self.check_open_trades(pair, rate, current_time) - - # debugging - if return_statement and self.print_trades: - # use str.join() for speed - out = (current_time.strftime("%c"), " Bought: ", pair, ", rate: ", str(rate), ", rebuy: ", str(self.custom_info[pair]["rebuy"]), ", trades: ", str(self.custom_info[pair]["num_trades"])) - print("".join(out)) - - return return_statement - - def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float, - rate: float, time_in_force: str, sell_reason: str, - current_time: 'datetime', **kwargs) -> bool: - - if self.config["allow_position_stacking"]: - - # unlock open pairs limit after every sell - self.unlock_reason('Open pairs limit') - - # unlock open pairs limit after last item is sold - if self.custom_info[pair]["num_trades"] == 1: - # decrement open_pairs_count by 1 if last item is sold - self.custom_num_open_pairs["num_open_pairs"]-=1 - self.custom_info[pair]["resell"] = 0 - # reset rate - self.custom_info[pair]["latest_open_rate"] = 0.0 - self.unlock_reason('Trades per pair limit') - - # change dataframe to produce sell signal after a sell - if self.custom_info[pair]["num_trades"] >= self.max_trades_per_pair: - self.custom_info[pair]["resell"] = 1 - - # decrement number of trades by 1: - self.custom_info[pair]["num_trades"]-=1 - - # debugging stuff - if self.print_trades: - # use str.join() for speed - out = (current_time.strftime("%c"), " Sold: ", pair, ", rate: ", str(rate),", profit: ", str(trade.calc_profit_ratio(rate)), ", resell: ", str(self.custom_info[pair]["resell"]), ", trades: ", str(self.custom_info[pair]["num_trades"])) - print("".join(out)) - - return True - - def check_open_trades(self, pair: str, rate: float, current_time: datetime): - - # retrieve information about current open pairs - tr_info = self.get_trade_information(pair) - - # update number of open trades for the pair - self.custom_info[pair]["num_trades"] = tr_info[1] - self.custom_num_open_pairs["num_open_pairs"] = len(tr_info[0]) - # update value of the last open price - self.custom_info[pair]["latest_open_rate"] = tr_info[2] - - # don't buy if we have enough trades for this pair - if self.custom_info[pair]["num_trades"] >= self.max_trades_per_pair: - # lock if we already have enough pairs open, will be unlocked after last item of a pair is sold - self.lock_pair(pair, until=datetime.now(timezone.utc) + timedelta(days=100), reason='Trades per pair limit') - self.custom_info[pair]["rebuy"] = 0 - return False - - # don't buy if we have enough pairs - if self.custom_num_open_pairs["num_open_pairs"] >= self.max_open_pairs: - if not pair in tr_info[0]: - # lock if this pair is not in our list, will be unlocked after the next sell - self.lock_pair(pair, until=datetime.now(timezone.utc) + timedelta(days=100), reason='Open pairs limit') - self.custom_info[pair]["rebuy"] = 0 - return False - - # don't buy at a higher price, try until time limit is exceeded; skips if it's the first trade' - if rate > self.custom_info[pair]["latest_open_rate"] and self.custom_info[pair]["latest_open_rate"] != 0.0: - # how long do we want to try buying cheaper before we look for other pairs? - if (current_time - self.custom_info[pair]['last_open_date']).seconds/3600 > self.rebuy_time_limit_hours: - self.custom_info[pair]["rebuy"] = 0 - self.unlock_reason('Open pairs limit') - return False - - # set rebuy flag if num_trades < limit-1 - if self.custom_info[pair]["num_trades"] < self.max_trades_per_pair-1: - self.custom_info[pair]["rebuy"] = 1 - else: - self.custom_info[pair]["rebuy"] = 0 - - # update rate - self.custom_info[pair]["latest_open_rate"] = rate - - #update date open - self.custom_info[pair]["last_open_date"] = current_time - - # increment trade count by 1 - self.custom_info[pair]["num_trades"]+=1 - - return True - - # custom function to help with the strategy - def get_trade_information(self, pair:str): - - latest_open_rate, trade_count = 0, 0.0 - # store all open pairs - open_pairs = [] - - ### start nested function - def compare_trade(trade: Trade): - nonlocal trade_count, latest_open_rate, pair - if trade.pair == pair: - # update latest_rate - latest_open_rate = trade.open_rate - trade_count+=1 - return trade.pair - ### end nested function - - # replaced for loop with map for speed - open_pairs = map(compare_trade, Trade.get_open_trades()) - # remove duplicates - open_pairs = (list(dict.fromkeys(open_pairs))) - - #print(*open_pairs, sep="\n") - - # put this all together to reduce the amount of loops - return open_pairs, trade_count, latest_open_rate From 2eb33707c93379a5a40bf7d2e6f6270d47563db8 Mon Sep 17 00:00:00 2001 From: incrementby1 Date: Wed, 27 Oct 2021 15:58:41 +0200 Subject: [PATCH 09/14] Undo changes --- freqtrade/configuration/configuration.py | 7 +------ freqtrade/freqtradebot.py | 18 ++++++------------ 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index d4cf09821..9aa4b794e 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -137,12 +137,6 @@ class Configuration: setup_logging(config) def _process_trading_options(self, config: Dict[str, Any]) -> None: - - # Allow_position_stacking defaults to False - if not config.get('allow_position_stacking'): - config['allow_position_stacking'] = False - logger.info('Allow_position_stacking is set to ' + str(config['allow_position_stacking'])) - if config['runmode'] not in TRADING_MODES: return @@ -504,3 +498,4 @@ class Configuration: config['pairs'] = load_file(pairs_file) if 'pairs' in config: config['pairs'].sort() + diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 850cd1700..4def3747c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -4,7 +4,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade() import copy import logging import traceback -from datetime import datetime, timedelta, timezone +from datetime import datetime, timezone from math import isclose from threading import Lock from typing import Any, Dict, List, Optional @@ -359,12 +359,10 @@ class FreqtradeBot(LoggingMixin): logger.info("Active pair whitelist is empty.") return trades_created # Remove pairs for currently opened trades from the whitelist - # Allow rebuying of the same pair if allow_position_stacking is set to True - if not self.config['allow_position_stacking']: - for trade in Trade.get_open_trades(): - if trade.pair in whitelist: - whitelist.remove(trade.pair) - logger.debug('Ignoring %s in pair whitelist', trade.pair) + for trade in Trade.get_open_trades(): + if trade.pair in whitelist: + whitelist.remove(trade.pair) + logger.debug('Ignoring %s in pair whitelist', trade.pair) if not whitelist: logger.info("No currency pair in active pair whitelist, " @@ -594,11 +592,6 @@ class FreqtradeBot(LoggingMixin): self._notify_enter(trade, order_type) - # Lock pair for 1 timeframe duration to prevent immediate rebuys - if self.config['allow_position_stacking']: - self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc) + timedelta(minutes=timeframe_to_minutes(self.config['timeframe'])), - reason='Prevent immediate rebuys') - return True def _notify_enter(self, trade: Trade, order_type: str) -> None: @@ -1436,3 +1429,4 @@ class FreqtradeBot(LoggingMixin): return max( min(valid_custom_price, max_custom_price_allowed), min_custom_price_allowed) + From dc605e29aa8ade2cff0d6e105b243df13b65c7fd Mon Sep 17 00:00:00 2001 From: incrementby1 Date: Wed, 27 Oct 2021 21:04:08 +0200 Subject: [PATCH 10/14] removed empty lines for flake8 --- freqtrade/configuration/configuration.py | 1 - freqtrade/freqtradebot.py | 1 - 2 files changed, 2 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 9aa4b794e..822577916 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -498,4 +498,3 @@ class Configuration: config['pairs'] = load_file(pairs_file) if 'pairs' in config: config['pairs'].sort() - diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4def3747c..bf4742fdc 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1429,4 +1429,3 @@ class FreqtradeBot(LoggingMixin): return max( min(valid_custom_price, max_custom_price_allowed), min_custom_price_allowed) - From 02e69e16676d44706afa5d79b116e4cf55249960 Mon Sep 17 00:00:00 2001 From: incrementby1 Date: Thu, 28 Oct 2021 15:16:07 +0200 Subject: [PATCH 11/14] Changes to unlock_reason: - introducing filter - replaced get_all_locks with a query for speed . removed logging in backtesting mode for speed . replaced for-loop with map-function for speed Changes to models.py: - changed string representation of Pairlock to also contain reason and active-state --- freqtrade/persistence/models.py | 3 +-- freqtrade/persistence/pairlock_middleware.py | 23 +++++++++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index bc5ef961a..04f1d67b2 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -896,7 +896,7 @@ class PairLock(_DECL_BASE): lock_time = self.lock_time.strftime(DATETIME_PRINT_FORMAT) lock_end_time = self.lock_end_time.strftime(DATETIME_PRINT_FORMAT) return (f'PairLock(id={self.id}, pair={self.pair}, lock_time={lock_time}, ' - f'lock_end_time={lock_end_time})') + f'lock_end_time={lock_end_time}, reason={self.reason}, active={self.active})') @staticmethod def query_pair_locks(pair: Optional[str], now: datetime) -> Query: @@ -905,7 +905,6 @@ class PairLock(_DECL_BASE): :param pair: Pair to check for. Returns all current locks if pair is empty :param now: Datetime object (generated via datetime.now(timezone.utc)). """ - filters = [PairLock.lock_end_time > now, # Only active locks PairLock.active.is_(True), ] diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index 6e0164182..386c3d1d7 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -113,13 +113,26 @@ class PairLocks(): """ if not now: now = datetime.now(timezone.utc) - logger.info(f"Releasing all locks with reason \'{reason}\'.") - locks = PairLocks.get_all_locks() - for lock in locks: - if lock.reason == reason: - lock.active = False + + def local_unlock(lock): + lock.active = False + if PairLocks.use_db: + # used in live modes + logger.info(f"Releasing all locks with reason \'{reason}\':") + filters = [PairLock.lock_end_time > now, + PairLock.active.is_(True), + PairLock.reason == reason + ] + locks = PairLock.query.filter(*filters) + for lock in locks: + logger.info(f"Releasing lock for \'{lock.pair}\' with reason \'{reason}\'.") + lock.active = False PairLock.query.session.commit() + else: + # no logging in backtesting to increase speed + locks = filter(lambda reason: reason == reason, PairLocks.locks) + locks = map(local_unlock, locks) @staticmethod def is_global_lock(now: Optional[datetime] = None) -> bool: From 658006e7eedfd6a09fa7ee439e5fee1dbc81752b Mon Sep 17 00:00:00 2001 From: incrementby1 Date: Thu, 28 Oct 2021 23:29:26 +0200 Subject: [PATCH 12/14] removed wrong use of map and filter function --- freqtrade/persistence/pairlock_middleware.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index 386c3d1d7..f1ed50ec7 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -114,9 +114,6 @@ class PairLocks(): if not now: now = datetime.now(timezone.utc) - def local_unlock(lock): - lock.active = False - if PairLocks.use_db: # used in live modes logger.info(f"Releasing all locks with reason \'{reason}\':") @@ -126,13 +123,15 @@ class PairLocks(): ] locks = PairLock.query.filter(*filters) for lock in locks: - logger.info(f"Releasing lock for \'{lock.pair}\' with reason \'{reason}\'.") + logger.info(f"Releasing lock for {lock.pair} with reason \'{reason}\'.") lock.active = False PairLock.query.session.commit() else: - # no logging in backtesting to increase speed - locks = filter(lambda reason: reason == reason, PairLocks.locks) - locks = map(local_unlock, locks) + # used in backtesting mode; don't show log messages for speed + locks = PairLocks.get_locks(None) + for lock in locks: + if lock.reason == reason: + lock.active = False @staticmethod def is_global_lock(now: Optional[datetime] = None) -> bool: @@ -159,7 +158,9 @@ class PairLocks(): @staticmethod def get_all_locks() -> List[PairLock]: - + """ + Return all locks, also locks with expired end date + """ if PairLocks.use_db: return PairLock.query.all() else: From e9d71f26b3f28ac6ef0cb0dcd265b6f1eb66d7fe Mon Sep 17 00:00:00 2001 From: incrementby1 Date: Fri, 29 Oct 2021 00:03:20 +0200 Subject: [PATCH 13/14] small changes --- freqtrade/persistence/pairlock_middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index f1ed50ec7..e74948813 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -128,7 +128,7 @@ class PairLocks(): PairLock.query.session.commit() else: # used in backtesting mode; don't show log messages for speed - locks = PairLocks.get_locks(None) + locks = PairLocks.get_pair_locks(None) for lock in locks: if lock.reason == reason: lock.active = False From c579fcfc19ca91e5567eca1ad8b8d9f16a577f3d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Oct 2021 09:39:40 +0200 Subject: [PATCH 14/14] Add tests and documentation for unlock_reason --- docs/strategy-customization.md | 3 ++- freqtrade/persistence/pairlock_middleware.py | 4 ++-- tests/plugins/test_pairlocks.py | 25 ++++++++++++++++++++ tests/strategy/test_interface.py | 7 ++++++ 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 0bfc0a2f6..84d6b2320 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -894,7 +894,8 @@ Sometimes it may be desired to lock a pair after certain events happen (e.g. mul Freqtrade has an easy method to do this from within the strategy, by calling `self.lock_pair(pair, until, [reason])`. `until` must be a datetime object in the future, after which trading will be re-enabled for that pair, while `reason` is an optional string detailing why the pair was locked. -Locks can also be lifted manually, by calling `self.unlock_pair(pair)`. +Locks can also be lifted manually, by calling `self.unlock_pair(pair)` or `self.unlock_reason()` - providing reason the pair was locked with. +`self.unlock_reason()` will unlock all pairs currently locked with the provided reason. To verify if a pair is currently locked, use `self.is_pair_locked(pair)`. diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index e74948813..afbd9781b 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -116,14 +116,14 @@ class PairLocks(): if PairLocks.use_db: # used in live modes - logger.info(f"Releasing all locks with reason \'{reason}\':") + logger.info(f"Releasing all locks with reason '{reason}':") filters = [PairLock.lock_end_time > now, PairLock.active.is_(True), PairLock.reason == reason ] locks = PairLock.query.filter(*filters) for lock in locks: - logger.info(f"Releasing lock for {lock.pair} with reason \'{reason}\'.") + logger.info(f"Releasing lock for {lock.pair} with reason '{reason}'.") lock.active = False PairLock.query.session.commit() else: diff --git a/tests/plugins/test_pairlocks.py b/tests/plugins/test_pairlocks.py index c694fd7c1..f9e5583ed 100644 --- a/tests/plugins/test_pairlocks.py +++ b/tests/plugins/test_pairlocks.py @@ -116,3 +116,28 @@ def test_PairLocks_getlongestlock(use_db): PairLocks.reset_locks() PairLocks.use_db = True + + +@pytest.mark.parametrize('use_db', (False, True)) +@pytest.mark.usefixtures("init_persistence") +def test_PairLocks_reason(use_db): + PairLocks.timeframe = '5m' + PairLocks.use_db = use_db + # No lock should be present + if use_db: + assert len(PairLock.query.all()) == 0 + + assert PairLocks.use_db == use_db + + PairLocks.lock_pair('XRP/USDT', arrow.utcnow().shift(minutes=4).datetime, 'TestLock1') + PairLocks.lock_pair('ETH/USDT', arrow.utcnow().shift(minutes=4).datetime, 'TestLock2') + + assert PairLocks.is_pair_locked('XRP/USDT') + assert PairLocks.is_pair_locked('ETH/USDT') + + PairLocks.unlock_reason('TestLock1') + assert not PairLocks.is_pair_locked('XRP/USDT') + assert PairLocks.is_pair_locked('ETH/USDT') + + PairLocks.reset_locks() + PairLocks.use_db = True diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index dcb9e3e64..ebd950fd6 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -575,6 +575,13 @@ def test_is_pair_locked(default_conf): strategy.unlock_pair(pair) assert not strategy.is_pair_locked(pair) + # Lock with reason + reason = "TestLockR" + strategy.lock_pair(pair, arrow.now(timezone.utc).shift(minutes=4).datetime, reason) + assert strategy.is_pair_locked(pair) + strategy.unlock_reason(reason) + assert not strategy.is_pair_locked(pair) + pair = 'BTC/USDT' # Lock until 14:30 lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc)