Merge pull request #10023 from freqtrade/feat/lock_api

Add lock post endpoint
This commit is contained in:
Matthias 2024-03-31 14:05:05 +02:00 committed by GitHub
commit fcfd25d50b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 79 additions and 18 deletions

View File

@ -166,6 +166,7 @@ freqtrade-client --config rest_config.json <command> [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. | `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. | `locks` | Displays currently locked pairs.
| `delete_lock <lock_id>` | Deletes (disables) the lock by id. | `delete_lock <lock_id>` | Deletes (disables) the lock by id.
| `locks add <pair>, <until>, [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. | `profit` | Display a summary of your profit/loss from close trades and some stats about your performance.
| `forceexit <trade_id>` | Instantly exits the given trade (Ignoring `minimum_roi`). | `forceexit <trade_id>` | Instantly exits the given trade (Ignoring `minimum_roi`).
| `forceexit all` | Instantly exits all open trades (Ignoring `minimum_roi`). | `forceexit all` | Instantly exits all open trades (Ignoring `minimum_roi`).

View File

@ -1,7 +1,7 @@
from datetime import date, datetime from datetime import date, datetime
from typing import Any, Dict, List, Optional, Union 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.constants import IntOrInf
from freqtrade.enums import MarginMode, OrderTypeValues, SignalDirection, TradingMode from freqtrade.enums import MarginMode, OrderTypeValues, SignalDirection, TradingMode
@ -378,6 +378,13 @@ class Locks(BaseModel):
locks: List[LockModel] locks: List[LockModel]
class LocksPayload(BaseModel):
pair: str
side: str = '*' # Default to both sides
until: AwareDatetime
reason: Optional[str] = None
class DeleteLockRequest(BaseModel): class DeleteLockRequest(BaseModel):
pair: Optional[str] = None pair: Optional[str] = None
lockid: Optional[int] = None lockid: Optional[int] = None

View File

@ -15,10 +15,10 @@ from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, Blac
DeleteLockRequest, DeleteTrade, Entry, DeleteLockRequest, DeleteTrade, Entry,
ExchangeListResponse, Exit, ForceEnterPayload, ExchangeListResponse, Exit, ForceEnterPayload,
ForceEnterResponse, ForceExitPayload, ForceEnterResponse, ForceExitPayload,
FreqAIModelListResponse, Health, Locks, Logs, FreqAIModelListResponse, Health, Locks,
MixTag, OpenTradeSchema, PairHistory, LocksPayload, Logs, MixTag, OpenTradeSchema,
PerformanceEntry, Ping, PlotConfig, Profit, PairHistory, PerformanceEntry, Ping, PlotConfig,
ResultMsg, ShowConfig, Stats, StatusMsg, Profit, ResultMsg, ShowConfig, Stats, StatusMsg,
StrategyListResponse, StrategyResponse, SysInfo, StrategyListResponse, StrategyResponse, SysInfo,
Version, WhitelistResponse) Version, WhitelistResponse)
from freqtrade.rpc.api_server.deps import get_config, get_exchange, get_rpc, get_rpc_optional 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) 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']) @router.get('/logs', response_model=Logs, tags=['info'])
def logs(limit: Optional[int] = None): def logs(limit: Optional[int] = None):
return RPC._rpc_get_logs(limit) return RPC._rpc_get_logs(limit)

View File

@ -1104,6 +1104,16 @@ class RPC:
return self._rpc_locks() 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: def _rpc_whitelist(self) -> Dict:
""" Returns the currently active whitelist""" """ Returns the currently active whitelist"""
res = {'method': self._freqtrade.pairlists.name_list, res = {'method': self._freqtrade.pairlists.name_list,

View File

@ -7,7 +7,7 @@ so it can be used as a standalone script, and can be installed independently.
import json import json
import logging import logging
from typing import Optional from typing import Any, Dict, List, Optional, Union
from urllib.parse import urlencode, urlparse, urlunparse from urllib.parse import urlencode, urlparse, urlunparse
import requests import requests
@ -16,6 +16,9 @@ from requests.exceptions import ConnectionError
logger = logging.getLogger("ft_rest_client") logger = logging.getLogger("ft_rest_client")
ParamsT = Optional[Dict[str, Any]]
PostDataT = Optional[Union[Dict[str, Any], List[Dict[str, Any]]]]
class FtRestClient: class FtRestClient:
@ -58,13 +61,13 @@ class FtRestClient:
except ConnectionError: except ConnectionError:
logger.warning("Connection error") 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) 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) 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) return self._call("POST", apipath, params=params, data=data)
def start(self): def start(self):
@ -148,6 +151,25 @@ class FtRestClient:
""" """
return self._delete(f"locks/{lock_id}") 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): def daily(self, days=None):
"""Return the profits for each day, and amount of trades. """Return the profits for each day, and amount of trades.

View File

@ -61,6 +61,7 @@ def test_FtRestClient_call_invalid(caplog):
('exits', []), ('exits', []),
('mix_tags', []), ('mix_tags', []),
('locks', []), ('locks', []),
('lock_add', ["XRP/USDT", '2024-01-01 20:00:00Z', '*', 'rand']),
('delete_lock', [2]), ('delete_lock', [2]),
('daily', []), ('daily', []),
('daily', [15]), ('daily', [15]),

View File

@ -88,7 +88,8 @@ ignore_missing_imports = true
namespace_packages = false namespace_packages = false
warn_unused_ignores = true warn_unused_ignores = true
exclude = [ exclude = [
'^build_helpers\.py$' '^build_helpers\.py$',
'^ft_client/build/.*$',
] ]
plugins = [ plugins = [
"sqlalchemy.ext.mypy.plugin" "sqlalchemy.ext.mypy.plugin"

View File

@ -11,7 +11,6 @@ from freqtrade.enums import SignalDirection, State, TradingMode
from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError
from freqtrade.persistence import Order, Trade from freqtrade.persistence import Order, Trade
from freqtrade.persistence.key_value_store import set_startup_time 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 import RPC, RPCException
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from tests.conftest import (EXMS, create_mock_trades, create_mock_trades_usdt, 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") @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) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
pair = 'ETH/BTC' pair = 'ETH/BTC'
PairLocks.lock_pair(pair, datetime.now(timezone.utc) + timedelta(minutes=4)) rpc._rpc_add_lock(pair, datetime.now(timezone.utc) + timedelta(minutes=4), '', '*')
PairLocks.lock_pair(pair, datetime.now(timezone.utc) + timedelta(minutes=5)) rpc._rpc_add_lock(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=10), '', '*')
locks = rpc._rpc_locks() locks = rpc._rpc_locks()
assert locks['lock_count'] == 3 assert locks['lock_count'] == 3
locks1 = rpc._rpc_delete_lock(lockid=locks['locks'][0]['id']) locks1 = rpc._rpc_delete_lock(lockid=locks['locks'][0]['id'])

View File

@ -23,12 +23,13 @@ from freqtrade.enums import CandleType, RunMode, State, TradingMode
from freqtrade.exceptions import DependencyException, ExchangeError, OperationalException from freqtrade.exceptions import DependencyException, ExchangeError, OperationalException
from freqtrade.loggers import setup_logging, setup_logging_pre from freqtrade.loggers import setup_logging, setup_logging_pre
from freqtrade.optimize.backtesting import Backtesting 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 import RPC
from freqtrade.rpc.api_server import ApiServer 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.api_auth import create_token, get_user_from_token
from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer
from freqtrade.rpc.api_server.webserver_bgwork import ApiBG 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, 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) 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'] == 0
assert rc.json()['lock_count'] == len(rc.json()['locks']) assert rc.json()['lock_count'] == len(rc.json()['locks'])
PairLocks.lock_pair('ETH/BTC', datetime.now(timezone.utc) + timedelta(minutes=4), 'randreason') rc = client_post(client, f"{BASE_URI}/locks", [
PairLocks.lock_pair('XRP/BTC', datetime.now(timezone.utc) + timedelta(minutes=20), 'deadbeef') {
"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") rc = client_get(client, f"{BASE_URI}/locks")
assert_response(rc) assert_response(rc)