Merge pull request #5567 from samgermain/lev-freqtradebot

Lev freqtradebot
This commit is contained in:
Matthias 2021-10-20 15:48:07 +02:00 committed by GitHub
commit 79a91dc31b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1388 additions and 755 deletions

View File

@ -39,6 +39,8 @@ DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume']
# Don't modify sequence of DEFAULT_TRADES_COLUMNS # Don't modify sequence of DEFAULT_TRADES_COLUMNS
# it has wide consequences for stored trades files # it has wide consequences for stored trades files
DEFAULT_TRADES_COLUMNS = ['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost'] DEFAULT_TRADES_COLUMNS = ['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost']
TRADING_MODES = ['spot', 'margin', 'futures']
COLLATERAL_TYPES = ['cross', 'isolated']
LAST_BT_RESULT_FN = '.last_result.json' LAST_BT_RESULT_FN = '.last_result.json'
FTHYPT_FILEVERSION = 'fthypt_fileversion' FTHYPT_FILEVERSION = 'fthypt_fileversion'
@ -146,6 +148,8 @@ CONF_SCHEMA = {
'sell_profit_offset': {'type': 'number'}, 'sell_profit_offset': {'type': 'number'},
'ignore_roi_if_buy_signal': {'type': 'boolean'}, 'ignore_roi_if_buy_signal': {'type': 'boolean'},
'ignore_buying_expired_candle_after': {'type': 'number'}, 'ignore_buying_expired_candle_after': {'type': 'number'},
'trading_mode': {'type': 'string', 'enum': TRADING_MODES},
'collateral_type': {'type': 'string', 'enum': COLLATERAL_TYPES},
'bot_name': {'type': 'string'}, 'bot_name': {'type': 'string'},
'unfilledtimeout': { 'unfilledtimeout': {
'type': 'object', 'type': 'object',
@ -193,7 +197,7 @@ CONF_SCHEMA = {
'required': ['price_side'] 'required': ['price_side']
}, },
'custom_price_max_distance_ratio': { 'custom_price_max_distance_ratio': {
'type': 'number', 'minimum': 0.0 'type': 'number', 'minimum': 0.0
}, },
'order_types': { 'order_types': {
'type': 'object', 'type': 'object',

View File

@ -5,15 +5,21 @@ class RPCMessageType(Enum):
STATUS = 'status' STATUS = 'status'
WARNING = 'warning' WARNING = 'warning'
STARTUP = 'startup' STARTUP = 'startup'
BUY = 'buy' BUY = 'buy'
BUY_FILL = 'buy_fill' BUY_FILL = 'buy_fill'
BUY_CANCEL = 'buy_cancel' BUY_CANCEL = 'buy_cancel'
SELL = 'sell' SELL = 'sell'
SELL_FILL = 'sell_fill' SELL_FILL = 'sell_fill'
SELL_CANCEL = 'sell_cancel' SELL_CANCEL = 'sell_cancel'
PROTECTION_TRIGGER = 'protection_trigger' PROTECTION_TRIGGER = 'protection_trigger'
PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global' PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global'
SHORT = 'short'
SHORT_FILL = 'short_fill'
SHORT_CANCEL = 'short_cancel'
def __repr__(self): def __repr__(self):
return self.value return self.value

View File

@ -804,8 +804,14 @@ class Exchange:
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
self._lev_prep(pair, leverage) self._lev_prep(pair, leverage)
order = self._api.create_order(pair, ordertype, side, order = self._api.create_order(
amount, rate_for_order, params) pair,
ordertype,
side,
amount,
rate_for_order,
params
)
self._log_exchange_response('create_order', order) self._log_exchange_response('create_order', order)
return order return order

View File

@ -7,7 +7,7 @@ import traceback
from datetime import datetime, time, timezone from datetime import datetime, time, timezone
from math import isclose from math import isclose
from threading import Lock from threading import Lock
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional, Tuple
import arrow import arrow
from schedule import Scheduler from schedule import Scheduler
@ -17,7 +17,8 @@ from freqtrade.configuration import validate_config_consistency
from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.converter import order_book_to_dataframe
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.edge import Edge from freqtrade.edge import Edge
from freqtrade.enums import RPCMessageType, SellType, State, TradingMode from freqtrade.enums import (Collateral, RPCMessageType, SellType, SignalDirection, State,
TradingMode)
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
InvalidOrderException, PricingError) InvalidOrderException, PricingError)
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
@ -101,14 +102,19 @@ class FreqtradeBot(LoggingMixin):
initial_state = self.config.get('initial_state') initial_state = self.config.get('initial_state')
self.state = State[initial_state.upper()] if initial_state else State.STOPPED self.state = State[initial_state.upper()] if initial_state else State.STOPPED
# Protect sell-logic from forcesell and vice versa # Protect exit-logic from forcesell and vice versa
self._exit_lock = Lock() self._exit_lock = Lock()
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
self.trading_mode: TradingMode = TradingMode.SPOT
self.collateral_type: Optional[Collateral] = None
if 'trading_mode' in self.config: if 'trading_mode' in self.config:
self.trading_mode = TradingMode(self.config['trading_mode']) self.trading_mode = TradingMode(self.config['trading_mode'])
else:
self.trading_mode = TradingMode.SPOT if 'collateral_type' in self.config:
self.collateral_type = Collateral(self.config['collateral_type'])
self._schedule = Scheduler() self._schedule = Scheduler()
if self.trading_mode == TradingMode.FUTURES: if self.trading_mode == TradingMode.FUTURES:
@ -194,7 +200,7 @@ class FreqtradeBot(LoggingMixin):
# Protect from collisions with forceexit. # Protect from collisions with forceexit.
# Without this, freqtrade my try to recreate stoploss_on_exchange orders # Without this, freqtrade my try to recreate stoploss_on_exchange orders
# while selling is in process, since telegram messages arrive in an different thread. # while exiting is in process, since telegram messages arrive in an different thread.
with self._exit_lock: with self._exit_lock:
trades = Trade.get_open_trades() trades = Trade.get_open_trades()
# First process current opened trades (positions) # First process current opened trades (positions)
@ -305,21 +311,26 @@ class FreqtradeBot(LoggingMixin):
trades: List[Trade] = Trade.get_closed_trades_without_assigned_fees() trades: List[Trade] = Trade.get_closed_trades_without_assigned_fees()
for trade in trades: for trade in trades:
if not trade.is_open and not trade.fee_updated(trade.exit_side):
if not trade.is_open and not trade.fee_updated('sell'):
# Get sell fee # Get sell fee
order = trade.select_order('sell', False) order = trade.select_order(trade.exit_side, False)
if order: if order:
logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.") logger.info(
f"Updating {trade.exit_side}-fee on trade {trade}"
f"for order {order.order_id}."
)
self.update_trade_state(trade, order.order_id, self.update_trade_state(trade, order.order_id,
stoploss_order=order.ft_order_side == 'stoploss') stoploss_order=order.ft_order_side == 'stoploss')
trades: List[Trade] = Trade.get_open_trades_without_assigned_fees() trades: List[Trade] = Trade.get_open_trades_without_assigned_fees()
for trade in trades: for trade in trades:
if trade.is_open and not trade.fee_updated('buy'): if trade.is_open and not trade.fee_updated(trade.enter_side):
order = trade.select_order('buy', False) order = trade.select_order(trade.enter_side, False)
if order: if order:
logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") logger.info(
f"Updating {trade.enter_side}-fee on trade {trade}"
f"for order {order.order_id}."
)
self.update_trade_state(trade, order.order_id) self.update_trade_state(trade, order.order_id)
def handle_insufficient_funds(self, trade: Trade): def handle_insufficient_funds(self, trade: Trade):
@ -327,8 +338,8 @@ class FreqtradeBot(LoggingMixin):
Determine if we ever opened a exiting order for this trade. Determine if we ever opened a exiting order for this trade.
If not, try update entering fees - otherwise "refind" the open order we obviously lost. If not, try update entering fees - otherwise "refind" the open order we obviously lost.
""" """
sell_order = trade.select_order('sell', None) exit_order = trade.select_order(trade.exit_side, None)
if sell_order: if exit_order:
self.refind_lost_order(trade) self.refind_lost_order(trade)
else: else:
self.reupdate_enter_order_fees(trade) self.reupdate_enter_order_fees(trade)
@ -338,10 +349,11 @@ class FreqtradeBot(LoggingMixin):
Get buy order from database, and try to reupdate. Get buy order from database, and try to reupdate.
Handles trades where the initial fee-update did not work. Handles trades where the initial fee-update did not work.
""" """
logger.info(f"Trying to reupdate buy fees for {trade}") logger.info(f"Trying to reupdate {trade.enter_side} fees for {trade}")
order = trade.select_order('buy', False) order = trade.select_order(trade.enter_side, False)
if order: if order:
logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") logger.info(
f"Updating {trade.enter_side}-fee on trade {trade} for order {order.order_id}.")
self.update_trade_state(trade, order.order_id) self.update_trade_state(trade, order.order_id)
def refind_lost_order(self, trade): def refind_lost_order(self, trade):
@ -357,7 +369,7 @@ class FreqtradeBot(LoggingMixin):
if not order.ft_is_open: if not order.ft_is_open:
logger.debug(f"Order {order} is no longer open.") logger.debug(f"Order {order} is no longer open.")
continue continue
if order.ft_order_side == 'buy': if order.ft_order_side == trade.enter_side:
# Skip buy side - this is handled by reupdate_enter_order_fees # Skip buy side - this is handled by reupdate_enter_order_fees
continue continue
try: try:
@ -367,7 +379,7 @@ class FreqtradeBot(LoggingMixin):
if fo and fo['status'] == 'open': if fo and fo['status'] == 'open':
# Assume this as the open stoploss order # Assume this as the open stoploss order
trade.stoploss_order_id = order.order_id trade.stoploss_order_id = order.order_id
elif order.ft_order_side == 'sell': elif order.ft_order_side == trade.exit_side:
if fo and fo['status'] == 'open': if fo and fo['status'] == 'open':
# Assume this as the open order # Assume this as the open order
trade.open_order_id = order.order_id trade.open_order_id = order.order_id
@ -456,7 +468,9 @@ class FreqtradeBot(LoggingMixin):
# running get_signal on historical data fetched # running get_signal on historical data fetched
(signal, enter_tag) = self.strategy.get_entry_signal( (signal, enter_tag) = self.strategy.get_entry_signal(
pair, self.strategy.timeframe, analyzed_df pair,
self.strategy.timeframe,
analyzed_df
) )
if signal: if signal:
@ -465,19 +479,31 @@ class FreqtradeBot(LoggingMixin):
bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {}) bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {})
if ((bid_check_dom.get('enabled', False)) and if ((bid_check_dom.get('enabled', False)) and
(bid_check_dom.get('bids_to_ask_delta', 0) > 0)): (bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
# TODO-lev: Does the below need to be adjusted for shorts? if self._check_depth_of_market(pair, bid_check_dom, side=signal):
if self._check_depth_of_market_buy(pair, bid_check_dom): return self.execute_entry(
# TODO-lev: pass in "enter" as side. pair,
stake_amount,
return self.execute_entry(pair, stake_amount, enter_tag=enter_tag) enter_tag=enter_tag,
is_short=(signal == SignalDirection.SHORT)
)
else: else:
return False return False
return self.execute_entry(pair, stake_amount, enter_tag=enter_tag) return self.execute_entry(
pair,
stake_amount,
enter_tag=enter_tag,
is_short=(signal == SignalDirection.SHORT)
)
else: else:
return False return False
def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool: def _check_depth_of_market(
self,
pair: str,
conf: Dict,
side: SignalDirection
) -> bool:
""" """
Checks depth of market before executing a buy Checks depth of market before executing a buy
""" """
@ -487,9 +513,17 @@ class FreqtradeBot(LoggingMixin):
order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks']) 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_bids = order_book_data_frame['b_size'].sum()
order_book_asks = order_book_data_frame['a_size'].sum() order_book_asks = order_book_data_frame['a_size'].sum()
bids_ask_delta = order_book_bids / order_book_asks
enter_side = order_book_bids if side == SignalDirection.LONG else order_book_asks
exit_side = order_book_asks if side == SignalDirection.LONG else order_book_bids
bids_ask_delta = enter_side / exit_side
bids = f"Bids: {order_book_bids}"
asks = f"Asks: {order_book_asks}"
delta = f"Delta: {bids_ask_delta}"
logger.info( logger.info(
f"Bids: {order_book_bids}, Asks: {order_book_asks}, Delta: {bids_ask_delta}, " f"{bids}, {asks}, {delta}, Direction: {side.value}"
f"Bid Price: {order_book['bids'][0][0]}, Ask Price: {order_book['asks'][0][0]}, " 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 Bid Quantity: {order_book['bids'][0][1]}, "
f"Immediate Ask Quantity: {order_book['asks'][0][1]}." f"Immediate Ask Quantity: {order_book['asks'][0][1]}."
@ -501,21 +535,65 @@ class FreqtradeBot(LoggingMixin):
logger.info(f"Bids to asks delta for {pair} does not satisfy condition.") logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
return False return False
def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None, def leverage_prep(
forcebuy: bool = False, enter_tag: Optional[str] = None) -> bool: self,
pair: str,
open_rate: float,
amount: float,
leverage: float,
is_short: bool
) -> Tuple[float, Optional[float]]:
interest_rate = 0.0
isolated_liq = None
# TODO-lev: Uncomment once liq and interest merged in
# if TradingMode == TradingMode.MARGIN:
# interest_rate = self.exchange.get_interest_rate(
# pair=pair,
# open_rate=open_rate,
# is_short=is_short
# )
# if self.collateral_type == Collateral.ISOLATED:
# isolated_liq = liquidation_price(
# exchange_name=self.exchange.name,
# trading_mode=self.trading_mode,
# open_rate=open_rate,
# amount=amount,
# leverage=leverage,
# is_short=is_short
# )
return interest_rate, isolated_liq
def execute_entry(
self,
pair: str,
stake_amount: float,
price: Optional[float] = None,
forcebuy: bool = False,
leverage: float = 1.0,
is_short: bool = False,
enter_tag: Optional[str] = None
) -> bool:
""" """
Executes a limit buy for the given pair Executes a limit buy for the given pair
:param pair: pair for which we want to create a LIMIT_BUY :param pair: pair for which we want to create a LIMIT_BUY
:param stake_amount: amount of stake-currency for the pair :param stake_amount: amount of stake-currency for the pair
:param leverage: amount of leverage applied to this trade
:return: True if a buy order is created, false if it fails. :return: True if a buy order is created, false if it fails.
""" """
time_in_force = self.strategy.order_time_in_force['buy'] time_in_force = self.strategy.order_time_in_force['buy']
[side, name] = ['sell', 'Short'] if is_short else ['buy', 'Long']
if price: if price:
enter_limit_requested = price enter_limit_requested = price
else: else:
# Calculate price # Calculate price
proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side="buy") proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side=side)
custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
default_retval=proposed_enter_rate)( default_retval=proposed_enter_rate)(
pair=pair, current_time=datetime.now(timezone.utc), pair=pair, current_time=datetime.now(timezone.utc),
@ -524,10 +602,14 @@ class FreqtradeBot(LoggingMixin):
enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate) enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate)
if not enter_limit_requested: if not enter_limit_requested:
raise PricingError('Could not determine buy price.') raise PricingError(f'Could not determine {side} price.')
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, enter_limit_requested, min_stake_amount = self.exchange.get_min_pair_stake_amount(
self.strategy.stoploss) pair,
enter_limit_requested,
self.strategy.stoploss,
leverage=leverage
)
if not self.edge: if not self.edge:
max_stake_amount = self.wallets.get_available_stake_amount() max_stake_amount = self.wallets.get_available_stake_amount()
@ -543,10 +625,12 @@ class FreqtradeBot(LoggingMixin):
if not stake_amount: if not stake_amount:
return False return False
logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: " logger.info(
f"{stake_amount} ...") f"{name} signal found: about create a new trade for {pair} with stake_amount: "
f"{stake_amount} ..."
)
amount = stake_amount / enter_limit_requested amount = (stake_amount / enter_limit_requested) * leverage
order_type = self.strategy.order_types['buy'] order_type = self.strategy.order_types['buy']
if forcebuy: if forcebuy:
# Forcebuy can define a different ordertype # Forcebuy can define a different ordertype
@ -558,15 +642,21 @@ class FreqtradeBot(LoggingMixin):
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( 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, pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
time_in_force=time_in_force, current_time=datetime.now(timezone.utc), time_in_force=time_in_force, current_time=datetime.now(timezone.utc),
side='long' side='short' if is_short else 'long'
): ):
logger.info(f"User requested abortion of buying {pair}") logger.info(f"User requested abortion of buying {pair}")
return False return False
amount = self.exchange.amount_to_precision(pair, amount) amount = self.exchange.amount_to_precision(pair, amount)
order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy", order = self.exchange.create_order(
amount=amount, rate=enter_limit_requested, pair=pair,
time_in_force=time_in_force) ordertype=order_type,
order_obj = Order.parse_from_ccxt_object(order, pair, 'buy') side=side,
amount=amount,
rate=enter_limit_requested,
time_in_force=time_in_force,
leverage=leverage
)
order_obj = Order.parse_from_ccxt_object(order, pair, side)
order_id = order['id'] order_id = order['id']
order_status = order.get('status', None) order_status = order.get('status', None)
@ -579,17 +669,17 @@ class FreqtradeBot(LoggingMixin):
# return false if the order is not filled # return false if the order is not filled
if float(order['filled']) == 0: if float(order['filled']) == 0:
logger.warning('Buy %s order with time in force %s for %s is %s by %s.' logger.warning('%s %s order with time in force %s for %s is %s by %s.'
' zero amount is fulfilled.', ' zero amount is fulfilled.',
order_tif, order_type, pair, order_status, self.exchange.name) name, order_tif, order_type, pair, order_status, self.exchange.name)
return False return False
else: else:
# the order is partially fulfilled # the order is partially fulfilled
# in case of IOC orders we can check immediately # in case of IOC orders we can check immediately
# if the order is fulfilled fully or partially # if the order is fulfilled fully or partially
logger.warning('Buy %s order with time in force %s for %s is %s by %s.' logger.warning('%s %s order with time in force %s for %s is %s by %s.'
' %s amount fulfilled out of %s (%s remaining which is canceled).', ' %s amount fulfilled out of %s (%s remaining which is canceled).',
order_tif, order_type, pair, order_status, self.exchange.name, name, order_tif, order_type, pair, order_status, self.exchange.name,
order['filled'], order['amount'], order['remaining'] order['filled'], order['amount'], order['remaining']
) )
stake_amount = order['cost'] stake_amount = order['cost']
@ -602,6 +692,14 @@ class FreqtradeBot(LoggingMixin):
amount = safe_value_fallback(order, 'filled', 'amount') amount = safe_value_fallback(order, 'filled', 'amount')
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') enter_limit_filled_price = safe_value_fallback(order, 'average', 'price')
interest_rate, isolated_liq = self.leverage_prep(
leverage=leverage,
pair=pair,
amount=amount,
open_rate=enter_limit_filled_price,
is_short=is_short
)
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
open_date = datetime.now(timezone.utc) open_date = datetime.now(timezone.utc)
@ -627,6 +725,10 @@ class FreqtradeBot(LoggingMixin):
# TODO-lev: compatibility layer for buy_tag (!) # TODO-lev: compatibility layer for buy_tag (!)
buy_tag=enter_tag, buy_tag=enter_tag,
timeframe=timeframe_to_minutes(self.config['timeframe']), timeframe=timeframe_to_minutes(self.config['timeframe']),
leverage=leverage,
is_short=is_short,
interest_rate=interest_rate,
isolated_liq=isolated_liq,
trading_mode=self.trading_mode, trading_mode=self.trading_mode,
funding_fees=funding_fees funding_fees=funding_fees
) )
@ -652,7 +754,7 @@ class FreqtradeBot(LoggingMixin):
""" """
msg = { msg = {
'trade_id': trade.id, 'trade_id': trade.id,
'type': RPCMessageType.BUY, 'type': RPCMessageType.SHORT if trade.is_short else RPCMessageType.BUY,
'buy_tag': trade.buy_tag, 'buy_tag': trade.buy_tag,
'exchange': self.exchange.name.capitalize(), 'exchange': self.exchange.name.capitalize(),
'pair': trade.pair, 'pair': trade.pair,
@ -673,11 +775,11 @@ class FreqtradeBot(LoggingMixin):
""" """
Sends rpc notification when a entry order cancel occurred. Sends rpc notification when a entry order cancel occurred.
""" """
current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="buy") current_rate = self.exchange.get_rate(trade.pair, refresh=False, side=trade.enter_side)
msg_type = RPCMessageType.SHORT_CANCEL if trade.is_short else RPCMessageType.BUY_CANCEL
msg = { msg = {
'trade_id': trade.id, 'trade_id': trade.id,
'type': RPCMessageType.BUY_CANCEL, 'type': msg_type,
'buy_tag': trade.buy_tag, 'buy_tag': trade.buy_tag,
'exchange': self.exchange.name.capitalize(), 'exchange': self.exchange.name.capitalize(),
'pair': trade.pair, 'pair': trade.pair,
@ -696,9 +798,10 @@ class FreqtradeBot(LoggingMixin):
self.rpc.send_msg(msg) self.rpc.send_msg(msg)
def _notify_enter_fill(self, trade: Trade) -> None: def _notify_enter_fill(self, trade: Trade) -> None:
msg_type = RPCMessageType.SHORT_FILL if trade.is_short else RPCMessageType.BUY_FILL
msg = { msg = {
'trade_id': trade.id, 'trade_id': trade.id,
'type': RPCMessageType.BUY_FILL, 'type': msg_type,
'buy_tag': trade.buy_tag, 'buy_tag': trade.buy_tag,
'exchange': self.exchange.name.capitalize(), 'exchange': self.exchange.name.capitalize(),
'pair': trade.pair, 'pair': trade.pair,
@ -752,6 +855,7 @@ class FreqtradeBot(LoggingMixin):
logger.debug('Handling %s ...', trade) logger.debug('Handling %s ...', trade)
(enter, exit_) = (False, False) (enter, exit_) = (False, False)
exit_signal_type = "exit_short" if trade.is_short else "exit_long"
# TODO-lev: change to use_exit_signal, ignore_roi_if_enter_signal # TODO-lev: change to use_exit_signal, ignore_roi_if_enter_signal
if (self.config.get('use_sell_signal', True) or if (self.config.get('use_sell_signal', True) or
@ -762,15 +866,16 @@ class FreqtradeBot(LoggingMixin):
(enter, exit_) = self.strategy.get_exit_signal( (enter, exit_) = self.strategy.get_exit_signal(
trade.pair, trade.pair,
self.strategy.timeframe, self.strategy.timeframe,
analyzed_df, is_short=trade.is_short analyzed_df,
is_short=trade.is_short
) )
# TODO-lev: side should depend on trade side. logger.debug('checking exit')
exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side=trade.exit_side)
if self._check_and_execute_exit(trade, exit_rate, enter, exit_): if self._check_and_execute_exit(trade, exit_rate, enter, exit_):
return True return True
logger.debug('Found no sell signal for %s.', trade) logger.debug(f'Found no {exit_signal_type} signal for %s.', trade)
return False return False
def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool: def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool:
@ -855,7 +960,10 @@ class FreqtradeBot(LoggingMixin):
# If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange # If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange
if not stoploss_order: if not stoploss_order:
stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss
stop_price = trade.open_rate * (1 + stoploss) if trade.is_short:
stop_price = trade.open_rate * (1 - stoploss)
else:
stop_price = trade.open_rate * (1 + stoploss)
if self.create_stoploss_order(trade=trade, stop_price=stop_price): if self.create_stoploss_order(trade=trade, stop_price=stop_price):
trade.stoploss_last_update = datetime.utcnow() trade.stoploss_last_update = datetime.utcnow()
@ -880,11 +988,11 @@ class FreqtradeBot(LoggingMixin):
# if trailing stoploss is enabled we check if stoploss value has changed # 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 # in which case we cancel stoploss order and put another one with new
# value immediately # value immediately
self.handle_trailing_stoploss_on_exchange(trade, stoploss_order, side=trade.exit_side) self.handle_trailing_stoploss_on_exchange(trade, stoploss_order)
return False return False
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict, side: str) -> None: def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> None:
""" """
Check to see if stoploss on exchange should be updated Check to see if stoploss on exchange should be updated
in case of trailing stoploss on exchange in case of trailing stoploss on exchange
@ -892,7 +1000,7 @@ class FreqtradeBot(LoggingMixin):
:param order: Current on exchange stoploss order :param order: Current on exchange stoploss order
:return: None :return: None
""" """
if self.exchange.stoploss_adjust(trade.stop_loss, order, side): if self.exchange.stoploss_adjust(trade.stop_loss, order, side=trade.exit_side):
# we check if the update is necessary # we check if the update is necessary
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat: if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:
@ -918,7 +1026,11 @@ class FreqtradeBot(LoggingMixin):
Check and execute trade exit Check and execute trade exit
""" """
should_exit: SellCheckTuple = self.strategy.should_exit( should_exit: SellCheckTuple = self.strategy.should_exit(
trade, exit_rate, datetime.now(timezone.utc), enter=enter, exit_=exit_, trade,
exit_rate,
datetime.now(timezone.utc),
enter=enter,
exit_=exit_,
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
) )
@ -959,24 +1071,23 @@ class FreqtradeBot(LoggingMixin):
continue continue
fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order) fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order)
is_entering = order['side'] == trade.enter_side
not_closed = order['status'] == 'open' or fully_cancelled
side = trade.enter_side if is_entering else trade.exit_side
timed_out = self._check_timed_out(side, order)
time_method = 'check_sell_timeout' if order['side'] == 'sell' else 'check_buy_timeout'
if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and ( if not_closed and (fully_cancelled or timed_out or (
fully_cancelled strategy_safe_wrapper(getattr(self.strategy, time_method), default_retval=False)(
or self._check_timed_out('buy', order) pair=trade.pair,
or strategy_safe_wrapper(self.strategy.check_buy_timeout, trade=trade,
default_retval=False)(pair=trade.pair, order=order
trade=trade, )
order=order))): )):
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) if is_entering:
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and ( else:
fully_cancelled self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
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: def cancel_all_open_orders(self) -> None:
""" """
@ -991,10 +1102,10 @@ class FreqtradeBot(LoggingMixin):
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
continue continue
if order['side'] == 'buy': if order['side'] == trade.enter_side:
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
elif order['side'] == 'sell': elif order['side'] == trade.exit_side:
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
Trade.commit() Trade.commit()
@ -1016,7 +1127,7 @@ class FreqtradeBot(LoggingMixin):
if filled_val > 0 and filled_stake < minstake: if filled_val > 0 and filled_stake < minstake:
logger.warning( logger.warning(
f"Order {trade.open_order_id} for {trade.pair} not cancelled, " 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.") f"as the filled amount of {filled_val} would result in an unexitable trade.")
return False return False
corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
trade.amount) trade.amount)
@ -1031,12 +1142,16 @@ class FreqtradeBot(LoggingMixin):
corder = order corder = order
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
logger.info('Buy order %s for %s.', reason, trade) side = trade.enter_side.capitalize()
logger.info('%s order %s for %s.', side, reason, trade)
# Using filled to determine the filled amount # Using filled to determine the filled amount
filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled') filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled')
if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC): if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC):
logger.info('Buy order fully cancelled. Removing %s from database.', trade) logger.info(
'%s order fully cancelled. Removing %s from database.',
side, trade
)
# if trade is not partially completed, just delete the trade # if trade is not partially completed, just delete the trade
trade.delete() trade.delete()
was_trade_fully_canceled = True was_trade_fully_canceled = True
@ -1054,11 +1169,11 @@ class FreqtradeBot(LoggingMixin):
self.update_trade_state(trade, trade.open_order_id, corder) self.update_trade_state(trade, trade.open_order_id, corder)
trade.open_order_id = None trade.open_order_id = None
logger.info('Partial buy order timeout for %s.', trade) logger.info('Partial %s order timeout for %s.', trade.enter_side, trade)
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}" reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
self.wallets.update() self.wallets.update()
self._notify_enter_cancel(trade, order_type=self.strategy.order_types['buy'], self._notify_enter_cancel(trade, order_type=self.strategy.order_types[trade.enter_side],
reason=reason) reason=reason)
return was_trade_fully_canceled return was_trade_fully_canceled
@ -1076,12 +1191,13 @@ class FreqtradeBot(LoggingMixin):
trade.amount) trade.amount)
trade.update_order(co) trade.update_order(co)
except InvalidOrderException: except InvalidOrderException:
logger.exception(f"Could not cancel sell order {trade.open_order_id}") logger.exception(
f"Could not cancel {trade.exit_side} order {trade.open_order_id}")
return 'error cancelling order' return 'error cancelling order'
logger.info('Sell order %s for %s.', reason, trade) logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade)
else: else:
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
logger.info('Sell order %s for %s.', reason, trade) logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade)
trade.update_order(order) trade.update_order(order)
trade.close_rate = None trade.close_rate = None
@ -1098,7 +1214,7 @@ class FreqtradeBot(LoggingMixin):
self.wallets.update() self.wallets.update()
self._notify_exit_cancel( self._notify_exit_cancel(
trade, trade,
order_type=self.strategy.order_types['sell'], order_type=self.strategy.order_types[trade.exit_side],
reason=reason reason=reason
) )
return reason return reason
@ -1129,7 +1245,12 @@ class FreqtradeBot(LoggingMixin):
raise DependencyException( raise DependencyException(
f"Not enough amount to exit trade. Trade-amount: {amount}, Wallet: {wallet_amount}") f"Not enough amount to exit trade. Trade-amount: {amount}, Wallet: {wallet_amount}")
def execute_trade_exit(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool: def execute_trade_exit(
self,
trade: Trade,
limit: float,
sell_reason: SellCheckTuple, # TODO-lev update to exit_reason
) -> bool:
""" """
Executes a trade exit for the given trade and limit Executes a trade exit for the given trade and limit
:param trade: Trade instance :param trade: Trade instance
@ -1137,13 +1258,13 @@ class FreqtradeBot(LoggingMixin):
:param sell_reason: Reason the sell was triggered :param sell_reason: Reason the sell was triggered
:return: True if it succeeds (supported) False (not supported) :return: True if it succeeds (supported) False (not supported)
""" """
sell_type = 'sell' # TODO-lev: Update to exit exit_type = 'sell' # TODO-lev: Update to exit
if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
sell_type = 'stoploss' exit_type = 'stoploss'
# if stoploss is on exchange and we are on dry_run mode, # if stoploss is on exchange and we are on dry_run mode,
# we consider the sell price stop price # we consider the sell price stop price
if self.config['dry_run'] and sell_type == 'stoploss' \ if self.config['dry_run'] and exit_type == 'stoploss' \
and self.strategy.order_types['stoploss_on_exchange']: and self.strategy.order_types['stoploss_on_exchange']:
limit = trade.stop_loss limit = trade.stop_loss
@ -1167,7 +1288,7 @@ class FreqtradeBot(LoggingMixin):
except InvalidOrderException: except InvalidOrderException:
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
order_type = self.strategy.order_types[sell_type] order_type = self.strategy.order_types[exit_type]
if sell_reason.sell_type == SellType.EMERGENCY_SELL: if sell_reason.sell_type == SellType.EMERGENCY_SELL:
# Emergency sells (default to market!) # Emergency sells (default to market!)
order_type = self.strategy.order_types.get("emergencysell", "market") order_type = self.strategy.order_types.get("emergencysell", "market")
@ -1177,7 +1298,7 @@ class FreqtradeBot(LoggingMixin):
order_type = self.strategy.order_types.get("forcesell", order_type) order_type = self.strategy.order_types.get("forcesell", order_type)
amount = self._safe_exit_amount(trade.pair, trade.amount) amount = self._safe_exit_amount(trade.pair, trade.amount)
time_in_force = self.strategy.order_time_in_force['sell'] time_in_force = self.strategy.order_time_in_force['sell'] # TODO-lev update to exit
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( 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, pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit,
@ -1191,7 +1312,7 @@ class FreqtradeBot(LoggingMixin):
order = self.exchange.create_order( order = self.exchange.create_order(
pair=trade.pair, pair=trade.pair,
ordertype=order_type, ordertype=order_type,
side="sell", side=trade.exit_side,
amount=amount, amount=amount,
rate=limit, rate=limit,
time_in_force=time_in_force time_in_force=time_in_force
@ -1202,7 +1323,7 @@ class FreqtradeBot(LoggingMixin):
self.handle_insufficient_funds(trade) self.handle_insufficient_funds(trade)
return False return False
order_obj = Order.parse_from_ccxt_object(order, trade.pair, 'sell') order_obj = Order.parse_from_ccxt_object(order, trade.pair, trade.exit_side)
trade.orders.append(order_obj) trade.orders.append(order_obj)
trade.open_order_id = order['id'] trade.open_order_id = order['id']
@ -1230,7 +1351,7 @@ class FreqtradeBot(LoggingMixin):
profit_trade = trade.calc_profit(rate=profit_rate) profit_trade = trade.calc_profit(rate=profit_rate)
# Use cached rates here - it was updated seconds ago. # Use cached rates here - it was updated seconds ago.
current_rate = self.exchange.get_rate( current_rate = self.exchange.get_rate(
trade.pair, refresh=False, side="sell") if not fill else None trade.pair, refresh=False, side=trade.exit_side) if not fill else None
profit_ratio = trade.calc_profit_ratio(profit_rate) profit_ratio = trade.calc_profit_ratio(profit_rate)
gain = "profit" if profit_ratio > 0 else "loss" gain = "profit" if profit_ratio > 0 else "loss"
@ -1275,7 +1396,7 @@ class FreqtradeBot(LoggingMixin):
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
profit_trade = trade.calc_profit(rate=profit_rate) profit_trade = trade.calc_profit(rate=profit_rate)
current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="sell") current_rate = self.exchange.get_rate(trade.pair, refresh=False, side=trade.exit_side)
profit_ratio = trade.calc_profit_ratio(profit_rate) profit_ratio = trade.calc_profit_ratio(profit_rate)
gain = "profit" if profit_ratio > 0 else "loss" gain = "profit" if profit_ratio > 0 else "loss"
@ -1390,7 +1511,7 @@ class FreqtradeBot(LoggingMixin):
self.wallets.update() self.wallets.update()
if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount: if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount:
# Eat into dust if we own more than base currency # Eat into dust if we own more than base currency
# TODO-lev: won't be in "base"(quote) currency for shorts # TODO-lev: won't be in base currency for shorts
logger.info(f"Fee amount for {trade} was in base currency - " logger.info(f"Fee amount for {trade} was in base currency - "
f"Eating Fee {fee_abs} into dust.") f"Eating Fee {fee_abs} into dust.")
elif fee_abs != 0: elif fee_abs != 0:

View File

@ -506,7 +506,6 @@ class LocalTrade():
lower_stop = new_loss < self.stop_loss lower_stop = new_loss < self.stop_loss
# stop losses only walk up, never down!, # stop losses only walk up, never down!,
# TODO-lev
# ? But adding more to a leveraged trade would create a lower liquidation price, # ? But adding more to a leveraged trade would create a lower liquidation price,
# ? decreasing the minimum stoploss # ? decreasing the minimum stoploss
if (higher_stop and not self.is_short) or (lower_stop and self.is_short): if (higher_stop and not self.is_short) or (lower_stop and self.is_short):

View File

@ -840,28 +840,32 @@ class IStrategy(ABC, HyperStrategyMixin):
else: else:
logger.warning("CustomStoploss function did not return valid stoploss") logger.warning("CustomStoploss function did not return valid stoploss")
if self.trailing_stop and trade.stop_loss < (low or current_rate): sl_lower_long = (trade.stop_loss < (low or current_rate) and not trade.is_short)
sl_higher_short = (trade.stop_loss > (high or current_rate) and trade.is_short)
if self.trailing_stop and (sl_lower_long or sl_higher_short):
# trailing stoploss handling # trailing stoploss handling
sl_offset = self.trailing_stop_positive_offset sl_offset = self.trailing_stop_positive_offset
# Make sure current_profit is calculated using high for backtesting. # Make sure current_profit is calculated using high for backtesting.
# TODO-lev: Check this function - high / low usage must be inversed for short trades! bound = low if trade.is_short else high
high_profit = current_profit if not high else trade.calc_profit_ratio(high) bound_profit = current_profit if not bound else trade.calc_profit_ratio(bound)
# Don't update stoploss if trailing_only_offset_is_reached is true. # Don't update stoploss if trailing_only_offset_is_reached is true.
if not (self.trailing_only_offset_is_reached and high_profit < sl_offset): if not (self.trailing_only_offset_is_reached and bound_profit < sl_offset):
# Specific handling for trailing_stop_positive # Specific handling for trailing_stop_positive
if self.trailing_stop_positive is not None and high_profit > sl_offset: if self.trailing_stop_positive is not None and bound_profit > sl_offset:
stop_loss_value = self.trailing_stop_positive stop_loss_value = self.trailing_stop_positive
logger.debug(f"{trade.pair} - Using positive stoploss: {stop_loss_value} " logger.debug(f"{trade.pair} - Using positive stoploss: {stop_loss_value} "
f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%") f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%")
trade.adjust_stop_loss(high or current_rate, stop_loss_value) trade.adjust_stop_loss(bound or current_rate, stop_loss_value)
sl_higher_short = (trade.stop_loss >= (low or current_rate) and not trade.is_short)
sl_lower_long = ((trade.stop_loss <= (high or current_rate) and trade.is_short))
# evaluate if the stoploss was hit if stoploss is not on exchange # evaluate if the stoploss was hit if stoploss is not on exchange
# in Dry-Run, this handles stoploss logic as well, as the logic will not be different to # in Dry-Run, this handles stoploss logic as well, as the logic will not be different to
# regular stoploss handling. # regular stoploss handling.
if ((trade.stop_loss >= (low or current_rate)) and if ((sl_higher_short or sl_lower_long) and
(not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])): (not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])):
sell_type = SellType.STOP_LOSS sell_type = SellType.STOP_LOSS
@ -870,12 +874,18 @@ class IStrategy(ABC, HyperStrategyMixin):
if trade.initial_stop_loss != trade.stop_loss: if trade.initial_stop_loss != trade.stop_loss:
sell_type = SellType.TRAILING_STOP_LOSS sell_type = SellType.TRAILING_STOP_LOSS
logger.debug( logger.debug(
f"{trade.pair} - HIT STOP: current price at {(low or current_rate):.6f}, " f"{trade.pair} - HIT STOP: current price at "
f"{((high if trade.is_short else low) or current_rate):.6f}, "
f"stoploss is {trade.stop_loss:.6f}, " f"stoploss is {trade.stop_loss:.6f}, "
f"initial stoploss was at {trade.initial_stop_loss:.6f}, " f"initial stoploss was at {trade.initial_stop_loss:.6f}, "
f"trade opened at {trade.open_rate:.6f}") f"trade opened at {trade.open_rate:.6f}")
new_stoploss = (
trade.stop_loss + trade.initial_stop_loss
if trade.is_short else
trade.stop_loss - trade.initial_stop_loss
)
logger.debug(f"{trade.pair} - Trailing stop saved " logger.debug(f"{trade.pair} - Trailing stop saved "
f"{trade.stop_loss - trade.initial_stop_loss:.6f}") f"{new_stoploss:.6f}")
return SellCheckTuple(sell_type=sell_type) return SellCheckTuple(sell_type=sell_type)

View File

@ -58,6 +58,8 @@ class SampleStrategy(IStrategy):
# Hyperoptable parameters # Hyperoptable parameters
buy_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) buy_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True)
sell_rsi = IntParameter(low=50, high=100, default=70, space='sell', optimize=True, load=True) sell_rsi = IntParameter(low=50, high=100, default=70, space='sell', optimize=True, load=True)
short_rsi = IntParameter(low=51, high=100, default=70, space='sell', optimize=True, load=True)
exit_short_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True)
# Optimal timeframe for the strategy. # Optimal timeframe for the strategy.
timeframe = '5m' timeframe = '5m'
@ -354,6 +356,16 @@ class SampleStrategy(IStrategy):
), ),
'enter_long'] = 1 'enter_long'] = 1
dataframe.loc[
(
# Signal: RSI crosses above 70
(qtpylib.crossed_above(dataframe['rsi'], self.short_rsi.value)) &
(dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
'enter_short'] = 1
return dataframe return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
@ -371,5 +383,18 @@ class SampleStrategy(IStrategy):
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling (dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling
(dataframe['volume'] > 0) # Make sure Volume is not 0 (dataframe['volume'] > 0) # Make sure Volume is not 0
), ),
'exit_long'] = 1 'exit_long'] = 1
dataframe.loc[
(
# Signal: RSI crosses above 30
(qtpylib.crossed_above(dataframe['rsi'], self.exit_short_rsi.value)) &
# Guard: tema below BB middle
(dataframe['tema'] <= dataframe['bb_middleband']) &
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
'exit_short'] = 1
return dataframe return dataframe

View File

@ -903,7 +903,7 @@ def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, tmpdir):
mocker.patch( mocker.patch(
'freqtrade.optimize.hyperopt_tools.HyperoptTools._test_hyperopt_results_exist', 'freqtrade.optimize.hyperopt_tools.HyperoptTools._test_hyperopt_results_exist',
return_value=True return_value=True
) )
def fake_iterator(*args, **kwargs): def fake_iterator(*args, **kwargs):
yield from [saved_hyperopt_results] yield from [saved_hyperopt_results]
@ -1309,9 +1309,10 @@ def test_start_list_data(testdatadir, capsys):
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
# TODO-lev: Short trades?
def test_show_trades(mocker, fee, capsys, caplog): def test_show_trades(mocker, fee, capsys, caplog):
mocker.patch("freqtrade.persistence.init_db") mocker.patch("freqtrade.persistence.init_db")
create_mock_trades(fee) create_mock_trades(fee, False)
args = [ args = [
"show-trades", "show-trades",
"--db-url", "--db-url",

View File

@ -209,8 +209,14 @@ def get_patched_worker(mocker, config) -> Worker:
return Worker(args=None, config=config) return Worker(args=None, config=config)
def patch_get_signal(freqtrade: FreqtradeBot, enter_long=True, exit_long=False, def patch_get_signal(
enter_short=False, exit_short=False, enter_tag: Optional[str] = None) -> None: freqtrade: FreqtradeBot,
enter_long=True,
exit_long=False,
enter_short=False,
exit_short=False,
enter_tag: Optional[str] = None
) -> None:
""" """
:param mocker: mocker to patch IStrategy class :param mocker: mocker to patch IStrategy class
:param value: which value IStrategy.get_signal() must return :param value: which value IStrategy.get_signal() must return
@ -241,7 +247,7 @@ def patch_get_signal(freqtrade: FreqtradeBot, enter_long=True, exit_long=False,
freqtrade.exchange.refresh_latest_ohlcv = lambda p: None freqtrade.exchange.refresh_latest_ohlcv = lambda p: None
def create_mock_trades(fee, use_db: bool = True): def create_mock_trades(fee, is_short: bool, use_db: bool = True):
""" """
Create some fake trades ... Create some fake trades ...
""" """
@ -252,22 +258,22 @@ def create_mock_trades(fee, use_db: bool = True):
LocalTrade.add_bt_trade(trade) LocalTrade.add_bt_trade(trade)
# Simulate dry_run entries # Simulate dry_run entries
trade = mock_trade_1(fee) trade = mock_trade_1(fee, is_short)
add_trade(trade) add_trade(trade)
trade = mock_trade_2(fee) trade = mock_trade_2(fee, is_short)
add_trade(trade) add_trade(trade)
trade = mock_trade_3(fee) trade = mock_trade_3(fee, is_short)
add_trade(trade) add_trade(trade)
trade = mock_trade_4(fee) trade = mock_trade_4(fee, is_short)
add_trade(trade) add_trade(trade)
trade = mock_trade_5(fee) trade = mock_trade_5(fee, is_short)
add_trade(trade) add_trade(trade)
trade = mock_trade_6(fee) trade = mock_trade_6(fee, is_short)
add_trade(trade) add_trade(trade)
if use_db: if use_db:
@ -286,22 +292,22 @@ def create_mock_trades_with_leverage(fee, use_db: bool = True):
LocalTrade.add_bt_trade(trade) LocalTrade.add_bt_trade(trade)
# Simulate dry_run entries # Simulate dry_run entries
trade = mock_trade_1(fee) trade = mock_trade_1(fee, False)
add_trade(trade) add_trade(trade)
trade = mock_trade_2(fee) trade = mock_trade_2(fee, False)
add_trade(trade) add_trade(trade)
trade = mock_trade_3(fee) trade = mock_trade_3(fee, False)
add_trade(trade) add_trade(trade)
trade = mock_trade_4(fee) trade = mock_trade_4(fee, False)
add_trade(trade) add_trade(trade)
trade = mock_trade_5(fee) trade = mock_trade_5(fee, False)
add_trade(trade) add_trade(trade)
trade = mock_trade_6(fee) trade = mock_trade_6(fee, False)
add_trade(trade) add_trade(trade)
trade = short_trade(fee) trade = short_trade(fee)
@ -324,7 +330,7 @@ def create_mock_trades_usdt(fee, use_db: bool = True):
else: else:
LocalTrade.add_bt_trade(trade) LocalTrade.add_bt_trade(trade)
# Simulate dry_run entries # Simulate dry_run entries
trade = mock_trade_usdt_1(fee) trade = mock_trade_usdt_1(fee)
add_trade(trade) add_trade(trade)
@ -2297,6 +2303,7 @@ def limit_sell_order_usdt_open():
'timestamp': arrow.utcnow().int_timestamp, 'timestamp': arrow.utcnow().int_timestamp,
'price': 2.20, 'price': 2.20,
'amount': 30.0, 'amount': 30.0,
'cost': 66.0,
'filled': 0.0, 'filled': 0.0,
'remaining': 30.0, 'remaining': 30.0,
'status': 'open' 'status': 'open'
@ -2342,3 +2349,27 @@ def market_sell_order_usdt():
'remaining': 0.0, 'remaining': 0.0,
'status': 'closed' 'status': 'closed'
} }
@pytest.fixture(scope='function')
def limit_order(limit_buy_order_usdt, limit_sell_order_usdt):
return {
'buy': limit_buy_order_usdt,
'sell': limit_sell_order_usdt
}
@pytest.fixture(scope='function')
def market_order(market_buy_order_usdt, market_sell_order_usdt):
return {
'buy': market_buy_order_usdt,
'sell': market_sell_order_usdt
}
@pytest.fixture(scope='function')
def limit_order_open(limit_buy_order_usdt_open, limit_sell_order_usdt_open):
return {
'buy': limit_buy_order_usdt_open,
'sell': limit_sell_order_usdt_open
}

View File

@ -6,12 +6,24 @@ from freqtrade.persistence.models import Order, Trade
MOCK_TRADE_COUNT = 6 MOCK_TRADE_COUNT = 6
def mock_order_1(): def enter_side(is_short: bool):
return "sell" if is_short else "buy"
def exit_side(is_short: bool):
return "buy" if is_short else "sell"
def direc(is_short: bool):
return "short" if is_short else "long"
def mock_order_1(is_short: bool):
return { return {
'id': '1234', 'id': f'1234_{direc(is_short)}',
'symbol': 'ETH/BTC', 'symbol': 'ETH/BTC',
'status': 'closed', 'status': 'closed',
'side': 'buy', 'side': enter_side(is_short),
'type': 'limit', 'type': 'limit',
'price': 0.123, 'price': 0.123,
'amount': 123.0, 'amount': 123.0,
@ -20,7 +32,7 @@ def mock_order_1():
} }
def mock_trade_1(fee): def mock_trade_1(fee, is_short: bool):
trade = Trade( trade = Trade(
pair='ETH/BTC', pair='ETH/BTC',
stake_amount=0.001, stake_amount=0.001,
@ -32,21 +44,22 @@ def mock_trade_1(fee):
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17), open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17),
open_rate=0.123, open_rate=0.123,
exchange='binance', exchange='binance',
open_order_id='dry_run_buy_12345', open_order_id=f'dry_run_buy_{direc(is_short)}_12345',
strategy='StrategyTestV3', strategy='StrategyTestV3',
timeframe=5, timeframe=5,
is_short=is_short
) )
o = Order.parse_from_ccxt_object(mock_order_1(), 'ETH/BTC', 'buy') o = Order.parse_from_ccxt_object(mock_order_1(is_short), 'ETH/BTC', enter_side(is_short))
trade.orders.append(o) trade.orders.append(o)
return trade return trade
def mock_order_2(): def mock_order_2(is_short: bool):
return { return {
'id': '1235', 'id': f'1235_{direc(is_short)}',
'symbol': 'ETC/BTC', 'symbol': 'ETC/BTC',
'status': 'closed', 'status': 'closed',
'side': 'buy', 'side': enter_side(is_short),
'type': 'limit', 'type': 'limit',
'price': 0.123, 'price': 0.123,
'amount': 123.0, 'amount': 123.0,
@ -55,12 +68,12 @@ def mock_order_2():
} }
def mock_order_2_sell(): def mock_order_2_sell(is_short: bool):
return { return {
'id': '12366', 'id': f'12366_{direc(is_short)}',
'symbol': 'ETC/BTC', 'symbol': 'ETC/BTC',
'status': 'closed', 'status': 'closed',
'side': 'sell', 'side': exit_side(is_short),
'type': 'limit', 'type': 'limit',
'price': 0.128, 'price': 0.128,
'amount': 123.0, 'amount': 123.0,
@ -69,7 +82,7 @@ def mock_order_2_sell():
} }
def mock_trade_2(fee): def mock_trade_2(fee, is_short: bool):
""" """
Closed trade... Closed trade...
""" """
@ -82,30 +95,31 @@ def mock_trade_2(fee):
fee_close=fee.return_value, fee_close=fee.return_value,
open_rate=0.123, open_rate=0.123,
close_rate=0.128, close_rate=0.128,
close_profit=0.005, close_profit=-0.005 if is_short else 0.005,
close_profit_abs=0.000584127, close_profit_abs=-0.005584127 if is_short else 0.000584127,
exchange='binance', exchange='binance',
is_open=False, is_open=False,
open_order_id='dry_run_sell_12345', open_order_id=f'dry_run_sell_{direc(is_short)}_12345',
strategy='StrategyTestV3', strategy='StrategyTestV3',
timeframe=5, timeframe=5,
sell_reason='sell_signal', sell_reason='sell_signal',
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2), close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
is_short=is_short
) )
o = Order.parse_from_ccxt_object(mock_order_2(), 'ETC/BTC', 'buy') o = Order.parse_from_ccxt_object(mock_order_2(is_short), 'ETC/BTC', enter_side(is_short))
trade.orders.append(o) trade.orders.append(o)
o = Order.parse_from_ccxt_object(mock_order_2_sell(), 'ETC/BTC', 'sell') o = Order.parse_from_ccxt_object(mock_order_2_sell(is_short), 'ETC/BTC', exit_side(is_short))
trade.orders.append(o) trade.orders.append(o)
return trade return trade
def mock_order_3(): def mock_order_3(is_short: bool):
return { return {
'id': '41231a12a', 'id': f'41231a12a_{direc(is_short)}',
'symbol': 'XRP/BTC', 'symbol': 'XRP/BTC',
'status': 'closed', 'status': 'closed',
'side': 'buy', 'side': enter_side(is_short),
'type': 'limit', 'type': 'limit',
'price': 0.05, 'price': 0.05,
'amount': 123.0, 'amount': 123.0,
@ -114,12 +128,12 @@ def mock_order_3():
} }
def mock_order_3_sell(): def mock_order_3_sell(is_short: bool):
return { return {
'id': '41231a666a', 'id': f'41231a666a_{direc(is_short)}',
'symbol': 'XRP/BTC', 'symbol': 'XRP/BTC',
'status': 'closed', 'status': 'closed',
'side': 'sell', 'side': exit_side(is_short),
'type': 'stop_loss_limit', 'type': 'stop_loss_limit',
'price': 0.06, 'price': 0.06,
'average': 0.06, 'average': 0.06,
@ -129,7 +143,7 @@ def mock_order_3_sell():
} }
def mock_trade_3(fee): def mock_trade_3(fee, is_short: bool):
""" """
Closed trade Closed trade
""" """
@ -142,8 +156,8 @@ def mock_trade_3(fee):
fee_close=fee.return_value, fee_close=fee.return_value,
open_rate=0.05, open_rate=0.05,
close_rate=0.06, close_rate=0.06,
close_profit=0.01, close_profit=-0.01 if is_short else 0.01,
close_profit_abs=0.000155, close_profit_abs=-0.001155 if is_short else 0.000155,
exchange='binance', exchange='binance',
is_open=False, is_open=False,
strategy='StrategyTestV3', strategy='StrategyTestV3',
@ -151,20 +165,21 @@ def mock_trade_3(fee):
sell_reason='roi', sell_reason='roi',
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc), close_date=datetime.now(tz=timezone.utc),
is_short=is_short
) )
o = Order.parse_from_ccxt_object(mock_order_3(), 'XRP/BTC', 'buy') o = Order.parse_from_ccxt_object(mock_order_3(is_short), 'XRP/BTC', enter_side(is_short))
trade.orders.append(o) trade.orders.append(o)
o = Order.parse_from_ccxt_object(mock_order_3_sell(), 'XRP/BTC', 'sell') o = Order.parse_from_ccxt_object(mock_order_3_sell(is_short), 'XRP/BTC', exit_side(is_short))
trade.orders.append(o) trade.orders.append(o)
return trade return trade
def mock_order_4(): def mock_order_4(is_short: bool):
return { return {
'id': 'prod_buy_12345', 'id': f'prod_buy_{direc(is_short)}_12345',
'symbol': 'ETC/BTC', 'symbol': 'ETC/BTC',
'status': 'open', 'status': 'open',
'side': 'buy', 'side': enter_side(is_short),
'type': 'limit', 'type': 'limit',
'price': 0.123, 'price': 0.123,
'amount': 123.0, 'amount': 123.0,
@ -173,7 +188,7 @@ def mock_order_4():
} }
def mock_trade_4(fee): def mock_trade_4(fee, is_short: bool):
""" """
Simulate prod entry Simulate prod entry
""" """
@ -188,21 +203,22 @@ def mock_trade_4(fee):
is_open=True, is_open=True,
open_rate=0.123, open_rate=0.123,
exchange='binance', exchange='binance',
open_order_id='prod_buy_12345', open_order_id=f'prod_buy_{direc(is_short)}_12345',
strategy='StrategyTestV3', strategy='StrategyTestV3',
timeframe=5, timeframe=5,
is_short=is_short
) )
o = Order.parse_from_ccxt_object(mock_order_4(), 'ETC/BTC', 'buy') o = Order.parse_from_ccxt_object(mock_order_4(is_short), 'ETC/BTC', enter_side(is_short))
trade.orders.append(o) trade.orders.append(o)
return trade return trade
def mock_order_5(): def mock_order_5(is_short: bool):
return { return {
'id': 'prod_buy_3455', 'id': f'prod_buy_{direc(is_short)}_3455',
'symbol': 'XRP/BTC', 'symbol': 'XRP/BTC',
'status': 'closed', 'status': 'closed',
'side': 'buy', 'side': enter_side(is_short),
'type': 'limit', 'type': 'limit',
'price': 0.123, 'price': 0.123,
'amount': 123.0, 'amount': 123.0,
@ -211,12 +227,12 @@ def mock_order_5():
} }
def mock_order_5_stoploss(): def mock_order_5_stoploss(is_short: bool):
return { return {
'id': 'prod_stoploss_3455', 'id': f'prod_stoploss_{direc(is_short)}_3455',
'symbol': 'XRP/BTC', 'symbol': 'XRP/BTC',
'status': 'open', 'status': 'open',
'side': 'sell', 'side': exit_side(is_short),
'type': 'stop_loss_limit', 'type': 'stop_loss_limit',
'price': 0.123, 'price': 0.123,
'amount': 123.0, 'amount': 123.0,
@ -225,7 +241,7 @@ def mock_order_5_stoploss():
} }
def mock_trade_5(fee): def mock_trade_5(fee, is_short: bool):
""" """
Simulate prod entry with stoploss Simulate prod entry with stoploss
""" """
@ -241,22 +257,23 @@ def mock_trade_5(fee):
open_rate=0.123, open_rate=0.123,
exchange='binance', exchange='binance',
strategy='SampleStrategy', strategy='SampleStrategy',
stoploss_order_id='prod_stoploss_3455', stoploss_order_id=f'prod_stoploss_{direc(is_short)}_3455',
timeframe=5, timeframe=5,
is_short=is_short
) )
o = Order.parse_from_ccxt_object(mock_order_5(), 'XRP/BTC', 'buy') o = Order.parse_from_ccxt_object(mock_order_5(is_short), 'XRP/BTC', enter_side(is_short))
trade.orders.append(o) trade.orders.append(o)
o = Order.parse_from_ccxt_object(mock_order_5_stoploss(), 'XRP/BTC', 'stoploss') o = Order.parse_from_ccxt_object(mock_order_5_stoploss(is_short), 'XRP/BTC', 'stoploss')
trade.orders.append(o) trade.orders.append(o)
return trade return trade
def mock_order_6(): def mock_order_6(is_short: bool):
return { return {
'id': 'prod_buy_6', 'id': f'prod_buy_{direc(is_short)}_6',
'symbol': 'LTC/BTC', 'symbol': 'LTC/BTC',
'status': 'closed', 'status': 'closed',
'side': 'buy', 'side': enter_side(is_short),
'type': 'limit', 'type': 'limit',
'price': 0.15, 'price': 0.15,
'amount': 2.0, 'amount': 2.0,
@ -265,23 +282,23 @@ def mock_order_6():
} }
def mock_order_6_sell(): def mock_order_6_sell(is_short: bool):
return { return {
'id': 'prod_sell_6', 'id': f'prod_sell_{direc(is_short)}_6',
'symbol': 'LTC/BTC', 'symbol': 'LTC/BTC',
'status': 'open', 'status': 'open',
'side': 'sell', 'side': exit_side(is_short),
'type': 'limit', 'type': 'limit',
'price': 0.20, 'price': 0.15 if is_short else 0.20,
'amount': 2.0, 'amount': 2.0,
'filled': 0.0, 'filled': 0.0,
'remaining': 2.0, 'remaining': 2.0,
} }
def mock_trade_6(fee): def mock_trade_6(fee, is_short: bool):
""" """
Simulate prod entry with open sell order Simulate prod entry with open exit order
""" """
trade = Trade( trade = Trade(
pair='LTC/BTC', pair='LTC/BTC',
@ -295,12 +312,12 @@ def mock_trade_6(fee):
open_rate=0.15, open_rate=0.15,
exchange='binance', exchange='binance',
strategy='SampleStrategy', strategy='SampleStrategy',
open_order_id="prod_sell_6", open_order_id=f"prod_sell_{direc(is_short)}_6",
timeframe=5, timeframe=5,
) )
o = Order.parse_from_ccxt_object(mock_order_6(), 'LTC/BTC', 'buy') o = Order.parse_from_ccxt_object(mock_order_6(is_short), 'LTC/BTC', enter_side(is_short))
trade.orders.append(o) trade.orders.append(o)
o = Order.parse_from_ccxt_object(mock_order_6_sell(), 'LTC/BTC', 'sell') o = Order.parse_from_ccxt_object(mock_order_6_sell(is_short), 'LTC/BTC', exit_side(is_short))
trade.orders.append(o) trade.orders.append(o)
return trade return trade

View File

@ -111,9 +111,10 @@ def test_load_backtest_data_multi(testdatadir):
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_load_trades_from_db(default_conf, fee, mocker): @pytest.mark.parametrize('is_short', [False, True])
def test_load_trades_from_db(default_conf, fee, is_short, mocker):
create_mock_trades(fee) create_mock_trades(fee, is_short)
# remove init so it does not init again # remove init so it does not init again
init_mock = mocker.patch('freqtrade.data.btanalysis.init_db', MagicMock()) init_mock = mocker.patch('freqtrade.data.btanalysis.init_db', MagicMock())

View File

@ -164,6 +164,8 @@ def test_get_balances_prod(default_conf, mocker):
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken", ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken",
"get_balances", "fetch_balance") "get_balances", "fetch_balance")
# TODO-lev: All these stoploss tests with shorts
@pytest.mark.parametrize('ordertype', ['market', 'limit']) @pytest.mark.parametrize('ordertype', ['market', 'limit'])
@pytest.mark.parametrize('side,adjustedprice', [ @pytest.mark.parametrize('side,adjustedprice', [

View File

@ -679,7 +679,7 @@ def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee, caplog) -> None
assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC'] assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC']
with time_machine.travel("2021-09-01 05:00:00 +00:00") as t: with time_machine.travel("2021-09-01 05:00:00 +00:00") as t:
create_mock_trades(fee) create_mock_trades(fee, False)
pm.refresh_pairlist() pm.refresh_pairlist()
assert pm.whitelist == ['XRP/BTC'] assert pm.whitelist == ['XRP/BTC']
assert log_has_re(r'Removing pair .* since .* is below .*', caplog) assert log_has_re(r'Removing pair .* since .* is below .*', caplog)

View File

@ -289,7 +289,8 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
rpc._rpc_daily_profit(0, stake_currency, fiat_display_currency) rpc._rpc_daily_profit(0, stake_currency, fiat_display_currency)
def test_rpc_trade_history(mocker, default_conf, markets, fee): @pytest.mark.parametrize('is_short', [True, False])
def test_rpc_trade_history(mocker, default_conf, markets, fee, is_short):
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
@ -297,7 +298,7 @@ def test_rpc_trade_history(mocker, default_conf, markets, fee):
) )
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
create_mock_trades(fee) create_mock_trades(fee, is_short)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
rpc._fiat_converter = CryptoToFiatConverter() rpc._fiat_converter = CryptoToFiatConverter()
trades = rpc._rpc_trade_history(2) trades = rpc._rpc_trade_history(2)
@ -314,7 +315,8 @@ def test_rpc_trade_history(mocker, default_conf, markets, fee):
assert trades['trades'][0]['pair'] == 'XRP/BTC' assert trades['trades'][0]['pair'] == 'XRP/BTC'
def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog): @pytest.mark.parametrize('is_short', [True, False])
def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short):
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
stoploss_mock = MagicMock() stoploss_mock = MagicMock()
cancel_mock = MagicMock() cancel_mock = MagicMock()
@ -327,7 +329,7 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog):
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
freqtradebot.strategy.order_types['stoploss_on_exchange'] = True freqtradebot.strategy.order_types['stoploss_on_exchange'] = True
create_mock_trades(fee) create_mock_trades(fee, is_short)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
with pytest.raises(RPCException, match='invalid argument'): with pytest.raises(RPCException, match='invalid argument'):
rpc._rpc_delete('200') rpc._rpc_delete('200')

View File

@ -458,7 +458,8 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers):
assert 'starting_capital_ratio' in response assert 'starting_capital_ratio' in response
def test_api_count(botclient, mocker, ticker, fee, markets): @pytest.mark.parametrize('is_short', [True, False])
def test_api_count(botclient, mocker, ticker, fee, markets, is_short):
ftbot, client = botclient ftbot, client = botclient
patch_get_signal(ftbot) patch_get_signal(ftbot)
mocker.patch.multiple( mocker.patch.multiple(
@ -475,7 +476,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets):
assert rc.json()["max"] == 1 assert rc.json()["max"] == 1
# Create some test data # Create some test data
create_mock_trades(fee) create_mock_trades(fee, is_short)
rc = client_get(client, f"{BASE_URI}/count") rc = client_get(client, f"{BASE_URI}/count")
assert_response(rc) assert_response(rc)
assert rc.json()["current"] == 4 assert rc.json()["current"] == 4
@ -556,7 +557,8 @@ def test_api_daily(botclient, mocker, ticker, fee, markets):
assert rc.json()['data'][0]['date'] == str(datetime.utcnow().date()) assert rc.json()['data'][0]['date'] == str(datetime.utcnow().date())
def test_api_trades(botclient, mocker, fee, markets): @pytest.mark.parametrize('is_short', [True, False])
def test_api_trades(botclient, mocker, fee, markets, is_short):
ftbot, client = botclient ftbot, client = botclient
patch_get_signal(ftbot) patch_get_signal(ftbot)
mocker.patch.multiple( mocker.patch.multiple(
@ -569,7 +571,7 @@ def test_api_trades(botclient, mocker, fee, markets):
assert rc.json()['trades_count'] == 0 assert rc.json()['trades_count'] == 0
assert rc.json()['total_trades'] == 0 assert rc.json()['total_trades'] == 0
create_mock_trades(fee) create_mock_trades(fee, is_short)
Trade.query.session.flush() Trade.query.session.flush()
rc = client_get(client, f"{BASE_URI}/trades") rc = client_get(client, f"{BASE_URI}/trades")
@ -584,6 +586,7 @@ def test_api_trades(botclient, mocker, fee, markets):
assert rc.json()['total_trades'] == 2 assert rc.json()['total_trades'] == 2
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
def test_api_trade_single(botclient, mocker, fee, ticker, markets): def test_api_trade_single(botclient, mocker, fee, ticker, markets):
ftbot, client = botclient ftbot, client = botclient
patch_get_signal(ftbot) patch_get_signal(ftbot)
@ -596,7 +599,7 @@ def test_api_trade_single(botclient, mocker, fee, ticker, markets):
assert_response(rc, 404) assert_response(rc, 404)
assert rc.json()['detail'] == 'Trade not found.' assert rc.json()['detail'] == 'Trade not found.'
create_mock_trades(fee) create_mock_trades(fee, False)
Trade.query.session.flush() Trade.query.session.flush()
rc = client_get(client, f"{BASE_URI}/trade/3") rc = client_get(client, f"{BASE_URI}/trade/3")
@ -604,6 +607,7 @@ def test_api_trade_single(botclient, mocker, fee, ticker, markets):
assert rc.json()['trade_id'] == 3 assert rc.json()['trade_id'] == 3
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
def test_api_delete_trade(botclient, mocker, fee, markets): def test_api_delete_trade(botclient, mocker, fee, markets):
ftbot, client = botclient ftbot, client = botclient
patch_get_signal(ftbot) patch_get_signal(ftbot)
@ -619,7 +623,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets):
# Error - trade won't exist yet. # Error - trade won't exist yet.
assert_response(rc, 502) assert_response(rc, 502)
create_mock_trades(fee) create_mock_trades(fee, False)
ftbot.strategy.order_types['stoploss_on_exchange'] = True ftbot.strategy.order_types['stoploss_on_exchange'] = True
trades = Trade.query.all() trades = Trade.query.all()
@ -695,6 +699,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
def test_api_profit(botclient, mocker, ticker, fee, markets): def test_api_profit(botclient, mocker, ticker, fee, markets):
ftbot, client = botclient ftbot, client = botclient
patch_get_signal(ftbot) patch_get_signal(ftbot)
@ -710,7 +715,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets):
assert_response(rc, 200) assert_response(rc, 200)
assert rc.json()['trade_count'] == 0 assert rc.json()['trade_count'] == 0
create_mock_trades(fee) create_mock_trades(fee, False)
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
rc = client_get(client, f"{BASE_URI}/profit") rc = client_get(client, f"{BASE_URI}/profit")
@ -746,7 +751,8 @@ def test_api_profit(botclient, mocker, ticker, fee, markets):
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_api_stats(botclient, mocker, ticker, fee, markets,): # TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
def test_api_stats(botclient, mocker, ticker, fee, markets):
ftbot, client = botclient ftbot, client = botclient
patch_get_signal(ftbot) patch_get_signal(ftbot)
mocker.patch.multiple( mocker.patch.multiple(
@ -762,7 +768,7 @@ def test_api_stats(botclient, mocker, ticker, fee, markets,):
assert 'durations' in rc.json() assert 'durations' in rc.json()
assert 'sell_reasons' in rc.json() assert 'sell_reasons' in rc.json()
create_mock_trades(fee) create_mock_trades(fee, False)
rc = client_get(client, f"{BASE_URI}/stats") rc = client_get(client, f"{BASE_URI}/stats")
assert_response(rc, 200) assert_response(rc, 200)
@ -820,6 +826,10 @@ def test_api_performance(botclient, fee):
{'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57, 'profit_abs': -0.1150375}] {'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57, 'profit_abs': -0.1150375}]
# TODO-lev: @pytest.mark.parametrize('is_short,side', [
# (True, "short"),
# (False, "long")
# ])
def test_api_status(botclient, mocker, ticker, fee, markets): def test_api_status(botclient, mocker, ticker, fee, markets):
ftbot, client = botclient ftbot, client = botclient
patch_get_signal(ftbot) patch_get_signal(ftbot)
@ -835,7 +845,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
rc = client_get(client, f"{BASE_URI}/status") rc = client_get(client, f"{BASE_URI}/status")
assert_response(rc, 200) assert_response(rc, 200)
assert rc.json() == [] assert rc.json() == []
create_mock_trades(fee) create_mock_trades(fee, False)
rc = client_get(client, f"{BASE_URI}/status") rc = client_get(client, f"{BASE_URI}/status")
assert_response(rc) assert_response(rc)
@ -888,7 +898,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
'is_open': True, 'is_open': True,
'max_rate': ANY, 'max_rate': ANY,
'min_rate': ANY, 'min_rate': ANY,
'open_order_id': 'dry_run_buy_12345', 'open_order_id': 'dry_run_buy_long_12345',
'open_rate_requested': ANY, 'open_rate_requested': ANY,
'open_trade_value': 15.1668225, 'open_trade_value': 15.1668225,
'sell_reason': None, 'sell_reason': None,

View File

@ -33,6 +33,7 @@ class DummyCls(Telegram):
""" """
Dummy class for testing the Telegram @authorized_only decorator Dummy class for testing the Telegram @authorized_only decorator
""" """
def __init__(self, rpc: RPC, config) -> None: def __init__(self, rpc: RPC, config) -> None:
super().__init__(rpc, config) super().__init__(rpc, config)
self.state = {'called': False} self.state = {'called': False}
@ -479,8 +480,9 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0] assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0]
@pytest.mark.parametrize('is_short', [True, False])
def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee, def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee,
limit_buy_order, limit_sell_order, mocker) -> None: limit_buy_order, limit_sell_order, mocker, is_short) -> None:
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
@ -496,7 +498,7 @@ def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee,
msg_mock.reset_mock() msg_mock.reset_mock()
# Create some test data # Create some test data
create_mock_trades(fee) create_mock_trades(fee, is_short)
telegram._stats(update=update, context=MagicMock()) telegram._stats(update=update, context=MagicMock())
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
@ -997,9 +999,9 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
msg = ('<pre> current max total stake\n--------- ----- -------------\n' msg = ('<pre> current max total stake\n--------- ----- -------------\n'
' 1 {} {}</pre>').format( ' 1 {} {}</pre>').format(
default_conf['max_open_trades'], default_conf['max_open_trades'],
default_conf['stake_amount'] default_conf['stake_amount']
) )
assert msg in msg_mock.call_args_list[0][0][0] assert msg in msg_mock.call_args_list[0][0][0]
@ -1159,6 +1161,7 @@ def test_edge_enabled(edge_conf, update, mocker) -> None:
assert 'Winrate' not in msg_mock.call_args_list[0][0][0] assert 'Winrate' not in msg_mock.call_args_list[0][0][0]
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
def test_telegram_trades(mocker, update, default_conf, fee): def test_telegram_trades(mocker, update, default_conf, fee):
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
@ -1177,7 +1180,7 @@ def test_telegram_trades(mocker, update, default_conf, fee):
assert "<pre>" not in msg_mock.call_args_list[0][0][0] assert "<pre>" not in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock() msg_mock.reset_mock()
create_mock_trades(fee) create_mock_trades(fee, False)
context = MagicMock() context = MagicMock()
context.args = [5] context.args = [5]
@ -1191,6 +1194,7 @@ def test_telegram_trades(mocker, update, default_conf, fee):
msg_mock.call_args_list[0][0][0])) msg_mock.call_args_list[0][0][0]))
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
def test_telegram_delete_trade(mocker, update, default_conf, fee): def test_telegram_delete_trade(mocker, update, default_conf, fee):
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
@ -1201,7 +1205,7 @@ def test_telegram_delete_trade(mocker, update, default_conf, fee):
assert "Trade-id not set." in msg_mock.call_args_list[0][0][0] assert "Trade-id not set." in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock() msg_mock.reset_mock()
create_mock_trades(fee) create_mock_trades(fee, False)
context = MagicMock() context = MagicMock()
context.args = [1] context.args = [1]

View File

@ -47,8 +47,8 @@ def test_returns_latest_signal(ohlcv_history):
mocked_history.loc[1, 'exit_long'] = 0 mocked_history.loc[1, 'exit_long'] = 0
mocked_history.loc[1, 'enter_long'] = 1 mocked_history.loc[1, 'enter_long'] = 1
assert _STRATEGY.get_entry_signal('ETH/BTC', '5m', mocked_history assert _STRATEGY.get_entry_signal(
) == (SignalDirection.LONG, None) 'ETH/BTC', '5m', mocked_history) == (SignalDirection.LONG, None)
assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (True, False) assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (True, False)
assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, False) assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, False)
mocked_history.loc[1, 'exit_long'] = 0 mocked_history.loc[1, 'exit_long'] = 0

File diff suppressed because it is too large Load Diff

View File

@ -1514,11 +1514,12 @@ def test_adjust_min_max_rates(fee):
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize('use_db', [True, False]) @pytest.mark.parametrize('use_db', [True, False])
def test_get_open(fee, use_db): @pytest.mark.parametrize('is_short', [True, False])
def test_get_open(fee, is_short, use_db):
Trade.use_db = use_db Trade.use_db = use_db
Trade.reset_trades() Trade.reset_trades()
create_mock_trades(fee, use_db) create_mock_trades(fee, is_short, use_db)
assert len(Trade.get_open_trades()) == 4 assert len(Trade.get_open_trades()) == 4
Trade.use_db = True Trade.use_db = True
@ -1874,14 +1875,15 @@ def test_fee_updated(fee):
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize('is_short', [True, False])
@pytest.mark.parametrize('use_db', [True, False]) @pytest.mark.parametrize('use_db', [True, False])
def test_total_open_trades_stakes(fee, use_db): def test_total_open_trades_stakes(fee, is_short, use_db):
Trade.use_db = use_db Trade.use_db = use_db
Trade.reset_trades() Trade.reset_trades()
res = Trade.total_open_trades_stakes() res = Trade.total_open_trades_stakes()
assert res == 0 assert res == 0
create_mock_trades(fee, use_db) create_mock_trades(fee, is_short, use_db)
res = Trade.total_open_trades_stakes() res = Trade.total_open_trades_stakes()
assert res == 0.004 assert res == 0.004
@ -1889,6 +1891,7 @@ def test_total_open_trades_stakes(fee, use_db):
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
@pytest.mark.parametrize('use_db', [True, False]) @pytest.mark.parametrize('use_db', [True, False])
def test_get_total_closed_profit(fee, use_db): def test_get_total_closed_profit(fee, use_db):
@ -1896,7 +1899,7 @@ def test_get_total_closed_profit(fee, use_db):
Trade.reset_trades() Trade.reset_trades()
res = Trade.get_total_closed_profit() res = Trade.get_total_closed_profit()
assert res == 0 assert res == 0
create_mock_trades(fee, use_db) create_mock_trades(fee, False, use_db)
res = Trade.get_total_closed_profit() res = Trade.get_total_closed_profit()
assert res == 0.000739127 assert res == 0.000739127
@ -1904,11 +1907,12 @@ def test_get_total_closed_profit(fee, use_db):
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
@pytest.mark.parametrize('use_db', [True, False]) @pytest.mark.parametrize('use_db', [True, False])
def test_get_trades_proxy(fee, use_db): def test_get_trades_proxy(fee, use_db):
Trade.use_db = use_db Trade.use_db = use_db
Trade.reset_trades() Trade.reset_trades()
create_mock_trades(fee, use_db) create_mock_trades(fee, False, use_db)
trades = Trade.get_trades_proxy() trades = Trade.get_trades_proxy()
assert len(trades) == 6 assert len(trades) == 6
@ -1937,9 +1941,10 @@ def test_get_trades_backtest():
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
# @pytest.mark.parametrize('is_short', [True, False])
def test_get_overall_performance(fee): def test_get_overall_performance(fee):
create_mock_trades(fee) create_mock_trades(fee, False)
res = Trade.get_overall_performance() res = Trade.get_overall_performance()
assert len(res) == 2 assert len(res) == 2
@ -1949,12 +1954,13 @@ def test_get_overall_performance(fee):
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
def test_get_best_pair(fee): def test_get_best_pair(fee):
res = Trade.get_best_pair() res = Trade.get_best_pair()
assert res is None assert res is None
create_mock_trades(fee) create_mock_trades(fee, False)
res = Trade.get_best_pair() res = Trade.get_best_pair()
assert len(res) == 2 assert len(res) == 2
assert res[0] == 'XRP/BTC' assert res[0] == 'XRP/BTC'
@ -2036,8 +2042,9 @@ def test_update_order_from_ccxt(caplog):
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
def test_select_order(fee): def test_select_order(fee):
create_mock_trades(fee) create_mock_trades(fee, False)
trades = Trade.get_trades().all() trades = Trade.get_trades().all()