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.
| `count` | Displays number of trades used and available.
| `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.
| `forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`).
| `forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`).
@ -182,6 +183,11 @@ count
daily
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 from the database.
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)
locks
Return current locks
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.
| `/count` | Displays number of trades used and available
| `/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
| `/forcesell <trade_id>` | Instantly sells the given trade (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]:
return {
'id': self.id,
'pair': self.pair,
'lock_time': self.lock_time.strftime(DATETIME_PRINT_FORMAT),
'lock_timestamp': int(self.lock_time.replace(tzinfo=timezone.utc).timestamp() * 1000),

View File

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

View File

@ -11,13 +11,14 @@ from freqtrade.data.history import get_datahandler
from freqtrade.exceptions import OperationalException
from freqtrade.rpc import RPC
from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload,
BlacklistResponse, Count, Daily, DeleteTrade,
ForceBuyPayload, ForceBuyResponse,
ForceSellPayload, Locks, Logs, OpenTradeSchema,
PairHistory, PerformanceEntry, Ping, PlotConfig,
Profit, ResultMsg, ShowConfig, Stats, StatusMsg,
StrategyListResponse, StrategyResponse,
TradeResponse, Version, WhitelistResponse)
BlacklistResponse, Count, Daily,
DeleteLockRequest, DeleteTrade, ForceBuyPayload,
ForceBuyResponse, ForceSellPayload, Locks, Logs,
OpenTradeSchema, PairHistory, PerformanceEntry,
Ping, PlotConfig, Profit, ResultMsg, ShowConfig,
Stats, StatusMsg, StrategyListResponse,
StrategyResponse, TradeResponse, Version,
WhitelistResponse)
from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional
from freqtrade.rpc.rpc import RPCException
@ -136,11 +137,21 @@ def whitelist(rpc: RPC = Depends(get_rpc)):
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)):
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'])
def logs(limit: Optional[int] = None, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_get_logs(limit)

View File

@ -3,7 +3,7 @@ This module contains class to define a RPC communications
"""
import logging
from abc import abstractmethod
from datetime import date, datetime, timedelta
from datetime import date, datetime, timedelta, timezone
from enum import Enum
from math import isnan
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.misc import shorten_date
from freqtrade.persistence import PairLocks, Trade
from freqtrade.persistence.models import PairLock
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from freqtrade.state import State
@ -663,7 +664,7 @@ class RPC:
}
def _rpc_locks(self) -> Dict[str, Any]:
""" Returns the current locks"""
""" Returns the current locks """
locks = PairLocks.get_pair_locks(None)
return {
@ -671,6 +672,25 @@ class RPC:
'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:
""" Returns the currently active whitelist"""
res = {'method': self._freqtrade.pairlists.name_list,

View File

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

View File

@ -118,6 +118,14 @@ class FtRestClient():
"""
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):
"""Return the amount of open trades.

View File

@ -1,7 +1,7 @@
# pragma pylint: disable=missing-docstring, C0103
# 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
import pytest
@ -10,6 +10,7 @@ from numpy import isnan
from freqtrade.edge import PairInfo
from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError
from freqtrade.persistence import Trade
from freqtrade.persistence.pairlock_middleware import PairLocks
from freqtrade.rpc import RPC, RPCException
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from freqtrade.state import State
@ -911,6 +912,24 @@ def test_rpcforcebuy_disabled(mocker, default_conf) -> 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:
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 '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):
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'], "
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], "
"['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']"
"]")
@ -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 '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: