Partial exit using average price (#6545)

Introduce Partial exits
This commit is contained in:
Kavinkumar 2022-07-31 17:49:04 +05:30 committed by GitHub
parent 369c6da5d8
commit a4bada3ebe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1462 additions and 347 deletions

View File

@ -629,7 +629,7 @@ class AwesomeStrategy(IStrategy):
The `position_adjustment_enable` strategy property enables the usage of `adjust_trade_position()` callback in the strategy. The `position_adjustment_enable` strategy property enables the usage of `adjust_trade_position()` callback in the strategy.
For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled. For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled.
`adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging). `adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging) or to increase or decrease positions.
`max_entry_position_adjustment` property is used to limit the number of additional buys per trade (on top of the first buy) that the bot can execute. By default, the value is -1 which means the bot have no limit on number of adjustment buys. `max_entry_position_adjustment` property is used to limit the number of additional buys per trade (on top of the first buy) that the bot can execute. By default, the value is -1 which means the bot have no limit on number of adjustment buys.
@ -637,10 +637,13 @@ The strategy is expected to return a stake_amount (in stake currency) between `m
If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored. If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored.
Additional orders also result in additional fees and those orders don't count towards `max_open_trades`. Additional orders also result in additional fees and those orders don't count towards `max_open_trades`.
This callback is **not** called when there is an open order (either buy or sell) waiting for execution, or when you have reached the maximum amount of extra buys that you have set on `max_entry_position_adjustment`. This callback is **not** called when there is an open order (either buy or sell) waiting for execution.
`adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible. `adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible.
Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position, no matter if it's a long or short trade. Modifications to leverage are not possible. Additional Buys are ignored once you have reached the maximum amount of extra buys that you have set on `max_entry_position_adjustment`, but the callback is called anyway looking for partial exits.
Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade. Modifications to leverage are not possible.
!!! Note "About stake size" !!! Note "About stake size"
Using fixed stake size means it will be the amount used for the first order, just like without position adjustment. Using fixed stake size means it will be the amount used for the first order, just like without position adjustment.
@ -649,12 +652,12 @@ Position adjustments will always be applied in the direction of the trade, so a
!!! Warning !!! Warning
Stoploss is still calculated from the initial opening price, not averaged price. Stoploss is still calculated from the initial opening price, not averaged price.
Regular stoploss rules still apply (cannot move down).
!!! Warning "/stopbuy"
While `/stopbuy` command stops the bot from entering new trades, the position adjustment feature will continue buying new orders on existing trades. While `/stopbuy` command stops the bot from entering new trades, the position adjustment feature will continue buying new orders on existing trades.
!!! Warning "Backtesting" !!! Warning "Backtesting"
During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so performance will be affected. During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so run-time performance will be affected.
``` python ``` python
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
@ -675,7 +678,7 @@ class DigDeeperStrategy(IStrategy):
max_dca_multiplier = 5.5 max_dca_multiplier = 5.5
# This is called when placing the initial order (opening trade) # This is called when placing the initial order (opening trade)
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: Optional[float], max_stake: float, proposed_stake: float, min_stake: Optional[float], max_stake: float,
leverage: float, entry_tag: Optional[str], side: str, leverage: float, entry_tag: Optional[str], side: str,
**kwargs) -> float: **kwargs) -> float:
@ -685,22 +688,41 @@ def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: f
return proposed_stake / self.max_dca_multiplier return proposed_stake / self.max_dca_multiplier
def adjust_trade_position(self, trade: Trade, current_time: datetime, def adjust_trade_position(self, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, min_stake: Optional[float], current_rate: float, current_profit: float,
max_stake: float, **kwargs): min_stake: Optional[float], max_stake: float,
current_entry_rate: float, current_exit_rate: float,
current_entry_profit: float, current_exit_profit: float,
**kwargs) -> Optional[float]:
""" """
Custom trade adjustment logic, returning the stake amount that a trade should be increased. Custom trade adjustment logic, returning the stake amount that a trade should be
This means extra buy orders with additional fees. increased or decreased.
This means extra buy or sell orders with additional fees.
Only called when `position_adjustment_enable` is set to True.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns None
:param trade: trade object. :param trade: trade object.
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param current_rate: Current buy rate. :param current_rate: Current buy rate.
:param current_profit: Current profit (as ratio), calculated based on current_rate. :param current_profit: Current profit (as ratio), calculated based on current_rate.
:param min_stake: Minimal stake size allowed by exchange. :param min_stake: Minimal stake size allowed by exchange (for both entries and exits)
:param max_stake: Balance available for trading. :param max_stake: Maximum stake allowed (either through balance, or by exchange limits).
:param current_entry_rate: Current rate using entry pricing.
:param current_exit_rate: Current rate using exit pricing.
:param current_entry_profit: Current profit using entry pricing.
:param current_exit_profit: Current profit using exit pricing.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: Stake amount to adjust your trade :return float: Stake amount to adjust your trade,
Positive values to increase position, Negative values to decrease position.
Return None for no action.
""" """
if current_profit > 0.05 and trade.nr_of_successful_exits == 0:
# Take half of the profit at +5%
return -(trade.amount / 2)
if current_profit > -0.05: if current_profit > -0.05:
return None return None
@ -735,6 +757,25 @@ def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: f
``` ```
### Position adjust calculations
* Entry rates are calculated using weighted averages.
* Exits will not influence the average entry rate.
* Partial exit relative profit is relative to the average entry price at this point.
* Final exit relative profit is calculated based on the total invested capital. (See example below)
??? example "Calculation example"
*This example assumes 0 fees for simplicity, and a long position on an imaginary coin.*
* Buy 100@8\$
* Buy 100@9\$ -> Avg price: 8.5\$
* Sell 100@10\$ -> Avg price: 8.5\$, realized profit 150\$, 17.65%
* Buy 150@11\$ -> Avg price: 10\$, realized profit 150\$, 17.65%
* Sell 100@12\$ -> Avg price: 10\$, total realized profit 350\$, 20%
* Sell 150@14\$ -> Avg price: 10\$, total realized profit 950\$, 40%
The total profit for this trade was 950$ on a 3350$ investment (`100@8$ + 100@9$ + 150@11$`). As such - the final relative profit is 28.35% (`950 / 3350`).
## Adjust Entry Price ## Adjust Entry Price
The `adjust_entry_price()` callback may be used by strategy developer to refresh/replace limit orders upon arrival of new candles. The `adjust_entry_price()` callback may be used by strategy developer to refresh/replace limit orders upon arrival of new candles.

View File

@ -14,6 +14,7 @@ class ExitType(Enum):
FORCE_EXIT = "force_exit" FORCE_EXIT = "force_exit"
EMERGENCY_EXIT = "emergency_exit" EMERGENCY_EXIT = "emergency_exit"
CUSTOM_EXIT = "custom_exit" CUSTOM_EXIT = "custom_exit"
PARTIAL_EXIT = "partial_exit"
NONE = "" NONE = ""
def __str__(self): def __str__(self):

View File

@ -1507,7 +1507,8 @@ class Exchange:
return price_side return price_side
def get_rate(self, pair: str, refresh: bool, def get_rate(self, pair: str, refresh: bool,
side: EntryExit, is_short: bool) -> float: side: EntryExit, is_short: bool,
order_book: Optional[dict] = None, ticker: Optional[dict] = None) -> float:
""" """
Calculates bid/ask target Calculates bid/ask target
bid rate - between current ask price and last price bid rate - between current ask price and last price
@ -1539,6 +1540,7 @@ class Exchange:
if conf_strategy.get('use_order_book', False): if conf_strategy.get('use_order_book', False):
order_book_top = conf_strategy.get('order_book_top', 1) order_book_top = conf_strategy.get('order_book_top', 1)
if order_book is None:
order_book = self.fetch_l2_order_book(pair, order_book_top) order_book = self.fetch_l2_order_book(pair, order_book_top)
logger.debug('order_book %s', order_book) logger.debug('order_book %s', order_book)
# top 1 = index 0 # top 1 = index 0
@ -1546,14 +1548,15 @@ class Exchange:
rate = order_book[f"{price_side}s"][order_book_top - 1][0] rate = order_book[f"{price_side}s"][order_book_top - 1][0]
except (IndexError, KeyError) as e: except (IndexError, KeyError) as e:
logger.warning( logger.warning(
f"{name} Price at location {order_book_top} from orderbook could not be " f"{pair} - {name} Price at location {order_book_top} from orderbook "
f"determined. Orderbook: {order_book}" f"could not be determined. Orderbook: {order_book}"
) )
raise PricingError from e raise PricingError from e
logger.debug(f"{name} price from orderbook {price_side_word}" logger.debug(f"{pair} - {name} price from orderbook {price_side_word}"
f"side - top {order_book_top} order book {side} rate {rate:.8f}") f"side - top {order_book_top} order book {side} rate {rate:.8f}")
else: else:
logger.debug(f"Using Last {price_side_word} / Last Price") logger.debug(f"Using Last {price_side_word} / Last Price")
if ticker is None:
ticker = self.fetch_ticker(pair) ticker = self.fetch_ticker(pair)
ticker_rate = ticker[price_side] ticker_rate = ticker[price_side]
if ticker['last'] and ticker_rate: if ticker['last'] and ticker_rate:
@ -1571,6 +1574,33 @@ class Exchange:
return rate return rate
def get_rates(self, pair: str, refresh: bool, is_short: bool) -> Tuple[float, float]:
entry_rate = None
exit_rate = None
if not refresh:
entry_rate = self._entry_rate_cache.get(pair)
exit_rate = self._exit_rate_cache.get(pair)
if entry_rate:
logger.debug(f"Using cached buy rate for {pair}.")
if exit_rate:
logger.debug(f"Using cached sell rate for {pair}.")
entry_pricing = self._config.get('entry_pricing', {})
exit_pricing = self._config.get('exit_pricing', {})
order_book = ticker = None
if not entry_rate and entry_pricing.get('use_order_book', False):
order_book_top = max(entry_pricing.get('order_book_top', 1),
exit_pricing.get('order_book_top', 1))
order_book = self.fetch_l2_order_book(pair, order_book_top)
entry_rate = self.get_rate(pair, refresh, 'entry', is_short, order_book=order_book)
elif not entry_rate:
ticker = self.fetch_ticker(pair)
entry_rate = self.get_rate(pair, refresh, 'entry', is_short, ticker=ticker)
if not exit_rate:
exit_rate = self.get_rate(pair, refresh, 'exit',
is_short, order_book=order_book, ticker=ticker)
return entry_rate, exit_rate
# Fee handling # Fee handling
@retrier @retrier

View File

@ -5,6 +5,7 @@ import copy
import logging import logging
import traceback import traceback
from datetime import datetime, time, timedelta, timezone from datetime import datetime, time, timedelta, timezone
from decimal import Decimal
from math import isclose from math import isclose
from threading import Lock from threading import Lock
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
@ -525,39 +526,61 @@ class FreqtradeBot(LoggingMixin):
If the strategy triggers the adjustment, a new order gets issued. If the strategy triggers the adjustment, a new order gets issued.
Once that completes, the existing trade is modified to match new data. Once that completes, the existing trade is modified to match new data.
""" """
if self.strategy.max_entry_position_adjustment > -1: current_entry_rate, current_exit_rate = self.exchange.get_rates(
count_of_buys = trade.nr_of_successful_entries trade.pair, True, trade.is_short)
if count_of_buys > self.strategy.max_entry_position_adjustment:
logger.debug(f"Max adjustment entries for {trade.pair} has been reached.")
return
else:
logger.debug("Max adjustment entries is set to unlimited.")
current_rate = self.exchange.get_rate(
trade.pair, side='entry', is_short=trade.is_short, refresh=True)
current_profit = trade.calc_profit_ratio(current_rate)
min_stake_amount = self.exchange.get_min_pair_stake_amount(trade.pair, current_entry_profit = trade.calc_profit_ratio(current_entry_rate)
current_rate, current_exit_profit = trade.calc_profit_ratio(current_exit_rate)
min_entry_stake = self.exchange.get_min_pair_stake_amount(trade.pair,
current_entry_rate,
self.strategy.stoploss) self.strategy.stoploss)
max_stake_amount = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate) min_exit_stake = self.exchange.get_min_pair_stake_amount(trade.pair,
current_exit_rate,
self.strategy.stoploss)
max_entry_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_entry_rate)
stake_available = self.wallets.get_available_stake_amount() stake_available = self.wallets.get_available_stake_amount()
logger.debug(f"Calling adjust_trade_position for pair {trade.pair}") logger.debug(f"Calling adjust_trade_position for pair {trade.pair}")
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
default_retval=None)( default_retval=None)(
trade=trade, current_time=datetime.now(timezone.utc), current_rate=current_rate, trade=trade,
current_profit=current_profit, min_stake=min_stake_amount, current_time=datetime.now(timezone.utc), current_rate=current_entry_rate,
max_stake=min(max_stake_amount, stake_available)) current_profit=current_entry_profit, min_stake=min_entry_stake,
max_stake=min(max_entry_stake, stake_available),
current_entry_rate=current_entry_rate, current_exit_rate=current_exit_rate,
current_entry_profit=current_entry_profit, current_exit_profit=current_exit_profit
)
if stake_amount is not None and stake_amount > 0.0: if stake_amount is not None and stake_amount > 0.0:
# We should increase our position # We should increase our position
self.execute_entry(trade.pair, stake_amount, price=current_rate, if self.strategy.max_entry_position_adjustment > -1:
count_of_entries = trade.nr_of_successful_entries
if count_of_entries > self.strategy.max_entry_position_adjustment:
logger.debug(f"Max adjustment entries for {trade.pair} has been reached.")
return
else:
logger.debug("Max adjustment entries is set to unlimited.")
self.execute_entry(trade.pair, stake_amount, price=current_entry_rate,
trade=trade, is_short=trade.is_short) trade=trade, is_short=trade.is_short)
if stake_amount is not None and stake_amount < 0.0: if stake_amount is not None and stake_amount < 0.0:
# We should decrease our position # We should decrease our position
# TODO: Selling part of the trade not implemented yet. amount = abs(float(Decimal(stake_amount) / Decimal(current_exit_rate)))
logger.error(f"Unable to decrease trade position / sell partially" if amount > trade.amount:
f" for pair {trade.pair}, feature not implemented.") # This is currently ineffective as remaining would become < min tradable
# Fixing this would require checking for 0.0 there -
# if we decide that this callback is allowed to "fully exit"
logger.info(
f"Adjusting amount to trade.amount as it is higher. {amount} > {trade.amount}")
amount = trade.amount
remaining = (trade.amount - amount) * current_exit_rate
if remaining < min_exit_stake:
logger.info(f'Remaining amount of {remaining} would be too small.')
return
self.execute_trade_exit(trade, current_exit_rate, exit_check=ExitCheckTuple(
exit_type=ExitType.PARTIAL_EXIT), sub_trade_amt=amount)
def _check_depth_of_market(self, pair: str, conf: Dict, side: SignalDirection) -> bool: def _check_depth_of_market(self, pair: str, conf: Dict, side: SignalDirection) -> bool:
""" """
@ -731,7 +754,7 @@ class FreqtradeBot(LoggingMixin):
# Updating wallets # Updating wallets
self.wallets.update() self.wallets.update()
self._notify_enter(trade, order, order_type) self._notify_enter(trade, order_obj, order_type, sub_trade=pos_adjust)
if pos_adjust: if pos_adjust:
if order_status == 'closed': if order_status == 'closed':
@ -740,8 +763,8 @@ class FreqtradeBot(LoggingMixin):
else: else:
logger.info(f"DCA order {order_status}, will wait for resolution: {trade}") logger.info(f"DCA order {order_status}, will wait for resolution: {trade}")
# Update fees if order is closed # Update fees if order is non-opened
if order_status == 'closed': if order_status in constants.NON_OPEN_EXCHANGE_STATES:
self.update_trade_state(trade, order_id, order) self.update_trade_state(trade, order_id, order)
return True return True
@ -830,13 +853,14 @@ class FreqtradeBot(LoggingMixin):
return enter_limit_requested, stake_amount, leverage return enter_limit_requested, stake_amount, leverage
def _notify_enter(self, trade: Trade, order: Dict, order_type: Optional[str] = None, def _notify_enter(self, trade: Trade, order: Order, order_type: Optional[str] = None,
fill: bool = False) -> None: fill: bool = False, sub_trade: bool = False) -> None:
""" """
Sends rpc notification when a entry order occurred. Sends rpc notification when a entry order occurred.
""" """
msg_type = RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY msg_type = RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY
open_rate = safe_value_fallback(order, 'average', 'price') open_rate = order.safe_price
if open_rate is None: if open_rate is None:
open_rate = trade.open_rate open_rate = trade.open_rate
@ -860,15 +884,17 @@ class FreqtradeBot(LoggingMixin):
'stake_amount': trade.stake_amount, 'stake_amount': trade.stake_amount,
'stake_currency': self.config['stake_currency'], 'stake_currency': self.config['stake_currency'],
'fiat_currency': self.config.get('fiat_display_currency', None), 'fiat_currency': self.config.get('fiat_display_currency', None),
'amount': safe_value_fallback(order, 'filled', 'amount') or trade.amount, 'amount': order.safe_amount_after_fee,
'open_date': trade.open_date or datetime.utcnow(), 'open_date': trade.open_date or datetime.utcnow(),
'current_rate': current_rate, 'current_rate': current_rate,
'sub_trade': sub_trade,
} }
# Send the message # Send the message
self.rpc.send_msg(msg) self.rpc.send_msg(msg)
def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str) -> None: def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str,
sub_trade: bool = False) -> None:
""" """
Sends rpc notification when a entry order cancel occurred. Sends rpc notification when a entry order cancel occurred.
""" """
@ -893,6 +919,7 @@ class FreqtradeBot(LoggingMixin):
'open_date': trade.open_date, 'open_date': trade.open_date,
'current_rate': current_rate, 'current_rate': current_rate,
'reason': reason, 'reason': reason,
'sub_trade': sub_trade,
} }
# Send the message # Send the message
@ -1366,16 +1393,22 @@ class FreqtradeBot(LoggingMixin):
trade.open_order_id = None trade.open_order_id = None
trade.exit_reason = None trade.exit_reason = None
cancelled = True cancelled = True
self.wallets.update()
else: else:
# TODO: figure out how to handle partially complete sell orders # TODO: figure out how to handle partially complete sell orders
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
cancelled = False cancelled = False
self.wallets.update() order_obj = trade.select_order_by_order_id(order['id'])
if not order_obj:
raise DependencyException(
f"Order_obj not found for {order['id']}. This should not have happened.")
sub_trade = order_obj.amount != trade.amount
self._notify_exit_cancel( self._notify_exit_cancel(
trade, trade,
order_type=self.strategy.order_types['exit'], order_type=self.strategy.order_types['exit'],
reason=reason reason=reason, order=order_obj, sub_trade=sub_trade
) )
return cancelled return cancelled
@ -1416,6 +1449,7 @@ class FreqtradeBot(LoggingMixin):
*, *,
exit_tag: Optional[str] = None, exit_tag: Optional[str] = None,
ordertype: Optional[str] = None, ordertype: Optional[str] = None,
sub_trade_amt: float = None,
) -> bool: ) -> bool:
""" """
Executes a trade exit for the given trade and limit Executes a trade exit for the given trade and limit
@ -1462,10 +1496,12 @@ class FreqtradeBot(LoggingMixin):
# Emergency sells (default to market!) # Emergency sells (default to market!)
order_type = self.strategy.order_types.get("emergency_exit", "market") order_type = self.strategy.order_types.get("emergency_exit", "market")
amount = self._safe_exit_amount(trade.pair, trade.amount) amount = self._safe_exit_amount(trade.pair, sub_trade_amt or trade.amount)
time_in_force = self.strategy.order_time_in_force['exit'] time_in_force = self.strategy.order_time_in_force['exit']
if (exit_check.exit_type != ExitType.LIQUIDATION and not strategy_safe_wrapper( if (exit_check.exit_type != ExitType.LIQUIDATION
and not sub_trade_amt
and not strategy_safe_wrapper(
self.strategy.confirm_trade_exit, default_retval=True)( 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,
time_in_force=time_in_force, exit_reason=exit_reason, time_in_force=time_in_force, exit_reason=exit_reason,
@ -1504,7 +1540,7 @@ class FreqtradeBot(LoggingMixin):
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
reason='Auto lock') reason='Auto lock')
self._notify_exit(trade, order_type) self._notify_exit(trade, order_type, sub_trade=bool(sub_trade_amt), order=order_obj)
# In case of market sell orders the order can be closed immediately # In case of market sell orders the order can be closed immediately
if order.get('status', 'unknown') in ('closed', 'expired'): if order.get('status', 'unknown') in ('closed', 'expired'):
self.update_trade_state(trade, trade.open_order_id, order) self.update_trade_state(trade, trade.open_order_id, order)
@ -1512,16 +1548,27 @@ class FreqtradeBot(LoggingMixin):
return True return True
def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False) -> None: def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False,
sub_trade: bool = False, order: Order = None) -> None:
""" """
Sends rpc notification when a sell occurred. Sends rpc notification when a sell occurred.
""" """
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
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, side='exit', is_short=trade.is_short, refresh=False) if not fill else None trade.pair, side='exit', is_short=trade.is_short, refresh=False) if not fill else None
# second condition is for mypy only; order will always be passed during sub trade
if sub_trade and order is not None:
amount = order.safe_filled if fill else order.amount
profit_rate = order.safe_price
profit = trade.calc_profit(rate=profit_rate, amount=amount, open_rate=trade.open_rate)
profit_ratio = trade.calc_profit_ratio(profit_rate, amount, trade.open_rate)
else:
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
profit = trade.calc_profit(rate=profit_rate) + trade.realized_profit
profit_ratio = trade.calc_profit_ratio(profit_rate) profit_ratio = trade.calc_profit_ratio(profit_rate)
amount = trade.amount
gain = "profit" if profit_ratio > 0 else "loss" gain = "profit" if profit_ratio > 0 else "loss"
msg = { msg = {
@ -1535,11 +1582,11 @@ class FreqtradeBot(LoggingMixin):
'gain': gain, 'gain': gain,
'limit': profit_rate, 'limit': profit_rate,
'order_type': order_type, 'order_type': order_type,
'amount': trade.amount, 'amount': amount,
'open_rate': trade.open_rate, 'open_rate': trade.open_rate,
'close_rate': trade.close_rate, 'close_rate': profit_rate,
'current_rate': current_rate, 'current_rate': current_rate,
'profit_amount': profit_trade, 'profit_amount': profit,
'profit_ratio': profit_ratio, 'profit_ratio': profit_ratio,
'buy_tag': trade.enter_tag, 'buy_tag': trade.enter_tag,
'enter_tag': trade.enter_tag, 'enter_tag': trade.enter_tag,
@ -1547,19 +1594,18 @@ class FreqtradeBot(LoggingMixin):
'exit_reason': trade.exit_reason, 'exit_reason': trade.exit_reason,
'open_date': trade.open_date, 'open_date': trade.open_date,
'close_date': trade.close_date or datetime.utcnow(), 'close_date': trade.close_date or datetime.utcnow(),
'stake_amount': trade.stake_amount,
'stake_currency': self.config['stake_currency'], 'stake_currency': self.config['stake_currency'],
'fiat_currency': self.config.get('fiat_display_currency'), 'fiat_currency': self.config.get('fiat_display_currency'),
'sub_trade': sub_trade,
'cumulative_profit': trade.realized_profit,
} }
if 'fiat_display_currency' in self.config:
msg.update({
'fiat_currency': self.config['fiat_display_currency'],
})
# Send the message # Send the message
self.rpc.send_msg(msg) self.rpc.send_msg(msg)
def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str) -> None: def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str,
order: Order, sub_trade: bool = False) -> None:
""" """
Sends rpc notification when a sell cancel occurred. Sends rpc notification when a sell cancel occurred.
""" """
@ -1585,7 +1631,7 @@ class FreqtradeBot(LoggingMixin):
'gain': gain, 'gain': gain,
'limit': profit_rate or 0, 'limit': profit_rate or 0,
'order_type': order_type, 'order_type': order_type,
'amount': trade.amount, 'amount': order.safe_amount_after_fee,
'open_rate': trade.open_rate, 'open_rate': trade.open_rate,
'current_rate': current_rate, 'current_rate': current_rate,
'profit_amount': profit_trade, 'profit_amount': profit_trade,
@ -1599,6 +1645,8 @@ class FreqtradeBot(LoggingMixin):
'stake_currency': self.config['stake_currency'], 'stake_currency': self.config['stake_currency'],
'fiat_currency': self.config.get('fiat_display_currency', None), 'fiat_currency': self.config.get('fiat_display_currency', None),
'reason': reason, 'reason': reason,
'sub_trade': sub_trade,
'stake_amount': trade.stake_amount,
} }
if 'fiat_display_currency' in self.config: if 'fiat_display_currency' in self.config:
@ -1653,14 +1701,18 @@ class FreqtradeBot(LoggingMixin):
self.handle_order_fee(trade, order_obj, order) self.handle_order_fee(trade, order_obj, order)
trade.update_trade(order_obj) trade.update_trade(order_obj)
# TODO: is the below necessary? it's already done in update_trade for filled buys
trade.recalc_trade_from_orders()
Trade.commit() Trade.commit()
if order['status'] in constants.NON_OPEN_EXCHANGE_STATES: if order.get('status') in constants.NON_OPEN_EXCHANGE_STATES:
# If a entry order was closed, force update on stoploss on exchange # If a entry order was closed, force update on stoploss on exchange
if order.get('side') == trade.entry_side: if order.get('side') == trade.entry_side:
trade = self.cancel_stoploss_on_exchange(trade) trade = self.cancel_stoploss_on_exchange(trade)
if not self.edge:
# TODO: should shorting/leverage be supported by Edge,
# then this will need to be fixed.
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
if order.get('side') == trade.entry_side or trade.amount > 0:
# Must also run for partial exits
# TODO: Margin will need to use interest_rate as well. # TODO: Margin will need to use interest_rate as well.
# interest_rate = self.exchange.get_interest_rate() # interest_rate = self.exchange.get_interest_rate()
trade.set_liquidation_price(self.exchange.get_liquidation_price( trade.set_liquidation_price(self.exchange.get_liquidation_price(
@ -1670,23 +1722,29 @@ class FreqtradeBot(LoggingMixin):
open_rate=trade.open_rate, open_rate=trade.open_rate,
is_short=trade.is_short is_short=trade.is_short
)) ))
if not self.edge:
# TODO: should shorting/leverage be supported by Edge,
# then this will need to be fixed.
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
# Updating wallets when order is closed # Updating wallets when order is closed
self.wallets.update() self.wallets.update()
if not trade.is_open: self.order_close_notify(trade, order_obj, stoploss_order, send_msg)
return False
def order_close_notify(
self, trade: Trade, order: Order, stoploss_order: bool, send_msg: bool):
"""send "fill" notifications"""
sub_trade = not isclose(order.safe_amount_after_fee,
trade.amount, abs_tol=constants.MATH_CLOSE_PREC)
if order.ft_order_side == trade.exit_side:
# Exit notification
if send_msg and not stoploss_order and not trade.open_order_id: if send_msg and not stoploss_order and not trade.open_order_id:
self._notify_exit(trade, '', True) self._notify_exit(trade, '', fill=True, sub_trade=sub_trade, order=order)
if not trade.is_open:
self.handle_protections(trade.pair, trade.trade_direction) self.handle_protections(trade.pair, trade.trade_direction)
elif send_msg and not trade.open_order_id and not stoploss_order: elif send_msg and not trade.open_order_id and not stoploss_order:
# Enter fill # Enter fill
self._notify_enter(trade, order, fill=True) self._notify_enter(trade, order, fill=True, sub_trade=sub_trade)
return False
def handle_protections(self, pair: str, side: LongShort) -> None: def handle_protections(self, pair: str, side: LongShort) -> None:
prot_trig = self.protections.stop_per_pair(pair, side=side) prot_trig = self.protections.stop_per_pair(pair, side=side)

59
freqtrade/optimize/backtesting.py Executable file → Normal file
View File

@ -503,16 +503,20 @@ class Backtesting:
def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple
) -> LocalTrade: ) -> LocalTrade:
current_profit = trade.calc_profit_ratio(row[OPEN_IDX]) current_rate = row[OPEN_IDX]
min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, row[OPEN_IDX], -0.1) current_date = row[DATE_IDX].to_pydatetime()
max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, row[OPEN_IDX]) current_profit = trade.calc_profit_ratio(current_rate)
min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, current_rate, -0.1)
max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate)
stake_available = self.wallets.get_available_stake_amount() stake_available = self.wallets.get_available_stake_amount()
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
default_retval=None)( default_retval=None)(
trade=trade, # type: ignore[arg-type] trade=trade, # type: ignore[arg-type]
current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX], current_time=current_date, current_rate=current_rate,
current_profit=current_profit, min_stake=min_stake, current_profit=current_profit, min_stake=min_stake,
max_stake=min(max_stake, stake_available)) max_stake=min(max_stake, stake_available),
current_entry_rate=current_rate, current_exit_rate=current_rate,
current_entry_profit=current_profit, current_exit_profit=current_profit)
# Check if we should increase our position # Check if we should increase our position
if stake_amount is not None and stake_amount > 0.0: if stake_amount is not None and stake_amount > 0.0:
@ -523,6 +527,24 @@ class Backtesting:
self.wallets.update() self.wallets.update()
return pos_trade return pos_trade
if stake_amount is not None and stake_amount < 0.0:
amount = abs(stake_amount) / current_rate
if amount > trade.amount:
# This is currently ineffective as remaining would become < min tradable
amount = trade.amount
remaining = (trade.amount - amount) * current_rate
if remaining < min_stake:
# Remaining stake is too low to be sold.
return trade
pos_trade = self._exit_trade(trade, row, current_rate, amount)
if pos_trade is not None:
order = pos_trade.orders[-1]
if self._get_order_filled(order.price, row):
order.close_bt_order(current_date, trade)
trade.recalc_trade_from_orders()
self.wallets.update()
return pos_trade
return trade return trade
def _get_order_filled(self, rate: float, row: Tuple) -> bool: def _get_order_filled(self, rate: float, row: Tuple) -> bool:
@ -602,7 +624,7 @@ class Backtesting:
self.strategy.confirm_trade_exit, default_retval=True)( self.strategy.confirm_trade_exit, default_retval=True)(
pair=trade.pair, pair=trade.pair,
trade=trade, # type: ignore[arg-type] trade=trade, # type: ignore[arg-type]
order_type='limit', order_type=order_type,
amount=trade.amount, amount=trade.amount,
rate=close_rate, rate=close_rate,
time_in_force=time_in_force, time_in_force=time_in_force,
@ -613,7 +635,15 @@ class Backtesting:
trade.exit_reason = exit_reason trade.exit_reason = exit_reason
return self._exit_trade(trade, row, close_rate, trade.amount)
return None
def _exit_trade(self, trade: LocalTrade, sell_row: Tuple,
close_rate: float, amount: float = None) -> Optional[LocalTrade]:
self.order_id_counter += 1 self.order_id_counter += 1
exit_candle_time = sell_row[DATE_IDX].to_pydatetime()
order_type = self.strategy.order_types['exit']
amount = amount or trade.amount
order = Order( order = Order(
id=self.order_id_counter, id=self.order_id_counter,
ft_trade_id=trade.id, ft_trade_id=trade.id,
@ -629,16 +659,14 @@ class Backtesting:
status="open", status="open",
price=close_rate, price=close_rate,
average=close_rate, average=close_rate,
amount=trade.amount, amount=amount,
filled=0, filled=0,
remaining=trade.amount, remaining=amount,
cost=trade.amount * close_rate, cost=amount * close_rate,
) )
trade.orders.append(order) trade.orders.append(order)
return trade return trade
return None
def _get_exit_trade_entry(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]: def _get_exit_trade_entry(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]:
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime() exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
@ -865,6 +893,8 @@ class Backtesting:
# Ignore trade if entry-order did not fill yet # Ignore trade if entry-order did not fill yet
continue continue
exit_row = data[pair][-1] exit_row = data[pair][-1]
self._exit_trade(trade, exit_row, exit_row[OPEN_IDX], trade.amount)
trade.orders[-1].close_bt_order(exit_row[DATE_IDX].to_pydatetime(), trade)
trade.close_date = exit_row[DATE_IDX].to_pydatetime() trade.close_date = exit_row[DATE_IDX].to_pydatetime()
trade.exit_reason = ExitType.FORCE_EXIT.value trade.exit_reason = ExitType.FORCE_EXIT.value
@ -1006,7 +1036,7 @@ class Backtesting:
return None return None
return row return row
def backtest(self, processed: Dict, def backtest(self, processed: Dict, # noqa: max-complexity: 13
start_date: datetime, end_date: datetime, start_date: datetime, end_date: datetime,
max_open_trades: int = 0, position_stacking: bool = False, max_open_trades: int = 0, position_stacking: bool = False,
enable_protections: bool = False) -> Dict[str, Any]: enable_protections: bool = False) -> Dict[str, Any]:
@ -1108,6 +1138,11 @@ class Backtesting:
if order and self._get_order_filled(order.price, row): if order and self._get_order_filled(order.price, row):
order.close_bt_order(current_time, trade) order.close_bt_order(current_time, trade)
trade.open_order_id = None trade.open_order_id = None
sub_trade = order.safe_amount_after_fee != trade.amount
if sub_trade:
order.close_bt_order(current_time, trade)
trade.recalc_trade_from_orders()
else:
trade.close_date = current_time trade.close_date = current_time
trade.close(order.price, show_msg=False) trade.close(order.price, show_msg=False)

View File

@ -95,6 +95,7 @@ def migrate_trades_and_orders_table(
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'))
strategy = get_column_def(cols, 'strategy', 'null') strategy = get_column_def(cols, 'strategy', 'null')
enter_tag = get_column_def(cols, 'buy_tag', get_column_def(cols, 'enter_tag', 'null')) enter_tag = get_column_def(cols, 'buy_tag', get_column_def(cols, 'enter_tag', 'null'))
realized_profit = get_column_def(cols, 'realized_profit', '0.0')
trading_mode = get_column_def(cols, 'trading_mode', 'null') trading_mode = get_column_def(cols, 'trading_mode', 'null')
@ -155,7 +156,7 @@ def migrate_trades_and_orders_table(
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,
interest_rate, funding_fees interest_rate, funding_fees, realized_profit
) )
select id, lower(exchange), pair, {base_currency} base_currency, select id, lower(exchange), pair, {base_currency} base_currency,
{stake_currency} stake_currency, {stake_currency} stake_currency,
@ -181,7 +182,7 @@ def migrate_trades_and_orders_table(
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs, {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs,
{trading_mode} trading_mode, {leverage} leverage, {liquidation_price} liquidation_price, {trading_mode} trading_mode, {leverage} leverage, {liquidation_price} liquidation_price,
{is_short} is_short, {interest_rate} interest_rate, {is_short} is_short, {interest_rate} interest_rate,
{funding_fees} funding_fees {funding_fees} funding_fees, {realized_profit} realized_profit
from {trade_back_name} from {trade_back_name}
""")) """))
@ -297,8 +298,9 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
# Check if migration necessary # Check if migration necessary
# Migrates both trades and orders table! # Migrates both trades and orders table!
if not has_column(cols_orders, 'stop_price'): # if ('orders' not in previous_tables
# if not has_column(cols_trades, 'base_currency'): # or not has_column(cols_orders, 'stop_price')):
if not has_column(cols_trades, 'realized_profit'):
logger.info(f"Running database migration for trades - " logger.info(f"Running database migration for trades - "
f"backup: {table_back_name}, {order_table_bak_name}") f"backup: {table_back_name}, {order_table_bak_name}")
migrate_trades_and_orders_table( migrate_trades_and_orders_table(

View File

@ -4,13 +4,15 @@ This module contains the class to persist trades into SQLite
import logging import logging
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from decimal import Decimal from decimal import Decimal
from math import isclose
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String, from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String,
UniqueConstraint, desc, func) UniqueConstraint, desc, func)
from sqlalchemy.orm import Query, lazyload, relationship from sqlalchemy.orm import Query, lazyload, relationship
from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES, BuySell, LongShort from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES,
BuySell, LongShort)
from freqtrade.enums import ExitType, TradingMode from freqtrade.enums import ExitType, TradingMode
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.leverage import interest from freqtrade.leverage import interest
@ -176,10 +178,9 @@ class Order(_DECL_BASE):
self.remaining = 0 self.remaining = 0
self.status = 'closed' self.status = 'closed'
self.ft_is_open = False self.ft_is_open = False
if (self.ft_order_side == trade.entry_side if (self.ft_order_side == trade.entry_side):
and len(trade.select_filled_orders(trade.entry_side)) == 1):
trade.open_rate = self.price trade.open_rate = self.price
trade.recalc_open_trade_value() trade.recalc_trade_from_orders()
trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct, refresh=True) trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct, refresh=True)
@staticmethod @staticmethod
@ -237,6 +238,7 @@ class LocalTrade():
trades: List['LocalTrade'] = [] trades: List['LocalTrade'] = []
trades_open: List['LocalTrade'] = [] trades_open: List['LocalTrade'] = []
total_profit: float = 0 total_profit: float = 0
realized_profit: float = 0
id: int = 0 id: int = 0
@ -447,6 +449,7 @@ class LocalTrade():
if self.close_date else None), if self.close_date else None),
'close_timestamp': int(self.close_date.replace( 'close_timestamp': int(self.close_date.replace(
tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None, tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None,
'realized_profit': self.realized_profit or 0.0,
'close_rate': self.close_rate, 'close_rate': self.close_rate,
'close_rate_requested': self.close_rate_requested, 'close_rate_requested': self.close_rate_requested,
'close_profit': self.close_profit, # Deprecated 'close_profit': self.close_profit, # Deprecated
@ -596,14 +599,28 @@ class LocalTrade():
if self.is_open: if self.is_open:
payment = "SELL" if self.is_short else "BUY" payment = "SELL" if self.is_short else "BUY"
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}.')
# condition to avoid reset value when updating fees
if self.open_order_id == order.order_id:
self.open_order_id = None self.open_order_id = None
else:
logger.warning(
f'Got different open_order_id {self.open_order_id} != {order.order_id}')
self.recalc_trade_from_orders() self.recalc_trade_from_orders()
elif order.ft_order_side == self.exit_side: elif order.ft_order_side == self.exit_side:
if self.is_open: if self.is_open:
payment = "BUY" if self.is_short else "SELL" payment = "BUY" if self.is_short else "SELL"
# * On margin shorts, you buy a little bit more than the amount (amount + interest) # * On margin shorts, you buy a little bit more than the amount (amount + interest)
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}.')
# condition to avoid reset value when updating fees
if self.open_order_id == order.order_id:
self.open_order_id = None
else:
logger.warning(
f'Got different open_order_id {self.open_order_id} != {order.order_id}')
if isclose(order.safe_amount_after_fee, self.amount, abs_tol=MATH_CLOSE_PREC):
self.close(order.safe_price) self.close(order.safe_price)
else:
self.recalc_trade_from_orders()
elif order.ft_order_side == 'stoploss': elif order.ft_order_side == 'stoploss':
self.stoploss_order_id = None self.stoploss_order_id = None
self.close_rate_requested = self.stop_loss self.close_rate_requested = self.stop_loss
@ -622,11 +639,11 @@ class LocalTrade():
""" """
self.close_rate = rate self.close_rate = rate
self.close_date = self.close_date or datetime.utcnow() self.close_date = self.close_date or datetime.utcnow()
self.close_profit = self.calc_profit_ratio(rate) self.close_profit_abs = self.calc_profit(rate) + self.realized_profit
self.close_profit_abs = self.calc_profit(rate)
self.is_open = False self.is_open = False
self.exit_order_status = 'closed' self.exit_order_status = 'closed'
self.open_order_id = None self.open_order_id = None
self.recalc_trade_from_orders(is_closing=True)
if show_msg: if show_msg:
logger.info( logger.info(
'Marking %s as closed as the trade is fulfilled and found no open orders for it.', 'Marking %s as closed as the trade is fulfilled and found no open orders for it.',
@ -672,12 +689,12 @@ class LocalTrade():
""" """
return len([o for o in self.orders if o.ft_order_side == self.exit_side]) return len([o for o in self.orders if o.ft_order_side == self.exit_side])
def _calc_open_trade_value(self) -> float: def _calc_open_trade_value(self, amount: float, open_rate: float) -> float:
""" """
Calculate the open_rate including open_fee. Calculate the open_rate including open_fee.
:return: Price in of the open trade incl. Fees :return: Price in of the open trade incl. Fees
""" """
open_trade = Decimal(self.amount) * Decimal(self.open_rate) open_trade = Decimal(amount) * Decimal(open_rate)
fees = open_trade * Decimal(self.fee_open) fees = open_trade * Decimal(self.fee_open)
if self.is_short: if self.is_short:
return float(open_trade - fees) return float(open_trade - fees)
@ -689,7 +706,7 @@ class LocalTrade():
Recalculate open_trade_value. Recalculate open_trade_value.
Must be called whenever open_rate, fee_open is changed. Must be called whenever open_rate, fee_open is changed.
""" """
self.open_trade_value = self._calc_open_trade_value() self.open_trade_value = self._calc_open_trade_value(self.amount, self.open_rate)
def calculate_interest(self) -> Decimal: def calculate_interest(self) -> Decimal:
""" """
@ -721,7 +738,7 @@ class LocalTrade():
else: else:
return close_trade - fees return close_trade - fees
def calc_close_trade_value(self, rate: float) -> float: def calc_close_trade_value(self, rate: float, amount: float = None) -> float:
""" """
Calculate the Trade's close value including fees Calculate the Trade's close value including fees
:param rate: rate to compare with. :param rate: rate to compare with.
@ -730,96 +747,143 @@ class LocalTrade():
if rate is None and not self.close_rate: if rate is None and not self.close_rate:
return 0.0 return 0.0
amount = Decimal(self.amount) amount1 = Decimal(amount or self.amount)
trading_mode = self.trading_mode or TradingMode.SPOT trading_mode = self.trading_mode or TradingMode.SPOT
if trading_mode == TradingMode.SPOT: if trading_mode == TradingMode.SPOT:
return float(self._calc_base_close(amount, rate, self.fee_close)) return float(self._calc_base_close(amount1, rate, self.fee_close))
elif (trading_mode == TradingMode.MARGIN): elif (trading_mode == TradingMode.MARGIN):
total_interest = self.calculate_interest() total_interest = self.calculate_interest()
if self.is_short: if self.is_short:
amount = amount + total_interest amount1 = amount1 + total_interest
return float(self._calc_base_close(amount, rate, self.fee_close)) return float(self._calc_base_close(amount1, rate, self.fee_close))
else: else:
# Currency already owned for longs, no need to purchase # Currency already owned for longs, no need to purchase
return float(self._calc_base_close(amount, rate, self.fee_close) - total_interest) return float(self._calc_base_close(amount1, rate, self.fee_close) - total_interest)
elif (trading_mode == TradingMode.FUTURES): elif (trading_mode == TradingMode.FUTURES):
funding_fees = self.funding_fees or 0.0 funding_fees = self.funding_fees or 0.0
# Positive funding_fees -> Trade has gained from fees. # Positive funding_fees -> Trade has gained from fees.
# Negative funding_fees -> Trade had to pay the fees. # Negative funding_fees -> Trade had to pay the fees.
if self.is_short: if self.is_short:
return float(self._calc_base_close(amount, rate, self.fee_close)) - funding_fees return float(self._calc_base_close(amount1, rate, self.fee_close)) - funding_fees
else: else:
return float(self._calc_base_close(amount, rate, self.fee_close)) + funding_fees return float(self._calc_base_close(amount1, rate, self.fee_close)) + funding_fees
else: else:
raise OperationalException( raise OperationalException(
f"{self.trading_mode.value} trading is not yet available using freqtrade") f"{self.trading_mode.value} trading is not yet available using freqtrade")
def calc_profit(self, rate: float) -> float: def calc_profit(self, rate: float, amount: float = None, open_rate: float = None) -> float:
""" """
Calculate the absolute profit in stake currency between Close and Open trade Calculate the absolute profit in stake currency between Close and Open trade
:param rate: close rate to compare with. :param rate: close rate to compare with.
:param amount: Amount to use for the calculation. Falls back to trade.amount if not set.
:param open_rate: open_rate to use. Defaults to self.open_rate if not provided.
:return: profit in stake currency as float :return: profit in stake currency as float
""" """
close_trade_value = self.calc_close_trade_value(rate) close_trade_value = self.calc_close_trade_value(rate, amount)
if amount is None or open_rate is None:
open_trade_value = self.open_trade_value
else:
open_trade_value = self._calc_open_trade_value(amount, open_rate)
if self.is_short: if self.is_short:
profit = self.open_trade_value - close_trade_value profit = open_trade_value - close_trade_value
else: else:
profit = close_trade_value - self.open_trade_value profit = close_trade_value - open_trade_value
return float(f"{profit:.8f}") return float(f"{profit:.8f}")
def calc_profit_ratio(self, rate: float) -> float: def calc_profit_ratio(
self, rate: float, amount: float = None, open_rate: float = None) -> float:
""" """
Calculates the profit as ratio (including fee). Calculates the profit as ratio (including fee).
:param rate: rate to compare with. :param rate: rate to compare with.
:param amount: Amount to use for the calculation. Falls back to trade.amount if not set.
:param open_rate: open_rate to use. Defaults to self.open_rate if not provided.
:return: profit ratio as float :return: profit ratio as float
""" """
close_trade_value = self.calc_close_trade_value(rate) close_trade_value = self.calc_close_trade_value(rate, amount)
if amount is None or open_rate is None:
open_trade_value = self.open_trade_value
else:
open_trade_value = self._calc_open_trade_value(amount, open_rate)
short_close_zero = (self.is_short and close_trade_value == 0.0) short_close_zero = (self.is_short and close_trade_value == 0.0)
long_close_zero = (not self.is_short and self.open_trade_value == 0.0) long_close_zero = (not self.is_short and open_trade_value == 0.0)
leverage = self.leverage or 1.0 leverage = self.leverage or 1.0
if (short_close_zero or long_close_zero): if (short_close_zero or long_close_zero):
return 0.0 return 0.0
else: else:
if self.is_short: if self.is_short:
profit_ratio = (1 - (close_trade_value / self.open_trade_value)) * leverage profit_ratio = (1 - (close_trade_value / open_trade_value)) * leverage
else: else:
profit_ratio = ((close_trade_value / self.open_trade_value) - 1) * leverage profit_ratio = ((close_trade_value / open_trade_value) - 1) * leverage
return float(f"{profit_ratio:.8f}") return float(f"{profit_ratio:.8f}")
def recalc_trade_from_orders(self): def recalc_trade_from_orders(self, is_closing: bool = False):
current_amount = 0.0
current_stake = 0.0
total_stake = 0.0 # Total stake after all buy orders (does not subtract!)
avg_price = 0.0
close_profit = 0.0
close_profit_abs = 0.0
total_amount = 0.0
total_stake = 0.0
for o in self.orders: for o in self.orders:
if (o.ft_is_open or if o.ft_is_open or not o.filled:
(o.ft_order_side != self.entry_side) or
(o.status not in NON_OPEN_EXCHANGE_STATES)):
continue continue
tmp_amount = o.safe_amount_after_fee tmp_amount = o.safe_amount_after_fee
tmp_price = o.average or o.price tmp_price = o.safe_price
if tmp_amount > 0.0 and tmp_price is not None:
total_amount += tmp_amount
total_stake += tmp_price * tmp_amount
if total_amount > 0: is_exit = o.ft_order_side != self.entry_side
side = -1 if is_exit else 1
if tmp_amount > 0.0 and tmp_price is not None:
current_amount += tmp_amount * side
price = avg_price if is_exit else tmp_price
current_stake += price * tmp_amount * side
if current_amount > 0:
avg_price = current_stake / current_amount
if is_exit:
# Process partial exits
exit_rate = o.safe_price
exit_amount = o.safe_amount_after_fee
profit = self.calc_profit(rate=exit_rate, amount=exit_amount, open_rate=avg_price)
close_profit_abs += profit
close_profit = self.calc_profit_ratio(
exit_rate, amount=exit_amount, open_rate=avg_price)
if current_amount <= 0:
profit = close_profit_abs
else:
total_stake = total_stake + self._calc_open_trade_value(tmp_amount, price)
if close_profit:
self.close_profit = close_profit
self.realized_profit = close_profit_abs
self.close_profit_abs = profit
if current_amount > 0:
# Trade is still open
# Leverage not updated, as we don't allow changing leverage through DCA at the moment. # Leverage not updated, as we don't allow changing leverage through DCA at the moment.
self.open_rate = total_stake / total_amount self.open_rate = current_stake / current_amount
self.stake_amount = total_stake / (self.leverage or 1.0) self.stake_amount = current_stake / (self.leverage or 1.0)
self.amount = total_amount self.amount = current_amount
self.fee_open_cost = self.fee_open * total_stake self.fee_open_cost = self.fee_open * current_stake
self.recalc_open_trade_value() self.recalc_open_trade_value()
if self.stop_loss_pct is not None and self.open_rate is not None: if self.stop_loss_pct is not None and self.open_rate is not None:
self.adjust_stop_loss(self.open_rate, self.stop_loss_pct) self.adjust_stop_loss(self.open_rate, self.stop_loss_pct)
elif is_closing and total_stake > 0:
# Close profit abs / maximum owned
# Fees are considered as they are part of close_profit_abs
self.close_profit = (close_profit_abs / total_stake) * self.leverage
def select_order_by_order_id(self, order_id: str) -> Optional[Order]: def select_order_by_order_id(self, order_id: str) -> Optional[Order]:
""" """
@ -841,7 +905,7 @@ class LocalTrade():
""" """
orders = self.orders orders = self.orders
if order_side: if order_side:
orders = [o for o in self.orders if o.ft_order_side == order_side] orders = [o for o in orders if o.ft_order_side == order_side]
if is_open is not None: if is_open is not None:
orders = [o for o in orders if o.ft_is_open == is_open] orders = [o for o in orders if o.ft_is_open == is_open]
if len(orders) > 0: if len(orders) > 0:
@ -856,9 +920,9 @@ class LocalTrade():
:return: array of Order objects :return: array of Order objects
""" """
return [o for o in self.orders if ((o.ft_order_side == order_side) or (order_side is None)) return [o for o in self.orders if ((o.ft_order_side == order_side) or (order_side is None))
and o.ft_is_open is False and and o.ft_is_open is False
(o.filled or 0) > 0 and and o.filled
o.status in NON_OPEN_EXCHANGE_STATES] and o.status in NON_OPEN_EXCHANGE_STATES]
def select_filled_or_open_orders(self) -> List['Order']: def select_filled_or_open_orders(self) -> List['Order']:
""" """
@ -1023,6 +1087,7 @@ class Trade(_DECL_BASE, LocalTrade):
open_trade_value = Column(Float) open_trade_value = Column(Float)
close_rate: Optional[float] = Column(Float) close_rate: Optional[float] = Column(Float)
close_rate_requested = Column(Float) close_rate_requested = Column(Float)
realized_profit = Column(Float, default=0.0)
close_profit = Column(Float) close_profit = Column(Float)
close_profit_abs = Column(Float) close_profit_abs = Column(Float)
stake_amount = Column(Float, nullable=False) stake_amount = Column(Float, nullable=False)
@ -1068,6 +1133,7 @@ class Trade(_DECL_BASE, LocalTrade):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.realized_profit = 0
self.recalc_open_trade_value() self.recalc_open_trade_value()
def delete(self) -> None: def delete(self) -> None:

View File

@ -201,7 +201,7 @@ class RPC:
trade_dict = trade.to_json() trade_dict = trade.to_json()
trade_dict.update(dict( trade_dict.update(dict(
close_profit=trade.close_profit if trade.close_profit is not None else None, close_profit=trade.close_profit if not trade.is_open else None,
current_rate=current_rate, current_rate=current_rate,
current_profit=current_profit, # Deprecated current_profit=current_profit, # Deprecated
current_profit_pct=round(current_profit * 100, 2), # Deprecated current_profit_pct=round(current_profit * 100, 2), # Deprecated

View File

@ -315,20 +315,36 @@ class Telegram(RPCHandler):
msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount( msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount(
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
msg['profit_extra'] = ( msg['profit_extra'] = (
f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}" f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']}")
f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']})")
else: else:
msg['profit_extra'] = '' msg['profit_extra'] = ''
msg['profit_extra'] = (
f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}"
f"{msg['profit_extra']})")
is_fill = msg['type'] == RPCMessageType.EXIT_FILL is_fill = msg['type'] == RPCMessageType.EXIT_FILL
is_sub_trade = msg.get('sub_trade')
is_sub_profit = msg['profit_amount'] != msg.get('cumulative_profit')
profit_prefix = ('Sub ' if is_sub_profit
else 'Cumulative ') if is_sub_trade else ''
cp_extra = ''
if is_sub_profit and is_sub_trade:
if self._rpc._fiat_converter:
cp_fiat = self._rpc._fiat_converter.convert_amount(
msg['cumulative_profit'], msg['stake_currency'], msg['fiat_currency'])
cp_extra = f" / {cp_fiat:.3f} {msg['fiat_currency']}"
else:
cp_extra = ''
cp_extra = f"*Cumulative Profit:* (`{msg['cumulative_profit']:.8f} " \
f"{msg['stake_currency']}{cp_extra}`)\n"
message = ( message = (
f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* " f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* "
f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n" f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n"
f"{self._add_analyzed_candle(msg['pair'])}" f"{self._add_analyzed_candle(msg['pair'])}"
f"*{'Profit' if is_fill else 'Unrealized Profit'}:* " f"*{f'{profit_prefix}Profit' if is_fill else f'Unrealized {profit_prefix}Profit'}:* "
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n" f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
f"{cp_extra}"
f"*Enter Tag:* `{msg['enter_tag']}`\n" f"*Enter Tag:* `{msg['enter_tag']}`\n"
f"*Exit Reason:* `{msg['exit_reason']}`\n" f"*Exit Reason:* `{msg['exit_reason']}`\n"
f"*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`\n"
f"*Direction:* `{msg['direction']}`\n" f"*Direction:* `{msg['direction']}`\n"
f"{msg['leverage_text']}" f"{msg['leverage_text']}"
f"*Amount:* `{msg['amount']:.8f}`\n" f"*Amount:* `{msg['amount']:.8f}`\n"
@ -336,11 +352,25 @@ class Telegram(RPCHandler):
) )
if msg['type'] == RPCMessageType.EXIT: if msg['type'] == RPCMessageType.EXIT:
message += (f"*Current Rate:* `{msg['current_rate']:.8f}`\n" message += (f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
f"*Close Rate:* `{msg['limit']:.8f}`") f"*Exit Rate:* `{msg['limit']:.8f}`")
elif msg['type'] == RPCMessageType.EXIT_FILL: elif msg['type'] == RPCMessageType.EXIT_FILL:
message += f"*Close Rate:* `{msg['close_rate']:.8f}`" message += f"*Exit Rate:* `{msg['close_rate']:.8f}`"
if msg.get('sub_trade'):
if self._rpc._fiat_converter:
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
else:
msg['stake_amount_fiat'] = 0
rem = round_coin_value(msg['stake_amount'], msg['stake_currency'])
message += f"\n*Remaining:* `({rem}"
if msg.get('fiat_currency', None):
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
message += ")`"
else:
message += f"\n*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`"
return message return message
def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str: def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str:
@ -353,7 +383,8 @@ class Telegram(RPCHandler):
elif msg_type in (RPCMessageType.ENTRY_CANCEL, RPCMessageType.EXIT_CANCEL): elif msg_type in (RPCMessageType.ENTRY_CANCEL, RPCMessageType.EXIT_CANCEL):
msg['message_side'] = 'enter' if msg_type in [RPCMessageType.ENTRY_CANCEL] else 'exit' msg['message_side'] = 'enter' if msg_type in [RPCMessageType.ENTRY_CANCEL] else 'exit'
message = (f"\N{WARNING SIGN} *{self._exchange_from_msg(msg)}:* " message = (f"\N{WARNING SIGN} *{self._exchange_from_msg(msg)}:* "
f"Cancelling {msg['message_side']} Order for {msg['pair']} " f"Cancelling {'partial ' if msg.get('sub_trade') else ''}"
f"{msg['message_side']} Order for {msg['pair']} "
f"(#{msg['trade_id']}). Reason: {msg['reason']}.") f"(#{msg['trade_id']}). Reason: {msg['reason']}.")
elif msg_type == RPCMessageType.PROTECTION_TRIGGER: elif msg_type == RPCMessageType.PROTECTION_TRIGGER:
@ -424,7 +455,7 @@ class Telegram(RPCHandler):
else: else:
return "\N{CROSS MARK}" return "\N{CROSS MARK}"
def _prepare_entry_details(self, filled_orders: List, quote_currency: str, is_open: bool): def _prepare_order_details(self, filled_orders: List, quote_currency: str, is_open: bool):
""" """
Prepare details of trade with entry adjustment enabled Prepare details of trade with entry adjustment enabled
""" """
@ -433,44 +464,51 @@ class Telegram(RPCHandler):
first_avg = filled_orders[0]["safe_price"] first_avg = filled_orders[0]["safe_price"]
for x, order in enumerate(filled_orders): for x, order in enumerate(filled_orders):
if not order['ft_is_entry'] or order['is_open'] is True: if order['is_open'] is True:
continue continue
wording = 'Entry' if order['ft_is_entry'] else 'Exit'
cur_entry_datetime = arrow.get(order["order_filled_date"]) cur_entry_datetime = arrow.get(order["order_filled_date"])
cur_entry_amount = order["amount"] cur_entry_amount = order["filled"] or order["amount"]
cur_entry_average = order["safe_price"] cur_entry_average = order["safe_price"]
lines.append(" ") lines.append(" ")
if x == 0: if x == 0:
lines.append(f"*Entry #{x+1}:*") lines.append(f"*{wording} #{x+1}:*")
lines.append( lines.append(
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})") f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
lines.append(f"*Average Entry Price:* {cur_entry_average}") lines.append(f"*Average Price:* {cur_entry_average}")
else: else:
sumA = 0 sumA = 0
sumB = 0 sumB = 0
for y in range(x): for y in range(x):
sumA += (filled_orders[y]["amount"] * filled_orders[y]["safe_price"]) amount = filled_orders[y]["filled"] or filled_orders[y]["amount"]
sumB += filled_orders[y]["amount"] sumA += amount * filled_orders[y]["safe_price"]
sumB += amount
prev_avg_price = sumA / sumB prev_avg_price = sumA / sumB
# TODO: This calculation ignores fees.
price_to_1st_entry = ((cur_entry_average - first_avg) / first_avg) price_to_1st_entry = ((cur_entry_average - first_avg) / first_avg)
minus_on_entry = 0 minus_on_entry = 0
if prev_avg_price: if prev_avg_price:
minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price
dur_entry = cur_entry_datetime - arrow.get( lines.append(f"*{wording} #{x+1}:* at {minus_on_entry:.2%} avg profit")
filled_orders[x - 1]["order_filled_date"])
days = dur_entry.days
hours, remainder = divmod(dur_entry.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
lines.append(f"*Entry #{x+1}:* at {minus_on_entry:.2%} avg profit")
if is_open: if is_open:
lines.append("({})".format(cur_entry_datetime lines.append("({})".format(cur_entry_datetime
.humanize(granularity=["day", "hour", "minute"]))) .humanize(granularity=["day", "hour", "minute"])))
lines.append( lines.append(
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})") f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
lines.append(f"*Average Entry Price:* {cur_entry_average} " lines.append(f"*Average {wording} Price:* {cur_entry_average} "
f"({price_to_1st_entry:.2%} from 1st entry rate)") f"({price_to_1st_entry:.2%} from 1st entry rate)")
lines.append(f"*Order filled at:* {order['order_filled_date']}") lines.append(f"*Order filled:* {order['order_filled_date']}")
lines.append(f"({days}d {hours}h {minutes}m {seconds}s from previous entry)")
# TODO: is this really useful?
# dur_entry = cur_entry_datetime - arrow.get(
# filled_orders[x - 1]["order_filled_date"])
# days = dur_entry.days
# hours, remainder = divmod(dur_entry.seconds, 3600)
# minutes, seconds = divmod(remainder, 60)
# lines.append(
# f"({days}d {hours}h {minutes}m {seconds}s from previous {wording.lower()})")
return lines return lines
@authorized_only @authorized_only
@ -486,7 +524,14 @@ class Telegram(RPCHandler):
if context.args and 'table' in context.args: if context.args and 'table' in context.args:
self._status_table(update, context) self._status_table(update, context)
return return
else:
self._status_msg(update, context)
def _status_msg(self, update: Update, context: CallbackContext) -> None:
"""
handler for `/status` and `/status <id>`.
"""
try: try:
# Check if there's at least one numerical ID provided. # Check if there's at least one numerical ID provided.
@ -529,6 +574,8 @@ class Telegram(RPCHandler):
]) ])
if r['is_open']: if r['is_open']:
if r.get('realized_profit'):
lines.append("*Realized Profit:* `{realized_profit:.8f}`")
if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] if (r['stop_loss_abs'] != r['initial_stop_loss_abs']
and r['initial_stop_loss_ratio'] is not None): and r['initial_stop_loss_ratio'] is not None):
# Adding initial stoploss only if it is different from stoploss # Adding initial stoploss only if it is different from stoploss
@ -546,7 +593,7 @@ class Telegram(RPCHandler):
else: else:
lines.append("*Open Order:* `{open_order}`") lines.append("*Open Order:* `{open_order}`")
lines_detail = self._prepare_entry_details( lines_detail = self._prepare_order_details(
r['orders'], r['quote_currency'], r['is_open']) r['orders'], r['quote_currency'], r['is_open'])
lines.extend(lines_detail if lines_detail else "") lines.extend(lines_detail if lines_detail else "")

View File

@ -463,10 +463,13 @@ class IStrategy(ABC, HyperStrategyMixin):
def adjust_trade_position(self, trade: Trade, current_time: datetime, def adjust_trade_position(self, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, current_rate: float, current_profit: float,
min_stake: Optional[float], max_stake: float, min_stake: Optional[float], max_stake: float,
current_entry_rate: float, current_exit_rate: float,
current_entry_profit: float, current_exit_profit: float,
**kwargs) -> Optional[float]: **kwargs) -> Optional[float]:
""" """
Custom trade adjustment logic, returning the stake amount that a trade should be increased. Custom trade adjustment logic, returning the stake amount that a trade should be
This means extra buy orders with additional fees. increased or decreased.
This means extra buy or sell orders with additional fees.
Only called when `position_adjustment_enable` is set to True. Only called when `position_adjustment_enable` is set to True.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
@ -477,10 +480,16 @@ class IStrategy(ABC, HyperStrategyMixin):
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param current_rate: Current buy rate. :param current_rate: Current buy rate.
:param current_profit: Current profit (as ratio), calculated based on current_rate. :param current_profit: Current profit (as ratio), calculated based on current_rate.
:param min_stake: Minimal stake size allowed by exchange. :param min_stake: Minimal stake size allowed by exchange (for both entries and exits)
:param max_stake: Balance available for trading. :param max_stake: Maximum stake allowed (either through balance, or by exchange limits).
:param current_entry_rate: Current rate using entry pricing.
:param current_exit_rate: Current rate using exit pricing.
:param current_entry_profit: Current profit using entry pricing.
:param current_exit_profit: Current profit using exit pricing.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: Stake amount to adjust your trade :return float: Stake amount to adjust your trade,
Positive values to increase position, Negative values to decrease position.
Return None for no action.
""" """
return None return None

View File

@ -247,12 +247,16 @@ def check_exit_timeout(self, pair: str, trade: 'Trade', order: 'Order',
""" """
return False return False
def adjust_trade_position(self, trade: 'Trade', current_time: 'datetime', def adjust_trade_position(self, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, min_stake: Optional[float], current_rate: float, current_profit: float,
max_stake: float, **kwargs) -> 'Optional[float]': min_stake: Optional[float], max_stake: float,
current_entry_rate: float, current_exit_rate: float,
current_entry_profit: float, current_exit_profit: float,
**kwargs) -> Optional[float]:
""" """
Custom trade adjustment logic, returning the stake amount that a trade should be increased. Custom trade adjustment logic, returning the stake amount that a trade should be
This means extra buy orders with additional fees. increased or decreased.
This means extra buy or sell orders with additional fees.
Only called when `position_adjustment_enable` is set to True. Only called when `position_adjustment_enable` is set to True.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
@ -263,10 +267,16 @@ def adjust_trade_position(self, trade: 'Trade', current_time: 'datetime',
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param current_rate: Current buy rate. :param current_rate: Current buy rate.
:param current_profit: Current profit (as ratio), calculated based on current_rate. :param current_profit: Current profit (as ratio), calculated based on current_rate.
:param min_stake: Minimal stake size allowed by exchange. :param min_stake: Minimal stake size allowed by exchange (for both entries and exits)
:param max_stake: Balance available for trading. :param max_stake: Maximum stake allowed (either through balance, or by exchange limits).
:param current_entry_rate: Current rate using entry pricing.
:param current_exit_rate: Current rate using exit pricing.
:param current_entry_profit: Current profit using entry pricing.
:param current_exit_profit: Current profit using exit pricing.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: Stake amount to adjust your trade :return float: Stake amount to adjust your trade,
Positive values to increase position, Negative values to decrease position.
Return None for no action.
""" """
return None return None

View File

@ -1627,8 +1627,8 @@ def limit_buy_order_open():
'timestamp': arrow.utcnow().int_timestamp * 1000, 'timestamp': arrow.utcnow().int_timestamp * 1000,
'datetime': arrow.utcnow().isoformat(), 'datetime': arrow.utcnow().isoformat(),
'price': 0.00001099, 'price': 0.00001099,
'average': 0.00001099,
'amount': 90.99181073, 'amount': 90.99181073,
'average': None,
'filled': 0.0, 'filled': 0.0,
'cost': 0.0009999, 'cost': 0.0009999,
'remaining': 90.99181073, 'remaining': 90.99181073,
@ -2817,6 +2817,7 @@ def limit_buy_order_usdt_open():
'datetime': arrow.utcnow().isoformat(), 'datetime': arrow.utcnow().isoformat(),
'timestamp': arrow.utcnow().int_timestamp * 1000, 'timestamp': arrow.utcnow().int_timestamp * 1000,
'price': 2.00, 'price': 2.00,
'average': 2.00,
'amount': 30.0, 'amount': 30.0,
'filled': 0.0, 'filled': 0.0,
'cost': 60.0, 'cost': 60.0,

View File

@ -27,6 +27,57 @@ from tests.conftest import get_mock_coro, get_patched_exchange, log_has, log_has
# Make sure to always keep one exchange here which is NOT subclassed!! # Make sure to always keep one exchange here which is NOT subclassed!!
EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx', 'gateio'] EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx', 'gateio']
get_entry_rate_data = [
('other', 20, 19, 10, 0.0, 20), # Full ask side
('ask', 20, 19, 10, 0.0, 20), # Full ask side
('ask', 20, 19, 10, 1.0, 10), # Full last side
('ask', 20, 19, 10, 0.5, 15), # Between ask and last
('ask', 20, 19, 10, 0.7, 13), # Between ask and last
('ask', 20, 19, 10, 0.3, 17), # Between ask and last
('ask', 5, 6, 10, 1.0, 5), # last bigger than ask
('ask', 5, 6, 10, 0.5, 5), # last bigger than ask
('ask', 20, 19, 10, None, 20), # price_last_balance missing
('ask', 10, 20, None, 0.5, 10), # last not available - uses ask
('ask', 4, 5, None, 0.5, 4), # last not available - uses ask
('ask', 4, 5, None, 1, 4), # last not available - uses ask
('ask', 4, 5, None, 0, 4), # last not available - uses ask
('same', 21, 20, 10, 0.0, 20), # Full bid side
('bid', 21, 20, 10, 0.0, 20), # Full bid side
('bid', 21, 20, 10, 1.0, 10), # Full last side
('bid', 21, 20, 10, 0.5, 15), # Between bid and last
('bid', 21, 20, 10, 0.7, 13), # Between bid and last
('bid', 21, 20, 10, 0.3, 17), # Between bid and last
('bid', 6, 5, 10, 1.0, 5), # last bigger than bid
('bid', 21, 20, 10, None, 20), # price_last_balance missing
('bid', 6, 5, 10, 0.5, 5), # last bigger than bid
('bid', 21, 20, None, 0.5, 20), # last not available - uses bid
('bid', 6, 5, None, 0.5, 5), # last not available - uses bid
('bid', 6, 5, None, 1, 5), # last not available - uses bid
('bid', 6, 5, None, 0, 5), # last not available - uses bid
]
get_sell_rate_data = [
('bid', 12.0, 11.0, 11.5, 0.0, 11.0), # full bid side
('bid', 12.0, 11.0, 11.5, 1.0, 11.5), # full last side
('bid', 12.0, 11.0, 11.5, 0.5, 11.25), # between bid and lat
('bid', 12.0, 11.2, 10.5, 0.0, 11.2), # Last smaller than bid
('bid', 12.0, 11.2, 10.5, 1.0, 11.2), # Last smaller than bid - uses bid
('bid', 12.0, 11.2, 10.5, 0.5, 11.2), # Last smaller than bid - uses bid
('bid', 0.003, 0.002, 0.005, 0.0, 0.002),
('bid', 0.003, 0.002, 0.005, None, 0.002),
('ask', 12.0, 11.0, 12.5, 0.0, 12.0), # full ask side
('ask', 12.0, 11.0, 12.5, 1.0, 12.5), # full last side
('ask', 12.0, 11.0, 12.5, 0.5, 12.25), # between bid and lat
('ask', 12.2, 11.2, 10.5, 0.0, 12.2), # Last smaller than ask
('ask', 12.0, 11.0, 10.5, 1.0, 12.0), # Last smaller than ask - uses ask
('ask', 12.0, 11.2, 10.5, 0.5, 12.0), # Last smaller than ask - uses ask
('ask', 10.0, 11.0, 11.0, 0.0, 10.0),
('ask', 10.11, 11.2, 11.0, 0.0, 10.11),
('ask', 0.001, 0.002, 11.0, 0.0, 0.001),
('ask', 0.006, 1.0, 11.0, 0.0, 0.006),
('ask', 0.006, 1.0, 11.0, None, 0.006),
]
def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
fun, mock_ccxt_fun, retries=API_RETRY_COUNT + 1, **kwargs): fun, mock_ccxt_fun, retries=API_RETRY_COUNT + 1, **kwargs):
@ -2360,34 +2411,7 @@ def test_fetch_l2_order_book_exception(default_conf, mocker, exchange_name):
exchange.fetch_l2_order_book(pair='ETH/BTC', limit=50) exchange.fetch_l2_order_book(pair='ETH/BTC', limit=50)
@pytest.mark.parametrize("side,ask,bid,last,last_ab,expected", [ @pytest.mark.parametrize("side,ask,bid,last,last_ab,expected", get_entry_rate_data)
('other', 20, 19, 10, 0.0, 20), # Full ask side
('ask', 20, 19, 10, 0.0, 20), # Full ask side
('ask', 20, 19, 10, 1.0, 10), # Full last side
('ask', 20, 19, 10, 0.5, 15), # Between ask and last
('ask', 20, 19, 10, 0.7, 13), # Between ask and last
('ask', 20, 19, 10, 0.3, 17), # Between ask and last
('ask', 5, 6, 10, 1.0, 5), # last bigger than ask
('ask', 5, 6, 10, 0.5, 5), # last bigger than ask
('ask', 20, 19, 10, None, 20), # price_last_balance missing
('ask', 10, 20, None, 0.5, 10), # last not available - uses ask
('ask', 4, 5, None, 0.5, 4), # last not available - uses ask
('ask', 4, 5, None, 1, 4), # last not available - uses ask
('ask', 4, 5, None, 0, 4), # last not available - uses ask
('same', 21, 20, 10, 0.0, 20), # Full bid side
('bid', 21, 20, 10, 0.0, 20), # Full bid side
('bid', 21, 20, 10, 1.0, 10), # Full last side
('bid', 21, 20, 10, 0.5, 15), # Between bid and last
('bid', 21, 20, 10, 0.7, 13), # Between bid and last
('bid', 21, 20, 10, 0.3, 17), # Between bid and last
('bid', 6, 5, 10, 1.0, 5), # last bigger than bid
('bid', 21, 20, 10, None, 20), # price_last_balance missing
('bid', 6, 5, 10, 0.5, 5), # last bigger than bid
('bid', 21, 20, None, 0.5, 20), # last not available - uses bid
('bid', 6, 5, None, 0.5, 5), # last not available - uses bid
('bid', 6, 5, None, 1, 5), # last not available - uses bid
('bid', 6, 5, None, 0, 5), # last not available - uses bid
])
def test_get_entry_rate(mocker, default_conf, caplog, side, ask, bid, def test_get_entry_rate(mocker, default_conf, caplog, side, ask, bid,
last, last_ab, expected) -> None: last, last_ab, expected) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
@ -2411,27 +2435,7 @@ def test_get_entry_rate(mocker, default_conf, caplog, side, ask, bid,
assert not log_has("Using cached entry rate for ETH/BTC.", caplog) assert not log_has("Using cached entry rate for ETH/BTC.", caplog)
@pytest.mark.parametrize('side,ask,bid,last,last_ab,expected', [ @pytest.mark.parametrize('side,ask,bid,last,last_ab,expected', get_sell_rate_data)
('bid', 12.0, 11.0, 11.5, 0.0, 11.0), # full bid side
('bid', 12.0, 11.0, 11.5, 1.0, 11.5), # full last side
('bid', 12.0, 11.0, 11.5, 0.5, 11.25), # between bid and lat
('bid', 12.0, 11.2, 10.5, 0.0, 11.2), # Last smaller than bid
('bid', 12.0, 11.2, 10.5, 1.0, 11.2), # Last smaller than bid - uses bid
('bid', 12.0, 11.2, 10.5, 0.5, 11.2), # Last smaller than bid - uses bid
('bid', 0.003, 0.002, 0.005, 0.0, 0.002),
('bid', 0.003, 0.002, 0.005, None, 0.002),
('ask', 12.0, 11.0, 12.5, 0.0, 12.0), # full ask side
('ask', 12.0, 11.0, 12.5, 1.0, 12.5), # full last side
('ask', 12.0, 11.0, 12.5, 0.5, 12.25), # between bid and lat
('ask', 12.2, 11.2, 10.5, 0.0, 12.2), # Last smaller than ask
('ask', 12.0, 11.0, 10.5, 1.0, 12.0), # Last smaller than ask - uses ask
('ask', 12.0, 11.2, 10.5, 0.5, 12.0), # Last smaller than ask - uses ask
('ask', 10.0, 11.0, 11.0, 0.0, 10.0),
('ask', 10.11, 11.2, 11.0, 0.0, 10.11),
('ask', 0.001, 0.002, 11.0, 0.0, 0.001),
('ask', 0.006, 1.0, 11.0, 0.0, 0.006),
('ask', 0.006, 1.0, 11.0, None, 0.006),
])
def test_get_exit_rate(default_conf, mocker, caplog, side, bid, ask, def test_get_exit_rate(default_conf, mocker, caplog, side, bid, ask,
last, last_ab, expected) -> None: last, last_ab, expected) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
@ -2481,14 +2485,14 @@ def test_get_ticker_rate_error(mocker, entry, default_conf, caplog, side, is_sho
@pytest.mark.parametrize('is_short,side,expected', [ @pytest.mark.parametrize('is_short,side,expected', [
(False, 'bid', 0.043936), # Value from order_book_l2 fitxure - bids side (False, 'bid', 0.043936), # Value from order_book_l2 fixture - bids side
(False, 'ask', 0.043949), # Value from order_book_l2 fitxure - asks side (False, 'ask', 0.043949), # Value from order_book_l2 fixture - asks side
(False, 'other', 0.043936), # Value from order_book_l2 fitxure - bids side (False, 'other', 0.043936), # Value from order_book_l2 fixture - bids side
(False, 'same', 0.043949), # Value from order_book_l2 fitxure - asks side (False, 'same', 0.043949), # Value from order_book_l2 fixture - asks side
(True, 'bid', 0.043936), # Value from order_book_l2 fitxure - bids side (True, 'bid', 0.043936), # Value from order_book_l2 fixture - bids side
(True, 'ask', 0.043949), # Value from order_book_l2 fitxure - asks side (True, 'ask', 0.043949), # Value from order_book_l2 fixture - asks side
(True, 'other', 0.043949), # Value from order_book_l2 fitxure - asks side (True, 'other', 0.043949), # Value from order_book_l2 fixture - asks side
(True, 'same', 0.043936), # Value from order_book_l2 fitxure - bids side (True, 'same', 0.043936), # Value from order_book_l2 fixture - bids side
]) ])
def test_get_exit_rate_orderbook( def test_get_exit_rate_orderbook(
default_conf, mocker, caplog, is_short, side, expected, order_book_l2): default_conf, mocker, caplog, is_short, side, expected, order_book_l2):
@ -2521,7 +2525,8 @@ def test_get_exit_rate_orderbook_exception(default_conf, mocker, caplog):
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
with pytest.raises(PricingError): with pytest.raises(PricingError):
exchange.get_rate(pair, refresh=True, side="exit", is_short=False) exchange.get_rate(pair, refresh=True, side="exit", is_short=False)
assert log_has_re(r"Exit Price at location 1 from orderbook could not be determined\..*", assert log_has_re(rf"{pair} - Exit Price at location 1 from orderbook "
rf"could not be determined\..*",
caplog) caplog)
@ -2548,6 +2553,84 @@ def test_get_exit_rate_exception(default_conf, mocker, is_short):
assert exchange.get_rate(pair, refresh=True, side="exit", is_short=is_short) == 0.13 assert exchange.get_rate(pair, refresh=True, side="exit", is_short=is_short) == 0.13
@pytest.mark.parametrize("side,ask,bid,last,last_ab,expected", get_entry_rate_data)
@pytest.mark.parametrize("side2", ['bid', 'ask'])
@pytest.mark.parametrize("use_order_book", [True, False])
def test_get_rates_testing_buy(mocker, default_conf, caplog, side, ask, bid,
last, last_ab, expected,
side2, use_order_book, order_book_l2) -> None:
caplog.set_level(logging.DEBUG)
if last_ab is None:
del default_conf['entry_pricing']['price_last_balance']
else:
default_conf['entry_pricing']['price_last_balance'] = last_ab
default_conf['entry_pricing']['price_side'] = side
default_conf['exit_pricing']['price_side'] = side2
default_conf['exit_pricing']['use_order_book'] = use_order_book
api_mock = MagicMock()
api_mock.fetch_l2_order_book = order_book_l2
api_mock.fetch_ticker = MagicMock(
return_value={'ask': ask, 'last': last, 'bid': bid})
exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert exchange.get_rates('ETH/BTC', refresh=True, is_short=False)[0] == expected
assert not log_has("Using cached buy rate for ETH/BTC.", caplog)
api_mock.fetch_l2_order_book.reset_mock()
api_mock.fetch_ticker.reset_mock()
assert exchange.get_rates('ETH/BTC', refresh=False, is_short=False)[0] == expected
assert log_has("Using cached buy rate for ETH/BTC.", caplog)
assert api_mock.fetch_l2_order_book.call_count == 0
assert api_mock.fetch_ticker.call_count == 0
# Running a 2nd time with Refresh on!
caplog.clear()
assert exchange.get_rates('ETH/BTC', refresh=True, is_short=False)[0] == expected
assert not log_has("Using cached buy rate for ETH/BTC.", caplog)
assert api_mock.fetch_l2_order_book.call_count == int(use_order_book)
assert api_mock.fetch_ticker.call_count == 1
@pytest.mark.parametrize('side,ask,bid,last,last_ab,expected', get_sell_rate_data)
@pytest.mark.parametrize("side2", ['bid', 'ask'])
@pytest.mark.parametrize("use_order_book", [True, False])
def test_get_rates_testing_sell(default_conf, mocker, caplog, side, bid, ask,
last, last_ab, expected,
side2, use_order_book, order_book_l2) -> None:
caplog.set_level(logging.DEBUG)
default_conf['exit_pricing']['price_side'] = side
if last_ab is not None:
default_conf['exit_pricing']['price_last_balance'] = last_ab
default_conf['entry_pricing']['price_side'] = side2
default_conf['entry_pricing']['use_order_book'] = use_order_book
api_mock = MagicMock()
api_mock.fetch_l2_order_book = order_book_l2
api_mock.fetch_ticker = MagicMock(
return_value={'ask': ask, 'last': last, 'bid': bid})
exchange = get_patched_exchange(mocker, default_conf, api_mock)
pair = "ETH/BTC"
# Test regular mode
rate = exchange.get_rates(pair, refresh=True, is_short=False)[1]
assert not log_has("Using cached sell rate for ETH/BTC.", caplog)
assert isinstance(rate, float)
assert rate == expected
# Use caching
api_mock.fetch_l2_order_book.reset_mock()
api_mock.fetch_ticker.reset_mock()
rate = exchange.get_rates(pair, refresh=False, is_short=False)[1]
assert rate == expected
assert log_has("Using cached sell rate for ETH/BTC.", caplog)
assert api_mock.fetch_l2_order_book.call_count == 0
assert api_mock.fetch_ticker.call_count == 0
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test___async_get_candle_history_sort(default_conf, mocker, exchange_name): async def test___async_get_candle_history_sort(default_conf, mocker, exchange_name):

View File

@ -1,8 +1,10 @@
# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument # pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument
from copy import deepcopy from copy import deepcopy
from unittest.mock import MagicMock
import pandas as pd import pandas as pd
import pytest
from arrow import Arrow from arrow import Arrow
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
@ -87,3 +89,87 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) ->
assert (round(ln.iloc[0]["open"], 6) == round(t["close_rate"], 6) or assert (round(ln.iloc[0]["open"], 6) == round(t["close_rate"], 6) or
round(ln.iloc[0]["low"], 6) < round( round(ln.iloc[0]["low"], 6) < round(
t["close_rate"], 6) < round(ln.iloc[0]["high"], 6)) t["close_rate"], 6) < round(ln.iloc[0]["high"], 6))
def test_backtest_position_adjustment_detailed(default_conf, fee, mocker) -> None:
default_conf['use_exit_signal'] = False
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=10)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
patch_exchange(mocker)
default_conf.update({
"stake_amount": 100.0,
"dry_run_wallet": 1000.0,
"strategy": "StrategyTestV3"
})
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
pair = 'XRP/USDT'
row = [
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0),
2.1, # Open
2.2, # High
1.9, # Low
2.1, # Close
1, # enter_long
0, # exit_long
0, # enter_short
0, # exit_short
'', # enter_tag
'', # exit_tag
]
trade = backtesting._enter_trade(pair, row=row, direction='long')
trade.orders[0].close_bt_order(row[0], trade)
assert trade
assert pytest.approx(trade.stake_amount) == 100.0
assert pytest.approx(trade.amount) == 47.61904762
assert len(trade.orders) == 1
backtesting.strategy.adjust_trade_position = MagicMock(return_value=None)
trade = backtesting._get_adjust_trade_entry_for_candle(trade, row)
assert trade
assert pytest.approx(trade.stake_amount) == 100.0
assert pytest.approx(trade.amount) == 47.61904762
assert len(trade.orders) == 1
# Increase position by 100
backtesting.strategy.adjust_trade_position = MagicMock(return_value=100)
trade = backtesting._get_adjust_trade_entry_for_candle(trade, row)
assert trade
assert pytest.approx(trade.stake_amount) == 200.0
assert pytest.approx(trade.amount) == 95.23809524
assert len(trade.orders) == 2
# Reduce by more than amount - no change to trade.
backtesting.strategy.adjust_trade_position = MagicMock(return_value=-500)
trade = backtesting._get_adjust_trade_entry_for_candle(trade, row)
assert trade
assert pytest.approx(trade.stake_amount) == 200.0
assert pytest.approx(trade.amount) == 95.23809524
assert len(trade.orders) == 2
assert trade.nr_of_successful_entries == 2
# Reduce position by 50
backtesting.strategy.adjust_trade_position = MagicMock(return_value=-100)
trade = backtesting._get_adjust_trade_entry_for_candle(trade, row)
assert trade
assert pytest.approx(trade.stake_amount) == 100.0
assert pytest.approx(trade.amount) == 47.61904762
assert len(trade.orders) == 3
assert trade.nr_of_successful_entries == 2
assert trade.nr_of_successful_exits == 1
# Adjust below minimum
backtesting.strategy.adjust_trade_position = MagicMock(return_value=-99)
trade = backtesting._get_adjust_trade_entry_for_candle(trade, row)
assert trade
assert pytest.approx(trade.stake_amount) == 100.0
assert pytest.approx(trade.amount) == 47.61904762
assert len(trade.orders) == 3
assert trade.nr_of_successful_entries == 2
assert trade.nr_of_successful_exits == 1

View File

@ -111,6 +111,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'stoploss_entry_dist': -0.00010475, 'stoploss_entry_dist': -0.00010475,
'stoploss_entry_dist_ratio': -0.10448878, 'stoploss_entry_dist_ratio': -0.10448878,
'open_order': None, 'open_order': None,
'realized_profit': 0.0,
'exchange': 'binance', 'exchange': 'binance',
'leverage': 1.0, 'leverage': 1.0,
'interest_rate': 0.0, 'interest_rate': 0.0,
@ -196,6 +197,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'stoploss_entry_dist_ratio': -0.10448878, 'stoploss_entry_dist_ratio': -0.10448878,
'open_order': None, 'open_order': None,
'exchange': 'binance', 'exchange': 'binance',
'realized_profit': 0.0,
'leverage': 1.0, 'leverage': 1.0,
'interest_rate': 0.0, 'interest_rate': 0.0,
'liquidation_price': None, 'liquidation_price': None,
@ -841,7 +843,8 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
'side': 'sell', 'side': 'sell',
'amount': amount, 'amount': amount,
'remaining': amount, 'remaining': amount,
'filled': 0.0 'filled': 0.0,
'id': trade.orders[0].order_id,
} }
) )
msg = rpc._rpc_force_exit('3') msg = rpc._rpc_force_exit('3')

View File

@ -272,7 +272,7 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None:
msg = msg_mock.call_args_list[0][0][0] msg = msg_mock.call_args_list[0][0][0]
assert re.search(r'Number of Entries.*2', msg) assert re.search(r'Number of Entries.*2', msg)
assert re.search(r'Average Entry Price', msg) assert re.search(r'Average Entry Price', msg)
assert re.search(r'Order filled at', msg) assert re.search(r'Order filled', msg)
assert re.search(r'Close Date:', msg) is None assert re.search(r'Close Date:', msg) is None
assert re.search(r'Close Profit:', msg) is None assert re.search(r'Close Profit:', msg) is None
@ -959,6 +959,9 @@ def test_telegram_forceexit_handle(default_conf, update, ticker, fee,
'open_date': ANY, 'open_date': ANY,
'close_date': ANY, 'close_date': ANY,
'close_rate': ANY, 'close_rate': ANY,
'stake_amount': 0.0009999999999054,
'sub_trade': False,
'cumulative_profit': 0.0,
} == last_msg } == last_msg
@ -1028,6 +1031,9 @@ def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee,
'open_date': ANY, 'open_date': ANY,
'close_date': ANY, 'close_date': ANY,
'close_rate': ANY, 'close_rate': ANY,
'stake_amount': 0.0009999999999054,
'sub_trade': False,
'cumulative_profit': 0.0,
} == last_msg } == last_msg
@ -1087,6 +1093,9 @@ def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -> None
'open_date': ANY, 'open_date': ANY,
'close_date': ANY, 'close_date': ANY,
'close_rate': ANY, 'close_rate': ANY,
'stake_amount': 0.0009999999999054,
'sub_trade': False,
'cumulative_profit': 0.0,
} == msg } == msg
@ -1789,7 +1798,6 @@ def test_send_msg_entry_fill_notification(default_conf, mocker, message_type, en
'leverage': leverage, 'leverage': leverage,
'stake_amount': 0.01465333, 'stake_amount': 0.01465333,
'direction': entered, 'direction': entered,
# 'stake_amount_fiat': 0.0,
'stake_currency': 'BTC', 'stake_currency': 'BTC',
'fiat_currency': 'USD', 'fiat_currency': 'USD',
'open_rate': 1.099e-05, 'open_rate': 1.099e-05,
@ -1806,6 +1814,33 @@ def test_send_msg_entry_fill_notification(default_conf, mocker, message_type, en
'*Total:* `(0.01465333 BTC, 180.895 USD)`' '*Total:* `(0.01465333 BTC, 180.895 USD)`'
) )
msg_mock.reset_mock()
telegram.send_msg({
'type': message_type,
'trade_id': 1,
'enter_tag': enter_signal,
'exchange': 'Binance',
'pair': 'ETH/BTC',
'leverage': leverage,
'stake_amount': 0.01465333,
'sub_trade': True,
'direction': entered,
'stake_currency': 'BTC',
'fiat_currency': 'USD',
'open_rate': 1.099e-05,
'amount': 1333.3333333333335,
'open_date': arrow.utcnow().shift(hours=-1)
})
assert msg_mock.call_args[0][0] == (
f'\N{CHECK MARK} *Binance (dry):* {entered}ed ETH/BTC (#1)\n'
f'*Enter Tag:* `{enter_signal}`\n'
'*Amount:* `1333.33333333`\n'
f"{leverage_text}"
'*Open Rate:* `0.00001099`\n'
'*Total:* `(0.01465333 BTC, 180.895 USD)`'
)
def test_send_msg_sell_notification(default_conf, mocker) -> None: def test_send_msg_sell_notification(default_conf, mocker) -> None:
@ -1840,12 +1875,51 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n' '*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n'
'*Enter Tag:* `buy_signal1`\n' '*Enter Tag:* `buy_signal1`\n'
'*Exit Reason:* `stop_loss`\n' '*Exit Reason:* `stop_loss`\n'
'*Duration:* `1:00:00 (60.0 min)`\n'
'*Direction:* `Long`\n' '*Direction:* `Long`\n'
'*Amount:* `1333.33333333`\n' '*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00007500`\n' '*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n' '*Current Rate:* `0.00003201`\n'
'*Close Rate:* `0.00003201`' '*Exit Rate:* `0.00003201`\n'
'*Duration:* `1:00:00 (60.0 min)`'
)
msg_mock.reset_mock()
telegram.send_msg({
'type': RPCMessageType.EXIT,
'trade_id': 1,
'exchange': 'Binance',
'pair': 'KEY/ETH',
'direction': 'Long',
'gain': 'loss',
'limit': 3.201e-05,
'amount': 1333.3333333333335,
'order_type': 'market',
'open_rate': 7.5e-05,
'current_rate': 3.201e-05,
'cumulative_profit': -0.15746268,
'profit_amount': -0.05746268,
'profit_ratio': -0.57405275,
'stake_currency': 'ETH',
'fiat_currency': 'USD',
'enter_tag': 'buy_signal1',
'exit_reason': ExitType.STOP_LOSS.value,
'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30),
'close_date': arrow.utcnow(),
'stake_amount': 0.01,
'sub_trade': True,
})
assert msg_mock.call_args[0][0] == (
'\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
'*Unrealized Sub Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n'
'*Cumulative Profit:* (`-0.15746268 ETH / -24.812 USD`)\n'
'*Enter Tag:* `buy_signal1`\n'
'*Exit Reason:* `stop_loss`\n'
'*Direction:* `Long`\n'
'*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n'
'*Exit Rate:* `0.00003201`\n'
'*Remaining:* `(0.01 ETH, -24.812 USD)`'
) )
msg_mock.reset_mock() msg_mock.reset_mock()
@ -1871,15 +1945,15 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
}) })
assert msg_mock.call_args[0][0] == ( assert msg_mock.call_args[0][0] == (
'\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n' '\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
'*Unrealized Profit:* `-57.41%`\n' '*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH)`\n'
'*Enter Tag:* `buy_signal1`\n' '*Enter Tag:* `buy_signal1`\n'
'*Exit Reason:* `stop_loss`\n' '*Exit Reason:* `stop_loss`\n'
'*Duration:* `1 day, 2:30:00 (1590.0 min)`\n'
'*Direction:* `Long`\n' '*Direction:* `Long`\n'
'*Amount:* `1333.33333333`\n' '*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00007500`\n' '*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n' '*Current Rate:* `0.00003201`\n'
'*Close Rate:* `0.00003201`' '*Exit Rate:* `0.00003201`\n'
'*Duration:* `1 day, 2:30:00 (1590.0 min)`'
) )
# Reset singleton function to avoid random breaks # Reset singleton function to avoid random breaks
telegram._rpc._fiat_converter.convert_amount = old_convamount telegram._rpc._fiat_converter.convert_amount = old_convamount
@ -1954,15 +2028,15 @@ def test_send_msg_sell_fill_notification(default_conf, mocker, direction,
leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else '' leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else ''
assert msg_mock.call_args[0][0] == ( assert msg_mock.call_args[0][0] == (
'\N{WARNING SIGN} *Binance (dry):* Exited KEY/ETH (#1)\n' '\N{WARNING SIGN} *Binance (dry):* Exited KEY/ETH (#1)\n'
'*Profit:* `-57.41%`\n' '*Profit:* `-57.41% (loss: -0.05746268 ETH)`\n'
f'*Enter Tag:* `{enter_signal}`\n' f'*Enter Tag:* `{enter_signal}`\n'
'*Exit Reason:* `stop_loss`\n' '*Exit Reason:* `stop_loss`\n'
'*Duration:* `1 day, 2:30:00 (1590.0 min)`\n'
f"*Direction:* `{direction}`\n" f"*Direction:* `{direction}`\n"
f"{leverage_text}" f"{leverage_text}"
'*Amount:* `1333.33333333`\n' '*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00007500`\n' '*Open Rate:* `0.00007500`\n'
'*Close Rate:* `0.00003201`' '*Exit Rate:* `0.00003201`\n'
'*Duration:* `1 day, 2:30:00 (1590.0 min)`'
) )
@ -2090,16 +2164,16 @@ def test_send_msg_sell_notification_no_fiat(
leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else '' leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else ''
assert msg_mock.call_args[0][0] == ( assert msg_mock.call_args[0][0] == (
'\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n' '\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
'*Unrealized Profit:* `-57.41%`\n' '*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH)`\n'
f'*Enter Tag:* `{enter_signal}`\n' f'*Enter Tag:* `{enter_signal}`\n'
'*Exit Reason:* `stop_loss`\n' '*Exit Reason:* `stop_loss`\n'
'*Duration:* `2:35:03 (155.1 min)`\n'
f'*Direction:* `{direction}`\n' f'*Direction:* `{direction}`\n'
f'{leverage_text}' f'{leverage_text}'
'*Amount:* `1333.33333333`\n' '*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00007500`\n' '*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n' '*Current Rate:* `0.00003201`\n'
'*Close Rate:* `0.00003201`' '*Exit Rate:* `0.00003201`\n'
'*Duration:* `2:35:03 (155.1 min)`'
) )

View File

@ -185,9 +185,12 @@ class StrategyTestV3(IStrategy):
return 3.0 return 3.0
def adjust_trade_position(self, trade: Trade, current_time: datetime, current_rate: float, def adjust_trade_position(self, trade: Trade, current_time: datetime,
current_profit: float, current_rate: float, current_profit: float,
min_stake: Optional[float], max_stake: float, **kwargs): min_stake: Optional[float], max_stake: float,
current_entry_rate: float, current_exit_rate: float,
current_entry_profit: float, current_exit_profit: float,
**kwargs) -> Optional[float]:
if current_profit < -0.0075: if current_profit < -0.0075:
orders = trade.select_filled_orders(trade.entry_side) orders = trade.select_filled_orders(trade.entry_side)

View File

@ -843,8 +843,8 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
# In case of closed order # In case of closed order
order['status'] = 'closed' order['status'] = 'closed'
order['price'] = 10 order['average'] = 10
order['cost'] = 100 order['cost'] = 300
order['id'] = '444' order['id'] = '444'
mocker.patch('freqtrade.exchange.Exchange.create_order', mocker.patch('freqtrade.exchange.Exchange.create_order',
@ -855,7 +855,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
assert trade assert trade
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.open_rate == 10 assert trade.open_rate == 10
assert trade.stake_amount == round(order['price'] * order['filled'] / leverage, 8) assert trade.stake_amount == round(order['average'] * order['filled'] / leverage, 8)
assert pytest.approx(trade.liquidation_price) == liq_price assert pytest.approx(trade.liquidation_price) == liq_price
# In case of rejected or expired order and partially filled # In case of rejected or expired order and partially filled
@ -863,8 +863,8 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
order['amount'] = 30.0 order['amount'] = 30.0
order['filled'] = 20.0 order['filled'] = 20.0
order['remaining'] = 10.00 order['remaining'] = 10.00
order['price'] = 0.5 order['average'] = 0.5
order['cost'] = 15.0 order['cost'] = 10.0
order['id'] = '555' order['id'] = '555'
mocker.patch('freqtrade.exchange.Exchange.create_order', mocker.patch('freqtrade.exchange.Exchange.create_order',
MagicMock(return_value=order)) MagicMock(return_value=order))
@ -872,9 +872,9 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
trade = Trade.query.all()[3] trade = Trade.query.all()[3]
trade.is_short = is_short trade.is_short = is_short
assert trade assert trade
assert trade.open_order_id == '555' assert trade.open_order_id is None
assert trade.open_rate == 0.5 assert trade.open_rate == 0.5
assert trade.stake_amount == round(order['price'] * order['filled'] / leverage, 8) assert trade.stake_amount == round(order['average'] * order['filled'] / leverage, 8)
# Test with custom stake # Test with custom stake
order['status'] = 'open' order['status'] = 'open'
@ -901,7 +901,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
order['amount'] = 30.0 * leverage order['amount'] = 30.0 * leverage
order['filled'] = 0.0 order['filled'] = 0.0
order['remaining'] = 30.0 order['remaining'] = 30.0
order['price'] = 0.5 order['average'] = 0.5
order['cost'] = 0.0 order['cost'] = 0.0
order['id'] = '66' order['id'] = '66'
mocker.patch('freqtrade.exchange.Exchange.create_order', mocker.patch('freqtrade.exchange.Exchange.create_order',
@ -1083,7 +1083,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
'last': 1.9 'last': 1.9
}), }),
create_order=MagicMock(side_effect=[ create_order=MagicMock(side_effect=[
{'id': enter_order['id']}, enter_order,
exit_order, exit_order,
]), ]),
get_fee=fee, get_fee=fee,
@ -1109,20 +1109,20 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
# should do nothing and return false # should do nothing and return false
trade.is_open = True trade.is_open = True
trade.open_order_id = None trade.open_order_id = None
trade.stoploss_order_id = 100 trade.stoploss_order_id = "100"
hanging_stoploss_order = MagicMock(return_value={'status': 'open'}) hanging_stoploss_order = MagicMock(return_value={'status': 'open'})
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', hanging_stoploss_order) mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', hanging_stoploss_order)
assert freqtrade.handle_stoploss_on_exchange(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert trade.stoploss_order_id == 100 assert trade.stoploss_order_id == "100"
# Third case: when stoploss was set but it was canceled for some reason # Third case: when stoploss was set but it was canceled for some reason
# should set a stoploss immediately and return False # should set a stoploss immediately and return False
caplog.clear() caplog.clear()
trade.is_open = True trade.is_open = True
trade.open_order_id = None trade.open_order_id = None
trade.stoploss_order_id = 100 trade.stoploss_order_id = "100"
canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'}) canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'})
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', canceled_stoploss_order) mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', canceled_stoploss_order)
@ -2039,6 +2039,7 @@ def test_update_trade_state_exception(mocker, default_conf_usdt, is_short, limit
trade = MagicMock() trade = MagicMock()
trade.open_order_id = '123' trade.open_order_id = '123'
trade.amount = 123
# Test raise of OperationalException exception # Test raise of OperationalException exception
mocker.patch( mocker.patch(
@ -2352,9 +2353,9 @@ def test_close_trade(
trade.is_short = is_short trade.is_short = is_short
assert trade assert trade
oobj = Order.parse_from_ccxt_object(enter_order, enter_order['symbol'], 'buy') oobj = Order.parse_from_ccxt_object(enter_order, enter_order['symbol'], trade.enter_side)
trade.update_trade(oobj) trade.update_trade(oobj)
oobj = Order.parse_from_ccxt_object(exit_order, exit_order['symbol'], 'sell') oobj = Order.parse_from_ccxt_object(exit_order, exit_order['symbol'], trade.exit_side)
trade.update_trade(oobj) trade.update_trade(oobj)
assert trade.is_open is False assert trade.is_open is False
@ -2397,8 +2398,8 @@ def test_manage_open_orders_entry_usercustom(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker_usdt, fetch_ticker=ticker_usdt,
fetch_order=MagicMock(return_value=old_order), fetch_order=MagicMock(return_value=old_order),
cancel_order_with_result=cancel_order_wr_mock,
cancel_order=cancel_order_mock, cancel_order=cancel_order_mock,
cancel_order_with_result=cancel_order_wr_mock,
get_fee=fee get_fee=fee
) )
freqtrade = FreqtradeBot(default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt)
@ -2446,7 +2447,9 @@ def test_manage_open_orders_entry(
) -> None: ) -> None:
old_order = limit_sell_order_old if is_short else limit_buy_order_old old_order = limit_sell_order_old if is_short else limit_buy_order_old
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
old_order['id'] = open_trade.open_order_id open_trade.open_order_id = old_order['id']
order = Order.parse_from_ccxt_object(old_order, 'mocked', 'buy')
open_trade.orders[0] = order
limit_buy_cancel = deepcopy(old_order) limit_buy_cancel = deepcopy(old_order)
limit_buy_cancel['status'] = 'canceled' limit_buy_cancel['status'] = 'canceled'
cancel_order_mock = MagicMock(return_value=limit_buy_cancel) cancel_order_mock = MagicMock(return_value=limit_buy_cancel)
@ -2637,7 +2640,9 @@ def test_manage_open_orders_exit_usercustom(
is_short, open_trade_usdt, caplog is_short, open_trade_usdt, caplog
) -> None: ) -> None:
default_conf_usdt["unfilledtimeout"] = {"entry": 1440, "exit": 1440, "exit_timeout_count": 1} default_conf_usdt["unfilledtimeout"] = {"entry": 1440, "exit": 1440, "exit_timeout_count": 1}
limit_sell_order_old['id'] = open_trade_usdt.open_order_id open_trade_usdt.open_order_id = limit_sell_order_old['id']
order = Order.parse_from_ccxt_object(limit_sell_order_old, 'mocked', 'sell')
open_trade_usdt.orders[0] = order
if is_short: if is_short:
limit_sell_order_old['side'] = 'buy' limit_sell_order_old['side'] = 'buy'
open_trade_usdt.is_short = is_short open_trade_usdt.is_short = is_short
@ -3250,6 +3255,9 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
'open_date': ANY, 'open_date': ANY,
'close_date': ANY, 'close_date': ANY,
'close_rate': ANY, 'close_rate': ANY,
'sub_trade': False,
'cumulative_profit': 0.0,
'stake_amount': pytest.approx(60),
} == last_msg } == last_msg
@ -3310,6 +3318,9 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd
'open_date': ANY, 'open_date': ANY,
'close_date': ANY, 'close_date': ANY,
'close_rate': ANY, 'close_rate': ANY,
'sub_trade': False,
'cumulative_profit': 0.0,
'stake_amount': pytest.approx(60),
} == last_msg } == last_msg
@ -3391,6 +3402,9 @@ def test_execute_trade_exit_custom_exit_price(
'open_date': ANY, 'open_date': ANY,
'close_date': ANY, 'close_date': ANY,
'close_rate': ANY, 'close_rate': ANY,
'sub_trade': False,
'cumulative_profit': 0.0,
'stake_amount': pytest.approx(60),
} == last_msg } == last_msg
@ -3459,6 +3473,9 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
'open_date': ANY, 'open_date': ANY,
'close_date': ANY, 'close_date': ANY,
'close_rate': ANY, 'close_rate': ANY,
'sub_trade': False,
'cumulative_profit': 0.0,
'stake_amount': pytest.approx(60),
} == last_msg } == last_msg
@ -3690,7 +3707,7 @@ def test_execute_trade_exit_market_order(
) )
assert not trade.is_open assert not trade.is_open
assert trade.close_profit == profit_ratio assert pytest.approx(trade.close_profit) == profit_ratio
assert rpc_mock.call_count == 4 assert rpc_mock.call_count == 4
last_msg = rpc_mock.call_args_list[-2][0][0] last_msg = rpc_mock.call_args_list[-2][0][0]
@ -3718,6 +3735,9 @@ def test_execute_trade_exit_market_order(
'open_date': ANY, 'open_date': ANY,
'close_date': ANY, 'close_date': ANY,
'close_rate': ANY, 'close_rate': ANY,
'sub_trade': False,
'cumulative_profit': 0.0,
'stake_amount': pytest.approx(60),
} == last_msg } == last_msg
@ -3789,7 +3809,7 @@ def test_exit_profit_only(
'last': bid 'last': bid
}), }),
create_order=MagicMock(side_effect=[ create_order=MagicMock(side_effect=[
limit_order_open[eside], limit_order[eside],
{'id': 1234553382}, {'id': 1234553382},
]), ]),
get_fee=fee, get_fee=fee,
@ -4081,7 +4101,7 @@ def test_trailing_stop_loss_positive(
'last': enter_price - (-0.01 if is_short else 0.01), 'last': enter_price - (-0.01 if is_short else 0.01),
}), }),
create_order=MagicMock(side_effect=[ create_order=MagicMock(side_effect=[
limit_order_open[eside], limit_order[eside],
{'id': 1234553382}, {'id': 1234553382},
]), ]),
get_fee=fee, get_fee=fee,
@ -4632,7 +4652,7 @@ def test_order_book_entry_pricing1(mocker, default_conf_usdt, order_book_l2, exc
with pytest.raises(PricingError): with pytest.raises(PricingError):
freqtrade.exchange.get_rate('ETH/USDT', side="entry", is_short=False, refresh=True) freqtrade.exchange.get_rate('ETH/USDT', side="entry", is_short=False, refresh=True)
assert log_has_re( assert log_has_re(
r'Entry Price at location 1 from orderbook could not be determined.', caplog) r'ETH/USDT - Entry Price at location 1 from orderbook could not be determined.', caplog)
else: else:
assert freqtrade.exchange.get_rate( assert freqtrade.exchange.get_rate(
'ETH/USDT', side="entry", is_short=False, refresh=True) == 0.043935 'ETH/USDT', side="entry", is_short=False, refresh=True) == 0.043935
@ -4711,7 +4731,8 @@ def test_order_book_exit_pricing(
return_value={'bids': [[]], 'asks': [[]]}) return_value={'bids': [[]], 'asks': [[]]})
with pytest.raises(PricingError): with pytest.raises(PricingError):
freqtrade.handle_trade(trade) freqtrade.handle_trade(trade)
assert log_has_re(r'Exit Price at location 1 from orderbook could not be determined\..*', assert log_has_re(
r"ETH/USDT - Exit Price at location 1 from orderbook could not be determined\..*",
caplog) caplog)
@ -5385,7 +5406,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
'status': None, 'status': None,
'price': 9, 'price': 9,
'amount': 12, 'amount': 12,
'cost': 100, 'cost': 108,
'ft_is_open': True, 'ft_is_open': True,
'id': '651', 'id': '651',
'order_id': '651' 'order_id': '651'
@ -5480,7 +5501,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
assert trade.open_order_id is None assert trade.open_order_id is None
assert pytest.approx(trade.open_rate) == 9.90909090909 assert pytest.approx(trade.open_rate) == 9.90909090909
assert trade.amount == 22 assert trade.amount == 22
assert trade.stake_amount == 218 assert pytest.approx(trade.stake_amount) == 218
orders = Order.query.all() orders = Order.query.all()
assert orders assert orders
@ -5533,6 +5554,329 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
# Make sure the closed order is found as the second order. # Make sure the closed order is found as the second order.
order = trade.select_order('buy', False) order = trade.select_order('buy', False)
assert order.order_id == '652' assert order.order_id == '652'
closed_sell_dca_order_1 = {
'ft_pair': pair,
'status': 'closed',
'ft_order_side': 'sell',
'side': 'sell',
'type': 'limit',
'price': 8,
'average': 8,
'amount': 15,
'filled': 15,
'cost': 120,
'ft_is_open': False,
'id': '653',
'order_id': '653'
}
mocker.patch('freqtrade.exchange.Exchange.create_order',
MagicMock(return_value=closed_sell_dca_order_1))
mocker.patch('freqtrade.exchange.Exchange.fetch_order',
MagicMock(return_value=closed_sell_dca_order_1))
mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
MagicMock(return_value=closed_sell_dca_order_1))
assert freqtrade.execute_trade_exit(trade=trade, limit=8,
exit_check=ExitCheckTuple(exit_type=ExitType.PARTIAL_EXIT),
sub_trade_amt=15)
# Assert trade is as expected (averaged dca)
trade = Trade.query.first()
assert trade
assert trade.open_order_id is None
assert trade.is_open
assert trade.amount == 22
assert trade.stake_amount == 192.05405405405406
assert pytest.approx(trade.open_rate) == 8.729729729729
orders = Order.query.all()
assert orders
assert len(orders) == 4
# Make sure the closed order is found as the second order.
order = trade.select_order('sell', False)
assert order.order_id == '653'
def test_position_adjust2(mocker, default_conf_usdt, fee) -> None:
"""
TODO: Should be adjusted to test both long and short
buy 100 @ 11
sell 50 @ 8
sell 50 @ 16
"""
patch_RPCManager(mocker)
patch_exchange(mocker)
patch_wallet(mocker, free=10000)
default_conf_usdt.update({
"position_adjustment_enable": True,
"dry_run": False,
"stake_amount": 200.0,
"dry_run_wallet": 1000.0,
})
freqtrade = FreqtradeBot(default_conf_usdt)
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True)
bid = 11
amount = 100
buy_rate_mock = MagicMock(return_value=bid)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_rate=buy_rate_mock,
fetch_ticker=MagicMock(return_value={
'bid': 10,
'ask': 12,
'last': 11
}),
get_min_pair_stake_amount=MagicMock(return_value=1),
get_fee=fee,
)
pair = 'ETH/USDT'
# Initial buy
closed_successful_buy_order = {
'pair': pair,
'ft_pair': pair,
'ft_order_side': 'buy',
'side': 'buy',
'type': 'limit',
'status': 'closed',
'price': bid,
'average': bid,
'cost': bid * amount,
'amount': amount,
'filled': amount,
'ft_is_open': False,
'id': '600',
'order_id': '600'
}
mocker.patch('freqtrade.exchange.Exchange.create_order',
MagicMock(return_value=closed_successful_buy_order))
mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
MagicMock(return_value=closed_successful_buy_order))
assert freqtrade.execute_entry(pair, amount)
# Should create an closed trade with an no open order id
# Order is filled and trade is open
orders = Order.query.all()
assert orders
assert len(orders) == 1
trade = Trade.query.first()
assert trade
assert trade.is_open is True
assert trade.open_order_id is None
assert trade.open_rate == bid
assert trade.stake_amount == bid * amount
# Assume it does nothing since order is closed and trade is open
freqtrade.update_closed_trades_without_assigned_fees()
trade = Trade.query.first()
assert trade
assert trade.is_open is True
assert trade.open_order_id is None
assert trade.open_rate == bid
assert trade.stake_amount == bid * amount
assert not trade.fee_updated(trade.entry_side)
freqtrade.manage_open_orders()
trade = Trade.query.first()
assert trade
assert trade.is_open is True
assert trade.open_order_id is None
assert trade.open_rate == bid
assert trade.stake_amount == bid * amount
assert not trade.fee_updated(trade.entry_side)
amount = 50
ask = 8
closed_sell_dca_order_1 = {
'ft_pair': pair,
'status': 'closed',
'ft_order_side': 'sell',
'side': 'sell',
'type': 'limit',
'price': ask,
'average': ask,
'amount': amount,
'filled': amount,
'cost': amount * ask,
'ft_is_open': False,
'id': '601',
'order_id': '601'
}
mocker.patch('freqtrade.exchange.Exchange.create_order',
MagicMock(return_value=closed_sell_dca_order_1))
mocker.patch('freqtrade.exchange.Exchange.fetch_order',
MagicMock(return_value=closed_sell_dca_order_1))
mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
MagicMock(return_value=closed_sell_dca_order_1))
assert freqtrade.execute_trade_exit(trade=trade, limit=ask,
exit_check=ExitCheckTuple(exit_type=ExitType.PARTIAL_EXIT),
sub_trade_amt=amount)
trades: List[Trade] = trade.get_open_trades_without_assigned_fees()
assert len(trades) == 1
# Assert trade is as expected (averaged dca)
trade = Trade.query.first()
assert trade
assert trade.open_order_id is None
assert trade.amount == 50
assert trade.open_rate == 11
assert trade.stake_amount == 550
assert pytest.approx(trade.realized_profit) == -152.375
assert pytest.approx(trade.close_profit_abs) == -152.375
orders = Order.query.all()
assert orders
assert len(orders) == 2
# Make sure the closed order is found as the second order.
order = trade.select_order('sell', False)
assert order.order_id == '601'
amount = 50
ask = 16
closed_sell_dca_order_2 = {
'ft_pair': pair,
'status': 'closed',
'ft_order_side': 'sell',
'side': 'sell',
'type': 'limit',
'price': ask,
'average': ask,
'amount': amount,
'filled': amount,
'cost': amount * ask,
'ft_is_open': False,
'id': '602',
'order_id': '602'
}
mocker.patch('freqtrade.exchange.Exchange.create_order',
MagicMock(return_value=closed_sell_dca_order_2))
mocker.patch('freqtrade.exchange.Exchange.fetch_order',
MagicMock(return_value=closed_sell_dca_order_2))
mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
MagicMock(return_value=closed_sell_dca_order_2))
assert freqtrade.execute_trade_exit(trade=trade, limit=ask,
exit_check=ExitCheckTuple(exit_type=ExitType.PARTIAL_EXIT),
sub_trade_amt=amount)
# Assert trade is as expected (averaged dca)
trade = Trade.query.first()
assert trade
assert trade.open_order_id is None
assert trade.amount == 50
assert trade.open_rate == 11
assert trade.stake_amount == 550
# Trade fully realized
assert pytest.approx(trade.realized_profit) == 94.25
assert pytest.approx(trade.close_profit_abs) == 94.25
orders = Order.query.all()
assert orders
assert len(orders) == 3
# Make sure the closed order is found as the second order.
order = trade.select_order('sell', False)
assert order.order_id == '602'
assert trade.is_open is False
@pytest.mark.parametrize('data', [
(
# tuple 1 - side amount, price
# tuple 2 - amount, open_rate, stake_amount, cumulative_profit, realized_profit, rel_profit
(('buy', 100, 10), (100.0, 10.0, 1000.0, 0.0, None, None)),
(('buy', 100, 15), (200.0, 12.5, 2500.0, 0.0, None, None)),
(('sell', 50, 12), (150.0, 12.5, 1875.0, -28.0625, -28.0625, -0.044788)),
(('sell', 100, 20), (50.0, 12.5, 625.0, 713.8125, 741.875, 0.59201995)),
(('sell', 50, 5), (50.0, 12.5, 625.0, 336.625, 336.625, 0.1343142)), # final profit (sum)
),
(
(('buy', 100, 3), (100.0, 3.0, 300.0, 0.0, None, None)),
(('buy', 100, 7), (200.0, 5.0, 1000.0, 0.0, None, None)),
(('sell', 100, 11), (100.0, 5.0, 500.0, 596.0, 596.0, 1.189027)),
(('buy', 150, 15), (250.0, 11.0, 2750.0, 596.0, 596.0, 1.189027)),
(('sell', 100, 19), (150.0, 11.0, 1650.0, 1388.5, 792.5, 0.7186579)),
(('sell', 150, 23), (150.0, 11.0, 1650.0, 3175.75, 3175.75, 0.9747170)), # final profit
)
])
def test_position_adjust3(mocker, default_conf_usdt, fee, data) -> None:
default_conf_usdt.update({
"position_adjustment_enable": True,
"dry_run": False,
"stake_amount": 200.0,
"dry_run_wallet": 1000.0,
})
patch_RPCManager(mocker)
patch_exchange(mocker)
patch_wallet(mocker, free=10000)
freqtrade = FreqtradeBot(default_conf_usdt)
trade = None
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True)
for idx, (order, result) in enumerate(data):
amount = order[1]
price = order[2]
price_mock = MagicMock(return_value=price)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_rate=price_mock,
fetch_ticker=MagicMock(return_value={
'bid': 10,
'ask': 12,
'last': 11
}),
get_min_pair_stake_amount=MagicMock(return_value=1),
get_fee=fee,
)
pair = 'ETH/USDT'
closed_successful_order = {
'pair': pair,
'ft_pair': pair,
'ft_order_side': order[0],
'side': order[0],
'type': 'limit',
'status': 'closed',
'price': price,
'average': price,
'cost': price * amount,
'amount': amount,
'filled': amount,
'ft_is_open': False,
'id': f'60{idx}',
'order_id': f'60{idx}'
}
mocker.patch('freqtrade.exchange.Exchange.create_order',
MagicMock(return_value=closed_successful_order))
mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
MagicMock(return_value=closed_successful_order))
if order[0] == 'buy':
assert freqtrade.execute_entry(pair, amount, trade=trade)
else:
assert freqtrade.execute_trade_exit(
trade=trade, limit=price,
exit_check=ExitCheckTuple(exit_type=ExitType.PARTIAL_EXIT),
sub_trade_amt=amount)
orders1 = Order.query.all()
assert orders1
assert len(orders1) == idx + 1
trade = Trade.query.first()
assert trade
if idx < len(data) - 1:
assert trade.is_open is True
assert trade.open_order_id is None
assert trade.amount == result[0]
assert trade.open_rate == result[1]
assert trade.stake_amount == result[2]
assert pytest.approx(trade.realized_profit) == result[3]
assert pytest.approx(trade.close_profit_abs) == result[4]
assert pytest.approx(trade.close_profit) == result[5]
order_obj = trade.select_order(order[0], False)
assert order_obj.order_id == f'60{idx}'
trade = Trade.query.first()
assert trade
assert trade.open_order_id is None
assert trade.is_open is False
def test_process_open_trade_positions_exception(mocker, default_conf_usdt, fee, caplog) -> None: def test_process_open_trade_positions_exception(mocker, default_conf_usdt, fee, caplog) -> None:
@ -5556,9 +5900,25 @@ def test_check_and_call_adjust_trade_position(mocker, default_conf_usdt, fee, ca
"max_entry_position_adjustment": 0, "max_entry_position_adjustment": 0,
}) })
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
buy_rate_mock = MagicMock(return_value=10)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_rate=buy_rate_mock,
fetch_ticker=MagicMock(return_value={
'bid': 10,
'ask': 12,
'last': 11
}),
get_min_pair_stake_amount=MagicMock(return_value=1),
get_fee=fee,
)
create_mock_trades(fee) create_mock_trades(fee)
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=10)
freqtrade.process_open_trade_positions() freqtrade.process_open_trade_positions()
assert log_has_re(r"Max adjustment entries for .* has been reached\.", caplog) assert log_has_re(r"Max adjustment entries for .* has been reached\.", caplog)
caplog.clear()
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-10)
freqtrade.process_open_trade_positions()
assert log_has_re(r"LIMIT_SELL has been fulfilled.*", caplog)

View File

@ -6,7 +6,7 @@ from freqtrade.enums import ExitCheckTuple, ExitType
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.persistence.models import Order from freqtrade.persistence.models import Order
from freqtrade.rpc.rpc import RPC from freqtrade.rpc.rpc import RPC
from tests.conftest import get_patched_freqtradebot, patch_get_signal from tests.conftest import get_patched_freqtradebot, log_has_re, patch_get_signal
def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
@ -455,3 +455,60 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
# Check the 2 filled orders equal the above amount # Check the 2 filled orders equal the above amount
assert pytest.approx(trade.orders[1].amount) == 30.150753768 assert pytest.approx(trade.orders[1].amount) == 30.150753768
assert pytest.approx(trade.orders[-1].amount) == 61.538461232 assert pytest.approx(trade.orders[-1].amount) == 61.538461232
def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog) -> None:
default_conf_usdt['position_adjustment_enable'] = True
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker_usdt,
get_fee=fee,
amount_to_precision=lambda s, x, y: y,
price_to_precision=lambda s, x, y: y,
get_min_pair_stake_amount=MagicMock(return_value=10),
)
patch_get_signal(freqtrade)
freqtrade.enter_positions()
assert len(Trade.get_trades().all()) == 1
trade = Trade.get_trades().first()
assert len(trade.orders) == 1
assert pytest.approx(trade.stake_amount) == 60
assert pytest.approx(trade.amount) == 30.0
assert trade.open_rate == 2.0
# Too small size
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-59)
freqtrade.process()
trade = Trade.get_trades().first()
assert len(trade.orders) == 1
assert pytest.approx(trade.stake_amount) == 60
assert pytest.approx(trade.amount) == 30.0
assert log_has_re("Remaining amount of 1.6.* would be too small.", caplog)
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-20)
freqtrade.process()
trade = Trade.get_trades().first()
assert len(trade.orders) == 2
assert trade.orders[-1].ft_order_side == 'sell'
assert pytest.approx(trade.stake_amount) == 40.198
assert pytest.approx(trade.amount) == 20.099
assert trade.open_rate == 2.0
assert trade.is_open
caplog.clear()
# Sell more than what we got (we got ~20 coins left)
# First adjusts the amount to 20 - then rejects.
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-50)
freqtrade.process()
assert log_has_re("Adjusting amount to trade.amount as it is higher.*", caplog)
assert log_has_re("Remaining amount of 0.0 would be too small.", caplog)
trade = Trade.get_trades().first()
assert len(trade.orders) == 2
assert trade.orders[-1].ft_order_side == 'sell'
assert pytest.approx(trade.stake_amount) == 40.198
assert trade.is_open

View File

@ -500,7 +500,7 @@ def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_
assert trade.close_profit is None assert trade.close_profit is None
assert trade.close_date is None assert trade.close_date is None
trade.open_order_id = 'something' trade.open_order_id = enter_order['id']
oobj = Order.parse_from_ccxt_object(enter_order, 'ADA/USDT', entry_side) oobj = Order.parse_from_ccxt_object(enter_order, 'ADA/USDT', entry_side)
trade.orders.append(oobj) trade.orders.append(oobj)
trade.update_trade(oobj) trade.update_trade(oobj)
@ -515,7 +515,7 @@ def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_
caplog) caplog)
caplog.clear() caplog.clear()
trade.open_order_id = 'something' trade.open_order_id = enter_order['id']
time_machine.move_to("2022-03-31 21:45:05 +00:00") time_machine.move_to("2022-03-31 21:45:05 +00:00")
oobj = Order.parse_from_ccxt_object(exit_order, 'ADA/USDT', exit_side) oobj = Order.parse_from_ccxt_object(exit_order, 'ADA/USDT', exit_side)
trade.orders.append(oobj) trade.orders.append(oobj)
@ -550,7 +550,7 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
leverage=1.0, leverage=1.0,
) )
trade.open_order_id = 'something' trade.open_order_id = 'mocked_market_buy'
oobj = Order.parse_from_ccxt_object(market_buy_order_usdt, 'ADA/USDT', 'buy') oobj = Order.parse_from_ccxt_object(market_buy_order_usdt, 'ADA/USDT', 'buy')
trade.orders.append(oobj) trade.orders.append(oobj)
trade.update_trade(oobj) trade.update_trade(oobj)
@ -565,7 +565,7 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
caplog.clear() caplog.clear()
trade.is_open = True trade.is_open = True
trade.open_order_id = 'something' trade.open_order_id = 'mocked_market_sell'
oobj = Order.parse_from_ccxt_object(market_sell_order_usdt, 'ADA/USDT', 'sell') oobj = Order.parse_from_ccxt_object(market_sell_order_usdt, 'ADA/USDT', 'sell')
trade.orders.append(oobj) trade.orders.append(oobj)
trade.update_trade(oobj) trade.update_trade(oobj)
@ -630,14 +630,14 @@ def test_calc_open_close_trade_price(
trade.open_rate = 2.0 trade.open_rate = 2.0
trade.close_rate = 2.2 trade.close_rate = 2.2
trade.recalc_open_trade_value() trade.recalc_open_trade_value()
assert isclose(trade._calc_open_trade_value(), open_value) assert isclose(trade._calc_open_trade_value(trade.amount, trade.open_rate), open_value)
assert isclose(trade.calc_close_trade_value(trade.close_rate), close_value) assert isclose(trade.calc_close_trade_value(trade.close_rate), close_value)
assert isclose(trade.calc_profit(trade.close_rate), round(profit, 8)) assert isclose(trade.calc_profit(trade.close_rate), round(profit, 8))
assert pytest.approx(trade.calc_profit_ratio(trade.close_rate)) == profit_ratio assert pytest.approx(trade.calc_profit_ratio(trade.close_rate)) == profit_ratio
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_trade_close(limit_buy_order_usdt, limit_sell_order_usdt, fee): def test_trade_close(fee):
trade = Trade( trade = Trade(
pair='ADA/USDT', pair='ADA/USDT',
stake_amount=60.0, stake_amount=60.0,
@ -815,7 +815,7 @@ def test_calc_open_trade_value(
trade.update_trade(oobj) # Buy @ 2.0 trade.update_trade(oobj) # Buy @ 2.0
# Get the open rate price with the standard fee rate # Get the open rate price with the standard fee rate
assert trade._calc_open_trade_value() == result assert trade._calc_open_trade_value(trade.amount, trade.open_rate) == result
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -905,7 +905,7 @@ def test_calc_close_trade_price(
('binance', False, 1, 1.9, 0.003, -3.3209999, -0.055211970, spot, 0), ('binance', False, 1, 1.9, 0.003, -3.3209999, -0.055211970, spot, 0),
('binance', False, 1, 2.2, 0.003, 5.6520000, 0.093965087, spot, 0), ('binance', False, 1, 2.2, 0.003, 5.6520000, 0.093965087, spot, 0),
# # FUTURES, funding_fee=1 # FUTURES, funding_fee=1
('binance', False, 1, 2.1, 0.0025, 3.6925, 0.06138819, futures, 1), ('binance', False, 1, 2.1, 0.0025, 3.6925, 0.06138819, futures, 1),
('binance', False, 3, 2.1, 0.0025, 3.6925, 0.18416458, futures, 1), ('binance', False, 3, 2.1, 0.0025, 3.6925, 0.18416458, futures, 1),
('binance', True, 1, 2.1, 0.0025, -2.3074999, -0.03855472, futures, 1), ('binance', True, 1, 2.1, 0.0025, -2.3074999, -0.03855472, futures, 1),
@ -1191,6 +1191,11 @@ def test_calc_profit(
assert pytest.approx(trade.calc_profit(rate=close_rate)) == round(profit, 8) assert pytest.approx(trade.calc_profit(rate=close_rate)) == round(profit, 8)
assert pytest.approx(trade.calc_profit_ratio(rate=close_rate)) == round(profit_ratio, 8) assert pytest.approx(trade.calc_profit_ratio(rate=close_rate)) == round(profit_ratio, 8)
assert pytest.approx(trade.calc_profit(close_rate, trade.amount,
trade.open_rate)) == round(profit, 8)
assert pytest.approx(trade.calc_profit_ratio(close_rate, trade.amount,
trade.open_rate)) == round(profit_ratio, 8)
def test_migrate_new(mocker, default_conf, fee, caplog): def test_migrate_new(mocker, default_conf, fee, caplog):
""" """
@ -1382,7 +1387,7 @@ def test_migrate_new(mocker, default_conf, fee, 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",
caplog) caplog)
assert trade.open_trade_value == trade._calc_open_trade_value() assert trade.open_trade_value == trade._calc_open_trade_value(trade.amount, trade.open_rate)
assert trade.close_profit_abs is None assert trade.close_profit_abs is None
orders = trade.orders orders = trade.orders
@ -1744,6 +1749,7 @@ def test_to_json(fee):
'stake_amount': 0.001, 'stake_amount': 0.001,
'trade_duration': None, 'trade_duration': None,
'trade_duration_s': None, 'trade_duration_s': None,
'realized_profit': 0.0,
'close_profit': None, 'close_profit': None,
'close_profit_pct': None, 'close_profit_pct': None,
'close_profit_abs': None, 'close_profit_abs': None,
@ -1820,6 +1826,7 @@ def test_to_json(fee):
'initial_stop_loss_abs': None, 'initial_stop_loss_abs': None,
'initial_stop_loss_pct': None, 'initial_stop_loss_pct': None,
'initial_stop_loss_ratio': None, 'initial_stop_loss_ratio': None,
'realized_profit': 0.0,
'close_profit': None, 'close_profit': None,
'close_profit_pct': None, 'close_profit_pct': None,
'close_profit_abs': None, 'close_profit_abs': None,
@ -2421,7 +2428,7 @@ def test_recalc_trade_from_orders(fee):
) )
assert fee.return_value == 0.0025 assert fee.return_value == 0.0025
assert trade._calc_open_trade_value() == o1_trade_val assert trade._calc_open_trade_value(trade.amount, trade.open_rate) == o1_trade_val
assert trade.amount == o1_amount assert trade.amount == o1_amount
assert trade.stake_amount == o1_cost assert trade.stake_amount == o1_cost
assert trade.open_rate == o1_rate assert trade.open_rate == o1_rate
@ -2533,7 +2540,8 @@ def test_recalc_trade_from_orders(fee):
assert pytest.approx(trade.fee_open_cost) == o1_fee_cost + o2_fee_cost + o3_fee_cost assert pytest.approx(trade.fee_open_cost) == o1_fee_cost + o2_fee_cost + o3_fee_cost
assert pytest.approx(trade.open_trade_value) == o1_trade_val + o2_trade_val + o3_trade_val assert pytest.approx(trade.open_trade_value) == o1_trade_val + o2_trade_val + o3_trade_val
# Just to make sure sell orders are ignored, let's calculate one more time. # Just to make sure full sell orders are ignored, let's calculate one more time.
sell1 = Order( sell1 = Order(
ft_order_side='sell', ft_order_side='sell',
ft_pair=trade.pair, ft_pair=trade.pair,
@ -2695,7 +2703,7 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short):
assert trade.open_trade_value == 2 * o1_trade_val assert trade.open_trade_value == 2 * o1_trade_val
assert trade.nr_of_successful_entries == 2 assert trade.nr_of_successful_entries == 2
# Just to make sure exit orders are ignored, let's calculate one more time. # Reduce position - this will reduce amount again.
sell1 = Order( sell1 = Order(
ft_order_side=exit_side, ft_order_side=exit_side,
ft_pair=trade.pair, ft_pair=trade.pair,
@ -2706,7 +2714,7 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short):
side=exit_side, side=exit_side,
price=4, price=4,
average=3, average=3,
filled=2, filled=o1_amount,
remaining=1, remaining=1,
cost=5, cost=5,
order_date=trade.open_date, order_date=trade.open_date,
@ -2715,11 +2723,11 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short):
trade.orders.append(sell1) trade.orders.append(sell1)
trade.recalc_trade_from_orders() trade.recalc_trade_from_orders()
assert trade.amount == 2 * o1_amount assert trade.amount == o1_amount
assert trade.stake_amount == 2 * o1_amount assert trade.stake_amount == o1_amount
assert trade.open_rate == o1_rate assert trade.open_rate == o1_rate
assert trade.fee_open_cost == 2 * o1_fee_cost assert trade.fee_open_cost == o1_fee_cost
assert trade.open_trade_value == 2 * o1_trade_val assert trade.open_trade_value == o1_trade_val
assert trade.nr_of_successful_entries == 2 assert trade.nr_of_successful_entries == 2
# Check with 1 order # Check with 1 order
@ -2743,11 +2751,11 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short):
trade.recalc_trade_from_orders() trade.recalc_trade_from_orders()
# Calling recalc with single initial order should not change anything # Calling recalc with single initial order should not change anything
assert trade.amount == 3 * o1_amount assert trade.amount == 2 * o1_amount
assert trade.stake_amount == 3 * o1_amount assert trade.stake_amount == 2 * o1_amount
assert trade.open_rate == o1_rate assert trade.open_rate == o1_rate
assert trade.fee_open_cost == 3 * o1_fee_cost assert trade.fee_open_cost == 2 * o1_fee_cost
assert trade.open_trade_value == 3 * o1_trade_val assert trade.open_trade_value == 2 * o1_trade_val
assert trade.nr_of_successful_entries == 3 assert trade.nr_of_successful_entries == 3
@ -2815,3 +2823,144 @@ def test_order_to_ccxt(limit_buy_order_open):
del raw_order['stopPrice'] del raw_order['stopPrice']
del limit_buy_order_open['datetime'] del limit_buy_order_open['datetime']
assert raw_order == limit_buy_order_open assert raw_order == limit_buy_order_open
@pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize('data', [
{
# tuple 1 - side, amount, price
# tuple 2 - amount, open_rate, stake_amount, cumulative_profit, realized_profit, rel_profit
'orders': [
(('buy', 100, 10), (100.0, 10.0, 1000.0, 0.0, None, None)),
(('buy', 100, 15), (200.0, 12.5, 2500.0, 0.0, None, None)),
(('sell', 50, 12), (150.0, 12.5, 1875.0, -25.0, -25.0, -0.04)),
(('sell', 100, 20), (50.0, 12.5, 625.0, 725.0, 750.0, 0.60)),
(('sell', 50, 5), (50.0, 12.5, 625.0, 350.0, -375.0, -0.60)),
],
'end_profit': 350.0,
'end_profit_ratio': 0.14,
'fee': 0.0,
},
{
'orders': [
(('buy', 100, 10), (100.0, 10.0, 1000.0, 0.0, None, None)),
(('buy', 100, 15), (200.0, 12.5, 2500.0, 0.0, None, None)),
(('sell', 50, 12), (150.0, 12.5, 1875.0, -28.0625, -28.0625, -0.044788)),
(('sell', 100, 20), (50.0, 12.5, 625.0, 713.8125, 741.875, 0.59201995)),
(('sell', 50, 5), (50.0, 12.5, 625.0, 336.625, -377.1875, -0.60199501)),
],
'end_profit': 336.625,
'end_profit_ratio': 0.1343142,
'fee': 0.0025,
},
{
'orders': [
(('buy', 100, 3), (100.0, 3.0, 300.0, 0.0, None, None)),
(('buy', 100, 7), (200.0, 5.0, 1000.0, 0.0, None, None)),
(('sell', 100, 11), (100.0, 5.0, 500.0, 596.0, 596.0, 1.189027)),
(('buy', 150, 15), (250.0, 11.0, 2750.0, 596.0, 596.0, 1.189027)),
(('sell', 100, 19), (150.0, 11.0, 1650.0, 1388.5, 792.5, 0.7186579)),
(('sell', 150, 23), (150.0, 11.0, 1650.0, 3175.75, 1787.25, 1.08048062)),
],
'end_profit': 3175.75,
'end_profit_ratio': 0.9747170,
'fee': 0.0025,
},
{
# Test above without fees
'orders': [
(('buy', 100, 3), (100.0, 3.0, 300.0, 0.0, None, None)),
(('buy', 100, 7), (200.0, 5.0, 1000.0, 0.0, None, None)),
(('sell', 100, 11), (100.0, 5.0, 500.0, 600.0, 600.0, 1.2)),
(('buy', 150, 15), (250.0, 11.0, 2750.0, 600.0, 600.0, 1.2)),
(('sell', 100, 19), (150.0, 11.0, 1650.0, 1400.0, 800.0, 0.72727273)),
(('sell', 150, 23), (150.0, 11.0, 1650.0, 3200.0, 1800.0, 1.09090909)),
],
'end_profit': 3200.0,
'end_profit_ratio': 0.98461538,
'fee': 0.0,
},
{
'orders': [
(('buy', 100, 8), (100.0, 8.0, 800.0, 0.0, None, None)),
(('buy', 100, 9), (200.0, 8.5, 1700.0, 0.0, None, None)),
(('sell', 100, 10), (100.0, 8.5, 850.0, 150.0, 150.0, 0.17647059)),
(('buy', 150, 11), (250.0, 10, 2500.0, 150.0, 150.0, 0.17647059)),
(('sell', 100, 12), (150.0, 10.0, 1500.0, 350.0, 350.0, 0.2)),
(('sell', 150, 14), (150.0, 10.0, 1500.0, 950.0, 950.0, 0.40)),
],
'end_profit': 950.0,
'end_profit_ratio': 0.283582,
'fee': 0.0,
},
])
def test_recalc_trade_from_orders_dca(data) -> None:
pair = 'ETH/USDT'
trade = Trade(
id=2,
pair=pair,
stake_amount=1000,
open_rate=data['orders'][0][0][2],
amount=data['orders'][0][0][1],
is_open=True,
open_date=arrow.utcnow().datetime,
fee_open=data['fee'],
fee_close=data['fee'],
exchange='binance',
is_short=False,
leverage=1.0,
trading_mode=TradingMode.SPOT
)
Trade.query.session.add(trade)
for idx, (order, result) in enumerate(data['orders']):
amount = order[1]
price = order[2]
order_obj = Order(
ft_order_side=order[0],
ft_pair=trade.pair,
order_id=f"order_{order[0]}_{idx}",
ft_is_open=False,
status="closed",
symbol=trade.pair,
order_type="market",
side=order[0],
price=price,
average=price,
filled=amount,
remaining=0,
cost=amount * price,
order_date=arrow.utcnow().shift(hours=-10 + idx).datetime,
order_filled_date=arrow.utcnow().shift(hours=-10 + idx).datetime,
)
trade.orders.append(order_obj)
trade.recalc_trade_from_orders()
Trade.commit()
orders1 = Order.query.all()
assert orders1
assert len(orders1) == idx + 1
trade = Trade.query.first()
assert trade
assert len(trade.orders) == idx + 1
if idx < len(data) - 1:
assert trade.is_open is True
assert trade.open_order_id is None
assert trade.amount == result[0]
assert trade.open_rate == result[1]
assert trade.stake_amount == result[2]
# TODO: enable the below.
assert pytest.approx(trade.realized_profit) == result[3]
# assert pytest.approx(trade.close_profit_abs) == result[4]
assert pytest.approx(trade.close_profit) == result[5]
trade.close(price)
assert pytest.approx(trade.close_profit_abs) == data['end_profit']
assert pytest.approx(trade.close_profit) == data['end_profit_ratio']
assert not trade.is_open
trade = Trade.query.first()
assert trade
assert trade.open_order_id is None