mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 10:21:59 +00:00
Merge pull request #5567 from samgermain/lev-freqtradebot
Lev freqtradebot
This commit is contained in:
commit
79a91dc31b
|
@ -39,6 +39,8 @@ DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume']
|
|||
# Don't modify sequence of DEFAULT_TRADES_COLUMNS
|
||||
# it has wide consequences for stored trades files
|
||||
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'
|
||||
FTHYPT_FILEVERSION = 'fthypt_fileversion'
|
||||
|
@ -146,6 +148,8 @@ CONF_SCHEMA = {
|
|||
'sell_profit_offset': {'type': 'number'},
|
||||
'ignore_roi_if_buy_signal': {'type': 'boolean'},
|
||||
'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'},
|
||||
'unfilledtimeout': {
|
||||
'type': 'object',
|
||||
|
@ -193,7 +197,7 @@ CONF_SCHEMA = {
|
|||
'required': ['price_side']
|
||||
},
|
||||
'custom_price_max_distance_ratio': {
|
||||
'type': 'number', 'minimum': 0.0
|
||||
'type': 'number', 'minimum': 0.0
|
||||
},
|
||||
'order_types': {
|
||||
'type': 'object',
|
||||
|
|
|
@ -5,15 +5,21 @@ class RPCMessageType(Enum):
|
|||
STATUS = 'status'
|
||||
WARNING = 'warning'
|
||||
STARTUP = 'startup'
|
||||
|
||||
BUY = 'buy'
|
||||
BUY_FILL = 'buy_fill'
|
||||
BUY_CANCEL = 'buy_cancel'
|
||||
|
||||
SELL = 'sell'
|
||||
SELL_FILL = 'sell_fill'
|
||||
SELL_CANCEL = 'sell_cancel'
|
||||
PROTECTION_TRIGGER = 'protection_trigger'
|
||||
PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global'
|
||||
|
||||
SHORT = 'short'
|
||||
SHORT_FILL = 'short_fill'
|
||||
SHORT_CANCEL = 'short_cancel'
|
||||
|
||||
def __repr__(self):
|
||||
return self.value
|
||||
|
||||
|
|
|
@ -804,8 +804,14 @@ class Exchange:
|
|||
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
|
||||
|
||||
self._lev_prep(pair, leverage)
|
||||
order = self._api.create_order(pair, ordertype, side,
|
||||
amount, rate_for_order, params)
|
||||
order = self._api.create_order(
|
||||
pair,
|
||||
ordertype,
|
||||
side,
|
||||
amount,
|
||||
rate_for_order,
|
||||
params
|
||||
)
|
||||
self._log_exchange_response('create_order', order)
|
||||
return order
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import traceback
|
|||
from datetime import datetime, time, timezone
|
||||
from math import isclose
|
||||
from threading import Lock
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import arrow
|
||||
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.dataprovider import DataProvider
|
||||
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,
|
||||
InvalidOrderException, PricingError)
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
||||
|
@ -101,14 +102,19 @@ class FreqtradeBot(LoggingMixin):
|
|||
initial_state = self.config.get('initial_state')
|
||||
self.state = State[initial_state.upper()] if initial_state else State.STOPPED
|
||||
|
||||
# Protect sell-logic from forcesell and vice versa
|
||||
# Protect exit-logic from forcesell and vice versa
|
||||
self._exit_lock = Lock()
|
||||
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:
|
||||
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()
|
||||
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
|
@ -194,7 +200,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
|
||||
# Protect from collisions with forceexit.
|
||||
# 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:
|
||||
trades = Trade.get_open_trades()
|
||||
# First process current opened trades (positions)
|
||||
|
@ -305,21 +311,26 @@ class FreqtradeBot(LoggingMixin):
|
|||
|
||||
trades: List[Trade] = Trade.get_closed_trades_without_assigned_fees()
|
||||
for trade in trades:
|
||||
|
||||
if not trade.is_open and not trade.fee_updated('sell'):
|
||||
if not trade.is_open and not trade.fee_updated(trade.exit_side):
|
||||
# Get sell fee
|
||||
order = trade.select_order('sell', False)
|
||||
order = trade.select_order(trade.exit_side, False)
|
||||
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,
|
||||
stoploss_order=order.ft_order_side == 'stoploss')
|
||||
|
||||
trades: List[Trade] = Trade.get_open_trades_without_assigned_fees()
|
||||
for trade in trades:
|
||||
if trade.is_open and not trade.fee_updated('buy'):
|
||||
order = trade.select_order('buy', False)
|
||||
if trade.is_open and not trade.fee_updated(trade.enter_side):
|
||||
order = trade.select_order(trade.enter_side, False)
|
||||
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)
|
||||
|
||||
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.
|
||||
If not, try update entering fees - otherwise "refind" the open order we obviously lost.
|
||||
"""
|
||||
sell_order = trade.select_order('sell', None)
|
||||
if sell_order:
|
||||
exit_order = trade.select_order(trade.exit_side, None)
|
||||
if exit_order:
|
||||
self.refind_lost_order(trade)
|
||||
else:
|
||||
self.reupdate_enter_order_fees(trade)
|
||||
|
@ -338,10 +349,11 @@ class FreqtradeBot(LoggingMixin):
|
|||
Get buy order from database, and try to reupdate.
|
||||
Handles trades where the initial fee-update did not work.
|
||||
"""
|
||||
logger.info(f"Trying to reupdate buy fees for {trade}")
|
||||
order = trade.select_order('buy', False)
|
||||
logger.info(f"Trying to reupdate {trade.enter_side} fees for {trade}")
|
||||
order = trade.select_order(trade.enter_side, False)
|
||||
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)
|
||||
|
||||
def refind_lost_order(self, trade):
|
||||
|
@ -357,7 +369,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
if not order.ft_is_open:
|
||||
logger.debug(f"Order {order} is no longer open.")
|
||||
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
|
||||
continue
|
||||
try:
|
||||
|
@ -367,7 +379,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
if fo and fo['status'] == 'open':
|
||||
# Assume this as the open stoploss order
|
||||
trade.stoploss_order_id = order.order_id
|
||||
elif order.ft_order_side == 'sell':
|
||||
elif order.ft_order_side == trade.exit_side:
|
||||
if fo and fo['status'] == 'open':
|
||||
# Assume this as the open order
|
||||
trade.open_order_id = order.order_id
|
||||
|
@ -456,7 +468,9 @@ class FreqtradeBot(LoggingMixin):
|
|||
|
||||
# running get_signal on historical data fetched
|
||||
(signal, enter_tag) = self.strategy.get_entry_signal(
|
||||
pair, self.strategy.timeframe, analyzed_df
|
||||
pair,
|
||||
self.strategy.timeframe,
|
||||
analyzed_df
|
||||
)
|
||||
|
||||
if signal:
|
||||
|
@ -465,19 +479,31 @@ class FreqtradeBot(LoggingMixin):
|
|||
bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {})
|
||||
if ((bid_check_dom.get('enabled', False)) and
|
||||
(bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
|
||||
# TODO-lev: Does the below need to be adjusted for shorts?
|
||||
if self._check_depth_of_market_buy(pair, bid_check_dom):
|
||||
# TODO-lev: pass in "enter" as side.
|
||||
|
||||
return self.execute_entry(pair, stake_amount, enter_tag=enter_tag)
|
||||
if self._check_depth_of_market(pair, bid_check_dom, side=signal):
|
||||
return self.execute_entry(
|
||||
pair,
|
||||
stake_amount,
|
||||
enter_tag=enter_tag,
|
||||
is_short=(signal == SignalDirection.SHORT)
|
||||
)
|
||||
else:
|
||||
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:
|
||||
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
|
||||
"""
|
||||
|
@ -487,9 +513,17 @@ class FreqtradeBot(LoggingMixin):
|
|||
order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks'])
|
||||
order_book_bids = order_book_data_frame['b_size'].sum()
|
||||
order_book_asks = order_book_data_frame['a_size'].sum()
|
||||
bids_ask_delta = order_book_bids / order_book_asks
|
||||
|
||||
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(
|
||||
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"Immediate Bid Quantity: {order_book['bids'][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.")
|
||||
return False
|
||||
|
||||
def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None,
|
||||
forcebuy: bool = False, enter_tag: Optional[str] = None) -> bool:
|
||||
def leverage_prep(
|
||||
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
|
||||
:param pair: pair for which we want to create a LIMIT_BUY
|
||||
: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.
|
||||
"""
|
||||
time_in_force = self.strategy.order_time_in_force['buy']
|
||||
|
||||
[side, name] = ['sell', 'Short'] if is_short else ['buy', 'Long']
|
||||
|
||||
if price:
|
||||
enter_limit_requested = price
|
||||
else:
|
||||
# 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,
|
||||
default_retval=proposed_enter_rate)(
|
||||
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)
|
||||
|
||||
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,
|
||||
self.strategy.stoploss)
|
||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(
|
||||
pair,
|
||||
enter_limit_requested,
|
||||
self.strategy.stoploss,
|
||||
leverage=leverage
|
||||
)
|
||||
|
||||
if not self.edge:
|
||||
max_stake_amount = self.wallets.get_available_stake_amount()
|
||||
|
@ -543,10 +625,12 @@ class FreqtradeBot(LoggingMixin):
|
|||
if not stake_amount:
|
||||
return False
|
||||
|
||||
logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: "
|
||||
f"{stake_amount} ...")
|
||||
logger.info(
|
||||
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']
|
||||
if forcebuy:
|
||||
# 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)(
|
||||
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
|
||||
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}")
|
||||
return False
|
||||
amount = self.exchange.amount_to_precision(pair, amount)
|
||||
order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy",
|
||||
amount=amount, rate=enter_limit_requested,
|
||||
time_in_force=time_in_force)
|
||||
order_obj = Order.parse_from_ccxt_object(order, pair, 'buy')
|
||||
order = self.exchange.create_order(
|
||||
pair=pair,
|
||||
ordertype=order_type,
|
||||
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_status = order.get('status', None)
|
||||
|
||||
|
@ -579,17 +669,17 @@ class FreqtradeBot(LoggingMixin):
|
|||
|
||||
# return false if the order is not filled
|
||||
if float(order['filled']) == 0:
|
||||
logger.warning('Buy %s order with time in force %s for %s is %s by %s.'
|
||||
logger.warning('%s %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)
|
||||
name, order_tif, order_type, pair, order_status, self.exchange.name)
|
||||
return False
|
||||
else:
|
||||
# the order is partially fulfilled
|
||||
# in case of IOC orders we can check immediately
|
||||
# if the order is fulfilled fully or partially
|
||||
logger.warning('Buy %s order with time in force %s for %s is %s by %s.'
|
||||
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).',
|
||||
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']
|
||||
)
|
||||
stake_amount = order['cost']
|
||||
|
@ -602,6 +692,14 @@ class FreqtradeBot(LoggingMixin):
|
|||
amount = safe_value_fallback(order, 'filled', 'amount')
|
||||
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 = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
||||
open_date = datetime.now(timezone.utc)
|
||||
|
@ -627,6 +725,10 @@ class FreqtradeBot(LoggingMixin):
|
|||
# TODO-lev: compatibility layer for buy_tag (!)
|
||||
buy_tag=enter_tag,
|
||||
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,
|
||||
funding_fees=funding_fees
|
||||
)
|
||||
|
@ -652,7 +754,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
"""
|
||||
msg = {
|
||||
'trade_id': trade.id,
|
||||
'type': RPCMessageType.BUY,
|
||||
'type': RPCMessageType.SHORT if trade.is_short else RPCMessageType.BUY,
|
||||
'buy_tag': trade.buy_tag,
|
||||
'exchange': self.exchange.name.capitalize(),
|
||||
'pair': trade.pair,
|
||||
|
@ -673,11 +775,11 @@ class FreqtradeBot(LoggingMixin):
|
|||
"""
|
||||
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 = {
|
||||
'trade_id': trade.id,
|
||||
'type': RPCMessageType.BUY_CANCEL,
|
||||
'type': msg_type,
|
||||
'buy_tag': trade.buy_tag,
|
||||
'exchange': self.exchange.name.capitalize(),
|
||||
'pair': trade.pair,
|
||||
|
@ -696,9 +798,10 @@ class FreqtradeBot(LoggingMixin):
|
|||
self.rpc.send_msg(msg)
|
||||
|
||||
def _notify_enter_fill(self, trade: Trade) -> None:
|
||||
msg_type = RPCMessageType.SHORT_FILL if trade.is_short else RPCMessageType.BUY_FILL
|
||||
msg = {
|
||||
'trade_id': trade.id,
|
||||
'type': RPCMessageType.BUY_FILL,
|
||||
'type': msg_type,
|
||||
'buy_tag': trade.buy_tag,
|
||||
'exchange': self.exchange.name.capitalize(),
|
||||
'pair': trade.pair,
|
||||
|
@ -752,6 +855,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
logger.debug('Handling %s ...', trade)
|
||||
|
||||
(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
|
||||
if (self.config.get('use_sell_signal', True) or
|
||||
|
@ -762,15 +866,16 @@ class FreqtradeBot(LoggingMixin):
|
|||
(enter, exit_) = self.strategy.get_exit_signal(
|
||||
trade.pair,
|
||||
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.
|
||||
exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell")
|
||||
logger.debug('checking exit')
|
||||
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_):
|
||||
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
|
||||
|
||||
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 not stoploss_order:
|
||||
stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss
|
||||
stop_price = trade.open_rate * (1 + stoploss)
|
||||
if 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):
|
||||
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
|
||||
# in which case we cancel stoploss order and put another one with new
|
||||
# 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
|
||||
|
||||
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
|
||||
in case of trailing stoploss on exchange
|
||||
|
@ -892,7 +1000,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
:param order: Current on exchange stoploss order
|
||||
: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
|
||||
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
|
||||
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:
|
||||
|
@ -918,7 +1026,11 @@ class FreqtradeBot(LoggingMixin):
|
|||
Check and execute trade 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
|
||||
)
|
||||
|
||||
|
@ -959,24 +1071,23 @@ class FreqtradeBot(LoggingMixin):
|
|||
continue
|
||||
|
||||
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 (
|
||||
fully_cancelled
|
||||
or self._check_timed_out('buy', order)
|
||||
or strategy_safe_wrapper(self.strategy.check_buy_timeout,
|
||||
default_retval=False)(pair=trade.pair,
|
||||
trade=trade,
|
||||
order=order))):
|
||||
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||
|
||||
elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and (
|
||||
fully_cancelled
|
||||
or self._check_timed_out('sell', order)
|
||||
or strategy_safe_wrapper(self.strategy.check_sell_timeout,
|
||||
default_retval=False)(pair=trade.pair,
|
||||
trade=trade,
|
||||
order=order))):
|
||||
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||
if not_closed and (fully_cancelled or timed_out or (
|
||||
strategy_safe_wrapper(getattr(self.strategy, time_method), default_retval=False)(
|
||||
pair=trade.pair,
|
||||
trade=trade,
|
||||
order=order
|
||||
)
|
||||
)):
|
||||
if is_entering:
|
||||
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||
else:
|
||||
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||
|
||||
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())
|
||||
continue
|
||||
|
||||
if order['side'] == 'buy':
|
||||
if order['side'] == trade.enter_side:
|
||||
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'])
|
||||
Trade.commit()
|
||||
|
||||
|
@ -1016,7 +1127,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
if filled_val > 0 and filled_stake < minstake:
|
||||
logger.warning(
|
||||
f"Order {trade.open_order_id} for {trade.pair} not cancelled, "
|
||||
f"as the filled amount of {filled_val} would result in an unsellable trade.")
|
||||
f"as the filled amount of {filled_val} would result in an unexitable trade.")
|
||||
return False
|
||||
corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
|
||||
trade.amount)
|
||||
|
@ -1031,12 +1142,16 @@ class FreqtradeBot(LoggingMixin):
|
|||
corder = order
|
||||
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
|
||||
filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled')
|
||||
if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC):
|
||||
logger.info('Buy order fully cancelled. Removing %s from database.', trade)
|
||||
logger.info(
|
||||
'%s order fully cancelled. Removing %s from database.',
|
||||
side, trade
|
||||
)
|
||||
# if trade is not partially completed, just delete the trade
|
||||
trade.delete()
|
||||
was_trade_fully_canceled = True
|
||||
|
@ -1054,11 +1169,11 @@ class FreqtradeBot(LoggingMixin):
|
|||
self.update_trade_state(trade, trade.open_order_id, corder)
|
||||
|
||||
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']}"
|
||||
|
||||
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)
|
||||
return was_trade_fully_canceled
|
||||
|
||||
|
@ -1076,12 +1191,13 @@ class FreqtradeBot(LoggingMixin):
|
|||
trade.amount)
|
||||
trade.update_order(co)
|
||||
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'
|
||||
logger.info('Sell order %s for %s.', reason, trade)
|
||||
logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade)
|
||||
else:
|
||||
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.close_rate = None
|
||||
|
@ -1098,7 +1214,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
self.wallets.update()
|
||||
self._notify_exit_cancel(
|
||||
trade,
|
||||
order_type=self.strategy.order_types['sell'],
|
||||
order_type=self.strategy.order_types[trade.exit_side],
|
||||
reason=reason
|
||||
)
|
||||
return reason
|
||||
|
@ -1129,7 +1245,12 @@ class FreqtradeBot(LoggingMixin):
|
|||
raise DependencyException(
|
||||
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
|
||||
:param trade: Trade instance
|
||||
|
@ -1137,13 +1258,13 @@ class FreqtradeBot(LoggingMixin):
|
|||
:param sell_reason: Reason the sell was triggered
|
||||
: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):
|
||||
sell_type = 'stoploss'
|
||||
exit_type = 'stoploss'
|
||||
|
||||
# if stoploss is on exchange and we are on dry_run mode,
|
||||
# we consider the sell price stop price
|
||||
if self.config['dry_run'] and sell_type == 'stoploss' \
|
||||
if self.config['dry_run'] and exit_type == 'stoploss' \
|
||||
and self.strategy.order_types['stoploss_on_exchange']:
|
||||
limit = trade.stop_loss
|
||||
|
||||
|
@ -1167,7 +1288,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
except InvalidOrderException:
|
||||
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:
|
||||
# Emergency sells (default to 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)
|
||||
|
||||
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)(
|
||||
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(
|
||||
pair=trade.pair,
|
||||
ordertype=order_type,
|
||||
side="sell",
|
||||
side=trade.exit_side,
|
||||
amount=amount,
|
||||
rate=limit,
|
||||
time_in_force=time_in_force
|
||||
|
@ -1202,7 +1323,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
self.handle_insufficient_funds(trade)
|
||||
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.open_order_id = order['id']
|
||||
|
@ -1230,7 +1351,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
profit_trade = trade.calc_profit(rate=profit_rate)
|
||||
# Use cached rates here - it was updated seconds ago.
|
||||
current_rate = self.exchange.get_rate(
|
||||
trade.pair, refresh=False, side="sell") if not fill else None
|
||||
trade.pair, refresh=False, side=trade.exit_side) if not fill else None
|
||||
profit_ratio = trade.calc_profit_ratio(profit_rate)
|
||||
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_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)
|
||||
gain = "profit" if profit_ratio > 0 else "loss"
|
||||
|
||||
|
@ -1390,7 +1511,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
self.wallets.update()
|
||||
if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount:
|
||||
# Eat into dust if we own more than base currency
|
||||
# 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 - "
|
||||
f"Eating Fee {fee_abs} into dust.")
|
||||
elif fee_abs != 0:
|
||||
|
|
|
@ -506,7 +506,6 @@ class LocalTrade():
|
|||
lower_stop = new_loss < self.stop_loss
|
||||
|
||||
# stop losses only walk up, never down!,
|
||||
# TODO-lev
|
||||
# ? But adding more to a leveraged trade would create a lower liquidation price,
|
||||
# ? decreasing the minimum stoploss
|
||||
if (higher_stop and not self.is_short) or (lower_stop and self.is_short):
|
||||
|
|
|
@ -840,28 +840,32 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||
else:
|
||||
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
|
||||
sl_offset = self.trailing_stop_positive_offset
|
||||
|
||||
# Make sure current_profit is calculated using high for backtesting.
|
||||
# TODO-lev: Check this function - high / low usage must be inversed for short trades!
|
||||
high_profit = current_profit if not high else trade.calc_profit_ratio(high)
|
||||
bound = low if trade.is_short else 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.
|
||||
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
|
||||
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
|
||||
logger.debug(f"{trade.pair} - Using positive stoploss: {stop_loss_value} "
|
||||
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
|
||||
# in Dry-Run, this handles stoploss logic as well, as the logic will not be different to
|
||||
# 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'])):
|
||||
|
||||
sell_type = SellType.STOP_LOSS
|
||||
|
@ -870,12 +874,18 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||
if trade.initial_stop_loss != trade.stop_loss:
|
||||
sell_type = SellType.TRAILING_STOP_LOSS
|
||||
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"initial stoploss was at {trade.initial_stop_loss:.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 "
|
||||
f"{trade.stop_loss - trade.initial_stop_loss:.6f}")
|
||||
f"{new_stoploss:.6f}")
|
||||
|
||||
return SellCheckTuple(sell_type=sell_type)
|
||||
|
||||
|
|
|
@ -58,6 +58,8 @@ class SampleStrategy(IStrategy):
|
|||
# Hyperoptable parameters
|
||||
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)
|
||||
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.
|
||||
timeframe = '5m'
|
||||
|
@ -354,6 +356,16 @@ class SampleStrategy(IStrategy):
|
|||
),
|
||||
'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
|
||||
|
||||
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['volume'] > 0) # Make sure Volume is not 0
|
||||
),
|
||||
|
||||
'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
|
||||
|
|
|
@ -903,7 +903,7 @@ def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, tmpdir):
|
|||
mocker.patch(
|
||||
'freqtrade.optimize.hyperopt_tools.HyperoptTools._test_hyperopt_results_exist',
|
||||
return_value=True
|
||||
)
|
||||
)
|
||||
|
||||
def fake_iterator(*args, **kwargs):
|
||||
yield from [saved_hyperopt_results]
|
||||
|
@ -1309,9 +1309,10 @@ def test_start_list_data(testdatadir, capsys):
|
|||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
# TODO-lev: Short trades?
|
||||
def test_show_trades(mocker, fee, capsys, caplog):
|
||||
mocker.patch("freqtrade.persistence.init_db")
|
||||
create_mock_trades(fee)
|
||||
create_mock_trades(fee, False)
|
||||
args = [
|
||||
"show-trades",
|
||||
"--db-url",
|
||||
|
|
|
@ -209,8 +209,14 @@ def get_patched_worker(mocker, config) -> Worker:
|
|||
return Worker(args=None, config=config)
|
||||
|
||||
|
||||
def patch_get_signal(freqtrade: FreqtradeBot, enter_long=True, exit_long=False,
|
||||
enter_short=False, exit_short=False, enter_tag: Optional[str] = None) -> None:
|
||||
def patch_get_signal(
|
||||
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 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
|
||||
|
||||
|
||||
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 ...
|
||||
"""
|
||||
|
@ -252,22 +258,22 @@ def create_mock_trades(fee, use_db: bool = True):
|
|||
LocalTrade.add_bt_trade(trade)
|
||||
|
||||
# Simulate dry_run entries
|
||||
trade = mock_trade_1(fee)
|
||||
trade = mock_trade_1(fee, is_short)
|
||||
add_trade(trade)
|
||||
|
||||
trade = mock_trade_2(fee)
|
||||
trade = mock_trade_2(fee, is_short)
|
||||
add_trade(trade)
|
||||
|
||||
trade = mock_trade_3(fee)
|
||||
trade = mock_trade_3(fee, is_short)
|
||||
add_trade(trade)
|
||||
|
||||
trade = mock_trade_4(fee)
|
||||
trade = mock_trade_4(fee, is_short)
|
||||
add_trade(trade)
|
||||
|
||||
trade = mock_trade_5(fee)
|
||||
trade = mock_trade_5(fee, is_short)
|
||||
add_trade(trade)
|
||||
|
||||
trade = mock_trade_6(fee)
|
||||
trade = mock_trade_6(fee, is_short)
|
||||
add_trade(trade)
|
||||
|
||||
if use_db:
|
||||
|
@ -286,22 +292,22 @@ def create_mock_trades_with_leverage(fee, use_db: bool = True):
|
|||
LocalTrade.add_bt_trade(trade)
|
||||
|
||||
# Simulate dry_run entries
|
||||
trade = mock_trade_1(fee)
|
||||
trade = mock_trade_1(fee, False)
|
||||
add_trade(trade)
|
||||
|
||||
trade = mock_trade_2(fee)
|
||||
trade = mock_trade_2(fee, False)
|
||||
add_trade(trade)
|
||||
|
||||
trade = mock_trade_3(fee)
|
||||
trade = mock_trade_3(fee, False)
|
||||
add_trade(trade)
|
||||
|
||||
trade = mock_trade_4(fee)
|
||||
trade = mock_trade_4(fee, False)
|
||||
add_trade(trade)
|
||||
|
||||
trade = mock_trade_5(fee)
|
||||
trade = mock_trade_5(fee, False)
|
||||
add_trade(trade)
|
||||
|
||||
trade = mock_trade_6(fee)
|
||||
trade = mock_trade_6(fee, False)
|
||||
add_trade(trade)
|
||||
|
||||
trade = short_trade(fee)
|
||||
|
@ -324,7 +330,7 @@ def create_mock_trades_usdt(fee, use_db: bool = True):
|
|||
else:
|
||||
LocalTrade.add_bt_trade(trade)
|
||||
|
||||
# Simulate dry_run entries
|
||||
# Simulate dry_run entries
|
||||
trade = mock_trade_usdt_1(fee)
|
||||
add_trade(trade)
|
||||
|
||||
|
@ -2297,6 +2303,7 @@ def limit_sell_order_usdt_open():
|
|||
'timestamp': arrow.utcnow().int_timestamp,
|
||||
'price': 2.20,
|
||||
'amount': 30.0,
|
||||
'cost': 66.0,
|
||||
'filled': 0.0,
|
||||
'remaining': 30.0,
|
||||
'status': 'open'
|
||||
|
@ -2342,3 +2349,27 @@ def market_sell_order_usdt():
|
|||
'remaining': 0.0,
|
||||
'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
|
||||
}
|
||||
|
|
|
@ -6,12 +6,24 @@ from freqtrade.persistence.models import Order, Trade
|
|||
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 {
|
||||
'id': '1234',
|
||||
'id': f'1234_{direc(is_short)}',
|
||||
'symbol': 'ETH/BTC',
|
||||
'status': 'closed',
|
||||
'side': 'buy',
|
||||
'side': enter_side(is_short),
|
||||
'type': 'limit',
|
||||
'price': 0.123,
|
||||
'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(
|
||||
pair='ETH/BTC',
|
||||
stake_amount=0.001,
|
||||
|
@ -32,21 +44,22 @@ def mock_trade_1(fee):
|
|||
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17),
|
||||
open_rate=0.123,
|
||||
exchange='binance',
|
||||
open_order_id='dry_run_buy_12345',
|
||||
open_order_id=f'dry_run_buy_{direc(is_short)}_12345',
|
||||
strategy='StrategyTestV3',
|
||||
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)
|
||||
return trade
|
||||
|
||||
|
||||
def mock_order_2():
|
||||
def mock_order_2(is_short: bool):
|
||||
return {
|
||||
'id': '1235',
|
||||
'id': f'1235_{direc(is_short)}',
|
||||
'symbol': 'ETC/BTC',
|
||||
'status': 'closed',
|
||||
'side': 'buy',
|
||||
'side': enter_side(is_short),
|
||||
'type': 'limit',
|
||||
'price': 0.123,
|
||||
'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 {
|
||||
'id': '12366',
|
||||
'id': f'12366_{direc(is_short)}',
|
||||
'symbol': 'ETC/BTC',
|
||||
'status': 'closed',
|
||||
'side': 'sell',
|
||||
'side': exit_side(is_short),
|
||||
'type': 'limit',
|
||||
'price': 0.128,
|
||||
'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...
|
||||
"""
|
||||
|
@ -82,30 +95,31 @@ def mock_trade_2(fee):
|
|||
fee_close=fee.return_value,
|
||||
open_rate=0.123,
|
||||
close_rate=0.128,
|
||||
close_profit=0.005,
|
||||
close_profit_abs=0.000584127,
|
||||
close_profit=-0.005 if is_short else 0.005,
|
||||
close_profit_abs=-0.005584127 if is_short else 0.000584127,
|
||||
exchange='binance',
|
||||
is_open=False,
|
||||
open_order_id='dry_run_sell_12345',
|
||||
open_order_id=f'dry_run_sell_{direc(is_short)}_12345',
|
||||
strategy='StrategyTestV3',
|
||||
timeframe=5,
|
||||
sell_reason='sell_signal',
|
||||
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
||||
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)
|
||||
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)
|
||||
return trade
|
||||
|
||||
|
||||
def mock_order_3():
|
||||
def mock_order_3(is_short: bool):
|
||||
return {
|
||||
'id': '41231a12a',
|
||||
'id': f'41231a12a_{direc(is_short)}',
|
||||
'symbol': 'XRP/BTC',
|
||||
'status': 'closed',
|
||||
'side': 'buy',
|
||||
'side': enter_side(is_short),
|
||||
'type': 'limit',
|
||||
'price': 0.05,
|
||||
'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 {
|
||||
'id': '41231a666a',
|
||||
'id': f'41231a666a_{direc(is_short)}',
|
||||
'symbol': 'XRP/BTC',
|
||||
'status': 'closed',
|
||||
'side': 'sell',
|
||||
'side': exit_side(is_short),
|
||||
'type': 'stop_loss_limit',
|
||||
'price': 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
|
||||
"""
|
||||
|
@ -142,8 +156,8 @@ def mock_trade_3(fee):
|
|||
fee_close=fee.return_value,
|
||||
open_rate=0.05,
|
||||
close_rate=0.06,
|
||||
close_profit=0.01,
|
||||
close_profit_abs=0.000155,
|
||||
close_profit=-0.01 if is_short else 0.01,
|
||||
close_profit_abs=-0.001155 if is_short else 0.000155,
|
||||
exchange='binance',
|
||||
is_open=False,
|
||||
strategy='StrategyTestV3',
|
||||
|
@ -151,20 +165,21 @@ def mock_trade_3(fee):
|
|||
sell_reason='roi',
|
||||
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
||||
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)
|
||||
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)
|
||||
return trade
|
||||
|
||||
|
||||
def mock_order_4():
|
||||
def mock_order_4(is_short: bool):
|
||||
return {
|
||||
'id': 'prod_buy_12345',
|
||||
'id': f'prod_buy_{direc(is_short)}_12345',
|
||||
'symbol': 'ETC/BTC',
|
||||
'status': 'open',
|
||||
'side': 'buy',
|
||||
'side': enter_side(is_short),
|
||||
'type': 'limit',
|
||||
'price': 0.123,
|
||||
'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
|
||||
"""
|
||||
|
@ -188,21 +203,22 @@ def mock_trade_4(fee):
|
|||
is_open=True,
|
||||
open_rate=0.123,
|
||||
exchange='binance',
|
||||
open_order_id='prod_buy_12345',
|
||||
open_order_id=f'prod_buy_{direc(is_short)}_12345',
|
||||
strategy='StrategyTestV3',
|
||||
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)
|
||||
return trade
|
||||
|
||||
|
||||
def mock_order_5():
|
||||
def mock_order_5(is_short: bool):
|
||||
return {
|
||||
'id': 'prod_buy_3455',
|
||||
'id': f'prod_buy_{direc(is_short)}_3455',
|
||||
'symbol': 'XRP/BTC',
|
||||
'status': 'closed',
|
||||
'side': 'buy',
|
||||
'side': enter_side(is_short),
|
||||
'type': 'limit',
|
||||
'price': 0.123,
|
||||
'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 {
|
||||
'id': 'prod_stoploss_3455',
|
||||
'id': f'prod_stoploss_{direc(is_short)}_3455',
|
||||
'symbol': 'XRP/BTC',
|
||||
'status': 'open',
|
||||
'side': 'sell',
|
||||
'side': exit_side(is_short),
|
||||
'type': 'stop_loss_limit',
|
||||
'price': 0.123,
|
||||
'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
|
||||
"""
|
||||
|
@ -241,22 +257,23 @@ def mock_trade_5(fee):
|
|||
open_rate=0.123,
|
||||
exchange='binance',
|
||||
strategy='SampleStrategy',
|
||||
stoploss_order_id='prod_stoploss_3455',
|
||||
stoploss_order_id=f'prod_stoploss_{direc(is_short)}_3455',
|
||||
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)
|
||||
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)
|
||||
return trade
|
||||
|
||||
|
||||
def mock_order_6():
|
||||
def mock_order_6(is_short: bool):
|
||||
return {
|
||||
'id': 'prod_buy_6',
|
||||
'id': f'prod_buy_{direc(is_short)}_6',
|
||||
'symbol': 'LTC/BTC',
|
||||
'status': 'closed',
|
||||
'side': 'buy',
|
||||
'side': enter_side(is_short),
|
||||
'type': 'limit',
|
||||
'price': 0.15,
|
||||
'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 {
|
||||
'id': 'prod_sell_6',
|
||||
'id': f'prod_sell_{direc(is_short)}_6',
|
||||
'symbol': 'LTC/BTC',
|
||||
'status': 'open',
|
||||
'side': 'sell',
|
||||
'side': exit_side(is_short),
|
||||
'type': 'limit',
|
||||
'price': 0.20,
|
||||
'price': 0.15 if is_short else 0.20,
|
||||
'amount': 2.0,
|
||||
'filled': 0.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(
|
||||
pair='LTC/BTC',
|
||||
|
@ -295,12 +312,12 @@ def mock_trade_6(fee):
|
|||
open_rate=0.15,
|
||||
exchange='binance',
|
||||
strategy='SampleStrategy',
|
||||
open_order_id="prod_sell_6",
|
||||
open_order_id=f"prod_sell_{direc(is_short)}_6",
|
||||
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)
|
||||
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)
|
||||
return trade
|
||||
|
||||
|
|
|
@ -111,9 +111,10 @@ def test_load_backtest_data_multi(testdatadir):
|
|||
|
||||
|
||||
@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
|
||||
init_mock = mocker.patch('freqtrade.data.btanalysis.init_db', MagicMock())
|
||||
|
||||
|
|
|
@ -164,6 +164,8 @@ def test_get_balances_prod(default_conf, mocker):
|
|||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken",
|
||||
"get_balances", "fetch_balance")
|
||||
|
||||
# TODO-lev: All these stoploss tests with shorts
|
||||
|
||||
|
||||
@pytest.mark.parametrize('ordertype', ['market', 'limit'])
|
||||
@pytest.mark.parametrize('side,adjustedprice', [
|
||||
|
|
|
@ -679,7 +679,7 @@ def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee, caplog) -> None
|
|||
assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC']
|
||||
|
||||
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()
|
||||
assert pm.whitelist == ['XRP/BTC']
|
||||
assert log_has_re(r'Removing pair .* since .* is below .*', caplog)
|
||||
|
|
|
@ -289,7 +289,8 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
|||
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.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
|
@ -297,7 +298,7 @@ def test_rpc_trade_history(mocker, default_conf, markets, fee):
|
|||
)
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
create_mock_trades(fee)
|
||||
create_mock_trades(fee, is_short)
|
||||
rpc = RPC(freqtradebot)
|
||||
rpc._fiat_converter = CryptoToFiatConverter()
|
||||
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'
|
||||
|
||||
|
||||
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())
|
||||
stoploss_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.strategy.order_types['stoploss_on_exchange'] = True
|
||||
create_mock_trades(fee)
|
||||
create_mock_trades(fee, is_short)
|
||||
rpc = RPC(freqtradebot)
|
||||
with pytest.raises(RPCException, match='invalid argument'):
|
||||
rpc._rpc_delete('200')
|
||||
|
|
|
@ -458,7 +458,8 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers):
|
|||
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
|
||||
patch_get_signal(ftbot)
|
||||
mocker.patch.multiple(
|
||||
|
@ -475,7 +476,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets):
|
|||
assert rc.json()["max"] == 1
|
||||
|
||||
# Create some test data
|
||||
create_mock_trades(fee)
|
||||
create_mock_trades(fee, is_short)
|
||||
rc = client_get(client, f"{BASE_URI}/count")
|
||||
assert_response(rc)
|
||||
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())
|
||||
|
||||
|
||||
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
|
||||
patch_get_signal(ftbot)
|
||||
mocker.patch.multiple(
|
||||
|
@ -569,7 +571,7 @@ def test_api_trades(botclient, mocker, fee, markets):
|
|||
assert rc.json()['trades_count'] == 0
|
||||
assert rc.json()['total_trades'] == 0
|
||||
|
||||
create_mock_trades(fee)
|
||||
create_mock_trades(fee, is_short)
|
||||
Trade.query.session.flush()
|
||||
|
||||
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
|
||||
|
||||
|
||||
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
|
||||
def test_api_trade_single(botclient, mocker, fee, ticker, markets):
|
||||
ftbot, client = botclient
|
||||
patch_get_signal(ftbot)
|
||||
|
@ -596,7 +599,7 @@ def test_api_trade_single(botclient, mocker, fee, ticker, markets):
|
|||
assert_response(rc, 404)
|
||||
assert rc.json()['detail'] == 'Trade not found.'
|
||||
|
||||
create_mock_trades(fee)
|
||||
create_mock_trades(fee, False)
|
||||
Trade.query.session.flush()
|
||||
|
||||
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
|
||||
|
||||
|
||||
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
|
||||
def test_api_delete_trade(botclient, mocker, fee, markets):
|
||||
ftbot, client = botclient
|
||||
patch_get_signal(ftbot)
|
||||
|
@ -619,7 +623,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets):
|
|||
# Error - trade won't exist yet.
|
||||
assert_response(rc, 502)
|
||||
|
||||
create_mock_trades(fee)
|
||||
create_mock_trades(fee, False)
|
||||
|
||||
ftbot.strategy.order_types['stoploss_on_exchange'] = True
|
||||
trades = Trade.query.all()
|
||||
|
@ -695,6 +699,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
|
|||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
|
||||
def test_api_profit(botclient, mocker, ticker, fee, markets):
|
||||
ftbot, client = botclient
|
||||
patch_get_signal(ftbot)
|
||||
|
@ -710,7 +715,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets):
|
|||
assert_response(rc, 200)
|
||||
assert rc.json()['trade_count'] == 0
|
||||
|
||||
create_mock_trades(fee)
|
||||
create_mock_trades(fee, False)
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
|
||||
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")
|
||||
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
|
||||
patch_get_signal(ftbot)
|
||||
mocker.patch.multiple(
|
||||
|
@ -762,7 +768,7 @@ def test_api_stats(botclient, mocker, ticker, fee, markets,):
|
|||
assert 'durations' 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")
|
||||
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}]
|
||||
|
||||
|
||||
# TODO-lev: @pytest.mark.parametrize('is_short,side', [
|
||||
# (True, "short"),
|
||||
# (False, "long")
|
||||
# ])
|
||||
def test_api_status(botclient, mocker, ticker, fee, markets):
|
||||
ftbot, client = botclient
|
||||
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")
|
||||
assert_response(rc, 200)
|
||||
assert rc.json() == []
|
||||
create_mock_trades(fee)
|
||||
create_mock_trades(fee, False)
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/status")
|
||||
assert_response(rc)
|
||||
|
@ -888,7 +898,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
|
|||
'is_open': True,
|
||||
'max_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_trade_value': 15.1668225,
|
||||
'sell_reason': None,
|
||||
|
|
|
@ -33,6 +33,7 @@ class DummyCls(Telegram):
|
|||
"""
|
||||
Dummy class for testing the Telegram @authorized_only decorator
|
||||
"""
|
||||
|
||||
def __init__(self, rpc: RPC, config) -> None:
|
||||
super().__init__(rpc, config)
|
||||
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]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('is_short', [True, False])
|
||||
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.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
|
@ -496,7 +498,7 @@ def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee,
|
|||
msg_mock.reset_mock()
|
||||
|
||||
# Create some test data
|
||||
create_mock_trades(fee)
|
||||
create_mock_trades(fee, is_short)
|
||||
|
||||
telegram._stats(update=update, context=MagicMock())
|
||||
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'
|
||||
' 1 {} {}</pre>').format(
|
||||
default_conf['max_open_trades'],
|
||||
default_conf['stake_amount']
|
||||
)
|
||||
default_conf['max_open_trades'],
|
||||
default_conf['stake_amount']
|
||||
)
|
||||
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]
|
||||
|
||||
|
||||
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
|
||||
def test_telegram_trades(mocker, update, default_conf, fee):
|
||||
|
||||
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]
|
||||
msg_mock.reset_mock()
|
||||
|
||||
create_mock_trades(fee)
|
||||
create_mock_trades(fee, False)
|
||||
|
||||
context = MagicMock()
|
||||
context.args = [5]
|
||||
|
@ -1191,6 +1194,7 @@ def test_telegram_trades(mocker, update, default_conf, fee):
|
|||
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):
|
||||
|
||||
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]
|
||||
|
||||
msg_mock.reset_mock()
|
||||
create_mock_trades(fee)
|
||||
create_mock_trades(fee, False)
|
||||
|
||||
context = MagicMock()
|
||||
context.args = [1]
|
||||
|
|
|
@ -47,8 +47,8 @@ def test_returns_latest_signal(ohlcv_history):
|
|||
mocked_history.loc[1, 'exit_long'] = 0
|
||||
mocked_history.loc[1, 'enter_long'] = 1
|
||||
|
||||
assert _STRATEGY.get_entry_signal('ETH/BTC', '5m', mocked_history
|
||||
) == (SignalDirection.LONG, None)
|
||||
assert _STRATEGY.get_entry_signal(
|
||||
'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, False)
|
||||
mocked_history.loc[1, 'exit_long'] = 0
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1514,11 +1514,12 @@ def test_adjust_min_max_rates(fee):
|
|||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
@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.reset_trades()
|
||||
|
||||
create_mock_trades(fee, use_db)
|
||||
create_mock_trades(fee, is_short, use_db)
|
||||
assert len(Trade.get_open_trades()) == 4
|
||||
|
||||
Trade.use_db = True
|
||||
|
@ -1874,14 +1875,15 @@ def test_fee_updated(fee):
|
|||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
@pytest.mark.parametrize('is_short', [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.reset_trades()
|
||||
res = Trade.total_open_trades_stakes()
|
||||
assert res == 0
|
||||
create_mock_trades(fee, use_db)
|
||||
create_mock_trades(fee, is_short, use_db)
|
||||
res = Trade.total_open_trades_stakes()
|
||||
assert res == 0.004
|
||||
|
||||
|
@ -1889,6 +1891,7 @@ def test_total_open_trades_stakes(fee, use_db):
|
|||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
|
||||
@pytest.mark.parametrize('use_db', [True, False])
|
||||
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()
|
||||
res = Trade.get_total_closed_profit()
|
||||
assert res == 0
|
||||
create_mock_trades(fee, use_db)
|
||||
create_mock_trades(fee, False, use_db)
|
||||
res = Trade.get_total_closed_profit()
|
||||
assert res == 0.000739127
|
||||
|
||||
|
@ -1904,11 +1907,12 @@ def test_get_total_closed_profit(fee, use_db):
|
|||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
|
||||
@pytest.mark.parametrize('use_db', [True, False])
|
||||
def test_get_trades_proxy(fee, use_db):
|
||||
Trade.use_db = use_db
|
||||
Trade.reset_trades()
|
||||
create_mock_trades(fee, use_db)
|
||||
create_mock_trades(fee, False, use_db)
|
||||
trades = Trade.get_trades_proxy()
|
||||
assert len(trades) == 6
|
||||
|
||||
|
@ -1937,9 +1941,10 @@ def test_get_trades_backtest():
|
|||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
# @pytest.mark.parametrize('is_short', [True, False])
|
||||
def test_get_overall_performance(fee):
|
||||
|
||||
create_mock_trades(fee)
|
||||
create_mock_trades(fee, False)
|
||||
res = Trade.get_overall_performance()
|
||||
|
||||
assert len(res) == 2
|
||||
|
@ -1949,12 +1954,13 @@ def test_get_overall_performance(fee):
|
|||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
|
||||
def test_get_best_pair(fee):
|
||||
|
||||
res = Trade.get_best_pair()
|
||||
assert res is None
|
||||
|
||||
create_mock_trades(fee)
|
||||
create_mock_trades(fee, False)
|
||||
res = Trade.get_best_pair()
|
||||
assert len(res) == 2
|
||||
assert res[0] == 'XRP/BTC'
|
||||
|
@ -2036,8 +2042,9 @@ def test_update_order_from_ccxt(caplog):
|
|||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
|
||||
def test_select_order(fee):
|
||||
create_mock_trades(fee)
|
||||
create_mock_trades(fee, False)
|
||||
|
||||
trades = Trade.get_trades().all()
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user