diff --git a/docs/rest-api.md b/docs/rest-api.md index 5b33bfa6f..666056a65 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -151,6 +151,8 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `performance` | Show performance of each finished trade grouped by pair. | `balance` | Show account balance per currency. | `daily ` | Shows profit or loss per day, over the last n days (n defaults to 7). +| `weekly ` | Shows profit or loss per week, over the last n days (n defaults to 4). +| `monthly ` | Shows profit or loss per month, over the last n days (n defaults to 3). | `stats` | Display a summary of profit / loss reasons as well as average holding times. | `whitelist` | Show the current whitelist. | `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist. diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 892865d43..422e5ac3a 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -157,7 +157,7 @@ class Stats(BaseModel): durations: Dict[str, Optional[float]] -class DailyRecord(BaseModel): +class DailyWeeklyMonthlyRecord(BaseModel): date: date abs_profit: float rel_profit: float @@ -166,8 +166,8 @@ class DailyRecord(BaseModel): trade_count: int -class Daily(BaseModel): - data: List[DailyRecord] +class DailyWeeklyMonthly(BaseModel): + data: List[DailyWeeklyMonthlyRecord] fiat_display_currency: str stake_currency: str diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 7299364a8..8b1bb2a48 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -11,7 +11,7 @@ from freqtrade.enums import CandleType, TradingMode from freqtrade.exceptions import OperationalException from freqtrade.rpc import RPC from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload, - BlacklistResponse, Count, Daily, + BlacklistResponse, Count, DailyWeeklyMonthly, DeleteLockRequest, DeleteTrade, ExchangeListResponse, ForceEnterPayload, ForceEnterResponse, ForceExitPayload, @@ -51,7 +51,8 @@ logger = logging.getLogger(__name__) # 2.30: new /pairlists endpoint # 2.31: new /backtest/history/ delete endpoint # 2.32: new /backtest/history/ patch endpoint -API_VERSION = 2.32 +# 2.33: Additional weekly/monthly metrics +API_VERSION = 2.33 # Public API, requires no auth. router_public = APIRouter() @@ -99,12 +100,24 @@ def stats(rpc: RPC = Depends(get_rpc)): return rpc._rpc_stats() -@router.get('/daily', response_model=Daily, tags=['info']) +@router.get('/daily', response_model=DailyWeeklyMonthly, tags=['info']) def daily(timescale: int = 7, rpc: RPC = Depends(get_rpc), config=Depends(get_config)): return rpc._rpc_timeunit_profit(timescale, config['stake_currency'], config.get('fiat_display_currency', '')) +@router.get('/weekly', response_model=DailyWeeklyMonthly, tags=['info']) +def weekly(timescale: int = 4, rpc: RPC = Depends(get_rpc), config=Depends(get_config)): + return rpc._rpc_timeunit_profit(timescale, config['stake_currency'], + config.get('fiat_display_currency', ''), 'weeks') + + +@router.get('/monthly', response_model=DailyWeeklyMonthly, tags=['info']) +def monthly(timescale: int = 3, rpc: RPC = Depends(get_rpc), config=Depends(get_config)): + return rpc._rpc_timeunit_profit(timescale, config['stake_currency'], + config.get('fiat_display_currency', ''), 'months') + + @router.get('/status', response_model=List[OpenTradeSchema], tags=['info']) def status(rpc: RPC = Depends(get_rpc)): try: diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 61f4384b7..497bc1c82 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -364,7 +364,7 @@ class RPC: data = [ { - 'date': f"{key.year}-{key.month:02d}" if timeunit == 'months' else key, + 'date': key, 'abs_profit': value["amount"], 'starting_balance': value["daily_stake"], 'rel_profit': value["rel_profit"], diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index aced89d7a..8972f4a16 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -51,6 +51,7 @@ class TimeunitMappings: message2: str callback: str default: int + dateformat: str def authorized_only(command_handler: Callable[..., Coroutine[Any, Any, None]]): @@ -736,10 +737,10 @@ class Telegram(RPCHandler): """ vals = { - 'days': TimeunitMappings('Day', 'Daily', 'days', 'update_daily', 7), + 'days': TimeunitMappings('Day', 'Daily', 'days', 'update_daily', 7, '%Y-%m-%d'), 'weeks': TimeunitMappings('Monday', 'Weekly', 'weeks (starting from Monday)', - 'update_weekly', 8), - 'months': TimeunitMappings('Month', 'Monthly', 'months', 'update_monthly', 6), + 'update_weekly', 8, '%Y-%m-%d'), + 'months': TimeunitMappings('Month', 'Monthly', 'months', 'update_monthly', 6, '%Y-%m'), } val = vals[unit] @@ -756,7 +757,7 @@ class Telegram(RPCHandler): unit ) stats_tab = tabulate( - [[f"{period['date']} ({period['trade_count']})", + [[f"{period['date']:{val.dateformat}} ({period['trade_count']})", f"{round_coin_value(period['abs_profit'], stats['stake_currency'])}", f"{period['fiat_value']:.2f} {stats['fiat_display_currency']}", f"{period['rel_profit']:.2%}", diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 2b4690287..dfe50cc2c 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -134,6 +134,20 @@ class FtRestClient: """ return self._get("daily", params={"timescale": days} if days else None) + def weekly(self, weeks=None): + """Return the profits for each week, and amount of trades. + + :return: json object + """ + return self._get("weekly", params={"timescale": weeks} if weeks else None) + + def monthly(self, months=None): + """Return the profits for each month, and amount of trades. + + :return: json object + """ + return self._get("monthly", params={"timescale": months} if months else None) + def edge(self): """Return information about edge. diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index a3c16d16e..856d10dc5 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -617,6 +617,47 @@ def test_api_daily(botclient, mocker, ticker, fee, markets): assert rc.json()['data'][0]['date'] == str(datetime.now(timezone.utc).date()) +def test_api_weekly(botclient, mocker, ticker, fee, markets, time_machine): + ftbot, client = botclient + patch_get_signal(ftbot) + mocker.patch.multiple( + EXMS, + get_balances=MagicMock(return_value=ticker), + fetch_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + time_machine.move_to("2023-03-31 21:45:05 +00:00") + rc = client_get(client, f"{BASE_URI}/weekly") + assert_response(rc) + assert len(rc.json()['data']) == 4 + assert rc.json()['stake_currency'] == 'BTC' + assert rc.json()['fiat_display_currency'] == 'USD' + # Moved to monday + assert rc.json()['data'][0]['date'] == '2023-03-27' + assert rc.json()['data'][1]['date'] == '2023-03-20' + + +def test_api_monthly(botclient, mocker, ticker, fee, markets, time_machine): + ftbot, client = botclient + patch_get_signal(ftbot) + mocker.patch.multiple( + EXMS, + get_balances=MagicMock(return_value=ticker), + fetch_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + time_machine.move_to("2023-03-31 21:45:05 +00:00") + rc = client_get(client, f"{BASE_URI}/monthly") + assert_response(rc) + assert len(rc.json()['data']) == 3 + assert rc.json()['stake_currency'] == 'BTC' + assert rc.json()['fiat_display_currency'] == 'USD' + assert rc.json()['data'][0]['date'] == '2023-03-01' + assert rc.json()['data'][1]['date'] == '2023-02-01' + + @pytest.mark.parametrize('is_short', [True, False]) def test_api_trades(botclient, mocker, fee, markets, is_short): ftbot, client = botclient