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.
| `locks` | Displays currently locked pairs.
| `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.
| `forceexit <trade_id>` | Instantly exits the given trade (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 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

View File

@ -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)

View File

@ -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,

View File

@ -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.

View File

@ -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]),

View File

@ -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"

View File

@ -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'])

View File

@ -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)