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

View File

@@ -1731,14 +1731,12 @@ class FreqtradeBot(LoggingMixin):
amount = order.safe_filled if fill else order.safe_amount
order_rate: float = order.safe_price
profit = trade.calc_profit(rate=order_rate, amount=amount, open_rate=trade.open_rate)
profit_ratio = trade.calc_profit_ratio(order_rate, amount, trade.open_rate)
profit = trade.calculate_profit(order_rate, amount, trade.open_rate)
else:
order_rate = trade.safe_close_rate
profit = trade.calc_profit(rate=order_rate) + (0.0 if fill else trade.realized_profit)
profit_ratio = trade.calc_profit_ratio(order_rate)
profit = trade.calculate_profit(rate=order_rate)
amount = trade.amount
gain = "profit" if profit_ratio > 0 else "loss"
gain = "profit" if profit.profit_ratio > 0 else "loss"
msg: RPCSellMsg = {
'type': (RPCMessageType.EXIT_FILL if fill
@@ -1756,8 +1754,8 @@ class FreqtradeBot(LoggingMixin):
'open_rate': trade.open_rate,
'close_rate': order_rate,
'current_rate': current_rate,
'profit_amount': profit,
'profit_ratio': profit_ratio,
'profit_amount': profit.profit_abs if fill else profit.total_profit,
'profit_ratio': profit.profit_ratio,
'buy_tag': trade.enter_tag,
'enter_tag': trade.enter_tag,
'sell_reason': trade.exit_reason, # Deprecated
@@ -1789,11 +1787,10 @@ class FreqtradeBot(LoggingMixin):
order = self.order_obj_or_raise(order_id, order_or_none)
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(
trade.pair, side='exit', is_short=trade.is_short, refresh=False)
profit_ratio = trade.calc_profit_ratio(profit_rate)
gain = "profit" if profit_ratio > 0 else "loss"
gain = "profit" if profit.profit_ratio > 0 else "loss"
msg: RPCSellCancelMsg = {
'type': RPCMessageType.EXIT_CANCEL,
@@ -1808,8 +1805,8 @@ class FreqtradeBot(LoggingMixin):
'amount': order.safe_amount_after_fee,
'open_rate': trade.open_rate,
'current_rate': current_rate,
'profit_amount': profit_trade,
'profit_ratio': profit_ratio,
'profit_amount': profit.profit_abs,
'profit_ratio': profit.profit_ratio,
'buy_tag': trade.enter_tag,
'enter_tag': trade.enter_tag,
'sell_reason': trade.exit_reason, # Deprecated

View File

@@ -3,6 +3,7 @@ This module contains the class to persist trades into SQLite
"""
import logging
from collections import defaultdict
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from math import isclose
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__)
@dataclass
class ProfitStruct:
profit_abs: float
profit_ratio: float
total_profit: float
total_profit_ratio: float
class Order(ModelBase):
"""
Order database model
@@ -888,11 +897,26 @@ class LocalTrade:
open_rate: Optional[float] = None) -> float:
"""
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 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
"""
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)
if amount is None or open_rate is None:
open_trade_value = self.open_trade_value
@@ -900,10 +924,33 @@ class LocalTrade:
open_trade_value = self._calc_open_trade_value(amount, open_rate)
if self.is_short:
profit = open_trade_value - close_trade_value
profit_abs = open_trade_value - close_trade_value
else:
profit = close_trade_value - open_trade_value
return float(f"{profit:.8f}")
profit_abs = close_trade_value - open_trade_value
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(
self, rate: float, amount: Optional[float] = None,
@@ -944,7 +991,6 @@ class LocalTrade:
avg_price = FtPrecise(0.0)
close_profit = 0.0
close_profit_abs = 0.0
profit = None
# Reset funding fees
self.funding_fees = 0.0
funding_fees = 0.0
@@ -974,11 +1020,9 @@ class LocalTrade:
exit_rate = o.safe_price
exit_amount = o.safe_amount_after_fee
profit = self.calc_profit(rate=exit_rate, amount=exit_amount,
open_rate=float(avg_price))
close_profit_abs += profit
close_profit = self.calc_profit_ratio(
exit_rate, amount=exit_amount, open_rate=avg_price)
prof = self.calculate_profit(exit_rate, exit_amount, float(avg_price))
close_profit_abs += prof.profit_abs
close_profit = prof.profit_ratio
else:
total_stake = total_stake + self._calc_open_trade_value(tmp_amount, price)
max_stake_amount += (tmp_amount * price)
@@ -988,7 +1032,7 @@ class LocalTrade:
if close_profit:
self.close_profit = close_profit
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(
float(current_amount), self.amount_precision, self.precision_mode, self.contract_size)

View File

@@ -174,6 +174,8 @@ class RPC:
order: Optional[Order] = None
current_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:
order = trade.select_order_by_order_id(trade.open_order_id)
# calculate profit and send message to user
@@ -184,23 +186,22 @@ class RPC:
except (ExchangeError, PricingError):
current_rate = NAN
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_abs = trade.calc_profit(
current_rate) if not isnan(current_rate) else NAN
current_profit = current_profit_abs = current_profit_fiat = NAN
if not isnan(current_rate):
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:
current_profit = current_profit_abs = current_profit_fiat = 0.0
else:
# Closed trade ...
current_rate = trade.close_rate
current_profit = trade.close_profit 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
if not isnan(current_profit_abs) and self._fiat_converter:
@@ -216,8 +217,11 @@ class RPC:
)
# Calculate guaranteed profit (in case of trailing stop)
stoploss_entry_dist = trade.calc_profit(trade.stop_loss)
stoploss_entry_dist_ratio = trade.calc_profit_ratio(trade.stop_loss)
stop_entry = trade.calculate_profit(trade.stop_loss)
stoploss_entry_dist = stop_entry.profit_abs
stoploss_entry_dist_ratio = stop_entry.profit_ratio
# calculate distance to stoploss
stoploss_current_dist = trade.stop_loss - current_rate
stoploss_current_dist_ratio = stoploss_current_dist / current_rate
@@ -267,8 +271,9 @@ class RPC:
profit_str = f'{NAN:.2%}'
else:
if trade.nr_of_successful_entries > 0:
trade_profit = trade.calc_profit(current_rate)
profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}'
profit = trade.calculate_profit(current_rate)
trade_profit = profit.profit_abs
profit_str = f'{profit.profit_ratio:.2%}'
else:
trade_profit = 0.0
profit_str = f'{0.0:.2f}'
@@ -487,9 +492,10 @@ class RPC:
profit_ratio = NAN
profit_abs = NAN
else:
profit_ratio = trade.calc_profit_ratio(rate=current_rate)
profit_abs = trade.calc_profit(
rate=trade.close_rate or current_rate) + trade.realized_profit
profit = trade.calculate_profit(trade.close_rate or current_rate)
profit_ratio = profit.profit_ratio
profit_abs = profit.total_profit
profit_all_coin.append(profit_abs)
profit_all_ratio.append(profit_ratio)

View File

@@ -1152,14 +1152,31 @@ def test_calc_profit(
leverage=lev,
fee_open=0.0025,
fee_close=fee_close,
max_stake_amount=60.0,
trading_mode=trading_mode,
funding_fees=funding_fees
)
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_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,
trade.open_rate)) == round(profit, 8)
assert pytest.approx(trade.calc_profit_ratio(close_rate, trade.amount,

View File

@@ -164,7 +164,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
response = deepcopy(gen_response)
response.update({
'max_stake_amount': 0.001,
'total_profit_ratio': pytest.approx(-0.00409),
'total_profit_ratio': pytest.approx(-0.00409153),
})
assert results[0] == response