Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions freqtrade/data/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,10 @@ def calculate_expectancy(trades: pd.DataFrame) -> tuple[float, float]:


def calculate_sortino(
trades: pd.DataFrame, min_date: datetime, max_date: datetime, starting_balance: float
trades: pd.DataFrame,
min_date: datetime | None,
max_date: datetime | None,
starting_balance: float,
) -> float:
"""
Calculate sortino
Expand Down Expand Up @@ -362,7 +365,10 @@ def calculate_sortino(


def calculate_sharpe(
trades: pd.DataFrame, min_date: datetime, max_date: datetime, starting_balance: float
trades: pd.DataFrame,
min_date: datetime | None,
max_date: datetime | None,
starting_balance: float,
) -> float:
"""
Calculate sharpe
Expand All @@ -389,7 +395,10 @@ def calculate_sharpe(


def calculate_calmar(
trades: pd.DataFrame, min_date: datetime, max_date: datetime, starting_balance: float
trades: pd.DataFrame,
min_date: datetime | None,
max_date: datetime | None,
starting_balance: float,
) -> float:
"""
Calculate calmar
Expand Down
5 changes: 5 additions & 0 deletions freqtrade/rpc/api_server/api_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ class Profit(BaseModel):
winrate: float
expectancy: float
expectancy_ratio: float
sharpe: float
sortino: float
sqn: float
calmar: float
cagr: float
max_drawdown: float
max_drawdown_abs: float
max_drawdown_start: str
Expand Down
14 changes: 2 additions & 12 deletions freqtrade/rpc/fiat_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@

import logging
from datetime import datetime
from typing import Any

from requests.exceptions import RequestException

from freqtrade.constants import SUPPORTED_FIAT, Config
from freqtrade.mixins.logging_mixin import LoggingMixin
from freqtrade.util import FtTTLCache
from freqtrade.util.coin_gecko import FtCoinGeckoApi
from freqtrade.util.singleton import SingletonMeta


logger = logging.getLogger(__name__)
Expand All @@ -32,26 +32,16 @@
}


class CryptoToFiatConverter(LoggingMixin):
class CryptoToFiatConverter(LoggingMixin, metaclass=SingletonMeta):
"""
Main class to initiate Crypto to FIAT.
This object contains a list of pair Crypto, FIAT
This object is also a Singleton
"""

__instance = None

_coinlistings: list[dict] = []
_backoff: float = 0.0

def __new__(cls, *args: Any, **kwargs: Any) -> Any:
"""
Singleton pattern to ensure only one instance is created.
"""
if not cls.__instance:
cls.__instance = super().__new__(cls)
return cls.__instance

def __init__(self, config: Config) -> None:
# Timeout: 6h
self._pair_price: FtTTLCache = FtTTLCache(maxsize=500, ttl=6 * 60 * 60)
Expand Down
44 changes: 43 additions & 1 deletion freqtrade/rpc/rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,16 @@
from freqtrade.configuration.timerange import TimeRange
from freqtrade.constants import CANCEL_REASON, DEFAULT_DATAFRAME_COLUMNS, Config
from freqtrade.data.history import load_data
from freqtrade.data.metrics import DrawDownResult, calculate_expectancy, calculate_max_drawdown
from freqtrade.data.metrics import (
DrawDownResult,
calculate_cagr,
calculate_calmar,
calculate_expectancy,
calculate_max_drawdown,
calculate_sharpe,
calculate_sortino,
calculate_sqn,
)
from freqtrade.enums import (
CandleType,
ExitCheckTuple,
Expand Down Expand Up @@ -689,6 +698,34 @@ def _rpc_trade_statistics(
last_date = trades[-1].open_date_utc if trades else None
num = float(len(durations) or 1)
bot_start = KeyValueStore.get_datetime_value("bot_start_time")

sharpe = calculate_sharpe(
trades=trades_df,
min_date=first_date,
max_date=last_date,
starting_balance=starting_balance,
)
sortino = calculate_sortino(
trades=trades_df,
min_date=first_date,
max_date=last_date,
starting_balance=starting_balance,
)
sqn = calculate_sqn(trades=trades_df, starting_balance=starting_balance)
calmar = calculate_calmar(
trades=trades_df,
min_date=first_date,
max_date=last_date,
starting_balance=starting_balance,
)
current_balance = self._freqtrade.wallets.get_total_stake_amount()
days_passed = max(1, (last_date - first_date).days) if first_date and last_date else 1
cagr = calculate_cagr(
starting_balance=starting_balance,
final_balance=current_balance,
days_passed=days_passed,
)

return {
"profit_closed_coin": profit_closed_coin_sum,
"profit_closed_percent_mean": round(profit_closed_ratio_mean * 100, 2),
Expand Down Expand Up @@ -725,6 +762,11 @@ def _rpc_trade_statistics(
"winrate": winrate,
"expectancy": expectancy,
"expectancy_ratio": expectancy_ratio,
"sharpe": sharpe,
"sortino": sortino,
"sqn": sqn,
"calmar": calmar,
"cagr": cagr,
"max_drawdown": drawdown.relative_account_drawdown,
"max_drawdown_abs": drawdown.drawdown_abs,
"max_drawdown_start": format_date(drawdown.high_date),
Expand Down
16 changes: 16 additions & 0 deletions freqtrade/util/singleton.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from typing import Any


class SingletonMeta(type):
"""
A thread-safe implementation of Singleton.
Use as metaclass to create singleton classes.
"""

_instances: dict = {}

def __call__(cls, *args: Any, **kwargs: Any) -> Any:
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
8 changes: 8 additions & 0 deletions tests/rpc/test_fiat_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
from tests.conftest import log_has, log_has_re


@pytest.fixture(autouse=True)
def reset_singleton_instance():
# Reset the singleton instance before each test
CryptoToFiatConverter._instances = {}
yield
CryptoToFiatConverter._instances = {}


def test_fiat_convert_is_singleton():
fiat_convert = CryptoToFiatConverter({"a": 22})
fiat_convert2 = CryptoToFiatConverter({})
Expand Down
22 changes: 6 additions & 16 deletions tests/rpc/test_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:

def test_rpc_status_table(default_conf, ticker, fee, mocker, time_machine) -> None:
time_machine.move_to("2024-05-10 11:15:00 +00:00", tick=False)
mocker.patch.multiple(
"freqtrade.rpc.fiat_convert.FtCoinGeckoApi",
get_price=MagicMock(return_value={"bitcoin": {"usd": 15000.0}}),
)
mocker.patch("freqtrade.rpc.rpc.CryptoToFiatConverter._find_price", return_value=15000.0)

mocker.patch("freqtrade.rpc.telegram.Telegram", MagicMock())
mocker.patch.multiple(
EXMS,
Expand Down Expand Up @@ -279,6 +275,8 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker, time_machine) -> No
# Test with fiat convert
rpc._config["fiat_display_currency"] = "USD"
rpc._fiat_converter = CryptoToFiatConverter({})
mocker.patch.object(rpc._fiat_converter, "get_price", return_value=15000.0)

result, headers, fiat_profit_sum, total_sum = rpc._rpc_status_table(
default_conf["stake_currency"], "USD"
)
Expand Down Expand Up @@ -447,7 +445,6 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short):


def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None:
mocker.patch("freqtrade.rpc.rpc.CryptoToFiatConverter._find_price", return_value=1.1)
mocker.patch("freqtrade.rpc.telegram.Telegram", MagicMock())
mocker.patch.multiple(
EXMS,
Expand All @@ -461,6 +458,7 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None:

rpc = RPC(freqtradebot)
rpc._fiat_converter = CryptoToFiatConverter({})
mocker.patch.object(rpc._fiat_converter, "get_price", return_value=1.1)

res = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert res["trade_count"] == 0
Expand Down Expand Up @@ -520,11 +518,6 @@ def test_rpc_balance_handle_error(default_conf, mocker):
}
# ETH will be skipped due to mocked Error below

mocker.patch.multiple(
"freqtrade.rpc.fiat_convert.FtCoinGeckoApi",
get_price=MagicMock(return_value={"bitcoin": {"usd": 15000.0}}),
)
mocker.patch("freqtrade.rpc.rpc.CryptoToFiatConverter._find_price", return_value=15000.0)
mocker.patch("freqtrade.rpc.telegram.Telegram", MagicMock())
mocker.patch.multiple(
EXMS,
Expand All @@ -536,6 +529,7 @@ def test_rpc_balance_handle_error(default_conf, mocker):
patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot)
rpc._fiat_converter = CryptoToFiatConverter({})
mocker.patch.object(rpc._fiat_converter, "get_price", return_value=15000.0)
res = rpc._rpc_balance(default_conf["stake_currency"], default_conf["fiat_display_currency"])
assert res["stake"] == "BTC"

Expand Down Expand Up @@ -606,11 +600,6 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers, proxy_coin, marg
}
]

mocker.patch.multiple(
"freqtrade.rpc.fiat_convert.FtCoinGeckoApi",
get_price=MagicMock(return_value={"bitcoin": {"usd": 1.2}}),
)
mocker.patch("freqtrade.rpc.rpc.CryptoToFiatConverter._find_price", return_value=1.2)
mocker.patch("freqtrade.rpc.telegram.Telegram", MagicMock())
mocker.patch.multiple(
EXMS,
Expand All @@ -629,6 +618,7 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers, proxy_coin, marg
patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot)
rpc._fiat_converter = CryptoToFiatConverter({})
mocker.patch.object(rpc._fiat_converter, "get_price", return_value=1.2)

result = rpc._rpc_balance(
default_conf_usdt["stake_currency"], default_conf_usdt["fiat_display_currency"]
Expand Down
20 changes: 20 additions & 0 deletions tests/rpc/test_rpc_apiserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -1199,6 +1199,11 @@ def test_api_logs(botclient):
"winrate": 0.0,
"expectancy": -0.0033695635,
"expectancy_ratio": -1.0,
"cagr": -0.0024567404889381805,
"calmar": -1910.497317469542,
"sharpe": -58.138247358830355,
"sortino": -58.138247358830355,
"sqn": -1.5215,
"trading_volume": 75.945,
},
),
Expand Down Expand Up @@ -1231,6 +1236,11 @@ def test_api_logs(botclient):
"winrate": 1.0,
"expectancy": 0.0003695635,
"expectancy_ratio": 100,
"cagr": 0.0002698167695580622,
"calmar": -100.0,
"sharpe": 65.81269184917424,
"sortino": -100.0,
"sqn": 1.7224,
"trading_volume": 75.945,
},
),
Expand Down Expand Up @@ -1263,6 +1273,11 @@ def test_api_logs(botclient):
"winrate": 0.5,
"expectancy": -0.0027145635000000003,
"expectancy_ratio": -0.48612137582114445,
"cagr": -0.0019796559404918757,
"calmar": -1857.4671689202785,
"sharpe": -36.14602907243071,
"sortino": -100.0,
"sqn": -0.946,
"trading_volume": 75.945,
},
),
Expand Down Expand Up @@ -1326,6 +1341,11 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected)
"winrate": expected["winrate"],
"expectancy": expected["expectancy"],
"expectancy_ratio": expected["expectancy_ratio"],
"sharpe": expected["sharpe"],
"sortino": expected["sortino"],
"sqn": expected["sqn"],
"calmar": expected["calmar"],
"cagr": expected["cagr"],
"max_drawdown": ANY,
"max_drawdown_abs": ANY,
"max_drawdown_start": ANY,
Expand Down
Loading
Loading