diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index dce708711..84e938e02 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -479,15 +479,15 @@ class FreqtradeBot(LoggingMixin): logger.debug(f"adjust_trade_position for pair {trade.pair}") sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") current_profit = trade.calc_profit_ratio(sell_rate) - stake_to_adjust = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( + stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( pair=trade.pair, trade=trade, current_time=datetime.now(timezone.utc), current_rate=sell_rate, current_profit=current_profit) - if stake_to_adjust != None and stake_to_adjust > 0.0: + if stake_amount != None and stake_amount > 0.0: # We should increase our position - self.execute_trade_position_change(trade.pair, stake_to_adjust, trade) + self.execute_entry(trade.pair, stake_amount, trade=trade) - if stake_to_adjust != None and stake_to_adjust < 0.0: + if stake_amount != None and stake_amount < 0.0: # We should decrease our position # TODO: Selling part of the trade not implemented yet. logger.error(f"Unable to decrease trade position / sell partially for pair {trade.pair}, feature not implemented.") @@ -495,90 +495,6 @@ class FreqtradeBot(LoggingMixin): return - - def execute_trade_position_change(self, pair: str, stake_amount: float, trade: Trade): - """ - Executes a buy order for the given pair using specific amount - :param pair: pair for which we want to create a buy order - :param amount: amount of tradable pair to buy - """ - time_in_force = self.strategy.order_time_in_force['buy'] - - # 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) - stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount) - - amount = self.exchange.amount_to_precision(pair, stake_amount / enter_limit_requested) - - if not stake_amount: - logger.info(f'Additional order failed to get stake amount for pair {pair}, amount={amount}, price={enter_limit_requested}') - return False - - logger.debug(f'Executing additional order: amount={amount}, stake={stake_amount}, price={enter_limit_requested}') - - order_type = self.strategy.order_types['buy'] - 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_status = order.get('status', None) - - 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(adjustment) %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(adjustment) %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') - - # 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') - - # Fee is applied only once because we make a LIMIT_BUY but the final trade will apply the sell fee. - fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') - - logger.info(f"Trade {pair} adjustment order status {order_status}") - - trade.orders.append(order_obj) - trade.recalc_trade_from_orders() - Trade.commit() - - # Updating wallets - self.wallets.update() - - self._notify_additional_buy(trade, order_obj, order_type) - - return True - - - def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool: """ Checks depth of market before executing a buy @@ -604,7 +520,8 @@ class FreqtradeBot(LoggingMixin): return False def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None, *, - ordertype: Optional[str] = None, buy_tag: Optional[str] = None) -> bool: + ordertype: Optional[str] = None, buy_tag: Optional[str] = None, + trade: Optional[Trade] = None) -> bool: """ Executes a limit buy for the given pair :param pair: pair for which we want to create a LIMIT_BUY @@ -631,7 +548,7 @@ class FreqtradeBot(LoggingMixin): min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, enter_limit_requested, self.strategy.stoploss) - if not self.edge: + if not self.edge and trade is None: max_stake_amount = self.wallets.get_available_stake_amount() stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, default_retval=stake_amount)( @@ -641,15 +558,20 @@ class FreqtradeBot(LoggingMixin): stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount) if not stake_amount: + logger.error(f"No stake amount? {pair} {stake_amount} {max_stake_amount}") return False - logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: " - f"{stake_amount} ...") + if trade is None: + logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: " + f"{stake_amount} ...") + else: + logger.info(f"Position adjust: about create a new order for {pair} with stake_amount: " + f"{stake_amount} ...") amount = stake_amount / enter_limit_requested order_type = ordertype or self.strategy.order_types['buy'] - if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( + if trade is None and 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}") @@ -696,32 +618,33 @@ class FreqtradeBot(LoggingMixin): # 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']) - ) + if trade is None: + 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) - + trade.recalc_trade_from_orders() Trade.query.session.add(trade) Trade.commit() # Updating wallets self.wallets.update() - self._notify_enter(trade, order_type) + self._notify_enter(trade, order, order_type) # Update fees if order is closed if order_status == 'closed': @@ -729,32 +652,7 @@ class FreqtradeBot(LoggingMixin): return True - def _notify_additional_buy(self, trade: Trade, order: Order, order_type: Optional[str] = None, - fill: bool = False) -> None: - """ - Sends rpc notification when a buy occurred. - """ - msg = { - 'trade_id': trade.id, - 'type': RPCMessageType.BUY_FILL if fill else RPCMessageType.BUY, - 'buy_tag': "adjust_trade_position", - 'exchange': self.exchange.name.capitalize(), - 'pair': trade.pair, - 'limit': order.price, # Deprecated (?) - 'open_rate': order.price, - 'order_type': order_type, - 'stake_amount': order.cost, - 'stake_currency': self.config['stake_currency'], - 'fiat_currency': self.config.get('fiat_display_currency', None), - 'amount': order.amount, - 'open_date': order.order_filled_date or datetime.utcnow(), - 'current_rate': order.price, - } - - # Send the message - self.rpc.send_msg(msg) - - def _notify_enter(self, trade: Trade, order_type: Optional[str] = None, + def _notify_enter(self, trade: Trade, order: Dict, order_type: Optional[str] = None, fill: bool = False) -> None: """ Sends rpc notification when a buy occurred. @@ -765,13 +663,13 @@ class FreqtradeBot(LoggingMixin): 'buy_tag': trade.buy_tag, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, - 'limit': trade.open_rate, # Deprecated (?) - 'open_rate': trade.open_rate, + 'limit': safe_value_fallback(order, 'average', 'price'), # Deprecated (?) + 'open_rate': safe_value_fallback(order, 'average', 'price'), '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, + 'amount': safe_value_fallback(order, 'filled', 'amount') or trade.amount, 'open_date': trade.open_date or datetime.utcnow(), 'current_rate': trade.open_rate_requested, } @@ -1462,7 +1360,7 @@ class FreqtradeBot(LoggingMixin): self.wallets.update() elif send_msg and not trade.open_order_id: # Buy fill - self._notify_enter(trade, fill=True) + self._notify_enter(trade, order, fill=True) return False diff --git a/tests/optimize/test_backtesting_adjust_position.py b/tests/optimize/test_backtesting_adjust_position.py index 78bc4758e..e32b9f6c6 100644 --- a/tests/optimize/test_backtesting_adjust_position.py +++ b/tests/optimize/test_backtesting_adjust_position.py @@ -35,7 +35,7 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> "position_adjustment_enable": True, "stake_amount": 100.0, "dry_run_wallet": 1000.0, - "strategy": "StrategyTestPositionAdjust" + "strategy": "StrategyTestV2" }) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) diff --git a/tests/strategy/strats/strategy_test_position_adjust.py b/tests/strategy/strats/strategy_test_position_adjust.py deleted file mode 100644 index cd786ed54..000000000 --- a/tests/strategy/strats/strategy_test_position_adjust.py +++ /dev/null @@ -1,165 +0,0 @@ -# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement - -import talib.abstract as ta -from pandas import DataFrame - -import freqtrade.vendor.qtpylib.indicators as qtpylib -from freqtrade.strategy.interface import IStrategy -from freqtrade.persistence import Trade -from datetime import datetime - -class StrategyTestPositionAdjust(IStrategy): - """ - Strategy used by tests freqtrade bot. - Please do not modify this strategy, it's intended for internal use only. - Please look at the SampleStrategy in the user_data/strategy directory - or strategy repository https://github.com/freqtrade/freqtrade-strategies - for samples and inspiration. - """ - INTERFACE_VERSION = 2 - - # Minimal ROI designed for the strategy - minimal_roi = { - "40": 0.0, - "30": 0.01, - "20": 0.02, - "0": 0.04 - } - - # Optimal stoploss designed for the strategy - stoploss = -0.10 - - # Optimal timeframe for the strategy - timeframe = '5m' - - # Optional order type mapping - order_types = { - 'buy': 'limit', - 'sell': 'limit', - 'stoploss': 'limit', - 'stoploss_on_exchange': False - } - - # Number of candles the strategy requires before producing valid signals - startup_candle_count: int = 20 - - # Optional time in force for orders - order_time_in_force = { - 'buy': 'gtc', - 'sell': 'gtc', - } - - 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 - """ - - # Momentum Indicator - # ------------------------------------ - - # ADX - dataframe['adx'] = ta.ADX(dataframe) - - # MACD - macd = ta.MACD(dataframe) - dataframe['macd'] = macd['macd'] - dataframe['macdsignal'] = macd['macdsignal'] - dataframe['macdhist'] = macd['macdhist'] - - # Minus Directional Indicator / Movement - dataframe['minus_di'] = ta.MINUS_DI(dataframe) - - # Plus Directional Indicator / Movement - dataframe['plus_di'] = ta.PLUS_DI(dataframe) - - # RSI - dataframe['rsi'] = ta.RSI(dataframe) - - # Stoch fast - stoch_fast = ta.STOCHF(dataframe) - dataframe['fastd'] = stoch_fast['fastd'] - dataframe['fastk'] = stoch_fast['fastk'] - - # 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'] - - # EMA - Exponential Moving Average - dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) - - 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 - :param metadata: Additional information, like the currently traded pair - :return: DataFrame with buy column - """ - dataframe.loc[ - ( - (dataframe['rsi'] < 35) & - (dataframe['fastd'] < 35) & - (dataframe['adx'] > 30) & - (dataframe['plus_di'] > 0.5) - ) | - ( - (dataframe['adx'] > 65) & - (dataframe['plus_di'] > 0.5) - ), - '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 - :param metadata: Additional information, like the currently traded pair - :return: DataFrame with buy column - """ - dataframe.loc[ - ( - ( - (qtpylib.crossed_above(dataframe['rsi'], 70)) | - (qtpylib.crossed_above(dataframe['fastd'], 70)) - ) & - (dataframe['adx'] > 10) & - (dataframe['minus_di'] > 0) - ) | - ( - (dataframe['adx'] > 70) & - (dataframe['minus_di'] > 0.5) - ), - 'sell'] = 1 - return dataframe - - def adjust_trade_position(self, pair: str, trade: Trade, current_time: datetime, - current_rate: float, current_profit: float, **kwargs): - - if current_profit < -0.0075: - return self.wallets.get_trade_stake_amount(pair, None) - - return None diff --git a/tests/strategy/strats/strategy_test_v2.py b/tests/strategy/strats/strategy_test_v2.py index 53e39526f..ac02b4436 100644 --- a/tests/strategy/strats/strategy_test_v2.py +++ b/tests/strategy/strats/strategy_test_v2.py @@ -5,7 +5,8 @@ from pandas import DataFrame import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.strategy.interface import IStrategy - +from freqtrade.persistence import Trade +from datetime import datetime class StrategyTestV2(IStrategy): """ @@ -154,3 +155,11 @@ class StrategyTestV2(IStrategy): ), 'sell'] = 1 return dataframe + + def adjust_trade_position(self, pair: str, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, **kwargs): + + if current_profit < -0.0075: + return self.wallets.get_trade_stake_amount(pair, None) + + return None