Merge branch 'develop' into feat/binance_trades_fast

This commit is contained in:
Matthias
2025-01-27 20:40:59 +01:00
17 changed files with 111 additions and 101 deletions

View File

@@ -560,7 +560,7 @@ jobs:
merge-multiple: true
- name: Publish to PyPI (Test)
uses: pypa/gh-action-pypi-publish@v1.12.3
uses: pypa/gh-action-pypi-publish@v1.12.4
with:
repository-url: https://test.pypi.org/legacy/
@@ -587,7 +587,7 @@ jobs:
merge-multiple: true
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@v1.12.3
uses: pypa/gh-action-pypi-publish@v1.12.4
deploy-docker:

View File

@@ -2,6 +2,6 @@ markdown==3.7
mkdocs==1.6.1
mkdocs-material==9.5.50
mdx_truly_sane_lists==1.3
pymdown-extensions==10.14
pymdown-extensions==10.14.1
jinja2==3.1.5
mike==2.1.3

View File

@@ -444,7 +444,7 @@ def _download_trades_history(
if not trades.empty and since < trades.iloc[-1]["timestamp"]:
# Reset since to the last available point
# - 5 seconds (to ensure we're getting all trades)
since = trades.iloc[-1]["timestamp"] - (5 * 1000)
since = int(trades.iloc[-1]["timestamp"] - (5 * 1000))
logger.info(
f"Using last trade date -5s - Downloading trades for {pair} "
f"since: {format_ms_time(since)}."

View File

@@ -367,7 +367,7 @@ class Binance(Exchange):
return {}
async def _async_get_trade_history_id_startup(
self, pair: str, since: int | None
self, pair: str, since: int
) -> tuple[list[list], str]:
"""
override for initial call

View File

@@ -2960,7 +2960,7 @@ class Exchange:
return trades[-1].get("timestamp")
async def _async_get_trade_history_id_startup(
self, pair: str, since: int | None
self, pair: str, since: int
) -> tuple[list[list], str]:
"""
override for initial trade_history_id call
@@ -2968,7 +2968,7 @@ class Exchange:
return await self._async_fetch_trades(pair, since=since)
async def _async_get_trade_history_id(
self, pair: str, until: int, since: int | None = None, from_id: str | None = None
self, pair: str, *, until: int, since: int, from_id: str | None = None
) -> tuple[str, list[list]]:
"""
Asynchronously gets trade history using fetch_trades
@@ -3022,7 +3022,7 @@ class Exchange:
return (pair, trades)
async def _async_get_trade_history_time(
self, pair: str, until: int, since: int | None = None
self, pair: str, until: int, since: int
) -> tuple[str, list[list]]:
"""
Asynchronously gets trade history using fetch_trades,
@@ -3063,7 +3063,7 @@ class Exchange:
async def _async_get_trade_history(
self,
pair: str,
since: int | None = None,
since: int,
until: int | None = None,
from_id: str | None = None,
) -> tuple[str, list[list]]:
@@ -3094,7 +3094,7 @@ class Exchange:
def get_historic_trades(
self,
pair: str,
since: int | None = None,
since: int,
until: int | None = None,
from_id: str | None = None,
) -> tuple[str, list]:

View File

@@ -6,7 +6,7 @@ import logging
from collections import defaultdict
from collections.abc import Sequence
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from datetime import datetime, timezone
from math import isclose
from typing import Any, ClassVar, Optional, cast
@@ -1914,19 +1914,20 @@ class Trade(ModelBase, LocalTrade):
return total_open_stake_amount or 0
@staticmethod
def get_overall_performance(minutes=None) -> list[dict[str, Any]]:
def _generic_performance_query(columns: list, filters: list, fallback: str = "") -> Select:
"""
Returns List of dicts containing all Trades, including profit and trade count
Retrieve a generic select object to calculate performance grouped on `columns`.
Returns the following columns:
- columns
- profit_ratio
- profit_sum_abs
- count
NOTE: Not supported in Backtesting.
"""
filters: list = [Trade.is_open.is_(False)]
if minutes:
start_date = datetime.now(timezone.utc) - timedelta(minutes=minutes)
filters.append(Trade.close_date >= start_date)
columns_coal = [func.coalesce(c, fallback).label(c.name) for c in columns]
pair_costs = (
select(
Trade.pair,
*columns_coal,
func.sum(
(
func.coalesce(Order.filled, Order.amount)
@@ -1939,35 +1940,46 @@ class Trade(ModelBase, LocalTrade):
.filter(
*filters,
Order.ft_order_side == case((Trade.is_short.is_(True), "sell"), else_="buy"),
Order.filled > 0,
)
# Order.filled.gt > 0
.group_by(Trade.pair)
.group_by(*columns)
.cte("pair_costs")
)
trades_grouped = (
select(
Trade.pair,
*columns_coal,
func.sum(Trade.close_profit_abs).label("profit_sum_abs"),
func.count(Trade.pair).label("count"),
func.count(*columns_coal).label("count"),
)
.filter(*filters)
.group_by(Trade.pair)
.group_by(*columns_coal)
.cte("trades_grouped")
)
q = (
select(
trades_grouped.c.pair,
*[trades_grouped.c[x.name] for x in columns],
(trades_grouped.c.profit_sum_abs / pair_costs.c.cost_per_pair).label(
"profit_ratio"
),
trades_grouped.c.profit_sum_abs,
trades_grouped.c.count,
)
.join(pair_costs, trades_grouped.c.pair == pair_costs.c.pair)
.join(pair_costs, *[trades_grouped.c[x.name] == pair_costs.c[x.name] for x in columns])
.order_by(desc("profit_sum_abs"))
)
pair_rates = Trade.session.execute(q).all()
return q
@staticmethod
def get_overall_performance(start_date: datetime | None = None) -> list[dict[str, Any]]:
"""
Returns List of dicts containing all Trades, including profit and trade count
NOTE: Not supported in Backtesting.
"""
filters: list = [Trade.is_open.is_(False)]
if start_date:
filters.append(Trade.close_date >= start_date)
pair_rates_query = Trade._generic_performance_query([Trade.pair], filters)
pair_rates = Trade.session.execute(pair_rates_query).all()
return [
{
"pair": pair,
@@ -1992,17 +2004,8 @@ class Trade(ModelBase, LocalTrade):
if pair is not None:
filters.append(Trade.pair == pair)
enter_tag_perf = Trade.session.execute(
select(
Trade.enter_tag,
func.sum(Trade.close_profit).label("profit_sum"),
func.sum(Trade.close_profit_abs).label("profit_sum_abs"),
func.count(Trade.pair).label("count"),
)
.filter(*filters)
.group_by(Trade.enter_tag)
.order_by(desc("profit_sum_abs"))
).all()
pair_rates_query = Trade._generic_performance_query([Trade.enter_tag], filters, "Other")
enter_tag_perf = Trade.session.execute(pair_rates_query).all()
return [
{
@@ -2026,17 +2029,9 @@ class Trade(ModelBase, LocalTrade):
filters: list = [Trade.is_open.is_(False)]
if pair is not None:
filters.append(Trade.pair == pair)
sell_tag_perf = Trade.session.execute(
select(
Trade.exit_reason,
func.sum(Trade.close_profit).label("profit_sum"),
func.sum(Trade.close_profit_abs).label("profit_sum_abs"),
func.count(Trade.pair).label("count"),
)
.filter(*filters)
.group_by(Trade.exit_reason)
.order_by(desc("profit_sum_abs"))
).all()
pair_rates_query = Trade._generic_performance_query([Trade.exit_reason], filters, "Other")
sell_tag_perf = Trade.session.execute(pair_rates_query).all()
return [
{
@@ -2117,13 +2112,9 @@ class Trade(ModelBase, LocalTrade):
if start_date:
filters.append(Trade.close_date >= start_date)
best_pair = Trade.session.execute(
select(Trade.pair, func.sum(Trade.close_profit).label("profit_sum"))
.filter(*filters)
.group_by(Trade.pair)
.order_by(desc("profit_sum"))
).first()
pair_rates_query = Trade._generic_performance_query([Trade.pair], filters)
best_pair = Trade.session.execute(pair_rates_query).first()
# returns pair, profit_ratio, abs_profit, count
return best_pair
@staticmethod

View File

@@ -3,12 +3,14 @@ Performance pair list filter
"""
import logging
from datetime import timedelta
import pandas as pd
from freqtrade.exchange.exchange_types import Tickers
from freqtrade.persistence import Trade
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
from freqtrade.util.datetime_helpers import dt_now
logger = logging.getLogger(__name__)
@@ -69,7 +71,8 @@ class PerformanceFilter(IPairList):
"""
# Get the trading performance for pairs from database
try:
performance = pd.DataFrame(Trade.get_overall_performance(self._minutes))
start_date = dt_now() - timedelta(minutes=self._minutes)
performance = pd.DataFrame(Trade.get_overall_performance(start_date))
except AttributeError:
# Performancefilter does not work in backtesting.
self.log_once("PerformanceFilter is not available in this mode.", logger.warning)

View File

@@ -150,6 +150,7 @@ class Profit(BaseModel):
best_pair: str
best_rate: float
best_pair_profit_ratio: float
best_pair_profit_abs: float
winning_trades: int
losing_trades: int
profit_factor: float

View File

@@ -665,6 +665,7 @@ class RPC:
"best_pair": best_pair[0] if best_pair else "",
"best_rate": round(best_pair[1] * 100, 2) if best_pair else 0, # Deprecated
"best_pair_profit_ratio": best_pair[1] if best_pair else 0,
"best_pair_profit_abs": best_pair[2] if best_pair else 0,
"winning_trades": winning_trades,
"losing_trades": losing_trades,
"profit_factor": profit_factor,

View File

@@ -1028,6 +1028,7 @@ class Telegram(RPCHandler):
avg_duration = stats["avg_duration"]
best_pair = stats["best_pair"]
best_pair_profit_ratio = stats["best_pair_profit_ratio"]
best_pair_profit_abs = fmt_coin(stats["best_pair_profit_abs"], stake_cur)
winrate = stats["winrate"]
expectancy = stats["expectancy"]
expectancy_ratio = stats["expectancy_ratio"]
@@ -1067,7 +1068,8 @@ class Telegram(RPCHandler):
if stats["closed_trade_count"] > 0:
markdown_msg += (
f"\n*Avg. Duration:* `{avg_duration}`\n"
f"*Best Performing:* `{best_pair}: {best_pair_profit_ratio:.2%}`\n"
f"*Best Performing:* `{best_pair}: {best_pair_profit_abs} "
f"({best_pair_profit_ratio:.2%})`\n"
f"*Trading volume:* `{fmt_coin(stats['trading_volume'], stake_cur)}`\n"
f"*Profit factor:* `{stats['profit_factor']:.2f}`\n"
f"*Max Drawdown:* `{stats['max_drawdown']:.2%} "

View File

@@ -7,9 +7,9 @@
-r docs/requirements-docs.txt
coveralls==4.0.1
ruff==0.9.2
ruff==0.9.3
mypy==1.14.1
pre-commit==4.0.1
pre-commit==4.1.0
pytest==8.3.4
pytest-asyncio==0.25.2
pytest-cov==6.0.0

View File

@@ -5,4 +5,4 @@
scipy==1.15.1
scikit-learn==1.6.1
ft-scikit-optimize==0.9.2
filelock==3.16.1
filelock==3.17.0

View File

@@ -4,7 +4,7 @@ bottleneck==1.4.2
numexpr==2.10.2
pandas-ta==0.3.14b
ccxt==4.4.49
ccxt==4.4.50
cryptography==42.0.8; platform_machine == 'armv7l'
cryptography==44.0.0; platform_machine != 'armv7l'
aiohttp==3.10.11
@@ -13,7 +13,7 @@ python-telegram-bot==21.10
# can't be hard-pinned due to telegram-bot pinning httpx with ~
httpx>=0.24.1
humanize==4.11.0
cachetools==5.5.0
cachetools==5.5.1
requests==2.32.3
urllib3==2.3.0
jsonschema==4.23.0
@@ -38,8 +38,8 @@ orjson==3.10.15
sdnotify==0.3.2
# API Server
fastapi==0.115.6
pydantic==2.10.5
fastapi==0.115.7
pydantic==2.10.6
uvicorn==0.34.0
pyjwt==2.10.1
aiofiles==24.1.0
@@ -47,7 +47,7 @@ psutil==6.1.1
# Building config files interactively
questionary==2.1.0
prompt-toolkit==3.0.36
prompt-toolkit==3.0.50
# Extensions to datetime library
python-dateutil==2.9.0.post0
pytz==2024.2

View File

@@ -1931,9 +1931,9 @@ def test_get_overall_performance(fee):
@pytest.mark.parametrize(
"is_short,pair,profit",
[
(True, "ETC/BTC", -0.005),
(False, "XRP/BTC", 0.01),
(None, "XRP/BTC", 0.01),
(True, "XRP/BTC", -0.00018780487),
(False, "ETC/BTC", 0.00003860975),
(None, "XRP/BTC", 0.000025203252),
],
)
def test_get_best_pair(fee, is_short, pair, profit):
@@ -1942,9 +1942,9 @@ def test_get_best_pair(fee, is_short, pair, profit):
create_mock_trades(fee, is_short)
res = Trade.get_best_pair()
assert len(res) == 2
assert len(res) == 4
assert res[0] == pair
assert res[1] == profit
assert pytest.approx(res[1]) == profit
@pytest.mark.usefixtures("init_persistence")
@@ -1954,9 +1954,9 @@ def test_get_best_pair_lev(fee):
create_mock_trades_with_leverage(fee)
res = Trade.get_best_pair()
assert len(res) == 2
assert res[0] == "DOGE/BTC"
assert res[1] == 0.1713156134055116
assert len(res) == 4
assert res[0] == "ETC/BTC"
assert pytest.approx(res[1]) == 0.00003860975
@pytest.mark.usefixtures("init_persistence")

View File

@@ -480,8 +480,8 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None:
assert stats["first_trade_humanized"] == "2 days ago"
assert stats["latest_trade_humanized"] == "17 minutes ago"
assert stats["avg_duration"] in ("0:17:40")
assert stats["best_pair"] == "XRP/USDT"
assert stats["best_rate"] == 10.0
assert stats["best_pair"] == "NEO/USDT"
assert stats["best_rate"] == 1.99
# Test non-available pair
mocker.patch(
@@ -492,8 +492,8 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None:
assert stats["first_trade_humanized"] == "2 days ago"
assert stats["latest_trade_humanized"] == "17 minutes ago"
assert stats["avg_duration"] in ("0:17:40")
assert stats["best_pair"] == "XRP/USDT"
assert stats["best_rate"] == 10.0
assert stats["best_pair"] == "NEO/USDT"
assert stats["best_rate"] == 1.99
assert isnan(stats["profit_all_coin"])
@@ -1018,14 +1018,14 @@ def test_enter_tag_performance_handle(default_conf, ticker, fee, mocker) -> None
assert len(res) == 3
assert res[0]["enter_tag"] == "TEST1"
assert res[0]["count"] == 1
assert res[0]["profit_pct"] == 5.0
assert res[0]["profit_pct"] == 1.99
res = rpc._rpc_enter_tag_performance(None)
assert len(res) == 3
assert res[0]["enter_tag"] == "TEST1"
assert res[0]["count"] == 1
assert res[0]["profit_pct"] == 5.0
assert res[0]["profit_pct"] == 1.99
def test_enter_tag_performance_handle_2(mocker, default_conf, markets, fee):
@@ -1041,17 +1041,20 @@ def test_enter_tag_performance_handle_2(mocker, default_conf, markets, fee):
assert len(res) == 2
assert res[0]["enter_tag"] == "TEST1"
assert res[0]["count"] == 1
assert pytest.approx(res[0]["profit_pct"]) == 0.5
assert pytest.approx(res[0]["profit_pct"]) == 0.0
assert pytest.approx(res[0]["profit_ratio"]) == 0.00003860975
assert res[1]["enter_tag"] == "Other"
assert res[1]["count"] == 1
assert pytest.approx(res[1]["profit_pct"]) == 1.0
assert pytest.approx(res[1]["profit_pct"]) == 0.0
assert pytest.approx(res[1]["profit_ratio"]) == 0.00002520325
# Test for a specific pair
res = rpc._rpc_enter_tag_performance("ETC/BTC")
assert len(res) == 1
assert res[0]["count"] == 1
assert res[0]["enter_tag"] == "TEST1"
assert pytest.approx(res[0]["profit_pct"]) == 0.5
assert pytest.approx(res[0]["profit_pct"]) == 0.0
assert pytest.approx(res[0]["profit_ratio"]) == 0.00003860975
def test_exit_reason_performance_handle(default_conf_usdt, ticker, fee, mocker) -> None:
@@ -1075,7 +1078,7 @@ def test_exit_reason_performance_handle(default_conf_usdt, ticker, fee, mocker)
assert len(res) == 3
assert res[0]["exit_reason"] == "exit_signal"
assert res[0]["count"] == 1
assert res[0]["profit_pct"] == 5.0
assert res[0]["profit_pct"] == 1.99
assert res[1]["exit_reason"] == "roi"
assert res[2]["exit_reason"] == "Other"
@@ -1094,17 +1097,20 @@ def test_exit_reason_performance_handle_2(mocker, default_conf, markets, fee):
assert len(res) == 2
assert res[0]["exit_reason"] == "sell_signal"
assert res[0]["count"] == 1
assert pytest.approx(res[0]["profit_pct"]) == 0.5
assert pytest.approx(res[0]["profit_pct"]) == 0.0
assert pytest.approx(res[0]["profit_ratio"]) == 0.00003860975
assert res[1]["exit_reason"] == "roi"
assert res[1]["count"] == 1
assert pytest.approx(res[1]["profit_pct"]) == 1.0
assert pytest.approx(res[1]["profit_pct"]) == 0.0
assert pytest.approx(res[1]["profit_ratio"]) == 0.000025203252
# Test for a specific pair
res = rpc._rpc_exit_reason_performance("ETC/BTC")
assert len(res) == 1
assert res[0]["count"] == 1
assert res[0]["exit_reason"] == "sell_signal"
assert pytest.approx(res[0]["profit_pct"]) == 0.5
assert pytest.approx(res[0]["profit_pct"]) == 0.0
assert pytest.approx(res[0]["profit_ratio"]) == 0.00003860975
def test_mix_tag_performance_handle(default_conf, ticker, fee, mocker) -> None:

View File

@@ -963,9 +963,10 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
(
True,
{
"best_pair": "ETC/BTC",
"best_rate": -0.5,
"best_pair_profit_ratio": -0.005,
"best_pair": "XRP/BTC",
"best_rate": -0.02,
"best_pair_profit_ratio": -0.00018780487,
"best_pair_profit_abs": -0.001155,
"profit_all_coin": 15.382312,
"profit_all_fiat": 189894.6470718,
"profit_all_percent_mean": 49.62,
@@ -994,9 +995,10 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
(
False,
{
"best_pair": "XRP/BTC",
"best_rate": 1.0,
"best_pair_profit_ratio": 0.01,
"best_pair": "ETC/BTC",
"best_rate": 0.0,
"best_pair_profit_ratio": 0.00003860975,
"best_pair_profit_abs": 0.000584127,
"profit_all_coin": -15.46546305,
"profit_all_fiat": -190921.14135225,
"profit_all_percent_mean": -49.62,
@@ -1026,8 +1028,9 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
None,
{
"best_pair": "XRP/BTC",
"best_rate": 1.0,
"best_pair_profit_ratio": 0.01,
"best_rate": 0.0,
"best_pair_profit_ratio": 0.000025203252,
"best_pair_profit_abs": 0.000155,
"profit_all_coin": -14.87167525,
"profit_all_fiat": -183590.83096125,
"profit_all_percent_mean": 0.13,
@@ -1080,7 +1083,8 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected)
assert rc.json() == {
"avg_duration": ANY,
"best_pair": expected["best_pair"],
"best_pair_profit_ratio": expected["best_pair_profit_ratio"],
"best_pair_profit_ratio": pytest.approx(expected["best_pair_profit_ratio"]),
"best_pair_profit_abs": expected["best_pair_profit_abs"],
"best_rate": expected["best_rate"],
"first_trade_date": ANY,
"first_trade_humanized": ANY,
@@ -1206,7 +1210,8 @@ def test_api_entries(botclient, fee):
resp = response[0]
assert resp["enter_tag"] == "TEST1"
assert resp["count"] == 1
assert resp["profit_pct"] == 0.5
assert resp["profit_pct"] == 0.0
assert pytest.approx(resp["profit_ratio"]) == 0.000038609756
def test_api_exits(botclient, fee):
@@ -1225,7 +1230,8 @@ def test_api_exits(botclient, fee):
resp = response[0]
assert resp["exit_reason"] == "sell_signal"
assert resp["count"] == 1
assert resp["profit_pct"] == 0.5
assert resp["profit_pct"] == 0.0
assert pytest.approx(resp["profit_ratio"]) == 0.000038609756
def test_api_mix_tag(botclient, fee):

View File

@@ -918,7 +918,7 @@ async def test_telegram_profit_handle(
)
assert "∙ `6.253 USD`" in msg_mock.call_args_list[-1][0][0]
assert "*Best Performing:* `ETH/USDT: 9.45%`" in msg_mock.call_args_list[-1][0][0]
assert "*Best Performing:* `ETH/USDT: 5.685 USDT (9.47%)`" in msg_mock.call_args_list[-1][0][0]
assert "*Max Drawdown:*" in msg_mock.call_args_list[-1][0][0]
assert "*Profit factor:*" in msg_mock.call_args_list[-1][0][0]
assert "*Winrate:*" in msg_mock.call_args_list[-1][0][0]
@@ -1611,7 +1611,7 @@ async def test_telegram_entry_tag_performance_handle(
await telegram._enter_tag_performance(update=update, context=context)
assert msg_mock.call_count == 1
assert "Entry Tag Performance" in msg_mock.call_args_list[0][0][0]
assert "`TEST1\t3.987 USDT (5.00%) (1)`" in msg_mock.call_args_list[0][0][0]
assert "`TEST1\t3.987 USDT (1.99%) (1)`" in msg_mock.call_args_list[0][0][0]
context.args = ["XRP/USDT"]
await telegram._enter_tag_performance(update=update, context=context)
@@ -1644,7 +1644,7 @@ async def test_telegram_exit_reason_performance_handle(
await telegram._exit_reason_performance(update=update, context=context)
assert msg_mock.call_count == 1
assert "Exit Reason Performance" in msg_mock.call_args_list[0][0][0]
assert "`roi\t2.842 USDT (10.00%) (1)`" in msg_mock.call_args_list[0][0][0]
assert "`roi\t2.842 USDT (9.47%) (1)`" in msg_mock.call_args_list[0][0][0]
context.args = ["XRP/USDT"]
await telegram._exit_reason_performance(update=update, context=context)