Fix some unit-tests. Use common trade entry code.

This commit is contained in:
Reigo Reinmets 2021-12-13 02:27:09 +02:00
parent 98255c18cf
commit 1017b68af9
4 changed files with 51 additions and 309 deletions

View File

@ -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
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,6 +618,7 @@ 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')
if trade is None:
trade = Trade(
pair=pair,
stake_amount=stake_amount,
@ -714,14 +637,14 @@ class FreqtradeBot(LoggingMixin):
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

View File

@ -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])

View File

@ -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

View File

@ -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