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
|
# 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',
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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())
|
||||||
|
|
||||||
|
@ -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', [
|
||||||
|
@ -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)
|
||||||
|
@ -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')
|
||||||
|
@ -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,
|
||||||
|
@ -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]
|
||||||
|
@ -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
@ -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()
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user