Merge pull request #9151 from freqtrade/refactor/calc_profits
Refactor calculate profits
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user