diff --git a/docs/rest-api.md b/docs/rest-api.md index 69a0f31f5..cf196559d 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -166,6 +166,7 @@ freqtrade-client --config rest_config.json [optional parameters] | `mix_tags [pair]` | Shows profit statistics for each combinations of enter tag + exit reasons for given pair (or all pairs if pair isn't given). Pair is optional. | `locks` | Displays currently locked pairs. | `delete_lock ` | Deletes (disables) the lock by id. +| `locks add , , [side], [reason]` | Locks a pair until "until". (Until will be rounded up to the nearest timeframe). | `profit` | Display a summary of your profit/loss from close trades and some stats about your performance. | `forceexit ` | Instantly exits the given trade (Ignoring `minimum_roi`). | `forceexit all` | Instantly exits all open trades (Ignoring `minimum_roi`). diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index af8d8ddf4..6d2c8a13d 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -1,7 +1,7 @@ from datetime import date, datetime from typing import Any, Dict, List, Optional, Union -from pydantic import BaseModel, RootModel, SerializeAsAny +from pydantic import AwareDatetime, BaseModel, RootModel, SerializeAsAny from freqtrade.constants import IntOrInf from freqtrade.enums import MarginMode, OrderTypeValues, SignalDirection, TradingMode @@ -378,6 +378,13 @@ class Locks(BaseModel): locks: List[LockModel] +class LocksPayload(BaseModel): + pair: str + side: str = '*' # Default to both sides + until: AwareDatetime + reason: Optional[str] = None + + class DeleteLockRequest(BaseModel): pair: Optional[str] = None lockid: Optional[int] = None diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 99fc3d451..8146fe276 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -15,10 +15,10 @@ from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, Blac DeleteLockRequest, DeleteTrade, Entry, ExchangeListResponse, Exit, ForceEnterPayload, ForceEnterResponse, ForceExitPayload, - FreqAIModelListResponse, Health, Locks, Logs, - MixTag, OpenTradeSchema, PairHistory, - PerformanceEntry, Ping, PlotConfig, Profit, - ResultMsg, ShowConfig, Stats, StatusMsg, + FreqAIModelListResponse, Health, Locks, + LocksPayload, Logs, MixTag, OpenTradeSchema, + PairHistory, PerformanceEntry, Ping, PlotConfig, + Profit, ResultMsg, ShowConfig, Stats, StatusMsg, StrategyListResponse, StrategyResponse, SysInfo, Version, WhitelistResponse) from freqtrade.rpc.api_server.deps import get_config, get_exchange, get_rpc, get_rpc_optional @@ -255,6 +255,13 @@ def delete_lock_pair(payload: DeleteLockRequest, rpc: RPC = Depends(get_rpc)): return rpc._rpc_delete_lock(lockid=payload.lockid, pair=payload.pair) +@router.post('/locks', response_model=Locks, tags=['info', 'locks']) +def add_locks(payload: List[LocksPayload], rpc: RPC = Depends(get_rpc)): + for lock in payload: + rpc._rpc_add_lock(lock.pair, lock.until, lock.reason, lock.side) + return rpc._rpc_locks() + + @router.get('/logs', response_model=Logs, tags=['info']) def logs(limit: Optional[int] = None): return RPC._rpc_get_logs(limit) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 8bb7f754f..a2a02cc68 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1104,6 +1104,16 @@ class RPC: return self._rpc_locks() + def _rpc_add_lock( + self, pair: str, until: datetime, reason: Optional[str], side: str) -> PairLock: + lock = PairLocks.lock_pair( + pair=pair, + until=until, + reason=reason, + side=side, + ) + return lock + def _rpc_whitelist(self) -> Dict: """ Returns the currently active whitelist""" res = {'method': self._freqtrade.pairlists.name_list, diff --git a/ft_client/freqtrade_client/ft_rest_client.py b/ft_client/freqtrade_client/ft_rest_client.py index de782ee65..20e6d6f6e 100755 --- a/ft_client/freqtrade_client/ft_rest_client.py +++ b/ft_client/freqtrade_client/ft_rest_client.py @@ -7,7 +7,7 @@ so it can be used as a standalone script, and can be installed independently. import json import logging -from typing import Optional +from typing import Any, Dict, List, Optional, Union from urllib.parse import urlencode, urlparse, urlunparse import requests @@ -16,6 +16,9 @@ from requests.exceptions import ConnectionError logger = logging.getLogger("ft_rest_client") +ParamsT = Optional[Dict[str, Any]] +PostDataT = Optional[Union[Dict[str, Any], List[Dict[str, Any]]]] + class FtRestClient: @@ -58,13 +61,13 @@ class FtRestClient: except ConnectionError: logger.warning("Connection error") - def _get(self, apipath, params: Optional[dict] = None): + def _get(self, apipath, params: ParamsT = None): return self._call("GET", apipath, params=params) - def _delete(self, apipath, params: Optional[dict] = None): + def _delete(self, apipath, params: ParamsT = None): return self._call("DELETE", apipath, params=params) - def _post(self, apipath, params: Optional[dict] = None, data: Optional[dict] = None): + def _post(self, apipath, params: ParamsT = None, data: PostDataT = None): return self._call("POST", apipath, params=params, data=data) def start(self): @@ -148,6 +151,25 @@ class FtRestClient: """ return self._delete(f"locks/{lock_id}") + def lock_add(self, pair: str, until: str, side: str = '*', reason: str = ''): + """Lock pair + + :param pair: Pair to lock + :param until: Lock until this date (format "2024-03-30 16:00:00Z") + :param side: Side to lock (long, short, *) + :param reason: Reason for the lock + :return: json object + """ + data = [ + { + "pair": pair, + "until": until, + "side": side, + "reason": reason + } + ] + return self._post("locks", data=data) + def daily(self, days=None): """Return the profits for each day, and amount of trades. diff --git a/ft_client/test_client/test_rest_client.py b/ft_client/test_client/test_rest_client.py index e7c2f32e6..13e32f1c5 100644 --- a/ft_client/test_client/test_rest_client.py +++ b/ft_client/test_client/test_rest_client.py @@ -61,6 +61,7 @@ def test_FtRestClient_call_invalid(caplog): ('exits', []), ('mix_tags', []), ('locks', []), + ('lock_add', ["XRP/USDT", '2024-01-01 20:00:00Z', '*', 'rand']), ('delete_lock', [2]), ('daily', []), ('daily', [15]), diff --git a/pyproject.toml b/pyproject.toml index f7a244c8b..6c5252c59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,7 +88,8 @@ ignore_missing_imports = true namespace_packages = false warn_unused_ignores = true exclude = [ - '^build_helpers\.py$' + '^build_helpers\.py$', + '^ft_client/build/.*$', ] plugins = [ "sqlalchemy.ext.mypy.plugin" diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 0bf39fab7..050c51fed 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -11,7 +11,6 @@ from freqtrade.enums import SignalDirection, State, TradingMode from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError from freqtrade.persistence import Order, Trade from freqtrade.persistence.key_value_store import set_startup_time -from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from tests.conftest import (EXMS, create_mock_trades, create_mock_trades_usdt, @@ -1171,14 +1170,15 @@ def test_rpc_force_entry_wrong_mode(mocker, default_conf) -> None: @pytest.mark.usefixtures("init_persistence") -def test_rpc_delete_lock(mocker, default_conf): +def test_rpc_add_and_delete_lock(mocker, default_conf): freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc = RPC(freqtradebot) pair = 'ETH/BTC' - PairLocks.lock_pair(pair, datetime.now(timezone.utc) + timedelta(minutes=4)) - PairLocks.lock_pair(pair, datetime.now(timezone.utc) + timedelta(minutes=5)) - PairLocks.lock_pair(pair, datetime.now(timezone.utc) + timedelta(minutes=10)) + rpc._rpc_add_lock(pair, datetime.now(timezone.utc) + timedelta(minutes=4), '', '*') + rpc._rpc_add_lock(pair, datetime.now(timezone.utc) + timedelta(minutes=5), '', '*') + rpc._rpc_add_lock(pair, datetime.now(timezone.utc) + timedelta(minutes=10), '', '*') + locks = rpc._rpc_locks() assert locks['lock_count'] == 3 locks1 = rpc._rpc_delete_lock(lockid=locks['locks'][0]['id']) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 1e008d98e..9fdff1c7c 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -23,12 +23,13 @@ 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 PairLocks, Trade +from freqtrade.persistence import 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 freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer from freqtrade.rpc.api_server.webserver_bgwork import ApiBG +from freqtrade.util.datetime_helpers import format_date from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, create_mock_trades, get_mock_coro, get_patched_freqtradebot, log_has, log_has_re, patch_get_signal) @@ -553,8 +554,19 @@ def test_api_locks(botclient): assert rc.json()['lock_count'] == 0 assert rc.json()['lock_count'] == len(rc.json()['locks']) - PairLocks.lock_pair('ETH/BTC', datetime.now(timezone.utc) + timedelta(minutes=4), 'randreason') - PairLocks.lock_pair('XRP/BTC', datetime.now(timezone.utc) + timedelta(minutes=20), 'deadbeef') + rc = client_post(client, f"{BASE_URI}/locks", [ + { + "pair": "ETH/BTC", + "until": f"{format_date(datetime.now(timezone.utc) + timedelta(minutes=4))}Z", + "reason": "randreason" + }, { + "pair": "XRP/BTC", + "until": f"{format_date(datetime.now(timezone.utc) + timedelta(minutes=20))}Z", + "reason": "deadbeef" + } + ]) + assert_response(rc) + assert rc.json()['lock_count'] == 2 rc = client_get(client, f"{BASE_URI}/locks") assert_response(rc)