From ef3a7d5c9245248496756f0841a91010ef1b10da Mon Sep 17 00:00:00 2001 From: David Arena Date: Mon, 16 Dec 2024 00:56:34 +0100 Subject: [PATCH 01/53] feat: api_server and client supporting list_custom_data --- freqtrade/rpc/api_server/api_schemas.py | 3 ++ freqtrade/rpc/api_server/api_v1.py | 6 ++++ freqtrade/rpc/rpc.py | 33 +++++++++++++------- ft_client/freqtrade_client/ft_rest_client.py | 16 ++++++++++ 4 files changed, 46 insertions(+), 12 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 97770ba13..5d31c1d99 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -617,3 +617,6 @@ class Health(BaseModel): bot_start_ts: int | None = None bot_startup: datetime | None = None bot_startup_ts: int | None = None + +class ListCustomData(BaseModel): + custom_data: list[dict[str, Any]] | None = None diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 552bee9cd..f8acb9e8a 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -27,6 +27,7 @@ from freqtrade.rpc.api_server.api_schemas import ( FreqAIModelListResponse, Health, HyperoptLossListResponse, + ListCustomData, Locks, LocksPayload, Logs, @@ -533,3 +534,8 @@ def sysinfo(): @router.get("/health", response_model=Health, tags=["info"]) def health(rpc: RPC = Depends(get_rpc)): return rpc.health() + +@router.get("/list_custom_data", response_model=ListCustomData, tags=["info"]) +def list_custom_data(trade_id: int | None = None, key: str | None = None, rpc: RPC = Depends(get_rpc)): + custom_data = rpc._rpc_list_custom_data(trade_id, key) + return ListCustomData(custom_data=custom_data) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index ac05f8714..5b8862556 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1097,19 +1097,28 @@ class RPC: "cancel_order_count": c_count, } - def _rpc_list_custom_data(self, trade_id: int, key: str | None) -> list[dict[str, Any]]: - # Query for trade - trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first() - if trade is None: - return [] - # Query custom_data - custom_data = [] - if key: - data = trade.get_custom_data(key=key) - if data: - custom_data = [data] + def _rpc_list_custom_data(self, trade_id: int | None = None, key: str | None = None) -> list[dict[str, Any]]: + # Query trades based on trade_id + if trade_id: + trades = Trade.get_trades(trade_filter=[Trade.id == trade_id]).all() else: - custom_data = trade.get_all_custom_data() + # If no trade_id, get all trades + trades = Trade.get_trades().all() + + if not trades: + return [] + + # Collect custom data + custom_data = [] + for trade in trades: + if key: + data = trade.get_custom_data(key=key) + if data: + custom_data.append(data) + else: + custom_data.extend(trade.get_all_custom_data()) + + # Format the results return [ { "id": data_entry.id, diff --git a/ft_client/freqtrade_client/ft_rest_client.py b/ft_client/freqtrade_client/ft_rest_client.py index 851583ee0..a7027fa42 100755 --- a/ft_client/freqtrade_client/ft_rest_client.py +++ b/ft_client/freqtrade_client/ft_rest_client.py @@ -474,3 +474,19 @@ class FtRestClient: :return: json object """ return self._get("health") + + def list_custom_data(self, trade_id=None, key=None): + """Lists custom_data of the running bot. + + :param tradeid: Optional keyword argument - Id of the trade (can be received via status command) + :param key: Optional keyword argument - Key of the custom data + + :return: json object + """ + params = {} + if trade_id is not None: + params["trade_id"] = trade_id + if key is not None: + params["key"] = key + + return self._get("list_custom_data", params=params) From 47613b1cf9306b4b08ff57f3e100f14767b182ed Mon Sep 17 00:00:00 2001 From: David Arena Date: Tue, 17 Dec 2024 18:32:27 +0100 Subject: [PATCH 02/53] fix: no tradeID only returns open trades --- freqtrade/rpc/rpc.py | 4 ++-- ft_client/freqtrade_client/ft_rest_client.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 5b8862556..68b749450 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1102,8 +1102,8 @@ class RPC: if trade_id: trades = Trade.get_trades(trade_filter=[Trade.id == trade_id]).all() else: - # If no trade_id, get all trades - trades = Trade.get_trades().all() + # If no trade_id, get all open trades + trades = Trade.get_open_trades() if not trades: return [] diff --git a/ft_client/freqtrade_client/ft_rest_client.py b/ft_client/freqtrade_client/ft_rest_client.py index a7027fa42..bd6617f73 100755 --- a/ft_client/freqtrade_client/ft_rest_client.py +++ b/ft_client/freqtrade_client/ft_rest_client.py @@ -477,6 +477,7 @@ class FtRestClient: def list_custom_data(self, trade_id=None, key=None): """Lists custom_data of the running bot. + Without a tradeid, returns all custom_data from open trades. :param tradeid: Optional keyword argument - Id of the trade (can be received via status command) :param key: Optional keyword argument - Key of the custom data From 83e56a09c2d135b11ba02b101996071f4dc616bc Mon Sep 17 00:00:00 2001 From: David Arena Date: Tue, 17 Dec 2024 19:22:02 +0100 Subject: [PATCH 03/53] fix: api url and rm key --- freqtrade/rpc/rpc.py | 19 +++++++------------ ft_client/freqtrade_client/ft_rest_client.py | 9 +++------ 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 68b749450..905bc78aa 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1097,26 +1097,21 @@ class RPC: "cancel_order_count": c_count, } - def _rpc_list_custom_data(self, trade_id: int | None = None, key: str | None = None) -> list[dict[str, Any]]: + def _rpc_list_custom_data(self, trade_id: int) -> list[dict[str, Any]]: # Query trades based on trade_id - if trade_id: - trades = Trade.get_trades(trade_filter=[Trade.id == trade_id]).all() - else: - # If no trade_id, get all open trades + if trade_id == -1: + #get all open trades trades = Trade.get_open_trades() + else: + trades = Trade.get_trades(trade_filter=[Trade.id == trade_id]).all() if not trades: return [] - + # Collect custom data custom_data = [] for trade in trades: - if key: - data = trade.get_custom_data(key=key) - if data: - custom_data.append(data) - else: - custom_data.extend(trade.get_all_custom_data()) + custom_data.extend(trade.get_all_custom_data()) # Format the results return [ diff --git a/ft_client/freqtrade_client/ft_rest_client.py b/ft_client/freqtrade_client/ft_rest_client.py index bd6617f73..39844b60b 100755 --- a/ft_client/freqtrade_client/ft_rest_client.py +++ b/ft_client/freqtrade_client/ft_rest_client.py @@ -477,17 +477,14 @@ class FtRestClient: def list_custom_data(self, trade_id=None, key=None): """Lists custom_data of the running bot. - Without a tradeid, returns all custom_data from open trades. - :param tradeid: Optional keyword argument - Id of the trade (can be received via status command) - :param key: Optional keyword argument - Key of the custom data + :param tradeid: Optional keyword argument - Id of the trade :return: json object """ params = {} + trade_id = -1 if trade_id is not None: params["trade_id"] = trade_id - if key is not None: - params["key"] = key - return self._get("list_custom_data", params=params) + return self._get("trades/{tradeid}/custom_data", params=params) From fc1c3a8f973dc5962a795812a00ad396b9ef2050 Mon Sep 17 00:00:00 2001 From: David Arena Date: Tue, 17 Dec 2024 19:22:09 +0100 Subject: [PATCH 04/53] fix --- freqtrade/rpc/api_server/api_v1.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index f8acb9e8a..20acfe96f 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -535,7 +535,7 @@ def sysinfo(): def health(rpc: RPC = Depends(get_rpc)): return rpc.health() -@router.get("/list_custom_data", response_model=ListCustomData, tags=["info"]) -def list_custom_data(trade_id: int | None = None, key: str | None = None, rpc: RPC = Depends(get_rpc)): - custom_data = rpc._rpc_list_custom_data(trade_id, key) +@router.get("/trades/{tradeid}/custom_data", response_model=ListCustomData, tags=["info"]) +def list_custom_data(trade_id: int, rpc: RPC = Depends(get_rpc)): + custom_data = rpc._rpc_list_custom_data(trade_id) return ListCustomData(custom_data=custom_data) From 9207cf501c0220e5ce27025e05210ffe1736f4fa Mon Sep 17 00:00:00 2001 From: David Arena Date: Tue, 17 Dec 2024 22:25:09 +0100 Subject: [PATCH 05/53] fix: returned object --- freqtrade/rpc/api_server/api_schemas.py | 3 ++- freqtrade/rpc/api_server/api_v1.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 5d31c1d99..db7a312c5 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -619,4 +619,5 @@ class Health(BaseModel): bot_startup_ts: int | None = None class ListCustomData(BaseModel): - custom_data: list[dict[str, Any]] | None = None + trade_id: int + custom_data: list[dict[str, Any]] diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 20acfe96f..086d3dce1 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -535,7 +535,7 @@ def sysinfo(): def health(rpc: RPC = Depends(get_rpc)): return rpc.health() -@router.get("/trades/{tradeid}/custom_data", response_model=ListCustomData, tags=["info"]) +@router.get("/trades/{tradeid}/custom-data", response_model=list[ListCustomData], tags=["info"]) def list_custom_data(trade_id: int, rpc: RPC = Depends(get_rpc)): custom_data = rpc._rpc_list_custom_data(trade_id) - return ListCustomData(custom_data=custom_data) + return custom_data From d0979d560ff18ec15749898200f17a6365045aea Mon Sep 17 00:00:00 2001 From: David Arena Date: Wed, 12 Feb 2025 18:40:44 +0100 Subject: [PATCH 06/53] fix: endpoints --- freqtrade/rpc/api_server/api_v1.py | 15 ++++++++++++--- freqtrade/rpc/rpc.py | 10 ++++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 086d3dce1..48d14ca4b 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -535,7 +535,16 @@ def sysinfo(): def health(rpc: RPC = Depends(get_rpc)): return rpc.health() -@router.get("/trades/{tradeid}/custom-data", response_model=list[ListCustomData], tags=["info"]) +@router.get("/trades/open/custom-data", response_model=list[ListCustomData], tags=["info"]) +def list_open_trades_custom_data(rpc: RPC = Depends(get_rpc)): + """ + Fetch custom data for all open trades. + """ + return rpc._rpc_list_custom_data() + +@router.get("/trades/{trade_id}/custom-data", response_model=list[ListCustomData], tags=["info"]) def list_custom_data(trade_id: int, rpc: RPC = Depends(get_rpc)): - custom_data = rpc._rpc_list_custom_data(trade_id) - return custom_data + """ + Fetch custom data for a specific trade. + """ + return rpc._rpc_list_custom_data(trade_id) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 905bc78aa..847cb91ee 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -7,7 +7,7 @@ from abc import abstractmethod from collections.abc import Generator, Sequence from datetime import date, datetime, timedelta, timezone from math import isnan -from typing import Any +from typing import Any, Optional import psutil from dateutil.relativedelta import relativedelta @@ -1097,9 +1097,11 @@ class RPC: "cancel_order_count": c_count, } - def _rpc_list_custom_data(self, trade_id: int) -> list[dict[str, Any]]: - # Query trades based on trade_id - if trade_id == -1: + def _rpc_list_custom_data(self, trade_id: Optional[int] = None) -> list[dict[str, Any]]: + """ + Fetch custom data for a specific trade, or all open trades if `trade_id` is not provided. + """ + if trade_id is None: #get all open trades trades = Trade.get_open_trades() else: From 552575c7e60c0f1379f71c0c262dd3d156a99cc5 Mon Sep 17 00:00:00 2001 From: David Arena Date: Wed, 12 Feb 2025 19:27:47 +0100 Subject: [PATCH 07/53] fix: key in _rpc_list_custom_data --- freqtrade/rpc/rpc.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 847cb91ee..931401eaa 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1097,7 +1097,7 @@ class RPC: "cancel_order_count": c_count, } - def _rpc_list_custom_data(self, trade_id: Optional[int] = None) -> list[dict[str, Any]]: + def _rpc_list_custom_data(self, trade_id: Optional[int] = None, key: Optional[str] = None) -> list[dict[str, Any]]: """ Fetch custom data for a specific trade, or all open trades if `trade_id` is not provided. """ @@ -1112,8 +1112,13 @@ class RPC: # Collect custom data custom_data = [] - for trade in trades: - custom_data.extend(trade.get_all_custom_data()) + if key: + data = trade.get_custom_data(key=key) + if data: + custom_data = [data] + else: + for trade in trades: + custom_data.extend(trade.get_all_custom_data()) # Format the results return [ From 2231ba3f04c9e5c5d45fe59d14c66e55c8ce4148 Mon Sep 17 00:00:00 2001 From: David Arena Date: Thu, 13 Feb 2025 02:02:15 +0100 Subject: [PATCH 08/53] fixes: ruff --- freqtrade/rpc/api_server/api_v1.py | 2 +- freqtrade/rpc/rpc.py | 2 +- ft_client/freqtrade_client/ft_rest_client.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index eef6b8eef..f4838afb1 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -518,7 +518,7 @@ def list_open_trades_custom_data(rpc: RPC = Depends(get_rpc)): return rpc._rpc_list_custom_data() @router.get("/trades/{trade_id}/custom-data", response_model=list[ListCustomData], tags=["info"]) -def list_custom_data(trade_id: int, rpc: RPC = Depends(get_rpc)): +def list_custom_data(trade_id: int, rpc: RPC = Depends(get_rpc)): """ Fetch custom data for a specific trade. """ diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 530b35da1..c96773322 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1132,7 +1132,7 @@ class RPC: # Collect custom data custom_data = [] if key: - data = trade.get_custom_data(key=key) + data = trades.get_custom_data(key=key) if data: custom_data = [data] else: diff --git a/ft_client/freqtrade_client/ft_rest_client.py b/ft_client/freqtrade_client/ft_rest_client.py index 7861e3cd2..1758e1f03 100755 --- a/ft_client/freqtrade_client/ft_rest_client.py +++ b/ft_client/freqtrade_client/ft_rest_client.py @@ -496,5 +496,5 @@ class FtRestClient: trade_id = -1 if trade_id is not None: params["trade_id"] = trade_id - + return self._get("trades/{tradeid}/custom_data", params=params) From 8182947f29b229107ebcbde064b26b5fd7c141d1 Mon Sep 17 00:00:00 2001 From: David Arena Date: Thu, 13 Feb 2025 02:03:19 +0100 Subject: [PATCH 09/53] fix --- freqtrade/rpc/rpc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c96773322..711170af4 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -7,7 +7,7 @@ from abc import abstractmethod from collections.abc import Generator, Sequence from datetime import date, datetime, timedelta, timezone from math import isnan -from typing import Any, TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Any import psutil from dateutil.relativedelta import relativedelta @@ -1116,7 +1116,7 @@ class RPC: "cancel_order_count": c_count, } - def _rpc_list_custom_data(self, trade_id: Optional[int] = None, key: Optional[str] = None) -> list[dict[str, Any]]: + def _rpc_list_custom_data(self, trade_id: int | None = None, key: str | None = None) -> list[dict[str, Any]]: """ Fetch custom data for a specific trade, or all open trades if `trade_id` is not provided. """ @@ -1128,7 +1128,7 @@ class RPC: if not trades: return [] - + # Collect custom data custom_data = [] if key: From a1a5cab04e31165922b41f727f966bf7082a9be4 Mon Sep 17 00:00:00 2001 From: David Arena Date: Thu, 13 Feb 2025 02:04:22 +0100 Subject: [PATCH 10/53] fix E501 --- freqtrade/rpc/rpc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 711170af4..680f4995f 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1116,7 +1116,8 @@ class RPC: "cancel_order_count": c_count, } - def _rpc_list_custom_data(self, trade_id: int | None = None, key: str | None = None) -> list[dict[str, Any]]: + def _rpc_list_custom_data( + self, trade_id: int | None = None, key: str | None = None) -> list[dict[str, Any]]: """ Fetch custom data for a specific trade, or all open trades if `trade_id` is not provided. """ From ceff757bc6fcb14f80abb35852b7c30126f33298 Mon Sep 17 00:00:00 2001 From: David Arena Date: Fri, 14 Feb 2025 12:44:42 +0100 Subject: [PATCH 11/53] fix: formating --- freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/api_server/api_v1.py | 2 ++ freqtrade/rpc/rpc.py | 5 +++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 984507d99..1c49d4d0a 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -638,6 +638,7 @@ class Health(BaseModel): bot_startup: datetime | None = None bot_startup_ts: int | None = None + class ListCustomData(BaseModel): trade_id: int custom_data: list[dict[str, Any]] diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index f4838afb1..89f632972 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -510,6 +510,7 @@ def sysinfo(): def health(rpc: RPC = Depends(get_rpc)): return rpc.health() + @router.get("/trades/open/custom-data", response_model=list[ListCustomData], tags=["info"]) def list_open_trades_custom_data(rpc: RPC = Depends(get_rpc)): """ @@ -517,6 +518,7 @@ def list_open_trades_custom_data(rpc: RPC = Depends(get_rpc)): """ return rpc._rpc_list_custom_data() + @router.get("/trades/{trade_id}/custom-data", response_model=list[ListCustomData], tags=["info"]) def list_custom_data(trade_id: int, rpc: RPC = Depends(get_rpc)): """ diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 680f4995f..9b68b6928 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1117,12 +1117,13 @@ class RPC: } def _rpc_list_custom_data( - self, trade_id: int | None = None, key: str | None = None) -> list[dict[str, Any]]: + self, trade_id: int | None = None, key: str | None = None + ) -> list[dict[str, Any]]: """ Fetch custom data for a specific trade, or all open trades if `trade_id` is not provided. """ if trade_id is None: - #get all open trades + # get all open trades trades = Trade.get_open_trades() else: trades = Trade.get_trades(trade_filter=[Trade.id == trade_id]).all() From aec496a73b244a7e18e08f885a95d4f4cd108603 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 7 Mar 2025 09:40:15 -0400 Subject: [PATCH 12/53] fix: update _rpc_list_custom_data with proper typing and custom_data collection loop --- freqtrade/rpc/rpc.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 68856c98a..31589492b 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1121,6 +1121,7 @@ class RPC: """ Fetch custom data for a specific trade, or all open trades if `trade_id` is not provided. """ + trades: Sequence[Trade] if trade_id is None: # get all open trades trades = Trade.get_open_trades() @@ -1130,15 +1131,15 @@ class RPC: if not trades: return [] - # Collect custom data custom_data = [] - if key: - data = trades.get_custom_data(key=key) - if data: - custom_data = [data] - else: - for trade in trades: - custom_data.extend(trade.get_all_custom_data()) + for trade in trades: + # Collect custom data + if key: + data = trade.get_custom_data(key=key) + if data: + custom_data = [data] + + custom_data.extend(trade.get_all_custom_data()) # Format the results return [ From 93c8a118241d88c7a35ca2d34f4b2e21db9f6c6d Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 7 Mar 2025 14:08:21 -0400 Subject: [PATCH 13/53] fix: update _rpc_list_custom_data to add all custom data only if key is not provided --- freqtrade/rpc/rpc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 31589492b..f1e6b1d12 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1138,8 +1138,8 @@ class RPC: data = trade.get_custom_data(key=key) if data: custom_data = [data] - - custom_data.extend(trade.get_all_custom_data()) + else: + custom_data.extend(trade.get_all_custom_data()) # Format the results return [ From 7770f082c80b4945d60bd7d5c0eab91e0ee4a143 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 10 Mar 2025 15:51:34 -0400 Subject: [PATCH 14/53] chore: relocate custom-data endpoints of api server near trade related endpoint, replace info tag by trading --- freqtrade/rpc/api_server/api_v1.py | 32 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 89f632972..6403c1819 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -214,6 +214,22 @@ def trade_reload(tradeid: int, rpc: RPC = Depends(get_rpc)): return rpc._rpc_trade_status([tradeid])[0] +@router.get("/trades/open/custom-data", response_model=list[ListCustomData], tags=["trading"]) +def list_open_trades_custom_data(rpc: RPC = Depends(get_rpc)): + """ + Fetch custom data for all open trades. + """ + return rpc._rpc_list_custom_data() + + +@router.get("/trades/{trade_id}/custom-data", response_model=list[ListCustomData], tags=["trading"]) +def list_custom_data(trade_id: int, rpc: RPC = Depends(get_rpc)): + """ + Fetch custom data for a specific trade. + """ + return rpc._rpc_list_custom_data(trade_id) + + # TODO: Missing response model @router.get("/edge", tags=["info"]) def edge(rpc: RPC = Depends(get_rpc)): @@ -509,19 +525,3 @@ def sysinfo(): @router.get("/health", response_model=Health, tags=["info"]) def health(rpc: RPC = Depends(get_rpc)): return rpc.health() - - -@router.get("/trades/open/custom-data", response_model=list[ListCustomData], tags=["info"]) -def list_open_trades_custom_data(rpc: RPC = Depends(get_rpc)): - """ - Fetch custom data for all open trades. - """ - return rpc._rpc_list_custom_data() - - -@router.get("/trades/{trade_id}/custom-data", response_model=list[ListCustomData], tags=["info"]) -def list_custom_data(trade_id: int, rpc: RPC = Depends(get_rpc)): - """ - Fetch custom data for a specific trade. - """ - return rpc._rpc_list_custom_data(trade_id) From fac049165840524d02a54bacd1bd8407db088a80 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 10 Mar 2025 16:01:16 -0400 Subject: [PATCH 15/53] fix: _rpc_list_custom_data send custom-data for all trades if a key is provided --- freqtrade/rpc/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index f1e6b1d12..5244963df 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1137,7 +1137,7 @@ class RPC: if key: data = trade.get_custom_data(key=key) if data: - custom_data = [data] + custom_data.append(data) else: custom_data.extend(trade.get_all_custom_data()) From 76aefccd03bf5ea20de77c70e82f68e5fc51d501 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 10 Mar 2025 16:18:42 -0400 Subject: [PATCH 16/53] fix: on custom-data endpoints key is now an optional parameter --- freqtrade/rpc/api_server/api_v1.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 6403c1819..3cb6965da 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -215,19 +215,21 @@ def trade_reload(tradeid: int, rpc: RPC = Depends(get_rpc)): @router.get("/trades/open/custom-data", response_model=list[ListCustomData], tags=["trading"]) -def list_open_trades_custom_data(rpc: RPC = Depends(get_rpc)): +def list_open_trades_custom_data(key: str | None = Query(None), rpc: RPC = Depends(get_rpc)): """ Fetch custom data for all open trades. + If a key is provided, it will be used to filter data accordingly. """ - return rpc._rpc_list_custom_data() + return rpc._rpc_list_custom_data(key=key) @router.get("/trades/{trade_id}/custom-data", response_model=list[ListCustomData], tags=["trading"]) -def list_custom_data(trade_id: int, rpc: RPC = Depends(get_rpc)): +def list_custom_data(trade_id: int, key: str | None = Query(None), rpc: RPC = Depends(get_rpc)): """ Fetch custom data for a specific trade. + If a key is provided, it will be used to filter data accordingly. """ - return rpc._rpc_list_custom_data(trade_id) + return rpc._rpc_list_custom_data(trade_id, key=key) # TODO: Missing response model From 673447794e6e0e79a0679cdd62acfc3d883b58be Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 10 Mar 2025 22:17:34 -0400 Subject: [PATCH 17/53] chore: implement pagination for _rpc_list_custom_data --- freqtrade/rpc/rpc.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 5244963df..479895077 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1116,15 +1116,18 @@ class RPC: } def _rpc_list_custom_data( - self, trade_id: int | None = None, key: str | None = None + self, trade_id: int | None = None, key: str | None = None, limit: int = 100, offset: int = 0 ) -> list[dict[str, Any]]: """ Fetch custom data for a specific trade, or all open trades if `trade_id` is not provided. + Pagination is applied via `limit` and `offset`. """ trades: Sequence[Trade] if trade_id is None: - # get all open trades - trades = Trade.get_open_trades() + # Get all open trades + trades = Trade.session.scalars( + Trade.get_trades_query([Trade.is_open.is_(True)]).limit(limit).offset(offset) + ).all() else: trades = Trade.get_trades(trade_filter=[Trade.id == trade_id]).all() @@ -1142,7 +1145,7 @@ class RPC: custom_data.extend(trade.get_all_custom_data()) # Format the results - return [ + formatted_results = [ { "id": data_entry.id, "ft_trade_id": data_entry.ft_trade_id, @@ -1155,6 +1158,8 @@ class RPC: for data_entry in custom_data ] + return formatted_results + def _rpc_performance(self) -> list[dict[str, Any]]: """ Handler for performance. From 743422ccf3ac2d6daa7309ac6ac631349b406532 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 11 Mar 2025 11:19:36 -0400 Subject: [PATCH 18/53] feat: implement pagination for open trades custom-data rpc endpoint --- freqtrade/rpc/api_server/api_v1.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 3cb6965da..51597b027 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -215,12 +215,18 @@ def trade_reload(tradeid: int, rpc: RPC = Depends(get_rpc)): @router.get("/trades/open/custom-data", response_model=list[ListCustomData], tags=["trading"]) -def list_open_trades_custom_data(key: str | None = Query(None), rpc: RPC = Depends(get_rpc)): +def list_open_trades_custom_data( + key: str | None = Query(None, description="Optional key to filter data"), + limit: int = Query(100, ge=1, description="Maximum number of different trades to return data"), + offset: int = Query(0, ge=0, description="Number of trades to skip for pagination"), + rpc: RPC = Depends(get_rpc), +): """ Fetch custom data for all open trades. If a key is provided, it will be used to filter data accordingly. + Pagination is implemented via the `limit` and `offset` parameters. """ - return rpc._rpc_list_custom_data(key=key) + return rpc._rpc_list_custom_data(key=key, limit=limit, offset=offset) @router.get("/trades/{trade_id}/custom-data", response_model=list[ListCustomData], tags=["trading"]) From 4c7ff7ab0c1ee6bcb74aa7deaa5860b9e71578db Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 11 Mar 2025 23:36:24 -0400 Subject: [PATCH 19/53] feat: add retrieval_mode in get_custom_data function to chose between value or full custom_data object --- freqtrade/persistence/trade_model.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 7aeae5874..c3532c783 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1351,13 +1351,20 @@ class LocalTrade: """ CustomDataWrapper.set_custom_data(trade_id=self.id, key=key, value=value) - def get_custom_data(self, key: str, default: Any = None) -> Any: + def get_custom_data(self, key: str, default: Any = None, retrieval_mode: str = "value") -> Any: """ - Get custom data for this trade + Get custom data for this trade. + :param key: key of the custom data + :param default: value to return if no data is found + :param retrieval_mode: 'value' (default) to return the custom data's value, + or 'object' to return the entire custom data object. """ data = CustomDataWrapper.get_custom_data(trade_id=self.id, key=key) if data: + if retrieval_mode == "object": + return data[0] + # default behavior: return only the value return data[0].value return default From 5402b1433654e17fa3a3093b538f17f9879cf819 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 11 Mar 2025 23:38:50 -0400 Subject: [PATCH 20/53] chore: enhance _rpc_list_custom_data error handling, output format and docstring --- freqtrade/rpc/rpc.py | 64 +++++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 479895077..c952b24d6 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1121,6 +1121,11 @@ class RPC: """ Fetch custom data for a specific trade, or all open trades if `trade_id` is not provided. Pagination is applied via `limit` and `offset`. + + Returns an array of dictionaries, each containing: + - "trade_id": the ID of the trade (int) + - "custom_data": a list of custom data dicts, each with the fields: + "id", "ft_trade_id", "cd_key", "cd_type", "cd_value", "created_at", "updated_at" """ trades: Sequence[Trade] if trade_id is None: @@ -1132,33 +1137,50 @@ class RPC: trades = Trade.get_trades(trade_filter=[Trade.id == trade_id]).all() if not trades: - return [] + raise RPCException( + f"No trade found for trade_id: {trade_id}" if trade_id else "No open trades found." + ) - custom_data = [] + results = [] for trade in trades: - # Collect custom data + # Depending on whether a specific key is provided, retrieve custom data accordingly. if key: - data = trade.get_custom_data(key=key) - if data: - custom_data.append(data) + data = trade.get_custom_data(key=key, retrieval_mode="object") + # If data exists, wrap it in a list so the output remains consistent. + custom_data = [data] if data else [] else: - custom_data.extend(trade.get_all_custom_data()) + custom_data = trade.get_all_custom_data() - # Format the results - formatted_results = [ - { - "id": data_entry.id, - "ft_trade_id": data_entry.ft_trade_id, - "cd_key": data_entry.cd_key, - "cd_type": data_entry.cd_type, - "cd_value": data_entry.cd_value, - "created_at": data_entry.created_at, - "updated_at": data_entry.updated_at, - } - for data_entry in custom_data - ] + # Format each custom data entry. + formatted_custom_data = [ + { + "id": data_entry.id, + "ft_trade_id": data_entry.ft_trade_id, + "cd_key": data_entry.cd_key, + "cd_type": data_entry.cd_type, + "cd_value": data_entry.cd_value, + "created_at": data_entry.created_at, + "updated_at": data_entry.updated_at, + } + for data_entry in custom_data + ] - return formatted_results + # Append result for the trade if any custom data was found. + if formatted_custom_data: + results.append({"trade_id": trade.id, "custom_data": formatted_custom_data}) + + # Handle case when there is no custom data found across trades. + if not results: + message_details = "found for any open trades." + if key and trade_id: + message_details = f"with key '{key}' found for Trade ID: {trade_id}." + elif trade_id: + message_details = f"found for Trade ID: {trade_id}." + elif key: + message_details = f"with key '{key}' found for any open trades." + raise RPCException(f"No custom_data {message_details}") + + return results def _rpc_performance(self) -> list[dict[str, Any]]: """ From 97faa7fc5a085c7f4947a47b7847f1b1b0d71c63 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 11 Mar 2025 23:43:30 -0400 Subject: [PATCH 21/53] feat: update api schema custom data related classes --- freqtrade/rpc/api_server/api_schemas.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 1c49d4d0a..87adb4d24 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -639,6 +639,16 @@ class Health(BaseModel): bot_startup_ts: int | None = None +class CustomDataEntry(BaseModel): + id: int + ft_trade_id: int + cd_key: str + cd_type: str + cd_value: Any + created_at: datetime + updated_at: datetime | None = None + + class ListCustomData(BaseModel): trade_id: int - custom_data: list[dict[str, Any]] + custom_data: list[CustomDataEntry] From 493b6f65920933f09d9d6b48b9d6ee5d327eca5f Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Wed, 12 Mar 2025 00:12:43 -0400 Subject: [PATCH 22/53] chore: update api custom-data related routes with better not found error handling --- freqtrade/rpc/api_server/api_v1.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 51597b027..1a171e90f 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -226,7 +226,10 @@ def list_open_trades_custom_data( If a key is provided, it will be used to filter data accordingly. Pagination is implemented via the `limit` and `offset` parameters. """ - return rpc._rpc_list_custom_data(key=key, limit=limit, offset=offset) + try: + return rpc._rpc_list_custom_data(key=key, limit=limit, offset=offset) + except RPCException as e: + raise HTTPException(status_code=404, detail=str(e)) @router.get("/trades/{trade_id}/custom-data", response_model=list[ListCustomData], tags=["trading"]) @@ -235,7 +238,10 @@ def list_custom_data(trade_id: int, key: str | None = Query(None), rpc: RPC = De Fetch custom data for a specific trade. If a key is provided, it will be used to filter data accordingly. """ - return rpc._rpc_list_custom_data(trade_id, key=key) + try: + return rpc._rpc_list_custom_data(trade_id, key=key) + except RPCException as e: + raise HTTPException(status_code=404, detail=str(e)) # TODO: Missing response model From f85891941ff514409861eb2606d9c67d1679c411 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Wed, 12 Mar 2025 00:17:12 -0400 Subject: [PATCH 23/53] chore: update telegram _list_custom_data according to _rpc_list_custom_data output format change --- freqtrade/rpc/telegram.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 5b4982346..bba052522 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1976,16 +1976,19 @@ class Telegram(RPCHandler): results = self._rpc._rpc_list_custom_data(trade_id, key) messages = [] if len(results) > 0: - messages.append("Found custom-data entr" + ("ies: " if len(results) > 1 else "y: ")) - for result in results: + trade_custom_data = results[0]["custom_data"] + messages.append( + "Found custom-data entr" + ("ies: " if len(trade_custom_data) > 1 else "y: ") + ) + for custom_data in trade_custom_data: lines = [ - f"*Key:* `{result['cd_key']}`", - f"*ID:* `{result['id']}`", - f"*Trade ID:* `{result['ft_trade_id']}`", - f"*Type:* `{result['cd_type']}`", - f"*Value:* `{result['cd_value']}`", - f"*Create Date:* `{format_date(result['created_at'])}`", - f"*Update Date:* `{format_date(result['updated_at'])}`", + f"*Key:* `{custom_data['cd_key']}`", + f"*ID:* `{custom_data['id']}`", + f"*Trade ID:* `{custom_data['ft_trade_id']}`", + f"*Type:* `{custom_data['cd_type']}`", + f"*Value:* `{custom_data['cd_value']}`", + f"*Create Date:* `{format_date(custom_data['created_at'])}`", + f"*Update Date:* `{format_date(custom_data['updated_at'])}`", ] # Filter empty lines using list-comprehension messages.append("\n".join([line for line in lines if line])) From fe1665473375185e97960482a0dd8ab838b5a127 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Wed, 12 Mar 2025 00:21:24 -0400 Subject: [PATCH 24/53] test: slight change on expected string in test_telegram_list_custom_data --- tests/rpc/test_rpc_telegram.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 1882e09c4..6fff97abd 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -2861,9 +2861,7 @@ async def test_telegram_list_custom_data(default_conf_usdt, update, ticker, fee, context.args = ["1"] await telegram._list_custom_data(update=update, context=context) assert msg_mock.call_count == 1 - assert ( - "Didn't find any custom-data entries for Trade ID: `1`" in msg_mock.call_args_list[0][0][0] - ) + assert "No custom_data found for Trade ID: 1." in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() # Add some custom data From 2c2cc086c352420107c8355c6e9f7ac31cbe2e0b Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Wed, 12 Mar 2025 00:24:17 -0400 Subject: [PATCH 25/53] test: add test_api_custom_data_single_trade to validate api route behaviour --- tests/rpc/test_rpc_apiserver.py | 80 +++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index af33cd95c..55a1b5a76 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -25,6 +25,7 @@ from freqtrade.exceptions import DependencyException, ExchangeError, Operational from freqtrade.loggers import setup_logging, setup_logging_pre from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import Trade +from freqtrade.persistence.custom_data import CustomDataWrapper from freqtrade.rpc import RPC from freqtrade.rpc.api_server import ApiServer from freqtrade.rpc.api_server.api_auth import create_token, get_user_from_token @@ -802,6 +803,85 @@ def test_api_trade_single(botclient, mocker, fee, ticker, markets, is_short): assert rc.json()["is_short"] == is_short +@pytest.mark.usefixtures("init_persistence") +def test_api_custom_data_single_trade(botclient, fee): + Trade.reset_trades() + CustomDataWrapper.reset_custom_data() + + create_mock_trades_usdt(fee, use_db=True) + + trade1 = Trade.get_trades_proxy()[0] + + assert trade1.get_all_custom_data() == [] + + trade1.set_custom_data("test_str", "test_value") + trade1.set_custom_data("test_int", 0) + trade1.set_custom_data("test_float", 1.54) + trade1.set_custom_data("test_bool", True) + trade1.set_custom_data("test_dict", {"test": "vl"}) + + trade1.set_custom_data("test_int", 1) + + _, client = botclient + + # CASE 1 Checking all custom data of trade 1 + rc = client_get(client, f"{BASE_URI}/trades/1/custom-data") + assert_response(rc) + + # Validate response JSON structure + response_json = rc.json() + + assert len(response_json) == 1 + + res_cust_data = response_json[0]["custom_data"] + expected_data_td_1 = [ + {"ft_trade_id": 1, "cd_key": "test_str", "cd_type": "str", "cd_value": "test_value"}, + {"ft_trade_id": 1, "cd_key": "test_int", "cd_type": "int", "cd_value": "1"}, + {"ft_trade_id": 1, "cd_key": "test_float", "cd_type": "float", "cd_value": "1.54"}, + {"ft_trade_id": 1, "cd_key": "test_bool", "cd_type": "bool", "cd_value": "True"}, + {"ft_trade_id": 1, "cd_key": "test_dict", "cd_type": "dict", "cd_value": '{"test": "vl"}'}, + ] + + # Ensure response contains exactly the expected number of entries + assert len(res_cust_data) == len(expected_data_td_1), ( + f"\nError: Expected {len(expected_data_td_1)} entries, but got {len(res_cust_data)}.\n" + ) + + # Validate each expected entry + for expected in expected_data_td_1: + matched_item = None + for item in res_cust_data: + if item["cd_key"] == expected["cd_key"]: + matched_item = item + break + + assert matched_item is not None, ( + f"\nError: Missing expected entry for key '{expected['cd_key']}'\n" + f"Expected: {expected}\n" + ) + + # Validate individual fields and print only incorrect values + mismatches = [] + for field in ["ft_trade_id", "cd_type", "cd_value"]: + if matched_item[field] != expected[field]: + mismatches.append(f"{field}: Expected {expected[field]}, Got {matched_item[field]}") + + assert not mismatches, f"\nError in entry '{expected['cd_key']}':\n" + "\n".join(mismatches) + + # CASE 2 Checking specific existing key custom data of trade 1 + rc = client_get(client, f"{BASE_URI}/trades/1/custom-data?key=test_dict") + assert_response(rc, 200) + + # CASE 3 Checking specific not existing key custom data of trade 1 + rc = client_get(client, f"{BASE_URI}/trades/1/custom-data&key=test") + assert_response(rc, 404) + + # CASE 4 Trying to get custom-data from not existing trade + rc = client_get(client, f"{BASE_URI}/trades/13/custom-data") + assert_response(rc, 404) + assert rc.json()["detail"] == "No trade found for trade_id: 13" + + @pytest.mark.parametrize("is_short", [True, False]) def test_api_delete_trade(botclient, mocker, fee, markets, is_short): ftbot, client = botclient From 429505b134c1f9687bea8a72478901cbf70b2128 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Wed, 12 Mar 2025 10:37:39 -0400 Subject: [PATCH 26/53] test: add test_api_custom_data_multiple_open_trades to validate api route behaviour --- tests/rpc/test_rpc_apiserver.py | 141 ++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 55a1b5a76..4cb7424bf 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -882,6 +882,147 @@ def test_api_custom_data_single_trade(botclient, fee): assert rc.json()["detail"] == "No trade found for trade_id: 13" +@pytest.mark.usefixtures("init_persistence") +def test_api_custom_data_multiple_open_trades(botclient, fee): + use_db = True + Trade.use_db = use_db + Trade.reset_trades() + CustomDataWrapper.reset_custom_data() + create_mock_trades(fee, False, use_db) + trades = Trade.get_trades_proxy() + assert len(trades) == 6 + + assert isinstance(trades[0], Trade) + + trades = Trade.get_trades_proxy(is_open=True) + assert len(trades) == 4 + + create_mock_trades_usdt(fee, use_db=True) + + trade1 = Trade.get_trades_proxy(is_open=True)[0] + trade2 = Trade.get_trades_proxy(is_open=True)[1] + + # Initially, no custom data should be present. + assert trade1.get_all_custom_data() == [] + assert trade2.get_all_custom_data() == [] + + # Set custom data for the two open trades. + trade1.set_custom_data("test_str", "test_value_t1") + trade1.set_custom_data("test_float", 1.54) + trade1.set_custom_data("test_dict", {"test_t1": "vl_t1"}) + + trade2.set_custom_data("test_str", "test_value_t2") + trade2.set_custom_data("test_float", 1.55) + trade2.set_custom_data("test_dict", {"test_t2": "vl_t2"}) + + _, client = botclient + + # CASE 1: Checking all custom data for both trades. + rc = client_get(client, f"{BASE_URI}/trades/open/custom-data") + assert_response(rc) + + response_json = rc.json() + + # Expecting two trade entries in the response + assert len(response_json) == 2, ( + f"\nError: Expected 2 trade entries, but got {len(response_json)}.\n" + ) + + # Define expected custom data for each trade. + # The keys now use the actual trade_ids from the custom data. + expected_custom_data = { + 1: [ + { + "id": 1, + "ft_trade_id": 1, + "cd_key": "test_str", + "cd_type": "str", + "cd_value": "test_value_t1", + }, + { + "id": 2, + "ft_trade_id": 1, + "cd_key": "test_float", + "cd_type": "float", + "cd_value": "1.54", + }, + { + "id": 3, + "ft_trade_id": 1, + "cd_key": "test_dict", + "cd_type": "dict", + "cd_value": '{"test_t1": "vl_t1"}', + }, + ], + 4: [ + { + "id": 4, + "ft_trade_id": 4, + "cd_key": "test_str", + "cd_type": "str", + "cd_value": "test_value_t2", + }, + { + "id": 5, + "ft_trade_id": 4, + "cd_key": "test_float", + "cd_type": "float", + "cd_value": "1.55", + }, + { + "id": 6, + "ft_trade_id": 4, + "cd_key": "test_dict", + "cd_type": "dict", + "cd_value": '{"test_t2": "vl_t2"}', + }, + ], + } + + # Iterate over each trade's data in the response and validate entries. + for trade_entry in response_json: + trade_id = trade_entry.get("trade_id") + assert trade_id in expected_custom_data, f"\nUnexpected trade_id: {trade_id}" + + custom_data_list = trade_entry.get("custom_data") + expected_data = expected_custom_data[trade_id] + assert len(custom_data_list) == len(expected_data), ( + f"\nError for trade_id {trade_id}: \ + Expected {len(expected_data)} entries, but got {len(custom_data_list)}.\n" + ) + + # For each expected entry, check that the response contains the correct entry. + for expected in expected_data: + matched_item = None + for item in custom_data_list: + if item["cd_key"] == expected["cd_key"]: + matched_item = item + break + + assert matched_item is not None, ( + f"\nError: For trade_id {trade_id}, \ + missing expected entry for key '{expected['cd_key']}'\n" + f"Expected: {expected}\n" + ) + + # Validate key fields. + mismatches = [] + for field in ["id", "ft_trade_id", "cd_key", "cd_type", "cd_value"]: + if matched_item[field] != expected[field]: + mismatches.append( + f"{field}: Expected {expected[field]}, Got {matched_item[field]}" + ) + # Check for field presence of created_at and updated_at without comparing values. + for field in ["created_at", "updated_at"]: + if field not in matched_item: + mismatches.append(f"Missing field: {field}") + + assert not mismatches, ( + f"\nError in entry '{expected['cd_key']}' for trade_id {trade_id}:\n" + + "\n".join(mismatches) + ) + + @pytest.mark.parametrize("is_short", [True, False]) def test_api_delete_trade(botclient, mocker, fee, markets, is_short): ftbot, client = botclient From eec16cfc8adc8a1d8afb079e705f73dd7805fb54 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 17 Mar 2025 14:58:16 -0400 Subject: [PATCH 27/53] chore: move list_custom_data closer to trades related functions --- ft_client/freqtrade_client/ft_rest_client.py | 28 ++++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/ft_client/freqtrade_client/ft_rest_client.py b/ft_client/freqtrade_client/ft_rest_client.py index 1758e1f03..dfdbb0a6a 100755 --- a/ft_client/freqtrade_client/ft_rest_client.py +++ b/ft_client/freqtrade_client/ft_rest_client.py @@ -269,6 +269,20 @@ class FtRestClient: params["offset"] = offset return self._get("trades", params) + def list_custom_data(self, trade_id=None, key=None): + """Lists custom_data of the running bot. + + :param tradeid: Optional keyword argument - Id of the trade + + :return: json object + """ + params = {} + trade_id = -1 + if trade_id is not None: + params["trade_id"] = trade_id + + return self._get("trades/{tradeid}/custom_data", params=params) + def trade(self, trade_id): """Return specific trade @@ -484,17 +498,3 @@ class FtRestClient: :return: json object """ return self._get("health") - - def list_custom_data(self, trade_id=None, key=None): - """Lists custom_data of the running bot. - - :param tradeid: Optional keyword argument - Id of the trade - - :return: json object - """ - params = {} - trade_id = -1 - if trade_id is not None: - params["trade_id"] = trade_id - - return self._get("trades/{tradeid}/custom_data", params=params) From 484943a640d3eb920afb835f5041b7d58fa0442e Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 17 Mar 2025 15:05:58 -0400 Subject: [PATCH 28/53] feat: set trade_id as required param in list_custom_data, add key as optional --- ft_client/freqtrade_client/ft_rest_client.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/ft_client/freqtrade_client/ft_rest_client.py b/ft_client/freqtrade_client/ft_rest_client.py index dfdbb0a6a..5b5f3acf2 100755 --- a/ft_client/freqtrade_client/ft_rest_client.py +++ b/ft_client/freqtrade_client/ft_rest_client.py @@ -269,17 +269,18 @@ class FtRestClient: params["offset"] = offset return self._get("trades", params) - def list_custom_data(self, trade_id=None, key=None): - """Lists custom_data of the running bot. + def list_custom_data(self, trade_id, key=None): + """List custom_data of the running bot for specific trade. - :param tradeid: Optional keyword argument - Id of the trade + :param tradeid: keyword argument - Id of the trade + :param key: Optional keyword argument - key of the custom-data :return: json object """ params = {} - trade_id = -1 - if trade_id is not None: - params["trade_id"] = trade_id + params["trade_id"] = trade_id + if key is not None: + params["key"] = key return self._get("trades/{tradeid}/custom_data", params=params) From 4a432760eda8230a7c992f083c4e3a4aec3895a4 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 17 Mar 2025 17:14:37 -0400 Subject: [PATCH 29/53] feat: add list_open_trades_custom_data to ft rest client --- ft_client/freqtrade_client/ft_rest_client.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/ft_client/freqtrade_client/ft_rest_client.py b/ft_client/freqtrade_client/ft_rest_client.py index 5b5f3acf2..799a28959 100755 --- a/ft_client/freqtrade_client/ft_rest_client.py +++ b/ft_client/freqtrade_client/ft_rest_client.py @@ -269,6 +269,23 @@ class FtRestClient: params["offset"] = offset return self._get("trades", params) + def list_open_trades_custom_data(self, key=None, limit=100, offset=0): + """List open trades custom_data of the running bot. + + :param key: Optional keyword argument - key of the custom-data + :param limit: limit of trades + :param offset: trades offset for pagination + + :return: json object + """ + params = {} + params["limit"] = limit + params["offset"] = offset + if key is not None: + params["key"] = key + + return self._get("trades/open/custom_data", params=params) + def list_custom_data(self, trade_id, key=None): """List custom_data of the running bot for specific trade. From ef58aaf9e92969910f2dd3a67dbbfe26c1d70f07 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 17 Mar 2025 17:33:40 -0400 Subject: [PATCH 30/53] chore: update rest client custom-data related functions inline comment and help content --- docs/rest-api.md | 13 +++++++++++++ ft_client/freqtrade_client/ft_rest_client.py | 12 +++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index b958c0927..10e4534c0 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -302,6 +302,19 @@ trades :param limit: Limits trades to the X last trades. Max 500 trades. :param offset: Offset by this amount of trades. +list_open_trades_custom_data + Return a dict containing open trades custom-datas + + :param key: str, optional - Key of the custom-data + :param limit: Limits trades to X trades. + :param offset: Offset by this amount of trades. + +list_custom_data + Return a dict containing custom-datas of a specified trade + + :param trade_id: int - ID of the trade + :param key: str, optional - Key of the custom-data + version Return the version of the bot. diff --git a/ft_client/freqtrade_client/ft_rest_client.py b/ft_client/freqtrade_client/ft_rest_client.py index 799a28959..0460bec29 100755 --- a/ft_client/freqtrade_client/ft_rest_client.py +++ b/ft_client/freqtrade_client/ft_rest_client.py @@ -272,10 +272,9 @@ class FtRestClient: def list_open_trades_custom_data(self, key=None, limit=100, offset=0): """List open trades custom_data of the running bot. - :param key: Optional keyword argument - key of the custom-data + :param key: str, optional - Key of the custom-data :param limit: limit of trades :param offset: trades offset for pagination - :return: json object """ params = {} @@ -287,12 +286,11 @@ class FtRestClient: return self._get("trades/open/custom_data", params=params) def list_custom_data(self, trade_id, key=None): - """List custom_data of the running bot for specific trade. + """List custom_data of the running bot for a specific trade. - :param tradeid: keyword argument - Id of the trade - :param key: Optional keyword argument - key of the custom-data - - :return: json object + :param trade_id: int - ID of the trade + :param key: str, optional - Key of the custom-data + :return: JSON object """ params = {} params["trade_id"] = trade_id From 87a64cbe6868a2d6015292a34ae4bf93c4e64e3a Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 17 Mar 2025 17:43:08 -0400 Subject: [PATCH 31/53] chore: small refactor in _rpc_list_custom_data --- freqtrade/rpc/rpc.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c952b24d6..f9edfb99a 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1151,22 +1151,20 @@ class RPC: else: custom_data = trade.get_all_custom_data() - # Format each custom data entry. - formatted_custom_data = [ - { - "id": data_entry.id, - "ft_trade_id": data_entry.ft_trade_id, - "cd_key": data_entry.cd_key, - "cd_type": data_entry.cd_type, - "cd_value": data_entry.cd_value, - "created_at": data_entry.created_at, - "updated_at": data_entry.updated_at, - } - for data_entry in custom_data - ] - - # Append result for the trade if any custom data was found. - if formatted_custom_data: + # Format and Append result for the trade if any custom data was found. + if custom_data: + formatted_custom_data = [ + { + "id": data_entry.id, + "ft_trade_id": data_entry.ft_trade_id, + "cd_key": data_entry.cd_key, + "cd_type": data_entry.cd_type, + "cd_value": data_entry.cd_value, + "created_at": data_entry.created_at, + "updated_at": data_entry.updated_at, + } + for data_entry in custom_data + ] results.append({"trade_id": trade.id, "custom_data": formatted_custom_data}) # Handle case when there is no custom data found across trades. From f66d81c4b8f1d81358731f1da5bbb0d4c94006b3 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 17 Mar 2025 17:45:29 -0400 Subject: [PATCH 32/53] chore: wording update in _rpc_list_custom_data --- freqtrade/rpc/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index f9edfb99a..f9c30d916 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1176,7 +1176,7 @@ class RPC: message_details = f"found for Trade ID: {trade_id}." elif key: message_details = f"with key '{key}' found for any open trades." - raise RPCException(f"No custom_data {message_details}") + raise RPCException(f"No custom-data {message_details}") return results From 23187f0c4136d7d4a9ec3852d46a9e038852de37 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 17 Mar 2025 17:52:29 -0400 Subject: [PATCH 33/53] chore: simplify error handling in _rpc_list_custom_data --- freqtrade/rpc/rpc.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index f9c30d916..e4ec1a6b5 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1167,16 +1167,15 @@ class RPC: ] results.append({"trade_id": trade.id, "custom_data": formatted_custom_data}) - # Handle case when there is no custom data found across trades. - if not results: - message_details = "found for any open trades." - if key and trade_id: - message_details = f"with key '{key}' found for Trade ID: {trade_id}." - elif trade_id: - message_details = f"found for Trade ID: {trade_id}." - elif key: - message_details = f"with key '{key}' found for any open trades." - raise RPCException(f"No custom-data {message_details}") + # Handle case when there is no custom data found across trades. + if not results: + message_details = "" + if key: + message_details += f"with key '{key}' " + message_details += ( + f"found for Trade ID: {trade_id}." if trade_id else "found for any open trades." + ) + raise RPCException(f"No custom-data {message_details}") return results From 0c7a2747d3382c267b2b09f66a6255480346c404 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 17 Mar 2025 18:02:04 -0400 Subject: [PATCH 34/53] chore: revert unnecessary get_custom_data changes --- freqtrade/persistence/trade_model.py | 7 +------ freqtrade/rpc/rpc.py | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index c3532c783..c1fbbc30c 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1351,20 +1351,15 @@ class LocalTrade: """ CustomDataWrapper.set_custom_data(trade_id=self.id, key=key, value=value) - def get_custom_data(self, key: str, default: Any = None, retrieval_mode: str = "value") -> Any: + def get_custom_data(self, key: str, default: Any = None) -> Any: """ Get custom data for this trade. :param key: key of the custom data :param default: value to return if no data is found - :param retrieval_mode: 'value' (default) to return the custom data's value, - or 'object' to return the entire custom data object. """ data = CustomDataWrapper.get_custom_data(trade_id=self.id, key=key) if data: - if retrieval_mode == "object": - return data[0] - # default behavior: return only the value return data[0].value return default diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e4ec1a6b5..3649adbf6 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1145,7 +1145,7 @@ class RPC: for trade in trades: # Depending on whether a specific key is provided, retrieve custom data accordingly. if key: - data = trade.get_custom_data(key=key, retrieval_mode="object") + data = trade.get_custom_data_entry(key=key) # If data exists, wrap it in a list so the output remains consistent. custom_data = [data] if data else [] else: From 68ad688665dc9a4f02b64a99a700b9d53920aecc Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 17 Mar 2025 18:04:36 -0400 Subject: [PATCH 35/53] test: update test_telegram_list_custom_data wording --- tests/rpc/test_rpc_telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 6fff97abd..d009c92ae 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -2861,7 +2861,7 @@ async def test_telegram_list_custom_data(default_conf_usdt, update, ticker, fee, context.args = ["1"] await telegram._list_custom_data(update=update, context=context) assert msg_mock.call_count == 1 - assert "No custom_data found for Trade ID: 1." in msg_mock.call_args_list[0][0][0] + assert "No custom-data found for Trade ID: 1." in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() # Add some custom data From 90219f040b9cef3c3ed688f8dfc1320ae46c7c8b Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Mon, 17 Mar 2025 23:18:44 -0400 Subject: [PATCH 36/53] chore: enhance list custom-data output format --- freqtrade/rpc/rpc.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 3649adbf6..94f53808f 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -33,7 +33,7 @@ from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import Exchange, timeframe_to_minutes, timeframe_to_msecs from freqtrade.exchange.exchange_utils import price_to_precision from freqtrade.loggers import bufferHandler -from freqtrade.persistence import KeyStoreKeys, KeyValueStore, PairLocks, Trade +from freqtrade.persistence import CustomDataWrapper, KeyStoreKeys, KeyValueStore, PairLocks, Trade from freqtrade.persistence.models import PairLock from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -1155,11 +1155,10 @@ class RPC: if custom_data: formatted_custom_data = [ { - "id": data_entry.id, - "ft_trade_id": data_entry.ft_trade_id, - "cd_key": data_entry.cd_key, - "cd_type": data_entry.cd_type, + "key": data_entry.cd_key, + "type": data_entry.cd_type, "cd_value": data_entry.cd_value, + "value": CustomDataWrapper._convert_custom_data(data_entry), "created_at": data_entry.created_at, "updated_at": data_entry.updated_at, } From ba0c22b6f0a87ba33b8cb921f73b722f58de67cd Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 18 Mar 2025 00:46:46 -0400 Subject: [PATCH 37/53] chore: enhance update rpc_list_custom_data output --- freqtrade/rpc/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 94f53808f..969d53687 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1158,7 +1158,7 @@ class RPC: "key": data_entry.cd_key, "type": data_entry.cd_type, "cd_value": data_entry.cd_value, - "value": CustomDataWrapper._convert_custom_data(data_entry), + "value": CustomDataWrapper._convert_custom_data(data_entry).value, "created_at": data_entry.created_at, "updated_at": data_entry.updated_at, } From 83a8651d41fa9f942b21e277e14620b0625536e0 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 18 Mar 2025 00:48:18 -0400 Subject: [PATCH 38/53] chore: update api schema according rpc_list_custom_data output --- freqtrade/rpc/api_server/api_schemas.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 87adb4d24..0cbda9629 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -640,11 +640,10 @@ class Health(BaseModel): class CustomDataEntry(BaseModel): - id: int - ft_trade_id: int - cd_key: str - cd_type: str + key: str + type: str cd_value: Any + value: Any created_at: datetime updated_at: datetime | None = None From 578ba9ea4a2791bce4c53769b6752d4f499920f7 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 18 Mar 2025 00:50:03 -0400 Subject: [PATCH 39/53] test: update custom-data api related tests according rpc_list_custom_data output --- tests/rpc/test_rpc_apiserver.py | 69 +++++++++++++++------------------ 1 file changed, 31 insertions(+), 38 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 4cb7424bf..c651940c5 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -835,11 +835,11 @@ def test_api_custom_data_single_trade(botclient, fee): res_cust_data = response_json[0]["custom_data"] expected_data_td_1 = [ - {"ft_trade_id": 1, "cd_key": "test_str", "cd_type": "str", "cd_value": "test_value"}, - {"ft_trade_id": 1, "cd_key": "test_int", "cd_type": "int", "cd_value": "1"}, - {"ft_trade_id": 1, "cd_key": "test_float", "cd_type": "float", "cd_value": "1.54"}, - {"ft_trade_id": 1, "cd_key": "test_bool", "cd_type": "bool", "cd_value": "True"}, - {"ft_trade_id": 1, "cd_key": "test_dict", "cd_type": "dict", "cd_value": '{"test": "vl"}'}, + {"key": "test_str", "type": "str", "cd_value": "test_value", "value": "test_value"}, + {"key": "test_int", "type": "int", "cd_value": "1", "value": 1}, + {"key": "test_float", "type": "float", "cd_value": "1.54", "value": 1.54}, + {"key": "test_bool", "type": "bool", "cd_value": "True", "value": True}, + {"key": "test_dict", "type": "dict", "cd_value": '{"test": "vl"}', "value": {"test": "vl"}}, ] # Ensure response contains exactly the expected number of entries @@ -851,22 +851,21 @@ def test_api_custom_data_single_trade(botclient, fee): for expected in expected_data_td_1: matched_item = None for item in res_cust_data: - if item["cd_key"] == expected["cd_key"]: + if item["key"] == expected["key"]: matched_item = item break assert matched_item is not None, ( - f"\nError: Missing expected entry for key '{expected['cd_key']}'\n" - f"Expected: {expected}\n" + f"\nError: Missing expected entry for key '{expected['key']}'\nExpected: {expected}\n" ) # Validate individual fields and print only incorrect values mismatches = [] - for field in ["ft_trade_id", "cd_type", "cd_value"]: + for field in ["key", "type", "cd_value", "value"]: if matched_item[field] != expected[field]: mismatches.append(f"{field}: Expected {expected[field]}, Got {matched_item[field]}") - assert not mismatches, f"\nError in entry '{expected['cd_key']}':\n" + "\n".join(mismatches) + assert not mismatches, f"\nError in entry '{expected['key']}':\n" + "\n".join(mismatches) # CASE 2 Checking specific existing key custom data of trade 1 rc = client_get(client, f"{BASE_URI}/trades/1/custom-data?key=test_dict") @@ -933,48 +932,42 @@ def test_api_custom_data_multiple_open_trades(botclient, fee): expected_custom_data = { 1: [ { - "id": 1, - "ft_trade_id": 1, - "cd_key": "test_str", - "cd_type": "str", + "key": "test_str", + "type": "str", "cd_value": "test_value_t1", + "value": "test_value_t1", }, { - "id": 2, - "ft_trade_id": 1, - "cd_key": "test_float", - "cd_type": "float", + "key": "test_float", + "type": "float", "cd_value": "1.54", + "value": 1.54, }, { - "id": 3, - "ft_trade_id": 1, - "cd_key": "test_dict", - "cd_type": "dict", + "key": "test_dict", + "type": "dict", "cd_value": '{"test_t1": "vl_t1"}', + "value": {"test_t1": "vl_t1"}, }, ], 4: [ { - "id": 4, - "ft_trade_id": 4, - "cd_key": "test_str", - "cd_type": "str", + "key": "test_str", + "type": "str", "cd_value": "test_value_t2", + "value": "test_value_t2", }, { - "id": 5, - "ft_trade_id": 4, - "cd_key": "test_float", - "cd_type": "float", + "key": "test_float", + "type": "float", "cd_value": "1.55", + "value": 1.55, }, { - "id": 6, - "ft_trade_id": 4, - "cd_key": "test_dict", - "cd_type": "dict", + "key": "test_dict", + "type": "dict", "cd_value": '{"test_t2": "vl_t2"}', + "value": {"test_t2": "vl_t2"}, }, ], } @@ -995,19 +988,19 @@ def test_api_custom_data_multiple_open_trades(botclient, fee): for expected in expected_data: matched_item = None for item in custom_data_list: - if item["cd_key"] == expected["cd_key"]: + if item["key"] == expected["key"]: matched_item = item break assert matched_item is not None, ( f"\nError: For trade_id {trade_id}, \ - missing expected entry for key '{expected['cd_key']}'\n" + missing expected entry for key '{expected['key']}'\n" f"Expected: {expected}\n" ) # Validate key fields. mismatches = [] - for field in ["id", "ft_trade_id", "cd_key", "cd_type", "cd_value"]: + for field in ["key", "type", "cd_value", "value"]: if matched_item[field] != expected[field]: mismatches.append( f"{field}: Expected {expected[field]}, Got {matched_item[field]}" @@ -1018,7 +1011,7 @@ def test_api_custom_data_multiple_open_trades(botclient, fee): mismatches.append(f"Missing field: {field}") assert not mismatches, ( - f"\nError in entry '{expected['cd_key']}' for trade_id {trade_id}:\n" + f"\nError in entry '{expected['key']}' for trade_id {trade_id}:\n" + "\n".join(mismatches) ) From 17e4f5ed1f794d3e2892f310eb941f5a953bbbf6 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 18 Mar 2025 00:51:18 -0400 Subject: [PATCH 40/53] chore: update telegram _list_custom_data --- freqtrade/rpc/telegram.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index bba052522..1646699e0 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1982,11 +1982,10 @@ class Telegram(RPCHandler): ) for custom_data in trade_custom_data: lines = [ - f"*Key:* `{custom_data['cd_key']}`", - f"*ID:* `{custom_data['id']}`", - f"*Trade ID:* `{custom_data['ft_trade_id']}`", - f"*Type:* `{custom_data['cd_type']}`", - f"*Value:* `{custom_data['cd_value']}`", + f"*Trade ID:* `{trade_id}`", + f"*Key:* `{custom_data['key']}`", + f"*Type:* `{custom_data['type']}`", + f"*Value:* `{custom_data['value']}`", f"*Create Date:* `{format_date(custom_data['created_at'])}`", f"*Update Date:* `{format_date(custom_data['updated_at'])}`", ] From 06406b7103b9557918657ad876262ce2fb5022a3 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 18 Mar 2025 00:52:21 -0400 Subject: [PATCH 41/53] test: update test_telegram_list_custom_data --- tests/rpc/test_rpc_telegram.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index d009c92ae..f4f8ca2e9 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -2874,11 +2874,11 @@ async def test_telegram_list_custom_data(default_conf_usdt, update, ticker, fee, assert msg_mock.call_count == 3 assert "Found custom-data entries: " in msg_mock.call_args_list[0][0][0] assert ( - "*Key:* `test_int`\n*ID:* `1`\n*Trade ID:* `1`\n*Type:* `int`\n*Value:* `1`\n*Create Date:*" + "*Trade ID:* `1`\n*Key:* `test_int`\n*Type:* `int`\n*Value:* `1`\n*Create Date:*" ) in msg_mock.call_args_list[1][0][0] assert ( - "*Key:* `test_dict`\n*ID:* `2`\n*Trade ID:* `1`\n*Type:* `dict`\n" - '*Value:* `{"test": "dict"}`\n*Create Date:* `' + "*Trade ID:* `1`\n*Key:* `test_dict`\n*Type:* `dict`\n" + "*Value:* `{'test': 'dict'}`\n*Create Date:* `" ) in msg_mock.call_args_list[2][0][0] msg_mock.reset_mock() From bd511c2158aa057a5e634dfd0beeeeb96c1c4ab1 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 18 Mar 2025 14:56:33 -0400 Subject: [PATCH 42/53] fix: rest client custom-data path --- ft_client/freqtrade_client/ft_rest_client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ft_client/freqtrade_client/ft_rest_client.py b/ft_client/freqtrade_client/ft_rest_client.py index 0460bec29..ef79efbe0 100755 --- a/ft_client/freqtrade_client/ft_rest_client.py +++ b/ft_client/freqtrade_client/ft_rest_client.py @@ -270,7 +270,7 @@ class FtRestClient: return self._get("trades", params) def list_open_trades_custom_data(self, key=None, limit=100, offset=0): - """List open trades custom_data of the running bot. + """List open trades custom-data of the running bot. :param key: str, optional - Key of the custom-data :param limit: limit of trades @@ -283,10 +283,10 @@ class FtRestClient: if key is not None: params["key"] = key - return self._get("trades/open/custom_data", params=params) + return self._get("trades/open/custom-data", params=params) def list_custom_data(self, trade_id, key=None): - """List custom_data of the running bot for a specific trade. + """List custom-data of the running bot for a specific trade. :param trade_id: int - ID of the trade :param key: str, optional - Key of the custom-data @@ -297,7 +297,7 @@ class FtRestClient: if key is not None: params["key"] = key - return self._get("trades/{tradeid}/custom_data", params=params) + return self._get("trades/{tradeid}/custom-data", params=params) def trade(self, trade_id): """Return specific trade From 9a1f2d42a7f3d33fcc00f10142a2664b48e7160d Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Wed, 19 Mar 2025 02:52:38 -0400 Subject: [PATCH 43/53] fix: list_custom_data trade id variable value --- ft_client/freqtrade_client/ft_rest_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ft_client/freqtrade_client/ft_rest_client.py b/ft_client/freqtrade_client/ft_rest_client.py index ef79efbe0..5e15bc185 100755 --- a/ft_client/freqtrade_client/ft_rest_client.py +++ b/ft_client/freqtrade_client/ft_rest_client.py @@ -288,7 +288,7 @@ class FtRestClient: def list_custom_data(self, trade_id, key=None): """List custom-data of the running bot for a specific trade. - :param trade_id: int - ID of the trade + :param trade_id: ID of the trade :param key: str, optional - Key of the custom-data :return: JSON object """ @@ -297,7 +297,7 @@ class FtRestClient: if key is not None: params["key"] = key - return self._get("trades/{tradeid}/custom-data", params=params) + return self._get(f"trades/{trade_id}/custom-data", params=params) def trade(self, trade_id): """Return specific trade From b83754c5a40307bdf48ee18143ffaee8e3b70b68 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Wed, 19 Mar 2025 02:55:51 -0400 Subject: [PATCH 44/53] chore: remove trade id from telegram response for list_custom_data --- freqtrade/rpc/telegram.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 320898899..739a2af86 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1987,7 +1987,6 @@ class Telegram(RPCHandler): ) for custom_data in trade_custom_data: lines = [ - f"*Trade ID:* `{trade_id}`", f"*Key:* `{custom_data['key']}`", f"*Type:* `{custom_data['type']}`", f"*Value:* `{custom_data['value']}`", From 61b29962c46cf208936cf4bc3e467866d86d044f Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Wed, 19 Mar 2025 03:10:23 -0400 Subject: [PATCH 45/53] test: update test after trade id removal from telegram response for list_custom_data --- tests/rpc/test_rpc_telegram.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 47148165d..71ce557e2 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -2916,11 +2916,10 @@ async def test_telegram_list_custom_data(default_conf_usdt, update, ticker, fee, assert msg_mock.call_count == 3 assert "Found custom-data entries: " in msg_mock.call_args_list[0][0][0] assert ( - "*Trade ID:* `1`\n*Key:* `test_int`\n*Type:* `int`\n*Value:* `1`\n*Create Date:*" + "*Key:* `test_int`\n*Type:* `int`\n*Value:* `1`\n*Create Date:*" ) in msg_mock.call_args_list[1][0][0] assert ( - "*Trade ID:* `1`\n*Key:* `test_dict`\n*Type:* `dict`\n" - "*Value:* `{'test': 'dict'}`\n*Create Date:* `" + "*Key:* `test_dict`\n*Type:* `dict`\n*Value:* `{'test': 'dict'}`\n*Create Date:* `" ) in msg_mock.call_args_list[2][0][0] msg_mock.reset_mock() From 95f5db9dcddf8ccf15074aba9e3b15814cd15d67 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Wed, 19 Mar 2025 03:11:43 -0400 Subject: [PATCH 46/53] fix: implement ordering for rpc_list_custom_data --- freqtrade/rpc/rpc.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 969d53687..de3761c90 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1130,8 +1130,12 @@ class RPC: trades: Sequence[Trade] if trade_id is None: # Get all open trades + order_by: Any = Trade.close_date.desc() trades = Trade.session.scalars( - Trade.get_trades_query([Trade.is_open.is_(True)]).limit(limit).offset(offset) + Trade.get_trades_query([Trade.is_open.is_(True)]) + .order_by(order_by) + .limit(limit) + .offset(offset) ).all() else: trades = Trade.get_trades(trade_filter=[Trade.id == trade_id]).all() From d3464ac2dc430782594aef3c5fe2f216c122f190 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Wed, 19 Mar 2025 15:18:33 -0400 Subject: [PATCH 47/53] chore: remove cd_value from rpc custom data output --- freqtrade/rpc/rpc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index de3761c90..737ba772e 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1161,7 +1161,6 @@ class RPC: { "key": data_entry.cd_key, "type": data_entry.cd_type, - "cd_value": data_entry.cd_value, "value": CustomDataWrapper._convert_custom_data(data_entry).value, "created_at": data_entry.created_at, "updated_at": data_entry.updated_at, From 0d7854ff1b83b6edee9c88dc9b9154d0db00ae75 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Wed, 19 Mar 2025 15:24:19 -0400 Subject: [PATCH 48/53] test: update tests after removal of cd_value field from list custom-data response --- tests/rpc/test_rpc_apiserver.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index c651940c5..2e3637760 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -835,11 +835,11 @@ def test_api_custom_data_single_trade(botclient, fee): res_cust_data = response_json[0]["custom_data"] expected_data_td_1 = [ - {"key": "test_str", "type": "str", "cd_value": "test_value", "value": "test_value"}, - {"key": "test_int", "type": "int", "cd_value": "1", "value": 1}, - {"key": "test_float", "type": "float", "cd_value": "1.54", "value": 1.54}, - {"key": "test_bool", "type": "bool", "cd_value": "True", "value": True}, - {"key": "test_dict", "type": "dict", "cd_value": '{"test": "vl"}', "value": {"test": "vl"}}, + {"key": "test_str", "type": "str", "value": "test_value"}, + {"key": "test_int", "type": "int", "value": 1}, + {"key": "test_float", "type": "float", "value": 1.54}, + {"key": "test_bool", "type": "bool", "value": True}, + {"key": "test_dict", "type": "dict", "value": {"test": "vl"}}, ] # Ensure response contains exactly the expected number of entries @@ -861,7 +861,7 @@ def test_api_custom_data_single_trade(botclient, fee): # Validate individual fields and print only incorrect values mismatches = [] - for field in ["key", "type", "cd_value", "value"]: + for field in ["key", "type", "value"]: if matched_item[field] != expected[field]: mismatches.append(f"{field}: Expected {expected[field]}, Got {matched_item[field]}") @@ -934,19 +934,16 @@ def test_api_custom_data_multiple_open_trades(botclient, fee): { "key": "test_str", "type": "str", - "cd_value": "test_value_t1", "value": "test_value_t1", }, { "key": "test_float", "type": "float", - "cd_value": "1.54", "value": 1.54, }, { "key": "test_dict", "type": "dict", - "cd_value": '{"test_t1": "vl_t1"}', "value": {"test_t1": "vl_t1"}, }, ], @@ -954,19 +951,16 @@ def test_api_custom_data_multiple_open_trades(botclient, fee): { "key": "test_str", "type": "str", - "cd_value": "test_value_t2", "value": "test_value_t2", }, { "key": "test_float", "type": "float", - "cd_value": "1.55", "value": 1.55, }, { "key": "test_dict", "type": "dict", - "cd_value": '{"test_t2": "vl_t2"}', "value": {"test_t2": "vl_t2"}, }, ], @@ -1000,7 +994,7 @@ def test_api_custom_data_multiple_open_trades(botclient, fee): # Validate key fields. mismatches = [] - for field in ["key", "type", "cd_value", "value"]: + for field in ["key", "type", "value"]: if matched_item[field] != expected[field]: mismatches.append( f"{field}: Expected {expected[field]}, Got {matched_item[field]}" From 48b7a85c905c2dcdc5882f755d66251bd8435968 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Wed, 19 Mar 2025 15:24:55 -0400 Subject: [PATCH 49/53] chore: update schema after removal of cd_value field from list custom-data response --- freqtrade/rpc/api_server/api_schemas.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 0cbda9629..975166458 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -642,7 +642,6 @@ class Health(BaseModel): class CustomDataEntry(BaseModel): key: str type: str - cd_value: Any value: Any created_at: datetime updated_at: datetime | None = None From 1b4f8dfa54320cf29f54e6702cc66a3835d6d659 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Wed, 19 Mar 2025 15:30:02 -0400 Subject: [PATCH 50/53] chore: use open_date for _rpc_list_custom_data output ordering --- freqtrade/rpc/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 737ba772e..edfeee353 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1130,7 +1130,7 @@ class RPC: trades: Sequence[Trade] if trade_id is None: # Get all open trades - order_by: Any = Trade.close_date.desc() + order_by: Any = Trade.open_date.desc() trades = Trade.session.scalars( Trade.get_trades_query([Trade.is_open.is_(True)]) .order_by(order_by) From 6d8011e075d5ac6c9e2fd5cdb9018222184466dc Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Wed, 19 Mar 2025 15:48:04 -0400 Subject: [PATCH 51/53] chore: use Trade id for _rpc_list_custom_data output ordering --- freqtrade/rpc/rpc.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index edfeee353..232f70853 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1130,10 +1130,9 @@ class RPC: trades: Sequence[Trade] if trade_id is None: # Get all open trades - order_by: Any = Trade.open_date.desc() trades = Trade.session.scalars( Trade.get_trades_query([Trade.is_open.is_(True)]) - .order_by(order_by) + .order_by(Trade.id) .limit(limit) .offset(offset) ).all() From 42e45a0a65b3a58002d23be4d2065567a509d7f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Mar 2025 06:43:50 +0100 Subject: [PATCH 52/53] chore: simplify import and docstring --- freqtrade/rpc/rpc.py | 2 +- tests/rpc/test_rpc_apiserver.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 232f70853..075ddd374 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1125,7 +1125,7 @@ class RPC: Returns an array of dictionaries, each containing: - "trade_id": the ID of the trade (int) - "custom_data": a list of custom data dicts, each with the fields: - "id", "ft_trade_id", "cd_key", "cd_type", "cd_value", "created_at", "updated_at" + "id", "key", "type", "value", "created_at", "updated_at" """ trades: Sequence[Trade] if trade_id is None: diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 2e3637760..a93f4c9de 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -24,8 +24,7 @@ from freqtrade.enums import CandleType, RunMode, State, TradingMode from freqtrade.exceptions import DependencyException, ExchangeError, OperationalException from freqtrade.loggers import setup_logging, setup_logging_pre from freqtrade.optimize.backtesting import Backtesting -from freqtrade.persistence import Trade -from freqtrade.persistence.custom_data import CustomDataWrapper +from freqtrade.persistence import CustomDataWrapper, Trade from freqtrade.rpc import RPC from freqtrade.rpc.api_server import ApiServer from freqtrade.rpc.api_server.api_auth import create_token, get_user_from_token From 425701ddcf5be2e2734b7198f68b88c097d1580b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Mar 2025 06:44:34 +0100 Subject: [PATCH 53/53] test: simplify assert message --- tests/rpc/test_rpc_apiserver.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index a93f4c9de..99d8350e9 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -843,7 +843,7 @@ def test_api_custom_data_single_trade(botclient, fee): # Ensure response contains exactly the expected number of entries assert len(res_cust_data) == len(expected_data_td_1), ( - f"\nError: Expected {len(expected_data_td_1)} entries, but got {len(res_cust_data)}.\n" + f"Expected {len(expected_data_td_1)} entries, but got {len(res_cust_data)}.\n" ) # Validate each expected entry @@ -855,7 +855,7 @@ def test_api_custom_data_single_trade(botclient, fee): break assert matched_item is not None, ( - f"\nError: Missing expected entry for key '{expected['key']}'\nExpected: {expected}\n" + f"Missing expected entry for key '{expected['key']}'\nExpected: {expected}\n" ) # Validate individual fields and print only incorrect values @@ -864,7 +864,7 @@ def test_api_custom_data_single_trade(botclient, fee): if matched_item[field] != expected[field]: mismatches.append(f"{field}: Expected {expected[field]}, Got {matched_item[field]}") - assert not mismatches, f"\nError in entry '{expected['key']}':\n" + "\n".join(mismatches) + assert not mismatches, f"Error in entry '{expected['key']}':\n" + "\n".join(mismatches) # CASE 2 Checking specific existing key custom data of trade 1 rc = client_get(client, f"{BASE_URI}/trades/1/custom-data?key=test_dict") @@ -922,9 +922,7 @@ def test_api_custom_data_multiple_open_trades(botclient, fee): response_json = rc.json() # Expecting two trade entries in the response - assert len(response_json) == 2, ( - f"\nError: Expected 2 trade entries, but got {len(response_json)}.\n" - ) + assert len(response_json) == 2, f"Expected 2 trade entries, but got {len(response_json)}.\n" # Define expected custom data for each trade. # The keys now use the actual trade_ids from the custom data. @@ -973,8 +971,8 @@ def test_api_custom_data_multiple_open_trades(botclient, fee): custom_data_list = trade_entry.get("custom_data") expected_data = expected_custom_data[trade_id] assert len(custom_data_list) == len(expected_data), ( - f"\nError for trade_id {trade_id}: \ - Expected {len(expected_data)} entries, but got {len(custom_data_list)}.\n" + f"Error for trade_id {trade_id}: " + f"Expected {len(expected_data)} entries, but got {len(custom_data_list)}.\n" ) # For each expected entry, check that the response contains the correct entry. @@ -986,8 +984,8 @@ def test_api_custom_data_multiple_open_trades(botclient, fee): break assert matched_item is not None, ( - f"\nError: For trade_id {trade_id}, \ - missing expected entry for key '{expected['key']}'\n" + f"For trade_id {trade_id}, " + f"missing expected entry for key '{expected['key']}'\n" f"Expected: {expected}\n" ) @@ -1004,7 +1002,7 @@ def test_api_custom_data_multiple_open_trades(botclient, fee): mismatches.append(f"Missing field: {field}") assert not mismatches, ( - f"\nError in entry '{expected['key']}' for trade_id {trade_id}:\n" + f"Error in entry '{expected['key']}' for trade_id {trade_id}:\n" + "\n".join(mismatches) )