mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-09-20 09:31:12 +00:00
Merge pull request #10023 from freqtrade/feat/lock_api
Add lock post endpoint
This commit is contained in:
commit
fcfd25d50b
|
@ -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`).
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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]),
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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'])
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user