Merge pull request #4469 from freqtrade/rpc/locks

Add RPC methods to remove locks
This commit is contained in:
Matthias 2021-03-02 10:41:08 +01:00 committed by GitHub
commit 6eb253c31e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 140 additions and 25 deletions

View File

@ -131,6 +131,7 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
| `status` | Lists all open trades. | `status` | Lists all open trades.
| `count` | Displays number of trades used and available. | `count` | Displays number of trades used and available.
| `locks` | Displays currently locked pairs. | `locks` | Displays currently locked pairs.
| `delete_lock <lock_id>` | Deletes (disables) the lock by id.
| `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.
| `forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`). | `forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`).
| `forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). | `forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`).
@ -182,6 +183,11 @@ count
daily daily
Return the amount of open trades. Return the amount of open trades.
delete_lock
Delete (disable) lock from the database.
:param lock_id: ID for the lock to delete
delete_trade delete_trade
Delete trade from the database. Delete trade from the database.
Tries to close open orders. Requires manual handling of this asset on the exchange. Tries to close open orders. Requires manual handling of this asset on the exchange.
@ -202,6 +208,9 @@ forcesell
:param tradeid: Id of the trade (can be received via status command) :param tradeid: Id of the trade (can be received via status command)
locks
Return current locks
logs logs
Show latest logs. Show latest logs.

View File

@ -146,6 +146,7 @@ official commands. You can ask at any moment for help with `/help`.
| `/delete <trade_id>` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange. | `/delete <trade_id>` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange.
| `/count` | Displays number of trades used and available | `/count` | Displays number of trades used and available
| `/locks` | Show currently locked pairs. | `/locks` | Show currently locked pairs.
| `/unlock <pair | lock_id>` | Remove the lock for this pair (or for this lock id).
| `/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
| `/forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`). | `/forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`).
| `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). | `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`).

View File

@ -765,6 +765,7 @@ class PairLock(_DECL_BASE):
def to_json(self) -> Dict[str, Any]: def to_json(self) -> Dict[str, Any]:
return { return {
'id': self.id,
'pair': self.pair, 'pair': self.pair,
'lock_time': self.lock_time.strftime(DATETIME_PRINT_FORMAT), 'lock_time': self.lock_time.strftime(DATETIME_PRINT_FORMAT),
'lock_timestamp': int(self.lock_time.replace(tzinfo=timezone.utc).timestamp() * 1000), 'lock_timestamp': int(self.lock_time.replace(tzinfo=timezone.utc).timestamp() * 1000),

View File

@ -210,6 +210,7 @@ class ForceBuyResponse(BaseModel):
class LockModel(BaseModel): class LockModel(BaseModel):
id: int
active: bool active: bool
lock_end_time: str lock_end_time: str
lock_end_timestamp: int lock_end_timestamp: int
@ -224,6 +225,11 @@ class Locks(BaseModel):
locks: List[LockModel] locks: List[LockModel]
class DeleteLockRequest(BaseModel):
pair: Optional[str]
lockid: Optional[int]
class Logs(BaseModel): class Logs(BaseModel):
log_count: int log_count: int
logs: List[List] logs: List[List]

View File

@ -11,13 +11,14 @@ from freqtrade.data.history import get_datahandler
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.rpc import RPC from freqtrade.rpc import RPC
from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload, from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload,
BlacklistResponse, Count, Daily, DeleteTrade, BlacklistResponse, Count, Daily,
ForceBuyPayload, ForceBuyResponse, DeleteLockRequest, DeleteTrade, ForceBuyPayload,
ForceSellPayload, Locks, Logs, OpenTradeSchema, ForceBuyResponse, ForceSellPayload, Locks, Logs,
PairHistory, PerformanceEntry, Ping, PlotConfig, OpenTradeSchema, PairHistory, PerformanceEntry,
Profit, ResultMsg, ShowConfig, Stats, StatusMsg, Ping, PlotConfig, Profit, ResultMsg, ShowConfig,
StrategyListResponse, StrategyResponse, Stats, StatusMsg, StrategyListResponse,
TradeResponse, Version, WhitelistResponse) StrategyResponse, TradeResponse, Version,
WhitelistResponse)
from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional
from freqtrade.rpc.rpc import RPCException from freqtrade.rpc.rpc import RPCException
@ -136,11 +137,21 @@ def whitelist(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_whitelist() return rpc._rpc_whitelist()
@router.get('/locks', response_model=Locks, tags=['info']) @router.get('/locks', response_model=Locks, tags=['info', 'locks'])
def locks(rpc: RPC = Depends(get_rpc)): def locks(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_locks() return rpc._rpc_locks()
@router.delete('/locks/{lockid}', response_model=Locks, tags=['info', 'locks'])
def delete_lock(lockid: int, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_delete_lock(lockid=lockid)
@router.post('/locks/delete', response_model=Locks, tags=['info', 'locks'])
def delete_lock_pair(payload: DeleteLockRequest, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_delete_lock(lockid=payload.lockid, pair=payload.pair)
@router.get('/logs', response_model=Logs, tags=['info']) @router.get('/logs', response_model=Logs, tags=['info'])
def logs(limit: Optional[int] = None, rpc: RPC = Depends(get_rpc)): def logs(limit: Optional[int] = None, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_get_logs(limit) return rpc._rpc_get_logs(limit)

View File

@ -3,7 +3,7 @@ This module contains class to define a RPC communications
""" """
import logging import logging
from abc import abstractmethod from abc import abstractmethod
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta, timezone
from enum import Enum from enum import Enum
from math import isnan from math import isnan
from typing import Any, Dict, List, Optional, Tuple, Union from typing import Any, Dict, List, Optional, Tuple, Union
@ -20,6 +20,7 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
from freqtrade.loggers import bufferHandler from freqtrade.loggers import bufferHandler
from freqtrade.misc import shorten_date from freqtrade.misc import shorten_date
from freqtrade.persistence import PairLocks, Trade from freqtrade.persistence import PairLocks, Trade
from freqtrade.persistence.models import PairLock
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from freqtrade.state import State from freqtrade.state import State
@ -663,7 +664,7 @@ class RPC:
} }
def _rpc_locks(self) -> Dict[str, Any]: def _rpc_locks(self) -> Dict[str, Any]:
""" Returns the current locks""" """ Returns the current locks """
locks = PairLocks.get_pair_locks(None) locks = PairLocks.get_pair_locks(None)
return { return {
@ -671,6 +672,25 @@ class RPC:
'locks': [lock.to_json() for lock in locks] 'locks': [lock.to_json() for lock in locks]
} }
def _rpc_delete_lock(self, lockid: Optional[int] = None,
pair: Optional[str] = None) -> Dict[str, Any]:
""" Delete specific lock(s) """
locks = []
if pair:
locks = PairLocks.get_pair_locks(pair)
if lockid:
locks = PairLock.query.filter(PairLock.id == lockid).all()
for lock in locks:
lock.active = False
lock.lock_end_time = datetime.now(timezone.utc)
# session is always the same
PairLock.session.flush()
return self._rpc_locks()
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

@ -6,6 +6,7 @@ This module manage Telegram communication
import json import json
import logging import logging
from datetime import timedelta from datetime import timedelta
from html import escape
from itertools import chain from itertools import chain
from typing import Any, Callable, Dict, List, Union from typing import Any, Callable, Dict, List, Union
@ -144,6 +145,7 @@ class Telegram(RPCHandler):
CommandHandler('daily', self._daily), CommandHandler('daily', self._daily),
CommandHandler('count', self._count), CommandHandler('count', self._count),
CommandHandler('locks', self._locks), CommandHandler('locks', self._locks),
CommandHandler(['unlock', 'delete_locks'], self._delete_locks),
CommandHandler(['reload_config', 'reload_conf'], self._reload_config), CommandHandler(['reload_config', 'reload_conf'], self._reload_config),
CommandHandler(['show_config', 'show_conf'], self._show_config), CommandHandler(['show_config', 'show_conf'], self._show_config),
CommandHandler('stopbuy', self._stopbuy), CommandHandler('stopbuy', self._stopbuy),
@ -719,19 +721,35 @@ class Telegram(RPCHandler):
Handler for /locks. Handler for /locks.
Returns the currently active locks Returns the currently active locks
""" """
try:
locks = self._rpc._rpc_locks() locks = self._rpc._rpc_locks()
message = tabulate([[ message = tabulate([[
lock['id'],
lock['pair'], lock['pair'],
lock['lock_end_time'], lock['lock_end_time'],
lock['reason']] for lock in locks['locks']], lock['reason']] for lock in locks['locks']],
headers=['Pair', 'Until', 'Reason'], headers=['ID', 'Pair', 'Until', 'Reason'],
tablefmt='simple') tablefmt='simple')
message = "<pre>{}</pre>".format(message) message = f"<pre>{escape(message)}</pre>"
logger.debug(message) logger.debug(message)
self._send_msg(message, parse_mode=ParseMode.HTML) self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e)) @authorized_only
def _delete_locks(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /delete_locks.
Returns the currently active locks
"""
arg = context.args[0] if context.args and len(context.args) > 0 else None
lockid = None
pair = None
if arg:
try:
lockid = int(arg)
except ValueError:
pair = arg
self._rpc._rpc_delete_lock(lockid=lockid, pair=pair)
self._locks(update, context)
@authorized_only @authorized_only
def _whitelist(self, update: Update, context: CallbackContext) -> None: def _whitelist(self, update: Update, context: CallbackContext) -> None:
@ -850,6 +868,7 @@ class Telegram(RPCHandler):
"Avg. holding durationsfor buys and sells.`\n" "Avg. holding durationsfor buys and sells.`\n"
"*/count:* `Show number of active trades compared to allowed number of trades`\n" "*/count:* `Show number of active trades compared to allowed number of trades`\n"
"*/locks:* `Show currently locked pairs`\n" "*/locks:* `Show currently locked pairs`\n"
"*/unlock <pair|id>:* `Unlock this Pair (or this lock id if it's numeric)`\n"
"*/balance:* `Show account balance per currency`\n" "*/balance:* `Show account balance per currency`\n"
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
"*/reload_config:* `Reload configuration file` \n" "*/reload_config:* `Reload configuration file` \n"

View File

@ -118,6 +118,14 @@ class FtRestClient():
""" """
return self._get("locks") return self._get("locks")
def delete_lock(self, lock_id):
"""Delete (disable) lock from the database.
:param lock_id: ID for the lock to delete
:return: json object
"""
return self._delete("locks/{}".format(lock_id))
def daily(self, days=None): def daily(self, days=None):
"""Return the amount of open trades. """Return the amount of open trades.

View File

@ -1,7 +1,7 @@
# pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=missing-docstring, C0103
# pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments # pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments
from datetime import datetime from datetime import datetime, timedelta, timezone
from unittest.mock import ANY, MagicMock, PropertyMock from unittest.mock import ANY, MagicMock, PropertyMock
import pytest import pytest
@ -10,6 +10,7 @@ from numpy import isnan
from freqtrade.edge import PairInfo from freqtrade.edge import PairInfo
from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
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 freqtrade.state import State from freqtrade.state import State
@ -911,6 +912,24 @@ def test_rpcforcebuy_disabled(mocker, default_conf) -> None:
rpc._rpc_forcebuy(pair, None) rpc._rpc_forcebuy(pair, None)
@pytest.mark.usefixtures("init_persistence")
def test_rpc_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))
locks = rpc._rpc_locks()
assert locks['lock_count'] == 3
locks1 = rpc._rpc_delete_lock(lockid=locks['locks'][0]['id'])
assert locks1['lock_count'] == 2
locks2 = rpc._rpc_delete_lock(pair=pair)
assert locks2['lock_count'] == 0
def test_rpc_whitelist(mocker, default_conf) -> None: def test_rpc_whitelist(mocker, default_conf) -> None:
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())

View File

@ -418,6 +418,16 @@ def test_api_locks(botclient):
assert 'randreason' in (rc.json()['locks'][0]['reason'], rc.json()['locks'][1]['reason']) assert 'randreason' in (rc.json()['locks'][0]['reason'], rc.json()['locks'][1]['reason'])
assert 'deadbeef' in (rc.json()['locks'][0]['reason'], rc.json()['locks'][1]['reason']) assert 'deadbeef' in (rc.json()['locks'][0]['reason'], rc.json()['locks'][1]['reason'])
# Test deletions
rc = client_delete(client, f"{BASE_URI}/locks/1")
assert_response(rc)
assert rc.json()['lock_count'] == 1
rc = client_post(client, f"{BASE_URI}/locks/delete",
data='{"pair": "XRP/BTC"}')
assert_response(rc)
assert rc.json()['lock_count'] == 0
def test_api_show_config(botclient, mocker): def test_api_show_config(botclient, mocker):
ftbot, client = botclient ftbot, client = botclient

View File

@ -92,7 +92,8 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], " "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], "
"['delete'], ['performance'], ['stats'], ['daily'], ['count'], ['locks'], " "['delete'], ['performance'], ['stats'], ['daily'], ['count'], ['locks'], "
"['reload_config', 'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], " "['unlock', 'delete_locks'], ['reload_config', 'reload_conf'], "
"['show_config', 'show_conf'], ['stopbuy'], "
"['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']" "['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']"
"]") "]")
@ -981,6 +982,16 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None
assert 'deadbeef' in msg_mock.call_args_list[0][0][0] assert 'deadbeef' in msg_mock.call_args_list[0][0][0]
assert 'randreason' in msg_mock.call_args_list[0][0][0] assert 'randreason' in msg_mock.call_args_list[0][0][0]
context = MagicMock()
context.args = ['XRP/BTC']
msg_mock.reset_mock()
telegram._delete_locks(update=update, context=context)
assert 'ETH/BTC' in msg_mock.call_args_list[0][0][0]
assert 'randreason' in msg_mock.call_args_list[0][0][0]
assert 'XRP/BTC' not in msg_mock.call_args_list[0][0][0]
assert 'deadbeef' not in msg_mock.call_args_list[0][0][0]
def test_whitelist_static(default_conf, update, mocker) -> None: def test_whitelist_static(default_conf, update, mocker) -> None: