Add /locks Telegram endpoint
This commit is contained in:
parent
7caa6cfe31
commit
7a9768ffa6
@ -668,14 +668,14 @@ class PairLock(_DECL_BASE):
|
|||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
|
|
||||||
pair = Column(String, nullable=False)
|
pair = Column(String, nullable=False, index=True)
|
||||||
reason = Column(String, nullable=True)
|
reason = Column(String, nullable=True)
|
||||||
# Time the pair was locked (start time)
|
# Time the pair was locked (start time)
|
||||||
lock_time = Column(DateTime, nullable=False)
|
lock_time = Column(DateTime, nullable=False)
|
||||||
# Time until the pair is locked (end time)
|
# Time until the pair is locked (end time)
|
||||||
lock_end_time = Column(DateTime, nullable=False)
|
lock_end_time = Column(DateTime, nullable=False)
|
||||||
|
|
||||||
active = Column(Boolean, nullable=False, default=True)
|
active = Column(Boolean, nullable=False, default=True, index=True)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
lock_time = self.lock_time.strftime(DATETIME_PRINT_FORMAT)
|
lock_time = self.lock_time.strftime(DATETIME_PRINT_FORMAT)
|
||||||
@ -696,21 +696,24 @@ class PairLock(_DECL_BASE):
|
|||||||
PairLock.session.flush()
|
PairLock.session.flush()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_pair_locks(pair: str, now: Optional[datetime] = None) -> List['PairLock']:
|
def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> List['PairLock']:
|
||||||
"""
|
"""
|
||||||
Get all locks for this pair
|
Get all locks for this pair
|
||||||
:param pair: Pair to check for
|
:param pair: Pair to check for. Returns all current locks if pair is empty
|
||||||
:param now: Datetime object (generated via datetime.utcnow()). defaults to datetime.utcnow()
|
:param now: Datetime object (generated via datetime.now(timezone.utc)).
|
||||||
|
defaults to datetime.utcnow()
|
||||||
"""
|
"""
|
||||||
if not now:
|
if not now:
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
filters = [func.datetime(PairLock.lock_time) <= now,
|
||||||
|
func.datetime(PairLock.lock_end_time) >= now,
|
||||||
|
# Only active locks
|
||||||
|
PairLock.active.is_(True), ]
|
||||||
|
if pair:
|
||||||
|
filters.append(PairLock.pair == pair)
|
||||||
return PairLock.query.filter(
|
return PairLock.query.filter(
|
||||||
PairLock.pair == pair,
|
*filters
|
||||||
func.datetime(PairLock.lock_time) <= now,
|
|
||||||
func.datetime(PairLock.lock_end_time) >= now,
|
|
||||||
# Only active locks
|
|
||||||
PairLock.active.is_(True),
|
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -731,7 +734,8 @@ class PairLock(_DECL_BASE):
|
|||||||
def is_pair_locked(pair: str, now: Optional[datetime] = None) -> bool:
|
def is_pair_locked(pair: str, now: Optional[datetime] = None) -> bool:
|
||||||
"""
|
"""
|
||||||
:param pair: Pair to check for
|
:param pair: Pair to check for
|
||||||
:param now: Datetime object (generated via datetime.utcnow()). defaults to datetime.utcnow()
|
:param now: Datetime object (generated via datetime.now(timezone.utc)).
|
||||||
|
defaults to datetime.utcnow()
|
||||||
"""
|
"""
|
||||||
if not now:
|
if not now:
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
@ -743,3 +747,12 @@ class PairLock(_DECL_BASE):
|
|||||||
# Only active locks
|
# Only active locks
|
||||||
PairLock.active.is_(True),
|
PairLock.active.is_(True),
|
||||||
).scalar() is not None
|
).scalar() is not None
|
||||||
|
|
||||||
|
def to_json(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
'pair': self.pair,
|
||||||
|
'lock_time': self.lock_time,
|
||||||
|
'lock_end_time': self.lock_end_time,
|
||||||
|
'reason': self.reason,
|
||||||
|
'active': self.active,
|
||||||
|
}
|
||||||
|
@ -19,7 +19,7 @@ from freqtrade.exceptions import ExchangeError, PricingError
|
|||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
|
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 Trade
|
from freqtrade.persistence import PairLock, Trade
|
||||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
from freqtrade.strategy.interface import SellType
|
from freqtrade.strategy.interface import SellType
|
||||||
@ -599,6 +599,17 @@ class RPC:
|
|||||||
'total_stake': sum((trade.open_rate * trade.amount) for trade in trades)
|
'total_stake': sum((trade.open_rate * trade.amount) for trade in trades)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _rpc_locks(self) -> Dict[str, Any]:
|
||||||
|
""" Returns the current locks"""
|
||||||
|
if self._freqtrade.state != State.RUNNING:
|
||||||
|
raise RPCException('trader is not running')
|
||||||
|
|
||||||
|
locks = PairLock.get_pair_locks(None)
|
||||||
|
return {
|
||||||
|
'lock_count': len(locks),
|
||||||
|
'locks': [lock.to_json() for lock in 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,
|
||||||
|
@ -15,6 +15,7 @@ from telegram.ext import CallbackContext, CommandHandler, Updater
|
|||||||
from telegram.utils.helpers import escape_markdown
|
from telegram.utils.helpers import escape_markdown
|
||||||
|
|
||||||
from freqtrade.__init__ import __version__
|
from freqtrade.__init__ import __version__
|
||||||
|
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||||
from freqtrade.rpc import RPC, RPCException, RPCMessageType
|
from freqtrade.rpc import RPC, RPCException, RPCMessageType
|
||||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||||
|
|
||||||
@ -100,6 +101,8 @@ class Telegram(RPC):
|
|||||||
CommandHandler('performance', self._performance),
|
CommandHandler('performance', self._performance),
|
||||||
CommandHandler('daily', self._daily),
|
CommandHandler('daily', self._daily),
|
||||||
CommandHandler('count', self._count),
|
CommandHandler('count', self._count),
|
||||||
|
CommandHandler('locks', self._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),
|
||||||
@ -608,6 +611,29 @@ class Telegram(RPC):
|
|||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(e))
|
self._send_msg(str(e))
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _locks(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /locks.
|
||||||
|
Returns the number of trades running
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
locks = self._rpc_locks()
|
||||||
|
message = tabulate([[
|
||||||
|
lock['pair'],
|
||||||
|
lock['lock_end_time'].strftime(DATETIME_PRINT_FORMAT),
|
||||||
|
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))
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _whitelist(self, update: Update, context: CallbackContext) -> None:
|
def _whitelist(self, update: Update, context: CallbackContext) -> None:
|
||||||
"""
|
"""
|
||||||
@ -720,6 +746,7 @@ class Telegram(RPC):
|
|||||||
"*/performance:* `Show performance of each finished trade grouped by pair`\n"
|
"*/performance:* `Show performance of each finished trade grouped by pair`\n"
|
||||||
"*/daily <n>:* `Shows profit or loss per day, over the last n days`\n"
|
"*/daily <n>:* `Shows profit or loss per day, over the last n days`\n"
|
||||||
"*/count:* `Show number of trades running compared to allowed number of trades`"
|
"*/count:* `Show number of trades running compared to allowed number of trades`"
|
||||||
|
"*/locks:* `Show currently locked pairs`"
|
||||||
"\n"
|
"\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"
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
# pragma pylint: disable=protected-access, unused-argument, invalid-name
|
# pragma pylint: disable=protected-access, unused-argument, invalid-name
|
||||||
# pragma pylint: disable=too-many-lines, too-many-arguments
|
# pragma pylint: disable=too-many-lines, too-many-arguments
|
||||||
|
|
||||||
|
from freqtrade.persistence.models import PairLock
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from random import choice, randint
|
from random import choice, randint
|
||||||
@ -75,8 +76,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'], ['daily'], ['count'], ['reload_config', "
|
"['delete'], ['performance'], ['daily'], ['count'], ['locks'], "
|
||||||
"'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], "
|
"['reload_config', 'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], "
|
||||||
"['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']]")
|
"['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']]")
|
||||||
|
|
||||||
assert log_has(message_str, caplog)
|
assert log_has(message_str, caplog)
|
||||||
@ -1024,6 +1025,43 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
|
|||||||
assert msg in msg_mock.call_args_list[0][0][0]
|
assert msg in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None:
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
|
_init=MagicMock(),
|
||||||
|
_send_msg=msg_mock
|
||||||
|
)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
fetch_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
)
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
|
freqtradebot.state = State.STOPPED
|
||||||
|
telegram._locks(update=update, context=MagicMock())
|
||||||
|
assert msg_mock.call_count == 1
|
||||||
|
assert 'not running' in msg_mock.call_args_list[0][0][0]
|
||||||
|
msg_mock.reset_mock()
|
||||||
|
freqtradebot.state = State.RUNNING
|
||||||
|
|
||||||
|
PairLock.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=4).datetime, 'randreason')
|
||||||
|
PairLock.lock_pair('XRP/BTC', arrow.utcnow().shift(minutes=20).datetime, 'deadbeef')
|
||||||
|
|
||||||
|
telegram._locks(update=update, context=MagicMock())
|
||||||
|
|
||||||
|
assert 'Pair' in msg_mock.call_args_list[0][0][0]
|
||||||
|
assert 'Until' in msg_mock.call_args_list[0][0][0]
|
||||||
|
assert 'Reason\n' in msg_mock.call_args_list[0][0][0]
|
||||||
|
assert 'ETH/BTC' in msg_mock.call_args_list[0][0][0]
|
||||||
|
assert 'XRP/BTC' 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]
|
||||||
|
|
||||||
|
|
||||||
def test_whitelist_static(default_conf, update, mocker) -> None:
|
def test_whitelist_static(default_conf, update, mocker) -> None:
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
|
@ -1175,10 +1175,16 @@ def test_PairLock(default_conf):
|
|||||||
# XRP/BTC should not be locked now
|
# XRP/BTC should not be locked now
|
||||||
pair = 'XRP/BTC'
|
pair = 'XRP/BTC'
|
||||||
assert not PairLock.is_pair_locked(pair)
|
assert not PairLock.is_pair_locked(pair)
|
||||||
|
|
||||||
# Unlocking a pair that's not locked should not raise an error
|
# Unlocking a pair that's not locked should not raise an error
|
||||||
PairLock.unlock_pair(pair)
|
PairLock.unlock_pair(pair)
|
||||||
|
|
||||||
|
PairLock.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime)
|
||||||
|
assert PairLock.is_pair_locked(pair)
|
||||||
|
|
||||||
|
# Get both locks from above
|
||||||
|
locks = PairLock.get_pair_locks(None)
|
||||||
|
assert len(locks) == 2
|
||||||
|
|
||||||
# Unlock original pair
|
# Unlock original pair
|
||||||
pair = 'ETH/BTC'
|
pair = 'ETH/BTC'
|
||||||
PairLock.unlock_pair(pair)
|
PairLock.unlock_pair(pair)
|
||||||
|
Loading…
Reference in New Issue
Block a user