Merge branch 'develop' into feature/fetch-public-trades

This commit is contained in:
TheJoeSchr
2024-02-02 15:14:04 +01:00
committed by GitHub
20 changed files with 1489 additions and 1405 deletions
+8 -2
View File
@@ -22,9 +22,11 @@ from pandas import DataFrame, concat
from freqtrade.constants import (DEFAULT_TRADES_COLUMNS, DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BidAsk, from freqtrade.constants import (DEFAULT_TRADES_COLUMNS, DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BidAsk,
BuySell, Config, EntryExit, ExchangeConfig, BuySell, Config, EntryExit, ExchangeConfig,
ListPairsWithTimeframes, MakerTaker, OBLiteral, PairWithTimeframe) ListPairsWithTimeframes, MakerTaker, OBLiteral, PairWithTimeframe)
from freqtrade.data.converter import clean_duplicate_trades, clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list, public_trades_to_dataframe from freqtrade.data.converter import clean_duplicate_trades, clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list, public_trades_to_dataframe
from freqtrade.data.converter.converter import _calculate_ohlcv_candle_start_and_end from freqtrade.data.converter.converter import _calculate_ohlcv_candle_start_and_end
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, PriceType, TradingMode from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, PriceType, RunMode, TradingMode
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
InvalidOrderException, OperationalException, PricingError, InvalidOrderException, OperationalException, PricingError,
RetryableOrderError, TemporaryError) RetryableOrderError, TemporaryError)
@@ -629,7 +631,11 @@ class Exchange:
raise OperationalException( raise OperationalException(
f"Invalid timeframe '{timeframe}'. This exchange supports: {self.timeframes}") f"Invalid timeframe '{timeframe}'. This exchange supports: {self.timeframes}")
if timeframe and timeframe_to_minutes(timeframe) < 1: if (
timeframe
and self._config['runmode'] != RunMode.UTIL_EXCHANGE
and timeframe_to_minutes(timeframe) < 1
):
raise OperationalException("Timeframes < 1m are currently not supported by Freqtrade.") raise OperationalException("Timeframes < 1m are currently not supported by Freqtrade.")
def validate_ordertypes(self, order_types: Dict) -> None: def validate_ordertypes(self, order_types: Dict) -> None:
+64 -57
View File
@@ -432,10 +432,6 @@ class FreqtradeBot(LoggingMixin):
try: try:
fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair,
order.ft_order_side == 'stoploss') order.ft_order_side == 'stoploss')
if order.ft_order_side == 'stoploss':
if fo and fo['status'] == 'open':
# Assume this as the open stoploss order
trade.stoploss_order_id = order.order_id
if fo: if fo:
logger.info(f"Found {order} for trade {trade}.") logger.info(f"Found {order} for trade {trade}.")
self.update_trade_state(trade, order.order_id, fo, self.update_trade_state(trade, order.order_id, fo,
@@ -895,17 +891,15 @@ class FreqtradeBot(LoggingMixin):
def cancel_stoploss_on_exchange(self, trade: Trade) -> Trade: def cancel_stoploss_on_exchange(self, trade: Trade) -> Trade:
# First cancelling stoploss on exchange ... # First cancelling stoploss on exchange ...
if trade.stoploss_order_id: for oslo in trade.open_sl_orders:
try: try:
logger.info(f"Cancelling stoploss on exchange for {trade}") logger.info(f"Cancelling stoploss on exchange for {trade} "
f"order: {oslo.order_id}")
co = self.exchange.cancel_stoploss_order_with_result( co = self.exchange.cancel_stoploss_order_with_result(
trade.stoploss_order_id, trade.pair, trade.amount) oslo.order_id, trade.pair, trade.amount)
self.update_trade_state(trade, trade.stoploss_order_id, co, stoploss_order=True) self.update_trade_state(trade, oslo.order_id, co, stoploss_order=True)
# Reset stoploss order id.
trade.stoploss_order_id = None
except InvalidOrderException: except InvalidOrderException:
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id} " logger.exception(f"Could not cancel stoploss order {oslo.order_id} "
f"for pair {trade.pair}") f"for pair {trade.pair}")
return trade return trade
@@ -1080,7 +1074,7 @@ class FreqtradeBot(LoggingMixin):
if ( if (
not trade.has_open_orders not trade.has_open_orders
and not trade.stoploss_order_id and not trade.has_open_sl_orders
and not self.wallets.check_exit_amount(trade) and not self.wallets.check_exit_amount(trade)
): ):
logger.warning( logger.warning(
@@ -1190,8 +1184,6 @@ class FreqtradeBot(LoggingMixin):
order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss', order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss',
trade.amount, stop_price) trade.amount, stop_price)
trade.orders.append(order_obj) trade.orders.append(order_obj)
trade.stoploss_order_id = str(stoploss_order['id'])
trade.stoploss_last_update = datetime.now(timezone.utc)
return True return True
except InsufficientFundsError as e: except InsufficientFundsError as e:
logger.warning(f"Unable to place stoploss order {e}.") logger.warning(f"Unable to place stoploss order {e}.")
@@ -1199,13 +1191,11 @@ class FreqtradeBot(LoggingMixin):
self.handle_insufficient_funds(trade) self.handle_insufficient_funds(trade)
except InvalidOrderException as e: except InvalidOrderException as e:
trade.stoploss_order_id = None
logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.error(f'Unable to place a stoploss order on exchange. {e}')
logger.warning('Exiting the trade forcefully') logger.warning('Exiting the trade forcefully')
self.emergency_exit(trade, stop_price) self.emergency_exit(trade, stop_price)
except ExchangeError: except ExchangeError:
trade.stoploss_order_id = None
logger.exception('Unable to place a stoploss order on exchange.') logger.exception('Unable to place a stoploss order on exchange.')
return False return False
@@ -1219,27 +1209,28 @@ class FreqtradeBot(LoggingMixin):
""" """
logger.debug('Handling stoploss on exchange %s ...', trade) logger.debug('Handling stoploss on exchange %s ...', trade)
stoploss_order = None
try: stoploss_orders = []
# First we check if there is already a stoploss on exchange for slo in trade.open_sl_orders:
stoploss_order = self.exchange.fetch_stoploss_order( stoploss_order = None
trade.stoploss_order_id, trade.pair) if trade.stoploss_order_id else None try:
except InvalidOrderException as exception: # First we check if there is already a stoploss on exchange
logger.warning('Unable to fetch stoploss order: %s', exception) stoploss_order = self.exchange.fetch_stoploss_order(
slo.order_id, trade.pair) if slo.order_id else None
except InvalidOrderException as exception:
logger.warning('Unable to fetch stoploss order: %s', exception)
if stoploss_order: if stoploss_order:
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, stoploss_orders.append(stoploss_order)
stoploss_order=True) self.update_trade_state(trade, slo.order_id, stoploss_order,
stoploss_order=True)
# We check if stoploss order is fulfilled # We check if stoploss order is fulfilled
if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'):
trade.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value trade.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, self._notify_exit(trade, "stoploss", True)
stoploss_order=True) self.handle_protections(trade.pair, trade.trade_direction)
self._notify_exit(trade, "stoploss", True) return True
self.handle_protections(trade.pair, trade.trade_direction)
return True
if trade.has_open_orders or not trade.is_open: if trade.has_open_orders or not trade.is_open:
# Trade has an open Buy or Sell order, Stoploss-handling can't happen in this case # Trade has an open Buy or Sell order, Stoploss-handling can't happen in this case
@@ -1248,7 +1239,7 @@ class FreqtradeBot(LoggingMixin):
return False return False
# 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 len(stoploss_orders) == 0:
stop_price = trade.stoploss_or_liquidation stop_price = trade.stoploss_or_liquidation
if self.edge: if self.edge:
stoploss = self.edge.get_stoploss(pair=trade.pair) stoploss = self.edge.get_stoploss(pair=trade.pair)
@@ -1262,27 +1253,7 @@ class FreqtradeBot(LoggingMixin):
# in which case the trade will be closed - which we must check below. # in which case the trade will be closed - which we must check below.
return False return False
# If stoploss order is canceled for some reason we add it again self.manage_trade_stoploss_orders(trade, stoploss_orders)
if (trade.is_open
and stoploss_order
and stoploss_order['status'] in ('canceled', 'cancelled')):
if self.create_stoploss_order(trade=trade, stop_price=trade.stoploss_or_liquidation):
return False
else:
logger.warning('Stoploss order was cancelled, but unable to recreate one.')
# Finally we check if stoploss on exchange should be moved up because of trailing.
# Triggered Orders are now real orders - so don't replace stoploss anymore
if (
trade.is_open and stoploss_order
and stoploss_order.get('status_stop') != 'triggered'
and (self.config.get('trailing_stop', False)
or self.config.get('use_custom_stoploss', False))
):
# if trailing stoploss is enabled we check if stoploss value has changed
# in which case we cancel stoploss order and put another one with new
# value immediately
self.handle_trailing_stoploss_on_exchange(trade, stoploss_order)
return False return False
@@ -1318,6 +1289,42 @@ class FreqtradeBot(LoggingMixin):
logger.warning(f"Could not create trailing stoploss order " logger.warning(f"Could not create trailing stoploss order "
f"for pair {trade.pair}.") f"for pair {trade.pair}.")
def manage_trade_stoploss_orders(self, trade: Trade, stoploss_orders: List[Dict]):
"""
Perform required actions acording to existing stoploss orders of trade
:param trade: Corresponding Trade
:param stoploss_orders: Current on exchange stoploss orders
:return: None
"""
# If all stoploss orderd are canceled for some reason we add it again
canceled_sl_orders = [o for o in stoploss_orders
if o['status'] in ('canceled', 'cancelled')]
if (
trade.is_open and
len(stoploss_orders) > 0 and
len(stoploss_orders) == len(canceled_sl_orders)
):
if self.create_stoploss_order(trade=trade, stop_price=trade.stoploss_or_liquidation):
return False
else:
logger.warning('All Stoploss orders are cancelled, but unable to recreate one.')
active_sl_orders = [o for o in stoploss_orders if o not in canceled_sl_orders]
if len(active_sl_orders) > 0:
last_active_sl_order = active_sl_orders[-1]
# Finally we check if stoploss on exchange should be moved up because of trailing.
# Triggered Orders are now real orders - so don't replace stoploss anymore
if (trade.is_open and
last_active_sl_order.get('status_stop') != 'triggered' and
(self.config.get('trailing_stop', False) or
self.config.get('use_custom_stoploss', False))):
# if trailing stoploss is enabled we check if stoploss value has changed
# in which case we cancel stoploss order and put another one with new
# value immediately
self.handle_trailing_stoploss_on_exchange(trade, last_active_sl_order)
return
def manage_open_orders(self) -> None: def manage_open_orders(self) -> None:
""" """
Management of open orders on exchange. Unfilled orders might be cancelled if timeout Management of open orders on exchange. Unfilled orders might be cancelled if timeout
+5 -4
View File
@@ -185,13 +185,14 @@ class Backtesting:
# Load detail timeframe if specified # Load detail timeframe if specified
self.timeframe_detail = str(self.config.get('timeframe_detail', '')) self.timeframe_detail = str(self.config.get('timeframe_detail', ''))
if self.timeframe_detail: if self.timeframe_detail:
self.timeframe_detail_min = timeframe_to_minutes(self.timeframe_detail) timeframe_detail_min = timeframe_to_minutes(self.timeframe_detail)
if self.timeframe_min <= self.timeframe_detail_min: self.timeframe_detail_td = timedelta(minutes=timeframe_detail_min)
if self.timeframe_min <= timeframe_detail_min:
raise OperationalException( raise OperationalException(
"Detail timeframe must be smaller than strategy timeframe.") "Detail timeframe must be smaller than strategy timeframe.")
else: else:
self.timeframe_detail_min = 0 self.timeframe_detail_td = timedelta(seconds=0)
self.detail_data: Dict[str, DataFrame] = {} self.detail_data: Dict[str, DataFrame] = {}
self.futures_data: Dict[str, DataFrame] = {} self.futures_data: Dict[str, DataFrame] = {}
@@ -1268,7 +1269,7 @@ class Backtesting:
open_trade_count_start = self.backtest_loop( open_trade_count_start = self.backtest_loop(
det_row, pair, current_time_det, end_date, det_row, pair, current_time_det, end_date,
open_trade_count_start, trade_dir, is_first) open_trade_count_start, trade_dir, is_first)
current_time_det += timedelta(minutes=self.timeframe_detail_min) current_time_det += self.timeframe_detail_td
is_first = False is_first = False
else: else:
self.dataprovider._set_dataframe_max_date(current_time) self.dataprovider._set_dataframe_max_date(current_time)
+4 -10
View File
@@ -1,7 +1,7 @@
import logging import logging
from typing import List, Optional from typing import List, Optional
from sqlalchemy import inspect, select, text, tuple_, update from sqlalchemy import inspect, select, text, update
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.persistence.trade_model import Order, Trade from freqtrade.persistence.trade_model import Order, Trade
@@ -91,8 +91,6 @@ def migrate_trades_and_orders_table(
is_stop_loss_trailing = get_column_def( is_stop_loss_trailing = get_column_def(
cols, 'is_stop_loss_trailing', cols, 'is_stop_loss_trailing',
f'coalesce({stop_loss_pct}, 0.0) <> coalesce({initial_stop_loss_pct}, 0.0)') f'coalesce({stop_loss_pct}, 0.0) <> coalesce({initial_stop_loss_pct}, 0.0)')
stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null')
stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null')
max_rate = get_column_def(cols, 'max_rate', '0.0') max_rate = get_column_def(cols, 'max_rate', '0.0')
min_rate = get_column_def(cols, 'min_rate', 'null') min_rate = get_column_def(cols, 'min_rate', 'null')
exit_reason = get_column_def(cols, 'sell_reason', get_column_def(cols, 'exit_reason', 'null')) exit_reason = get_column_def(cols, 'sell_reason', get_column_def(cols, 'exit_reason', 'null'))
@@ -160,7 +158,7 @@ def migrate_trades_and_orders_table(
open_rate_requested, close_rate, close_rate_requested, close_profit, open_rate_requested, close_rate, close_rate_requested, close_profit,
stake_amount, amount, amount_requested, open_date, close_date, stake_amount, amount, amount_requested, open_date, close_date,
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
is_stop_loss_trailing, stoploss_order_id, stoploss_last_update, is_stop_loss_trailing,
max_rate, min_rate, exit_reason, exit_order_status, strategy, enter_tag, max_rate, min_rate, exit_reason, exit_order_status, strategy, enter_tag,
timeframe, open_trade_value, close_profit_abs, timeframe, open_trade_value, close_profit_abs,
trading_mode, leverage, liquidation_price, is_short, trading_mode, leverage, liquidation_price, is_short,
@@ -180,7 +178,6 @@ def migrate_trades_and_orders_table(
{initial_stop_loss} initial_stop_loss, {initial_stop_loss} initial_stop_loss,
{initial_stop_loss_pct} initial_stop_loss_pct, {initial_stop_loss_pct} initial_stop_loss_pct,
{is_stop_loss_trailing} is_stop_loss_trailing, {is_stop_loss_trailing} is_stop_loss_trailing,
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
{max_rate} max_rate, {min_rate} min_rate, {max_rate} max_rate, {min_rate} min_rate,
case when {exit_reason} = 'sell_signal' then 'exit_signal' case when {exit_reason} = 'sell_signal' then 'exit_signal'
when {exit_reason} = 'custom_sell' then 'custom_exit' when {exit_reason} = 'custom_sell' then 'custom_exit'
@@ -279,6 +276,8 @@ def fix_old_dry_orders(engine):
with engine.begin() as connection: with engine.begin() as connection:
# Update current dry-run Orders where # Update current dry-run Orders where
# - stoploss order is Open (will be replaced eventually)
# 2nd query:
# - current Order is open # - current Order is open
# - current Trade is closed # - current Trade is closed
# - current Order trade_id not equal to current Trade.id # - current Order trade_id not equal to current Trade.id
@@ -286,11 +285,6 @@ def fix_old_dry_orders(engine):
stmt = update(Order).where( stmt = update(Order).where(
Order.ft_is_open.is_(True), Order.ft_is_open.is_(True),
tuple_(Order.ft_trade_id, Order.order_id).not_in(
select(
Trade.id, Trade.stoploss_order_id
).where(Trade.stoploss_order_id.is_not(None))
),
Order.ft_order_side == 'stoploss', Order.ft_order_side == 'stoploss',
Order.order_id.like('dry%'), Order.order_id.like('dry%'),
+40 -23
View File
@@ -177,6 +177,8 @@ class Order(ModelBase):
order_date = safe_value_fallback(order, 'timestamp') order_date = safe_value_fallback(order, 'timestamp')
if order_date: if order_date:
self.order_date = datetime.fromtimestamp(order_date / 1000, tz=timezone.utc) self.order_date = datetime.fromtimestamp(order_date / 1000, tz=timezone.utc)
elif not self.order_date:
self.order_date = dt_now()
self.ft_is_open = True self.ft_is_open = True
if self.status in NON_OPEN_EXCHANGE_STATES: if self.status in NON_OPEN_EXCHANGE_STATES:
@@ -376,10 +378,6 @@ class LocalTrade:
# percentage value of the initial stop loss # percentage value of the initial stop loss
initial_stop_loss_pct: Optional[float] = None initial_stop_loss_pct: Optional[float] = None
is_stop_loss_trailing: bool = False is_stop_loss_trailing: bool = False
# stoploss order id which is on exchange
stoploss_order_id: Optional[str] = None
# last update time of the stoploss order on exchange
stoploss_last_update: Optional[datetime] = None
# absolute value of the highest reached price # absolute value of the highest reached price
max_rate: Optional[float] = None max_rate: Optional[float] = None
# Lowest price reached # Lowest price reached
@@ -469,8 +467,8 @@ class LocalTrade:
@property @property
def stoploss_last_update_utc(self): def stoploss_last_update_utc(self):
if self.stoploss_last_update: if self.has_open_sl_orders:
return self.stoploss_last_update.replace(tzinfo=timezone.utc) return max(o.order_date_utc for o in self.open_sl_orders)
return None return None
@property @property
@@ -526,7 +524,7 @@ class LocalTrade:
return [o for o in self.orders if o.ft_is_open and o.ft_order_side != 'stoploss'] return [o for o in self.orders if o.ft_is_open and o.ft_order_side != 'stoploss']
@property @property
def has_open_orders(self) -> int: def has_open_orders(self) -> bool:
""" """
True if there are open orders for this trade excluding stoploss orders True if there are open orders for this trade excluding stoploss orders
""" """
@@ -536,6 +534,37 @@ class LocalTrade:
] ]
return len(open_orders_wo_sl) > 0 return len(open_orders_wo_sl) > 0
@property
def open_sl_orders(self) -> List[Order]:
"""
All open stoploss orders for this trade
"""
return [
o for o in self.orders
if o.ft_order_side in ['stoploss'] and o.ft_is_open
]
@property
def has_open_sl_orders(self) -> bool:
"""
True if there are open stoploss orders for this trade
"""
open_sl_orders = [
o for o in self.orders
if o.ft_order_side in ['stoploss'] and o.ft_is_open
]
return len(open_sl_orders) > 0
@property
def sl_orders(self) -> List[Order]:
"""
All stoploss orders for this trade
"""
return [
o for o in self.orders
if o.ft_order_side in ['stoploss']
]
@property @property
def open_orders_ids(self) -> List[str]: def open_orders_ids(self) -> List[str]:
open_orders_ids_wo_sl = [ open_orders_ids_wo_sl = [
@@ -628,11 +657,10 @@ class LocalTrade:
'stop_loss_abs': self.stop_loss, 'stop_loss_abs': self.stop_loss,
'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None, 'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None,
'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None, 'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
'stoploss_order_id': self.stoploss_order_id, 'stoploss_last_update': (self.stoploss_last_update_utc.strftime(DATETIME_PRINT_FORMAT)
'stoploss_last_update': (self.stoploss_last_update.strftime(DATETIME_PRINT_FORMAT) if self.stoploss_last_update_utc else None),
if self.stoploss_last_update else None), 'stoploss_last_update_timestamp': int(self.stoploss_last_update_utc.timestamp() * 1000
'stoploss_last_update_timestamp': int(self.stoploss_last_update.replace( ) if self.stoploss_last_update_utc else None,
tzinfo=timezone.utc).timestamp() * 1000) if self.stoploss_last_update else None,
'initial_stop_loss_abs': self.initial_stop_loss, 'initial_stop_loss_abs': self.initial_stop_loss,
'initial_stop_loss_ratio': (self.initial_stop_loss_pct 'initial_stop_loss_ratio': (self.initial_stop_loss_pct
if self.initial_stop_loss_pct else None), if self.initial_stop_loss_pct else None),
@@ -793,7 +821,6 @@ class LocalTrade:
logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.') logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.')
elif order.ft_order_side == 'stoploss' and order.status not in ('open', ): elif order.ft_order_side == 'stoploss' and order.status not in ('open', ):
self.stoploss_order_id = None
self.close_rate_requested = self.stop_loss self.close_rate_requested = self.stop_loss
self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
if self.is_open and order.safe_filled > 0: if self.is_open and order.safe_filled > 0:
@@ -1370,11 +1397,6 @@ class LocalTrade:
exit_order_status=data["exit_order_status"], exit_order_status=data["exit_order_status"],
stop_loss=data["stop_loss_abs"], stop_loss=data["stop_loss_abs"],
stop_loss_pct=data["stop_loss_ratio"], stop_loss_pct=data["stop_loss_ratio"],
stoploss_order_id=data["stoploss_order_id"],
stoploss_last_update=(
datetime.fromtimestamp(data["stoploss_last_update_timestamp"] // 1000,
tz=timezone.utc)
if data["stoploss_last_update_timestamp"] else None),
initial_stop_loss=data["initial_stop_loss_abs"], initial_stop_loss=data["initial_stop_loss_abs"],
initial_stop_loss_pct=data["initial_stop_loss_ratio"], initial_stop_loss_pct=data["initial_stop_loss_ratio"],
min_rate=data["min_rate"], min_rate=data["min_rate"],
@@ -1481,11 +1503,6 @@ class Trade(ModelBase, LocalTrade):
Float(), nullable=True) # type: ignore Float(), nullable=True) # type: ignore
is_stop_loss_trailing: Mapped[bool] = mapped_column( is_stop_loss_trailing: Mapped[bool] = mapped_column(
nullable=False, default=False) # type: ignore nullable=False, default=False) # type: ignore
# stoploss order id which is on exchange
stoploss_order_id: Mapped[Optional[str]] = mapped_column(
String(255), nullable=True, index=True) # type: ignore
# last update time of the stoploss order on exchange
stoploss_last_update: Mapped[Optional[datetime]] = mapped_column(nullable=True) # type: ignore
# absolute value of the highest reached price # absolute value of the highest reached price
max_rate: Mapped[Optional[float]] = mapped_column( max_rate: Mapped[Optional[float]] = mapped_column(
Float(), nullable=True, default=0.0) # type: ignore Float(), nullable=True, default=0.0) # type: ignore
-1
View File
@@ -315,7 +315,6 @@ class TradeSchema(BaseModel):
stop_loss_abs: Optional[float] = None stop_loss_abs: Optional[float] = None
stop_loss_ratio: Optional[float] = None stop_loss_ratio: Optional[float] = None
stop_loss_pct: Optional[float] = None stop_loss_pct: Optional[float] = None
stoploss_order_id: Optional[str] = None
stoploss_last_update: Optional[str] = None stoploss_last_update: Optional[str] = None
stoploss_last_update_timestamp: Optional[int] = None stoploss_last_update_timestamp: Optional[int] = None
initial_stop_loss_abs: Optional[float] = None initial_stop_loss_abs: Optional[float] = None
+9 -8
View File
@@ -979,15 +979,16 @@ class RPC:
except (ExchangeError): except (ExchangeError):
pass pass
# cancel stoploss on exchange ... # cancel stoploss on exchange orders ...
if (self._freqtrade.strategy.order_types.get('stoploss_on_exchange') if (self._freqtrade.strategy.order_types.get('stoploss_on_exchange')
and trade.stoploss_order_id): and trade.has_open_sl_orders):
try:
self._freqtrade.exchange.cancel_stoploss_order(trade.stoploss_order_id, for oslo in trade.open_sl_orders:
trade.pair) try:
c_count += 1 self._freqtrade.exchange.cancel_stoploss_order(oslo.order_id, trade.pair)
except (ExchangeError): c_count += 1
pass except (ExchangeError):
pass
trade.delete() trade.delete()
self._freqtrade.wallets.update() self._freqtrade.wallets.update()
-1
View File
@@ -266,7 +266,6 @@ def mock_trade_5(fee, is_short: bool):
exchange='binance', exchange='binance',
strategy='SampleStrategy', strategy='SampleStrategy',
enter_tag='TEST1', enter_tag='TEST1',
stoploss_order_id=f'prod_stoploss_{direc(is_short)}_3455',
timeframe=5, timeframe=5,
is_short=is_short, is_short=is_short,
stop_loss_pct=0.10, stop_loss_pct=0.10,
-1
View File
@@ -282,7 +282,6 @@ def mock_trade_usdt_5(fee, is_short: bool):
open_rate=2.0, open_rate=2.0,
exchange='binance', exchange='binance',
strategy='SampleStrategy', strategy='SampleStrategy',
stoploss_order_id=f'prod_stoploss_3455_{direc(is_short)}',
timeframe=5, timeframe=5,
is_short=is_short, is_short=is_short,
) )
+8 -2
View File
@@ -9,7 +9,7 @@ import ccxt
import pytest import pytest
from pandas import DataFrame from pandas import DataFrame
from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.enums import CandleType, MarginMode, RunMode, TradingMode
from freqtrade.exceptions import (DDosProtection, DependencyException, ExchangeError, from freqtrade.exceptions import (DDosProtection, DependencyException, ExchangeError,
InsufficientFundsError, InvalidOrderException, InsufficientFundsError, InvalidOrderException,
OperationalException, PricingError, TemporaryError) OperationalException, PricingError, TemporaryError)
@@ -796,7 +796,9 @@ def test_validate_timeframes_failed(default_conf, mocker):
mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock))
mocker.patch(f'{EXMS}._load_markets', MagicMock(return_value={})) mocker.patch(f'{EXMS}._load_markets', MagicMock(return_value={}))
mocker.patch(f'{EXMS}.validate_pairs', MagicMock()) mocker.patch(f'{EXMS}.validate_pairs')
mocker.patch(f'{EXMS}.validate_stakecurrency')
mocker.patch(f'{EXMS}.validate_pricing')
with pytest.raises(OperationalException, with pytest.raises(OperationalException,
match=r"Invalid timeframe '3m'. This exchange supports.*"): match=r"Invalid timeframe '3m'. This exchange supports.*"):
Exchange(default_conf) Exchange(default_conf)
@@ -806,6 +808,10 @@ def test_validate_timeframes_failed(default_conf, mocker):
match=r"Timeframes < 1m are currently not supported by Freqtrade."): match=r"Timeframes < 1m are currently not supported by Freqtrade."):
Exchange(default_conf) Exchange(default_conf)
# Will not raise an exception in util mode.
default_conf['runmode'] = RunMode.UTIL_EXCHANGE
Exchange(default_conf)
def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker): def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker):
default_conf["timeframe"] = "3m" default_conf["timeframe"] = "3m"
View File
File diff suppressed because it is too large Load Diff
@@ -49,7 +49,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
stoploss_order_closed['filled'] = stoploss_order_closed['amount'] stoploss_order_closed['filled'] = stoploss_order_closed['amount']
# Sell first trade based on stoploss, keep 2nd and 3rd trade open # Sell first trade based on stoploss, keep 2nd and 3rd trade open
stop_orders = [stoploss_order_closed, stoploss_order_open, stoploss_order_open] stop_orders = [stoploss_order_closed, stoploss_order_open.copy(), stoploss_order_open.copy()]
stoploss_order_mock = MagicMock( stoploss_order_mock = MagicMock(
side_effect=stop_orders) side_effect=stop_orders)
# Sell 3rd trade (not called for the first trade) # Sell 3rd trade (not called for the first trade)
@@ -100,9 +100,10 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
stop_order = stop_orders[idx] stop_order = stop_orders[idx]
stop_order['id'] = f"stop{idx}" stop_order['id'] = f"stop{idx}"
oobj = Order.parse_from_ccxt_object(stop_order, trade.pair, 'stoploss') oobj = Order.parse_from_ccxt_object(stop_order, trade.pair, 'stoploss')
oobj.ft_is_open = True
trade.orders.append(oobj) trade.orders.append(oobj)
trade.stoploss_order_id = f"stop{idx}" assert len(trade.open_sl_orders) == 1
n = freqtrade.exit_positions(trades) n = freqtrade.exit_positions(trades)
assert n == 2 assert n == 2
@@ -113,6 +114,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
# Only order for 3rd trade needs to be cancelled # Only order for 3rd trade needs to be cancelled
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
assert stoploss_order_mock.call_count == 3
# Wallets must be updated between stoploss cancellation and selling, and will be updated again # Wallets must be updated between stoploss cancellation and selling, and will be updated again
# during update_trade_state # during update_trade_state
assert wallets_mock.call_count == 4 assert wallets_mock.call_count == 4
File diff suppressed because it is too large Load Diff
+3 -4
View File
@@ -74,7 +74,7 @@ def test_init_dryrun_db(default_conf, tmpdir):
assert Path(filename).is_file() assert Path(filename).is_file()
def test_migrate_new(mocker, default_conf, fee, caplog): def test_migrate(mocker, default_conf, fee, caplog):
""" """
Test Database migration (starting with new pairformat) Test Database migration (starting with new pairformat)
""" """
@@ -277,8 +277,6 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert trade.exit_reason is None assert trade.exit_reason is None
assert trade.strategy is None assert trade.strategy is None
assert trade.timeframe == '5m' assert trade.timeframe == '5m'
assert trade.stoploss_order_id == 'dry_stop_order_id222'
assert trade.stoploss_last_update is None
assert log_has("trying trades_bak1", caplog) assert log_has("trying trades_bak1", caplog)
assert log_has("trying trades_bak2", caplog) assert log_has("trying trades_bak2", caplog)
assert log_has("Running database migration for trades - backup: trades_bak2, orders_bak0", assert log_has("Running database migration for trades - backup: trades_bak2, orders_bak0",
@@ -294,9 +292,10 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert orders[0].order_id == 'dry_buy_order' assert orders[0].order_id == 'dry_buy_order'
assert orders[0].ft_order_side == 'buy' assert orders[0].ft_order_side == 'buy'
# All dry-run stoploss orders will be closed
assert orders[-1].order_id == 'dry_stop_order_id222' assert orders[-1].order_id == 'dry_stop_order_id222'
assert orders[-1].ft_order_side == 'stoploss' assert orders[-1].ft_order_side == 'stoploss'
assert orders[-1].ft_is_open is True assert orders[-1].ft_is_open is False
assert orders[1].order_id == 'dry_buy_order22' assert orders[1].order_id == 'dry_buy_order22'
assert orders[1].ft_order_side == 'buy' assert orders[1].ft_order_side == 'buy'
-2
View File
@@ -1432,7 +1432,6 @@ def test_to_json(fee):
'stop_loss_abs': None, 'stop_loss_abs': None,
'stop_loss_ratio': None, 'stop_loss_ratio': None,
'stop_loss_pct': None, 'stop_loss_pct': None,
'stoploss_order_id': None,
'stoploss_last_update': None, 'stoploss_last_update': None,
'stoploss_last_update_timestamp': None, 'stoploss_last_update_timestamp': None,
'initial_stop_loss_abs': None, 'initial_stop_loss_abs': None,
@@ -1500,7 +1499,6 @@ def test_to_json(fee):
'stop_loss_abs': None, 'stop_loss_abs': None,
'stop_loss_pct': None, 'stop_loss_pct': None,
'stop_loss_ratio': None, 'stop_loss_ratio': None,
'stoploss_order_id': None,
'stoploss_last_update': None, 'stoploss_last_update': None,
'stoploss_last_update_timestamp': None, 'stoploss_last_update_timestamp': None,
'initial_stop_loss_abs': None, 'initial_stop_loss_abs': None,
-1
View File
@@ -54,7 +54,6 @@ def test_trade_fromjson():
"stop_loss_abs": 0.1981, "stop_loss_abs": 0.1981,
"stop_loss_ratio": -0.216, "stop_loss_ratio": -0.216,
"stop_loss_pct": -21.6, "stop_loss_pct": -21.6,
"stoploss_order_id": null,
"stoploss_last_update": "2022-10-18 09:13:42", "stoploss_last_update": "2022-10-18 09:13:42",
"stoploss_last_update_timestamp": 1666077222000, "stoploss_last_update_timestamp": 1666077222000,
"initial_stop_loss_abs": 0.1981, "initial_stop_loss_abs": 0.1981,
-2
View File
@@ -63,7 +63,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'stop_loss_abs': 9.89e-06, 'stop_loss_abs': 9.89e-06,
'stop_loss_pct': -10.0, 'stop_loss_pct': -10.0,
'stop_loss_ratio': -0.1, 'stop_loss_ratio': -0.1,
'stoploss_order_id': None,
'stoploss_last_update': ANY, 'stoploss_last_update': ANY,
'stoploss_last_update_timestamp': ANY, 'stoploss_last_update_timestamp': ANY,
'initial_stop_loss_abs': 9.89e-06, 'initial_stop_loss_abs': 9.89e-06,
@@ -355,7 +354,6 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short):
rpc._rpc_delete('200') rpc._rpc_delete('200')
trades = Trade.session.scalars(select(Trade)).all() trades = Trade.session.scalars(select(Trade)).all()
trades[2].stoploss_order_id = '102'
trades[2].orders.append( trades[2].orders.append(
Order( Order(
ft_order_side='stoploss', ft_order_side='stoploss',
-2
View File
@@ -1174,7 +1174,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
'stop_loss_abs': ANY, 'stop_loss_abs': ANY,
'stop_loss_pct': ANY, 'stop_loss_pct': ANY,
'stop_loss_ratio': ANY, 'stop_loss_ratio': ANY,
'stoploss_order_id': None,
'stoploss_last_update': ANY, 'stoploss_last_update': ANY,
'stoploss_last_update_timestamp': ANY, 'stoploss_last_update_timestamp': ANY,
'initial_stop_loss_abs': 0.0, 'initial_stop_loss_abs': 0.0,
@@ -1378,7 +1377,6 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
'stop_loss_abs': None, 'stop_loss_abs': None,
'stop_loss_pct': None, 'stop_loss_pct': None,
'stop_loss_ratio': None, 'stop_loss_ratio': None,
'stoploss_order_id': None,
'stoploss_last_update': None, 'stoploss_last_update': None,
'stoploss_last_update_timestamp': None, 'stoploss_last_update_timestamp': None,
'initial_stop_loss_abs': None, 'initial_stop_loss_abs': None,