Merge pull request #9151 from freqtrade/refactor/calc_profits

Refactor calculate profits
This commit is contained in:
Matthias
2023-09-05 06:26:04 +02:00
committed by GitHub
5 changed files with 104 additions and 40 deletions
+9 -12
View File
@@ -1731,14 +1731,12 @@ class FreqtradeBot(LoggingMixin):
amount = order.safe_filled if fill else order.safe_amount amount = order.safe_filled if fill else order.safe_amount
order_rate: float = order.safe_price order_rate: float = order.safe_price
profit = trade.calc_profit(rate=order_rate, amount=amount, open_rate=trade.open_rate) profit = trade.calculate_profit(order_rate, amount, trade.open_rate)
profit_ratio = trade.calc_profit_ratio(order_rate, amount, trade.open_rate)
else: else:
order_rate = trade.safe_close_rate order_rate = trade.safe_close_rate
profit = trade.calc_profit(rate=order_rate) + (0.0 if fill else trade.realized_profit) profit = trade.calculate_profit(rate=order_rate)
profit_ratio = trade.calc_profit_ratio(order_rate)
amount = trade.amount amount = trade.amount
gain = "profit" if profit_ratio > 0 else "loss" gain = "profit" if profit.profit_ratio > 0 else "loss"
msg: RPCSellMsg = { msg: RPCSellMsg = {
'type': (RPCMessageType.EXIT_FILL if fill 'type': (RPCMessageType.EXIT_FILL if fill
@@ -1756,8 +1754,8 @@ class FreqtradeBot(LoggingMixin):
'open_rate': trade.open_rate, 'open_rate': trade.open_rate,
'close_rate': order_rate, 'close_rate': order_rate,
'current_rate': current_rate, 'current_rate': current_rate,
'profit_amount': profit, 'profit_amount': profit.profit_abs if fill else profit.total_profit,
'profit_ratio': profit_ratio, 'profit_ratio': profit.profit_ratio,
'buy_tag': trade.enter_tag, 'buy_tag': trade.enter_tag,
'enter_tag': trade.enter_tag, 'enter_tag': trade.enter_tag,
'sell_reason': trade.exit_reason, # Deprecated 'sell_reason': trade.exit_reason, # Deprecated
@@ -1789,11 +1787,10 @@ class FreqtradeBot(LoggingMixin):
order = self.order_obj_or_raise(order_id, order_or_none) order = self.order_obj_or_raise(order_id, order_or_none)
profit_rate: float = trade.safe_close_rate profit_rate: float = trade.safe_close_rate
profit_trade = trade.calc_profit(rate=profit_rate) profit = trade.calculate_profit(rate=profit_rate)
current_rate = self.exchange.get_rate( current_rate = self.exchange.get_rate(
trade.pair, side='exit', is_short=trade.is_short, refresh=False) trade.pair, side='exit', is_short=trade.is_short, refresh=False)
profit_ratio = trade.calc_profit_ratio(profit_rate) gain = "profit" if profit.profit_ratio > 0 else "loss"
gain = "profit" if profit_ratio > 0 else "loss"
msg: RPCSellCancelMsg = { msg: RPCSellCancelMsg = {
'type': RPCMessageType.EXIT_CANCEL, 'type': RPCMessageType.EXIT_CANCEL,
@@ -1808,8 +1805,8 @@ class FreqtradeBot(LoggingMixin):
'amount': order.safe_amount_after_fee, '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.profit_abs,
'profit_ratio': profit_ratio, 'profit_ratio': profit.profit_ratio,
'buy_tag': trade.enter_tag, 'buy_tag': trade.enter_tag,
'enter_tag': trade.enter_tag, 'enter_tag': trade.enter_tag,
'sell_reason': trade.exit_reason, # Deprecated 'sell_reason': trade.exit_reason, # Deprecated
+54 -10
View File
@@ -3,6 +3,7 @@ This module contains the class to persist trades into SQLite
""" """
import logging import logging
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from math import isclose from math import isclose
from typing import Any, ClassVar, Dict, List, Optional, Sequence, cast from typing import Any, ClassVar, Dict, List, Optional, Sequence, cast
@@ -26,6 +27,14 @@ from freqtrade.util import FtPrecise, dt_now
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@dataclass
class ProfitStruct:
profit_abs: float
profit_ratio: float
total_profit: float
total_profit_ratio: float
class Order(ModelBase): class Order(ModelBase):
""" """
Order database model Order database model
@@ -888,11 +897,26 @@ class LocalTrade:
open_rate: Optional[float] = None) -> float: open_rate: Optional[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
Deprecated - only available for backwards compatibility
: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 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. :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
""" """
prof = self.calculate_profit(rate, amount, open_rate)
return prof.profit_abs
def calculate_profit(self, rate: float, amount: Optional[float] = None,
open_rate: Optional[float] = None) -> ProfitStruct:
"""
Calculate profit metrics (absolute, ratio, total, total ratio).
All calculations include fees.
: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 structure, containing absolute and relative profits.
"""
close_trade_value = self.calc_close_trade_value(rate, amount) close_trade_value = self.calc_close_trade_value(rate, amount)
if amount is None or open_rate is None: if amount is None or open_rate is None:
open_trade_value = self.open_trade_value open_trade_value = self.open_trade_value
@@ -900,10 +924,33 @@ class LocalTrade:
open_trade_value = self._calc_open_trade_value(amount, open_rate) open_trade_value = self._calc_open_trade_value(amount, open_rate)
if self.is_short: if self.is_short:
profit = open_trade_value - close_trade_value profit_abs = open_trade_value - close_trade_value
else: else:
profit = close_trade_value - open_trade_value profit_abs = close_trade_value - open_trade_value
return float(f"{profit:.8f}")
try:
if self.is_short:
profit_ratio = (1 - (close_trade_value / open_trade_value)) * self.leverage
else:
profit_ratio = ((close_trade_value / open_trade_value) - 1) * self.leverage
profit_ratio = float(f"{profit_ratio:.8f}")
except ZeroDivisionError:
profit_ratio = 0.0
total_profit_abs = profit_abs + self.realized_profit
total_profit_ratio = (
(total_profit_abs / self.max_stake_amount) * self.leverage
if self.max_stake_amount else 0.0
)
total_profit_ratio = float(f"{total_profit_ratio:.8f}")
profit_abs = float(f"{profit_abs:.8f}")
return ProfitStruct(
profit_abs=profit_abs,
profit_ratio=profit_ratio,
total_profit=profit_abs + self.realized_profit,
total_profit_ratio=total_profit_ratio,
)
def calc_profit_ratio( def calc_profit_ratio(
self, rate: float, amount: Optional[float] = None, self, rate: float, amount: Optional[float] = None,
@@ -944,7 +991,6 @@ class LocalTrade:
avg_price = FtPrecise(0.0) avg_price = FtPrecise(0.0)
close_profit = 0.0 close_profit = 0.0
close_profit_abs = 0.0 close_profit_abs = 0.0
profit = None
# Reset funding fees # Reset funding fees
self.funding_fees = 0.0 self.funding_fees = 0.0
funding_fees = 0.0 funding_fees = 0.0
@@ -974,11 +1020,9 @@ class LocalTrade:
exit_rate = o.safe_price exit_rate = o.safe_price
exit_amount = o.safe_amount_after_fee exit_amount = o.safe_amount_after_fee
profit = self.calc_profit(rate=exit_rate, amount=exit_amount, prof = self.calculate_profit(exit_rate, exit_amount, float(avg_price))
open_rate=float(avg_price)) close_profit_abs += prof.profit_abs
close_profit_abs += profit close_profit = prof.profit_ratio
close_profit = self.calc_profit_ratio(
exit_rate, amount=exit_amount, open_rate=avg_price)
else: else:
total_stake = total_stake + self._calc_open_trade_value(tmp_amount, price) total_stake = total_stake + self._calc_open_trade_value(tmp_amount, price)
max_stake_amount += (tmp_amount * price) max_stake_amount += (tmp_amount * price)
@@ -988,7 +1032,7 @@ class LocalTrade:
if close_profit: if close_profit:
self.close_profit = close_profit self.close_profit = close_profit
self.realized_profit = close_profit_abs self.realized_profit = close_profit_abs
self.close_profit_abs = profit self.close_profit_abs = prof.profit_abs
current_amount_tr = amount_to_contract_precision( current_amount_tr = amount_to_contract_precision(
float(current_amount), self.amount_precision, self.precision_mode, self.contract_size) float(current_amount), self.amount_precision, self.precision_mode, self.contract_size)
+23 -17
View File
@@ -174,6 +174,8 @@ class RPC:
order: Optional[Order] = None order: Optional[Order] = None
current_profit_fiat: Optional[float] = None current_profit_fiat: Optional[float] = None
total_profit_fiat: Optional[float] = None total_profit_fiat: Optional[float] = None
total_profit_abs = 0.0
total_profit_ratio: Optional[float] = None
if trade.open_order_id: if trade.open_order_id:
order = trade.select_order_by_order_id(trade.open_order_id) order = trade.select_order_by_order_id(trade.open_order_id)
# calculate profit and send message to user # calculate profit and send message to user
@@ -184,23 +186,22 @@ class RPC:
except (ExchangeError, PricingError): except (ExchangeError, PricingError):
current_rate = NAN current_rate = NAN
if len(trade.select_filled_orders(trade.entry_side)) > 0: if len(trade.select_filled_orders(trade.entry_side)) > 0:
current_profit = trade.calc_profit_ratio(
current_rate) if not isnan(current_rate) else NAN current_profit = current_profit_abs = current_profit_fiat = NAN
current_profit_abs = trade.calc_profit( if not isnan(current_rate):
current_rate) if not isnan(current_rate) else NAN prof = trade.calculate_profit(current_rate)
current_profit = prof.profit_ratio
current_profit_abs = prof.profit_abs
total_profit_abs = prof.total_profit
total_profit_ratio = prof.total_profit_ratio
else: else:
current_profit = current_profit_abs = current_profit_fiat = 0.0 current_profit = current_profit_abs = current_profit_fiat = 0.0
else: else:
# Closed trade ... # Closed trade ...
current_rate = trade.close_rate current_rate = trade.close_rate
current_profit = trade.close_profit or 0.0 current_profit = trade.close_profit or 0.0
current_profit_abs = trade.close_profit_abs or 0.0 current_profit_abs = trade.close_profit_abs or 0.0
total_profit_abs = trade.realized_profit + current_profit_abs
total_profit_ratio: Optional[float] = None
if trade.max_stake_amount:
total_profit_ratio = (
(total_profit_abs / trade.max_stake_amount) * trade.leverage
)
# Calculate fiat profit # Calculate fiat profit
if not isnan(current_profit_abs) and self._fiat_converter: if not isnan(current_profit_abs) and self._fiat_converter:
@@ -216,8 +217,11 @@ class RPC:
) )
# Calculate guaranteed profit (in case of trailing stop) # Calculate guaranteed profit (in case of trailing stop)
stoploss_entry_dist = trade.calc_profit(trade.stop_loss) stop_entry = trade.calculate_profit(trade.stop_loss)
stoploss_entry_dist_ratio = trade.calc_profit_ratio(trade.stop_loss)
stoploss_entry_dist = stop_entry.profit_abs
stoploss_entry_dist_ratio = stop_entry.profit_ratio
# calculate distance to stoploss # calculate distance to stoploss
stoploss_current_dist = trade.stop_loss - current_rate stoploss_current_dist = trade.stop_loss - current_rate
stoploss_current_dist_ratio = stoploss_current_dist / current_rate stoploss_current_dist_ratio = stoploss_current_dist / current_rate
@@ -267,8 +271,9 @@ class RPC:
profit_str = f'{NAN:.2%}' profit_str = f'{NAN:.2%}'
else: else:
if trade.nr_of_successful_entries > 0: if trade.nr_of_successful_entries > 0:
trade_profit = trade.calc_profit(current_rate) profit = trade.calculate_profit(current_rate)
profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}' trade_profit = profit.profit_abs
profit_str = f'{profit.profit_ratio:.2%}'
else: else:
trade_profit = 0.0 trade_profit = 0.0
profit_str = f'{0.0:.2f}' profit_str = f'{0.0:.2f}'
@@ -487,9 +492,10 @@ class RPC:
profit_ratio = NAN profit_ratio = NAN
profit_abs = NAN profit_abs = NAN
else: else:
profit_ratio = trade.calc_profit_ratio(rate=current_rate) profit = trade.calculate_profit(trade.close_rate or current_rate)
profit_abs = trade.calc_profit(
rate=trade.close_rate or current_rate) + trade.realized_profit profit_ratio = profit.profit_ratio
profit_abs = profit.total_profit
profit_all_coin.append(profit_abs) profit_all_coin.append(profit_abs)
profit_all_ratio.append(profit_ratio) profit_all_ratio.append(profit_ratio)
+17
View File
@@ -1152,14 +1152,31 @@ def test_calc_profit(
leverage=lev, leverage=lev,
fee_open=0.0025, fee_open=0.0025,
fee_close=fee_close, fee_close=fee_close,
max_stake_amount=60.0,
trading_mode=trading_mode, trading_mode=trading_mode,
funding_fees=funding_fees funding_fees=funding_fees
) )
trade.open_order_id = 'something' trade.open_order_id = 'something'
profit_res = trade.calculate_profit(close_rate)
assert pytest.approx(profit_res.profit_abs) == round(profit, 8)
assert pytest.approx(profit_res.profit_ratio) == round(profit_ratio, 8)
val = trade.open_trade_value * (profit_res.profit_ratio) / lev
assert pytest.approx(val) == profit_res.profit_abs
assert pytest.approx(profit_res.total_profit) == round(profit, 8)
# assert pytest.approx(profit_res.total_profit_ratio) == round(profit_ratio, 8)
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)
profit_res2 = trade.calculate_profit(close_rate, trade.amount, trade.open_rate)
assert pytest.approx(profit_res2.profit_abs) == round(profit, 8)
assert pytest.approx(profit_res2.profit_ratio) == round(profit_ratio, 8)
assert pytest.approx(profit_res2.total_profit) == round(profit, 8)
# assert pytest.approx(profit_res2.total_profit_ratio) == round(profit_ratio, 8)
assert pytest.approx(trade.calc_profit(close_rate, trade.amount, assert pytest.approx(trade.calc_profit(close_rate, trade.amount,
trade.open_rate)) == round(profit, 8) trade.open_rate)) == round(profit, 8)
assert pytest.approx(trade.calc_profit_ratio(close_rate, trade.amount, assert pytest.approx(trade.calc_profit_ratio(close_rate, trade.amount,
+1 -1
View File
@@ -164,7 +164,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
response = deepcopy(gen_response) response = deepcopy(gen_response)
response.update({ response.update({
'max_stake_amount': 0.001, 'max_stake_amount': 0.001,
'total_profit_ratio': pytest.approx(-0.00409), 'total_profit_ratio': pytest.approx(-0.00409153),
}) })
assert results[0] == response assert results[0] == response