Skip to content

Commit 26ba076

Browse files
authored
Merge pull request freqtrade#11093 from arenstar/api-server-list-custom-data
feat: api_server and client supporting list_custom_data
2 parents b97f3ca + 425701d commit 26ba076

File tree

9 files changed

+373
-42
lines changed

9 files changed

+373
-42
lines changed

docs/rest-api.md

+13
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,19 @@ trades
302302
:param limit: Limits trades to the X last trades. Max 500 trades.
303303
:param offset: Offset by this amount of trades.
304304

305+
list_open_trades_custom_data
306+
Return a dict containing open trades custom-datas
307+
308+
:param key: str, optional - Key of the custom-data
309+
:param limit: Limits trades to X trades.
310+
:param offset: Offset by this amount of trades.
311+
312+
list_custom_data
313+
Return a dict containing custom-datas of a specified trade
314+
315+
:param trade_id: int - ID of the trade
316+
:param key: str, optional - Key of the custom-data
317+
305318
version
306319
Return the version of the bot.
307320

freqtrade/persistence/trade_model.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1353,8 +1353,10 @@ def set_custom_data(self, key: str, value: Any) -> None:
13531353

13541354
def get_custom_data(self, key: str, default: Any = None) -> Any:
13551355
"""
1356-
Get custom data for this trade
1356+
Get custom data for this trade.
1357+
13571358
:param key: key of the custom data
1359+
:param default: value to return if no data is found
13581360
"""
13591361
data = CustomDataWrapper.get_custom_data(trade_id=self.id, key=key)
13601362
if data:

freqtrade/rpc/api_server/api_schemas.py

+13
Original file line numberDiff line numberDiff line change
@@ -637,3 +637,16 @@ class Health(BaseModel):
637637
bot_start_ts: int | None = None
638638
bot_startup: datetime | None = None
639639
bot_startup_ts: int | None = None
640+
641+
642+
class CustomDataEntry(BaseModel):
643+
key: str
644+
type: str
645+
value: Any
646+
created_at: datetime
647+
updated_at: datetime | None = None
648+
649+
650+
class ListCustomData(BaseModel):
651+
trade_id: int
652+
custom_data: list[CustomDataEntry]

freqtrade/rpc/api_server/api_v1.py

+31
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
FreqAIModelListResponse,
3030
Health,
3131
HyperoptLossListResponse,
32+
ListCustomData,
3233
Locks,
3334
LocksPayload,
3435
Logs,
@@ -213,6 +214,36 @@ def trade_reload(tradeid: int, rpc: RPC = Depends(get_rpc)):
213214
return rpc._rpc_trade_status([tradeid])[0]
214215

215216

217+
@router.get("/trades/open/custom-data", response_model=list[ListCustomData], tags=["trading"])
218+
def list_open_trades_custom_data(
219+
key: str | None = Query(None, description="Optional key to filter data"),
220+
limit: int = Query(100, ge=1, description="Maximum number of different trades to return data"),
221+
offset: int = Query(0, ge=0, description="Number of trades to skip for pagination"),
222+
rpc: RPC = Depends(get_rpc),
223+
):
224+
"""
225+
Fetch custom data for all open trades.
226+
If a key is provided, it will be used to filter data accordingly.
227+
Pagination is implemented via the `limit` and `offset` parameters.
228+
"""
229+
try:
230+
return rpc._rpc_list_custom_data(key=key, limit=limit, offset=offset)
231+
except RPCException as e:
232+
raise HTTPException(status_code=404, detail=str(e))
233+
234+
235+
@router.get("/trades/{trade_id}/custom-data", response_model=list[ListCustomData], tags=["trading"])
236+
def list_custom_data(trade_id: int, key: str | None = Query(None), rpc: RPC = Depends(get_rpc)):
237+
"""
238+
Fetch custom data for a specific trade.
239+
If a key is provided, it will be used to filter data accordingly.
240+
"""
241+
try:
242+
return rpc._rpc_list_custom_data(trade_id, key=key)
243+
except RPCException as e:
244+
raise HTTPException(status_code=404, detail=str(e))
245+
246+
216247
# TODO: Missing response model
217248
@router.get("/edge", tags=["info"])
218249
def edge(rpc: RPC = Depends(get_rpc)):

freqtrade/rpc/rpc.py

+64-25
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from freqtrade.exchange import Exchange, timeframe_to_minutes, timeframe_to_msecs
3434
from freqtrade.exchange.exchange_utils import price_to_precision
3535
from freqtrade.loggers import bufferHandler
36-
from freqtrade.persistence import KeyStoreKeys, KeyValueStore, PairLocks, Trade
36+
from freqtrade.persistence import CustomDataWrapper, KeyStoreKeys, KeyValueStore, PairLocks, Trade
3737
from freqtrade.persistence.models import PairLock
3838
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
3939
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
@@ -1115,31 +1115,70 @@ def _rpc_delete(self, trade_id: int) -> dict[str, str | int]:
11151115
"cancel_order_count": c_count,
11161116
}
11171117

1118-
def _rpc_list_custom_data(self, trade_id: int, key: str | None) -> list[dict[str, Any]]:
1119-
# Query for trade
1120-
trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first()
1121-
if trade is None:
1122-
return []
1123-
# Query custom_data
1124-
custom_data = []
1125-
if key:
1126-
data = trade.get_custom_data(key=key)
1127-
if data:
1128-
custom_data = [data]
1118+
def _rpc_list_custom_data(
1119+
self, trade_id: int | None = None, key: str | None = None, limit: int = 100, offset: int = 0
1120+
) -> list[dict[str, Any]]:
1121+
"""
1122+
Fetch custom data for a specific trade, or all open trades if `trade_id` is not provided.
1123+
Pagination is applied via `limit` and `offset`.
1124+
1125+
Returns an array of dictionaries, each containing:
1126+
- "trade_id": the ID of the trade (int)
1127+
- "custom_data": a list of custom data dicts, each with the fields:
1128+
"id", "key", "type", "value", "created_at", "updated_at"
1129+
"""
1130+
trades: Sequence[Trade]
1131+
if trade_id is None:
1132+
# Get all open trades
1133+
trades = Trade.session.scalars(
1134+
Trade.get_trades_query([Trade.is_open.is_(True)])
1135+
.order_by(Trade.id)
1136+
.limit(limit)
1137+
.offset(offset)
1138+
).all()
11291139
else:
1130-
custom_data = trade.get_all_custom_data()
1131-
return [
1132-
{
1133-
"id": data_entry.id,
1134-
"ft_trade_id": data_entry.ft_trade_id,
1135-
"cd_key": data_entry.cd_key,
1136-
"cd_type": data_entry.cd_type,
1137-
"cd_value": data_entry.cd_value,
1138-
"created_at": data_entry.created_at,
1139-
"updated_at": data_entry.updated_at,
1140-
}
1141-
for data_entry in custom_data
1142-
]
1140+
trades = Trade.get_trades(trade_filter=[Trade.id == trade_id]).all()
1141+
1142+
if not trades:
1143+
raise RPCException(
1144+
f"No trade found for trade_id: {trade_id}" if trade_id else "No open trades found."
1145+
)
1146+
1147+
results = []
1148+
for trade in trades:
1149+
# Depending on whether a specific key is provided, retrieve custom data accordingly.
1150+
if key:
1151+
data = trade.get_custom_data_entry(key=key)
1152+
# If data exists, wrap it in a list so the output remains consistent.
1153+
custom_data = [data] if data else []
1154+
else:
1155+
custom_data = trade.get_all_custom_data()
1156+
1157+
# Format and Append result for the trade if any custom data was found.
1158+
if custom_data:
1159+
formatted_custom_data = [
1160+
{
1161+
"key": data_entry.cd_key,
1162+
"type": data_entry.cd_type,
1163+
"value": CustomDataWrapper._convert_custom_data(data_entry).value,
1164+
"created_at": data_entry.created_at,
1165+
"updated_at": data_entry.updated_at,
1166+
}
1167+
for data_entry in custom_data
1168+
]
1169+
results.append({"trade_id": trade.id, "custom_data": formatted_custom_data})
1170+
1171+
# Handle case when there is no custom data found across trades.
1172+
if not results:
1173+
message_details = ""
1174+
if key:
1175+
message_details += f"with key '{key}' "
1176+
message_details += (
1177+
f"found for Trade ID: {trade_id}." if trade_id else "found for any open trades."
1178+
)
1179+
raise RPCException(f"No custom-data {message_details}")
1180+
1181+
return results
11431182

11441183
def _rpc_performance(self) -> list[dict[str, Any]]:
11451184
"""

freqtrade/rpc/telegram.py

+10-9
Original file line numberDiff line numberDiff line change
@@ -1981,16 +1981,17 @@ async def _list_custom_data(self, update: Update, context: CallbackContext) -> N
19811981
results = self._rpc._rpc_list_custom_data(trade_id, key)
19821982
messages = []
19831983
if len(results) > 0:
1984-
messages.append("Found custom-data entr" + ("ies: " if len(results) > 1 else "y: "))
1985-
for result in results:
1984+
trade_custom_data = results[0]["custom_data"]
1985+
messages.append(
1986+
"Found custom-data entr" + ("ies: " if len(trade_custom_data) > 1 else "y: ")
1987+
)
1988+
for custom_data in trade_custom_data:
19861989
lines = [
1987-
f"*Key:* `{result['cd_key']}`",
1988-
f"*ID:* `{result['id']}`",
1989-
f"*Trade ID:* `{result['ft_trade_id']}`",
1990-
f"*Type:* `{result['cd_type']}`",
1991-
f"*Value:* `{result['cd_value']}`",
1992-
f"*Create Date:* `{format_date(result['created_at'])}`",
1993-
f"*Update Date:* `{format_date(result['updated_at'])}`",
1990+
f"*Key:* `{custom_data['key']}`",
1991+
f"*Type:* `{custom_data['type']}`",
1992+
f"*Value:* `{custom_data['value']}`",
1993+
f"*Create Date:* `{format_date(custom_data['created_at'])}`",
1994+
f"*Update Date:* `{format_date(custom_data['updated_at'])}`",
19941995
]
19951996
# Filter empty lines using list-comprehension
19961997
messages.append("\n".join([line for line in lines if line]))

ft_client/freqtrade_client/ft_rest_client.py

+30
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,36 @@ def trades(self, limit=None, offset=None):
269269
params["offset"] = offset
270270
return self._get("trades", params)
271271

272+
def list_open_trades_custom_data(self, key=None, limit=100, offset=0):
273+
"""List open trades custom-data of the running bot.
274+
275+
:param key: str, optional - Key of the custom-data
276+
:param limit: limit of trades
277+
:param offset: trades offset for pagination
278+
:return: json object
279+
"""
280+
params = {}
281+
params["limit"] = limit
282+
params["offset"] = offset
283+
if key is not None:
284+
params["key"] = key
285+
286+
return self._get("trades/open/custom-data", params=params)
287+
288+
def list_custom_data(self, trade_id, key=None):
289+
"""List custom-data of the running bot for a specific trade.
290+
291+
:param trade_id: ID of the trade
292+
:param key: str, optional - Key of the custom-data
293+
:return: JSON object
294+
"""
295+
params = {}
296+
params["trade_id"] = trade_id
297+
if key is not None:
298+
params["key"] = key
299+
300+
return self._get(f"trades/{trade_id}/custom-data", params=params)
301+
272302
def trade(self, trade_id):
273303
"""Return specific trade
274304

0 commit comments

Comments
 (0)