From 6eab20e337c5b855fd89e48a8a44a097e144a09f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Oct 2020 11:25:42 +0200 Subject: [PATCH 01/11] Use constant to format datetime --- freqtrade/persistence/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 93b39860a..03133107a 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -17,6 +17,7 @@ from sqlalchemy.orm.session import sessionmaker from sqlalchemy.pool import StaticPool from sqlalchemy.sql.schema import UniqueConstraint +from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.misc import safe_value_fallback from freqtrade.persistence.migrations import check_migrate @@ -251,7 +252,7 @@ class Trade(_DECL_BASE): self.recalc_open_trade_price() def __repr__(self): - open_since = self.open_date.strftime('%Y-%m-%d %H:%M:%S') if self.is_open else 'closed' + open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed' return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, ' f'open_rate={self.open_rate:.8f}, open_since={open_since})') From e513871fd5eed769558159f64c6187390fa8565b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Oct 2020 11:28:34 +0200 Subject: [PATCH 02/11] Persist pairlocks closes #3034 --- docs/strategy-customization.md | 6 +-- freqtrade/freqtradebot.py | 7 +-- freqtrade/persistence/__init__.py | 3 +- freqtrade/persistence/models.py | 76 +++++++++++++++++++++++++++++++ freqtrade/strategy/interface.py | 29 ++++++------ tests/strategy/test_interface.py | 18 ++++---- tests/test_freqtradebot.py | 4 +- 7 files changed, 111 insertions(+), 32 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index a6cdef864..c0506203f 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -693,15 +693,15 @@ Locked pairs will show the message `Pair is currently locked.`. Sometimes it may be desired to lock a pair after certain events happen (e.g. multiple losing trades in a row). -Freqtrade has an easy method to do this from within the strategy, by calling `self.lock_pair(pair, until)`. -`until` must be a datetime object in the future, after which trading will be reenabled for that pair. +Freqtrade has an easy method to do this from within the strategy, by calling `self.lock_pair(pair, until, [reason])`. +`until` must be a datetime object in the future, after which trading will be re-enabled for that pair, while `reason` is an optional string detailing why the pair was locked. Locks can also be lifted manually, by calling `self.unlock_pair(pair)`. To verify if a pair is currently locked, use `self.is_pair_locked(pair)`. !!! Note - Locked pairs are not persisted, so a restart of the bot, or calling `/reload_config` will reset locked pairs. + Locked pairs will always be rounded up to the next candle. So assuming a `5m` timeframe, a lock with `until` set to 10:18 will lock the pair until the candle from 10:15-10:20 will be finished. !!! Warning Locking pairs is not functioning during backtesting. diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index cfc68a3ec..e004ed51c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -937,8 +937,8 @@ class FreqtradeBot: self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, stoploss_order=True) # Lock pair for one candle to prevent immediate rebuys - self.strategy.lock_pair(trade.pair, - timeframe_to_next_date(self.config['timeframe'])) + self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe']), + reason='auto_lock_1_candle') self._notify_sell(trade, "stoploss") return True @@ -1264,7 +1264,8 @@ class FreqtradeBot: Trade.session.flush() # Lock pair for one candle to prevent immediate rebuys - self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe'])) + self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe']), + reason='auto_lock_1_candle') self._notify_sell(trade, order_type) diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index a3ec13e98..e184e7d9a 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,3 +1,4 @@ # flake8: noqa: F401 -from freqtrade.persistence.models import Order, Trade, clean_dry_run_db, cleanup_db, init_db +from freqtrade.persistence.models import (Order, PairLock, Trade, clean_dry_run_db, cleanup_db, + init_db) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 03133107a..b2f8f4274 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -64,6 +64,9 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None: # Copy session attributes to order object too Order.session = Trade.session Order.query = Order.session.query_property() + PairLock.session = Trade.session + PairLock.query = PairLock.session.query_property() + previous_tables = inspect(engine).get_table_names() _DECL_BASE.metadata.create_all(engine) check_migrate(engine, decl_base=_DECL_BASE, previous_tables=previous_tables) @@ -655,3 +658,76 @@ class Trade(_DECL_BASE): trade.stop_loss = None trade.adjust_stop_loss(trade.open_rate, desired_stoploss) logger.info(f"New stoploss: {trade.stop_loss}.") + + +class PairLock(_DECL_BASE): + """ + Pair Locks database model. + """ + __tablename__ = 'pair_lock' + + id = Column(Integer, primary_key=True) + + pair = Column(String, nullable=False) + reason = Column(String, nullable=True) + # Time the pair was locked (start time) + lock_time = Column(DateTime, nullable=False) + # Time until the pair is locked (end time) + lock_end_time = Column(DateTime, nullable=False) + + active = Column(Boolean, nullable=False, default=True) + + def __repr__(self): + lock_time = self.open_date.strftime(DATETIME_PRINT_FORMAT) + lock_end_time = self.open_date.strftime(DATETIME_PRINT_FORMAT) + return (f'PairLock(id={self.id}, pair={self.pair}, lock_time={lock_time}, ' + f'lock_end_time={lock_end_time})') + + @staticmethod + def get_pair_locks(pair: str, now: Optional[datetime] = None) -> List['PairLock']: + """ + Get all locks for this pair + :param pair: Pair to check for + :param now: Datetime object (generated via datetime.utcnow()). defaults to datetime.utcnow() + """ + if not now: + now = datetime.now(timezone.utc) + + return PairLock.query.filter( + PairLock.pair == pair, + func.datetime(PairLock.lock_time) <= now, + func.datetime(PairLock.lock_end_time) >= now, + # Only active locks + PairLock.active.is_(True), + ).all() + + @staticmethod + def unlock_pair(pair: str, now: Optional[datetime] = None) -> None: + """ + Release all locks for this pair. + """ + if not now: + now = datetime.now(timezone.utc) + + logger.info(f"Releasing all locks for {pair}.") + locks = PairLock.get_pair_locks(pair, now) + for lock in locks: + lock.active = False + PairLock.session.flush() + + @staticmethod + def is_pair_locked(pair: str, now: Optional[datetime] = None) -> bool: + """ + :param pair: Pair to check for + :param now: Datetime object (generated via datetime.utcnow()). defaults to datetime.utcnow() + """ + if not now: + now = datetime.now(timezone.utc) + + return PairLock.query.filter( + PairLock.pair == pair, + func.datetime(PairLock.lock_time) <= now, + func.datetime(PairLock.lock_end_time) >= now, + # Only active locks + PairLock.active.is_(True), + ).scalar() is not None diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index b6b36b1a4..d9485e27a 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -17,7 +17,7 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange.exchange import timeframe_to_next_date -from freqtrade.persistence import Trade +from freqtrade.persistence import PairLock, Trade from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -133,7 +133,6 @@ class IStrategy(ABC): self.config = config # Dict to determine if analysis is necessary self._last_candle_seen_per_pair: Dict[str, datetime] = {} - self._pair_locked_until: Dict[str, datetime] = {} @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -278,7 +277,7 @@ class IStrategy(ABC): """ return self.__class__.__name__ - def lock_pair(self, pair: str, until: datetime) -> None: + def lock_pair(self, pair: str, until: datetime, reason: str = None) -> None: """ Locks pair until a given timestamp happens. Locked pairs are not analyzed, and are prevented from opening new trades. @@ -288,8 +287,15 @@ class IStrategy(ABC): :param until: datetime in UTC until the pair should be blocked from opening new trades. Needs to be timezone aware `datetime.now(timezone.utc)` """ - if pair not in self._pair_locked_until or self._pair_locked_until[pair] < until: - self._pair_locked_until[pair] = until + lock = PairLock( + pair=pair, + lock_time=datetime.now(timezone.utc), + lock_end_time=until, + reason=reason, + active=True + ) + PairLock.session.add(lock) + PairLock.session.flush() def unlock_pair(self, pair: str) -> None: """ @@ -298,8 +304,7 @@ class IStrategy(ABC): manually from within the strategy, to allow an easy way to unlock pairs. :param pair: Unlock pair to allow trading again """ - if pair in self._pair_locked_until: - del self._pair_locked_until[pair] + PairLock.unlock_pair(pair, datetime.now(timezone.utc)) def is_pair_locked(self, pair: str, candle_date: datetime = None) -> bool: """ @@ -311,15 +316,13 @@ class IStrategy(ABC): :param candle_date: Date of the last candle. Optional, defaults to current date :returns: locking state of the pair in question. """ - if pair not in self._pair_locked_until: - return False + if not candle_date: - return self._pair_locked_until[pair] >= datetime.now(timezone.utc) + # Simple call ... + return PairLock.is_pair_locked(pair, candle_date) else: - # Locking should happen until a new candle arrives lock_time = timeframe_to_next_date(self.timeframe, candle_date) - # lock_time = candle_date + timedelta(minutes=timeframe_to_minutes(self.timeframe)) - return self._pair_locked_until[pair] > lock_time + return PairLock.is_pair_locked(pair, lock_time) def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 729b14f7b..e9d9bcc75 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -1,5 +1,4 @@ # pragma pylint: disable=missing-docstring, C0103 - import logging from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock @@ -12,7 +11,7 @@ from freqtrade.configuration import TimeRange from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data from freqtrade.exceptions import StrategyError -from freqtrade.persistence import Trade +from freqtrade.persistence import PairLock, Trade from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from tests.conftest import log_has, log_has_re @@ -360,11 +359,12 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> assert log_has('Skipping TA Analysis for already analyzed candle', caplog) +@pytest.mark.usefixtures("init_persistence") def test_is_pair_locked(default_conf): default_conf.update({'strategy': 'DefaultStrategy'}) strategy = StrategyResolver.load_strategy(default_conf) - # dict should be empty - assert not strategy._pair_locked_until + # No lock should be present + assert len(PairLock.query.all()) == 0 pair = 'ETH/BTC' assert not strategy.is_pair_locked(pair) @@ -372,11 +372,6 @@ def test_is_pair_locked(default_conf): # ETH/BTC locked for 4 minutes assert strategy.is_pair_locked(pair) - # Test lock does not change - lock = strategy._pair_locked_until[pair] - strategy.lock_pair(pair, arrow.utcnow().shift(minutes=2).datetime) - assert lock == strategy._pair_locked_until[pair] - # XRP/BTC should not be locked now pair = 'XRP/BTC' assert not strategy.is_pair_locked(pair) @@ -393,7 +388,10 @@ def test_is_pair_locked(default_conf): # Lock until 14:30 lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc) strategy.lock_pair(pair, lock_time) - # Lock is in the past ... + # Lock is in the past, so we must fake the lock + lock = PairLock.query.filter(PairLock.pair == pair).first() + lock.lock_time = lock_time - timedelta(hours=2) + assert not strategy.is_pair_locked(pair) # latest candle is from 14:20, lock goes to 14:30 assert strategy.is_pair_locked(pair, lock_time + timedelta(minutes=-10)) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index bb7ff26e7..2a1b0c3cc 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -15,7 +15,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie InvalidOrderException, OperationalException, PricingError, TemporaryError) from freqtrade.freqtradebot import FreqtradeBot -from freqtrade.persistence import Order, Trade +from freqtrade.persistence import Order, PairLock, Trade from freqtrade.rpc import RPCMessageType from freqtrade.state import RunMode, State from freqtrade.strategy.interface import SellCheckTuple, SellType @@ -2799,6 +2799,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c trade = Trade.query.first() Trade.session = MagicMock() + PairLock.session = MagicMock() freqtrade.config['dry_run'] = False trade.stoploss_order_id = "abcd" @@ -3249,7 +3250,6 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], sell_reason=SellType.STOP_LOSS) trade.close(ticker_sell_down()['bid']) - assert trade.pair in freqtrade.strategy._pair_locked_until assert freqtrade.strategy.is_pair_locked(trade.pair) # reinit - should buy other pair. From 7caa6cfe312621dae341245d973c4fcda6920c0a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Oct 2020 11:40:01 +0200 Subject: [PATCH 03/11] Add tests for pairlock --- freqtrade/persistence/models.py | 16 ++++++++++-- freqtrade/strategy/interface.py | 10 +------ tests/test_persistence.py | 46 ++++++++++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index b2f8f4274..4394b783a 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -678,11 +678,23 @@ class PairLock(_DECL_BASE): active = Column(Boolean, nullable=False, default=True) def __repr__(self): - lock_time = self.open_date.strftime(DATETIME_PRINT_FORMAT) - lock_end_time = self.open_date.strftime(DATETIME_PRINT_FORMAT) + lock_time = self.lock_time.strftime(DATETIME_PRINT_FORMAT) + lock_end_time = self.lock_end_time.strftime(DATETIME_PRINT_FORMAT) return (f'PairLock(id={self.id}, pair={self.pair}, lock_time={lock_time}, ' f'lock_end_time={lock_end_time})') + @staticmethod + def lock_pair(pair: str, until: datetime, reason: str = None) -> None: + lock = PairLock( + pair=pair, + lock_time=datetime.now(timezone.utc), + lock_end_time=until, + reason=reason, + active=True + ) + PairLock.session.add(lock) + PairLock.session.flush() + @staticmethod def get_pair_locks(pair: str, now: Optional[datetime] = None) -> List['PairLock']: """ diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index d9485e27a..36abfd05a 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -287,15 +287,7 @@ class IStrategy(ABC): :param until: datetime in UTC until the pair should be blocked from opening new trades. Needs to be timezone aware `datetime.now(timezone.utc)` """ - lock = PairLock( - pair=pair, - lock_time=datetime.now(timezone.utc), - lock_end_time=until, - reason=reason, - active=True - ) - PairLock.session.add(lock) - PairLock.session.flush() + PairLock.lock_pair(pair, until, reason) def unlock_pair(self, pair: str) -> None: """ diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 4216565ac..6ac1e36a4 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1,5 +1,6 @@ # pragma pylint: disable=missing-docstring, C0103 import logging +from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock import arrow @@ -8,7 +9,7 @@ from sqlalchemy import create_engine from freqtrade import constants from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.persistence import Order, Trade, clean_dry_run_db, init_db +from freqtrade.persistence import Order, PairLock, Trade, clean_dry_run_db, init_db from tests.conftest import create_mock_trades, log_has, log_has_re @@ -1158,3 +1159,46 @@ def test_select_order(fee): assert order.ft_order_side == 'stoploss' order = trades[4].select_order('sell', False) assert order is None + + +@pytest.mark.usefixtures("init_persistence") +def test_PairLock(default_conf): + # No lock should be present + assert len(PairLock.query.all()) == 0 + + pair = 'ETH/BTC' + assert not PairLock.is_pair_locked(pair) + PairLock.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime) + # ETH/BTC locked for 4 minutes + assert PairLock.is_pair_locked(pair) + + # XRP/BTC should not be locked now + pair = 'XRP/BTC' + assert not PairLock.is_pair_locked(pair) + + # Unlocking a pair that's not locked should not raise an error + PairLock.unlock_pair(pair) + + # Unlock original pair + pair = 'ETH/BTC' + PairLock.unlock_pair(pair) + assert not PairLock.is_pair_locked(pair) + + pair = 'BTC/USDT' + # Lock until 14:30 + lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc) + PairLock.lock_pair(pair, lock_time) + # Lock is in the past, so we must fake the lock + lock = PairLock.query.filter(PairLock.pair == pair).first() + lock.lock_time = lock_time - timedelta(hours=2) + + assert not PairLock.is_pair_locked(pair) + assert PairLock.is_pair_locked(pair, lock_time + timedelta(minutes=-10)) + assert PairLock.is_pair_locked(pair, lock_time + timedelta(minutes=-50)) + + # Should not be locked after time expired + assert not PairLock.is_pair_locked(pair, lock_time + timedelta(minutes=10)) + + locks = PairLock.get_pair_locks(pair, lock_time + timedelta(minutes=-2)) + assert len(locks) == 1 + assert 'PairLock' in str(locks[0]) From 7a9768ffa67a3640073c5bd5176fe5f24e054f61 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Oct 2020 15:15:35 +0200 Subject: [PATCH 04/11] Add /locks Telegram endpoint --- freqtrade/persistence/models.py | 35 ++++++++++++++++++--------- freqtrade/rpc/rpc.py | 13 +++++++++- freqtrade/rpc/telegram.py | 27 +++++++++++++++++++++ tests/rpc/test_rpc_telegram.py | 42 +++++++++++++++++++++++++++++++-- tests/test_persistence.py | 8 ++++++- 5 files changed, 110 insertions(+), 15 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 4394b783a..1f9a9a5b0 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -668,14 +668,14 @@ class PairLock(_DECL_BASE): id = Column(Integer, primary_key=True) - pair = Column(String, nullable=False) + pair = Column(String, nullable=False, index=True) reason = Column(String, nullable=True) # Time the pair was locked (start time) lock_time = Column(DateTime, nullable=False) # Time until the pair is locked (end time) 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): lock_time = self.lock_time.strftime(DATETIME_PRINT_FORMAT) @@ -696,21 +696,24 @@ class PairLock(_DECL_BASE): PairLock.session.flush() @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 - :param pair: Pair to check for - :param now: Datetime object (generated via datetime.utcnow()). defaults to datetime.utcnow() + :param pair: Pair to check for. Returns all current locks if pair is empty + :param now: Datetime object (generated via datetime.now(timezone.utc)). + defaults to datetime.utcnow() """ if not now: 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( - PairLock.pair == pair, - func.datetime(PairLock.lock_time) <= now, - func.datetime(PairLock.lock_end_time) >= now, - # Only active locks - PairLock.active.is_(True), + *filters ).all() @staticmethod @@ -731,7 +734,8 @@ class PairLock(_DECL_BASE): def is_pair_locked(pair: str, now: Optional[datetime] = None) -> bool: """ :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: now = datetime.now(timezone.utc) @@ -743,3 +747,12 @@ class PairLock(_DECL_BASE): # Only active locks PairLock.active.is_(True), ).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, + } diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 911b2d731..dbdb956b6 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -19,7 +19,7 @@ from freqtrade.exceptions import ExchangeError, PricingError 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 Trade +from freqtrade.persistence import PairLock, Trade from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State from freqtrade.strategy.interface import SellType @@ -599,6 +599,17 @@ class RPC: '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: """ Returns the currently active whitelist""" res = {'method': self._freqtrade.pairlists.name_list, diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 7a6607632..6a0fd5acd 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -15,6 +15,7 @@ from telegram.ext import CallbackContext, CommandHandler, Updater from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ +from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.rpc import RPC, RPCException, RPCMessageType from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -100,6 +101,8 @@ class Telegram(RPC): CommandHandler('performance', self._performance), CommandHandler('daily', self._daily), CommandHandler('count', self._count), + CommandHandler('locks', self._locks), + CommandHandler(['reload_config', 'reload_conf'], self._reload_config), CommandHandler(['show_config', 'show_conf'], self._show_config), CommandHandler('stopbuy', self._stopbuy), @@ -608,6 +611,29 @@ class Telegram(RPC): except RPCException as 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 = "
{}
".format(message) + logger.debug(message) + self._send_msg(message, parse_mode=ParseMode.HTML) + except RPCException as e: + self._send_msg(str(e)) + @authorized_only 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" "*/daily :* `Shows profit or loss per day, over the last n days`\n" "*/count:* `Show number of trades running compared to allowed number of trades`" + "*/locks:* `Show currently locked pairs`" "\n" "*/balance:* `Show account balance per currency`\n" "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 230df0df9..47d0a90c9 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -2,6 +2,7 @@ # pragma pylint: disable=protected-access, unused-argument, invalid-name # pragma pylint: disable=too-many-lines, too-many-arguments +from freqtrade.persistence.models import PairLock import re from datetime import datetime 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'], " "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], " - "['delete'], ['performance'], ['daily'], ['count'], ['reload_config', " - "'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], " + "['delete'], ['performance'], ['daily'], ['count'], ['locks'], " + "['reload_config', 'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], " "['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']]") 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] +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: msg_mock = MagicMock() mocker.patch.multiple( diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 6ac1e36a4..59b1fa31b 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1175,10 +1175,16 @@ def test_PairLock(default_conf): # XRP/BTC should not be locked now pair = 'XRP/BTC' assert not PairLock.is_pair_locked(pair) - # Unlocking a pair that's not locked should not raise an error 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 pair = 'ETH/BTC' PairLock.unlock_pair(pair) From cd2866eaec6898299336128d52f32bd1c33528a5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Oct 2020 17:58:07 +0200 Subject: [PATCH 05/11] Add rest endpoint for /locks --- freqtrade/persistence/models.py | 7 +++++-- freqtrade/rpc/api_server.py | 10 ++++++++++ scripts/rest_client.py | 7 +++++++ tests/rpc/test_rpc_apiserver.py | 29 +++++++++++++++++++++++++++-- 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 1f9a9a5b0..2621bb852 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -751,8 +751,11 @@ class PairLock(_DECL_BASE): def to_json(self) -> Dict[str, Any]: return { 'pair': self.pair, - 'lock_time': self.lock_time, - 'lock_end_time': self.lock_end_time, + 'lock_time': self.lock_time.strftime("%Y-%m-%d %H:%M:%S"), + 'lock_timestamp': int(self.lock_time.replace(tzinfo=timezone.utc).timestamp() * 1000), + 'lock_end_time': self.lock_end_time.strftime("%Y-%m-%d %H:%M:%S"), + 'lock_end_timestamp': int(self.lock_end_time.replace(tzinfo=timezone.utc + ).timestamp() * 1000), 'reason': self.reason, 'active': self.active, } diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index f31d7b0b5..89e0f88c7 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -192,6 +192,7 @@ class ApiServer(RPC): self.app.add_url_rule(f'{BASE_URI}/balance', 'balance', view_func=self._balance, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/count', 'count', view_func=self._count, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/locks', 'locks', view_func=self._locks, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/daily', 'daily', view_func=self._daily, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/edge', 'edge', view_func=self._edge, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/logs', 'log', view_func=self._get_logs, methods=['GET']) @@ -350,6 +351,15 @@ class ApiServer(RPC): msg = self._rpc_count() return jsonify(msg) + @require_login + @rpc_catch_errors + def _locks(self): + """ + Handler for /locks. + Returns the number of trades running + """ + return jsonify(self._rpc_locks()) + @require_login @rpc_catch_errors def _daily(self): diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 46966d447..268e81397 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -111,6 +111,13 @@ class FtRestClient(): """ return self._get("count") + def locks(self): + """Return current locks + + :return: json object + """ + return self._get("locks") + def daily(self, days=None): """Return the amount of open trades. diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index d0e5d3c37..2b4242f5a 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -2,8 +2,9 @@ Unit test file for rpc/api_server.py """ -from datetime import datetime +from datetime import datetime, timedelta from pathlib import Path +from time import sleep from unittest.mock import ANY, MagicMock, PropertyMock import pytest @@ -12,7 +13,7 @@ from requests.auth import _basic_auth_str from freqtrade.__init__ import __version__ from freqtrade.loggers import setup_logging, setup_logging_pre -from freqtrade.persistence import Trade +from freqtrade.persistence import PairLock, Trade from freqtrade.rpc.api_server import BASE_URI, ApiServer from freqtrade.state import State from tests.conftest import create_mock_trades, get_patched_freqtradebot, log_has, patch_get_signal @@ -328,6 +329,30 @@ def test_api_count(botclient, mocker, ticker, fee, markets): assert rc.json["max"] == 1.0 +def test_api_locks(botclient): + ftbot, client = botclient + + rc = client_get(client, f"{BASE_URI}/locks") + assert_response(rc) + + assert 'locks' in rc.json + + assert rc.json['lock_count'] == 0 + assert rc.json['lock_count'] == len(rc.json['locks']) + + PairLock.lock_pair('ETH/BTC', datetime.utcnow() + timedelta(minutes=4), 'randreason') + PairLock.lock_pair('XRP/BTC', datetime.utcnow() + timedelta(minutes=20), 'deadbeef') + + rc = client_get(client, f"{BASE_URI}/locks") + assert_response(rc) + + assert rc.json['lock_count'] == 2 + assert rc.json['lock_count'] == len(rc.json['locks']) + assert 'ETH/BTC' in (rc.json['locks'][0]['pair'], rc.json['locks'][1]['pair']) + 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']) + + def test_api_show_config(botclient, mocker): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) From 0daf77f313c71f00951787cbaa74cf2cc5a32dca Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Oct 2020 19:41:58 +0200 Subject: [PATCH 06/11] Don't check for lock start date --- freqtrade/persistence/models.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 2621bb852..da53a827f 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -706,8 +706,7 @@ class PairLock(_DECL_BASE): if not now: now = datetime.now(timezone.utc) - filters = [func.datetime(PairLock.lock_time) <= now, - func.datetime(PairLock.lock_end_time) >= now, + filters = [func.datetime(PairLock.lock_end_time) >= now, # Only active locks PairLock.active.is_(True), ] if pair: @@ -742,7 +741,6 @@ class PairLock(_DECL_BASE): return PairLock.query.filter( PairLock.pair == pair, - func.datetime(PairLock.lock_time) <= now, func.datetime(PairLock.lock_end_time) >= now, # Only active locks PairLock.active.is_(True), From 1156f5e68629bae8344a053c43f2cf48b8171a19 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Oct 2020 20:32:23 +0200 Subject: [PATCH 07/11] Use constant for times --- freqtrade/persistence/models.py | 10 +++++----- freqtrade/rpc/rpc.py | 4 ++-- freqtrade/rpc/telegram.py | 3 +-- tests/rpc/test_rpc_telegram.py | 3 +-- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index da53a827f..b06aab7df 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -281,7 +281,7 @@ class Trade(_DECL_BASE): 'fee_close_currency': self.fee_close_currency, 'open_date_hum': arrow.get(self.open_date).humanize(), - 'open_date': self.open_date.strftime("%Y-%m-%d %H:%M:%S"), + 'open_date': self.open_date.strftime(DATETIME_PRINT_FORMAT), 'open_timestamp': int(self.open_date.replace(tzinfo=timezone.utc).timestamp() * 1000), 'open_rate': self.open_rate, 'open_rate_requested': self.open_rate_requested, @@ -289,7 +289,7 @@ class Trade(_DECL_BASE): 'close_date_hum': (arrow.get(self.close_date).humanize() if self.close_date else None), - 'close_date': (self.close_date.strftime("%Y-%m-%d %H:%M:%S") + 'close_date': (self.close_date.strftime(DATETIME_PRINT_FORMAT) if self.close_date else None), 'close_timestamp': int(self.close_date.replace( tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None, @@ -305,7 +305,7 @@ class Trade(_DECL_BASE): 'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None, 'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None, 'stoploss_order_id': self.stoploss_order_id, - 'stoploss_last_update': (self.stoploss_last_update.strftime("%Y-%m-%d %H:%M:%S") + 'stoploss_last_update': (self.stoploss_last_update.strftime(DATETIME_PRINT_FORMAT) if self.stoploss_last_update else None), 'stoploss_last_update_timestamp': int(self.stoploss_last_update.replace( tzinfo=timezone.utc).timestamp() * 1000) if self.stoploss_last_update else None, @@ -749,9 +749,9 @@ class PairLock(_DECL_BASE): def to_json(self) -> Dict[str, Any]: return { 'pair': self.pair, - 'lock_time': self.lock_time.strftime("%Y-%m-%d %H:%M:%S"), + 'lock_time': self.lock_time.strftime(DATETIME_PRINT_FORMAT), 'lock_timestamp': int(self.lock_time.replace(tzinfo=timezone.utc).timestamp() * 1000), - 'lock_end_time': self.lock_end_time.strftime("%Y-%m-%d %H:%M:%S"), + 'lock_end_time': self.lock_end_time.strftime(DATETIME_PRINT_FORMAT), 'lock_end_timestamp': int(self.lock_end_time.replace(tzinfo=timezone.utc ).timestamp() * 1000), 'reason': self.reason, diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index dbdb956b6..de8bcaefb 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -13,7 +13,7 @@ from numpy import NAN, int64, mean from pandas import DataFrame from freqtrade.configuration.timerange import TimeRange -from freqtrade.constants import CANCEL_REASON +from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT from freqtrade.data.history import load_data from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs @@ -649,7 +649,7 @@ class RPC: buffer = bufferHandler.buffer[-limit:] else: buffer = bufferHandler.buffer - records = [[datetime.fromtimestamp(r.created).strftime("%Y-%m-%d %H:%M:%S"), + records = [[datetime.fromtimestamp(r.created).strftime(DATETIME_PRINT_FORMAT), r.created * 1000, r.name, r.levelname, r.message + ('\n' + r.exc_text if r.exc_text else '')] for r in buffer] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 6a0fd5acd..f3581c38f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -102,7 +102,6 @@ class Telegram(RPC): CommandHandler('daily', self._daily), CommandHandler('count', self._count), CommandHandler('locks', self._locks), - CommandHandler(['reload_config', 'reload_conf'], self._reload_config), CommandHandler(['show_config', 'show_conf'], self._show_config), CommandHandler('stopbuy', self._stopbuy), @@ -624,7 +623,7 @@ class Telegram(RPC): locks = self._rpc_locks() message = tabulate([[ lock['pair'], - lock['lock_end_time'].strftime(DATETIME_PRINT_FORMAT), + lock['lock_end_time'], lock['reason']] for lock in locks['locks']], headers=['Pair', 'Until', 'Reason'], tablefmt='simple') diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 47d0a90c9..c412313ad 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -2,7 +2,6 @@ # pragma pylint: disable=protected-access, unused-argument, invalid-name # pragma pylint: disable=too-many-lines, too-many-arguments -from freqtrade.persistence.models import PairLock import re from datetime import datetime from random import choice, randint @@ -19,7 +18,7 @@ from freqtrade.constants import CANCEL_REASON from freqtrade.edge import PairInfo from freqtrade.freqtradebot import FreqtradeBot from freqtrade.loggers import setup_logging -from freqtrade.persistence import Trade +from freqtrade.persistence import PairLock, Trade from freqtrade.rpc import RPCMessageType from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.state import State From 64e680d7eeb5f664cb241ecc596db962d0bbbe52 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Oct 2020 10:48:39 +0200 Subject: [PATCH 08/11] Document new api method --- docs/rest-api.md | 27 ++++++++++++++------------- freqtrade/rpc/telegram.py | 5 ++--- tests/strategy/test_interface.py | 3 --- tests/test_persistence.py | 3 --- 4 files changed, 16 insertions(+), 22 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 44f0b07cf..7726ab875 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -109,24 +109,25 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | Command | Description | |----------|-------------| | `ping` | Simple command testing the API Readiness - requires no authentication. -| `start` | Starts the trader -| `stop` | Stops the trader +| `start` | Starts the trader. +| `stop` | Stops the trader. | `stopbuy` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. -| `reload_config` | Reloads the configuration file +| `reload_config` | Reloads the configuration file. | `trades` | List last trades. | `delete_trade ` | Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange. -| `show_config` | Shows part of the current configuration with relevant settings to operation -| `logs` | Shows last log messages -| `status` | Lists all open trades -| `count` | Displays number of trades used and available -| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance +| `show_config` | Shows part of the current configuration with relevant settings to operation. +| `logs` | Shows last log messages. +| `status` | Lists all open trades. +| `count` | Displays number of trades used and available. +| `locks` | Displays currently locked pairs. +| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance. | `forcesell ` | Instantly sells the given trade (Ignoring `minimum_roi`). | `forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). | `forcebuy [rate]` | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True) -| `performance` | Show performance of each finished trade grouped by pair -| `balance` | Show account balance per currency -| `daily ` | Shows profit or loss per day, over the last n days (n defaults to 7) -| `whitelist` | Show the current whitelist +| `performance` | Show performance of each finished trade grouped by pair. +| `balance` | Show account balance per currency. +| `daily ` | Shows profit or loss per day, over the last n days (n defaults to 7). +| `whitelist` | Show the current whitelist. | `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist. | `edge` | Show validated pairs by Edge if it is enabled. | `pair_candles` | Returns dataframe for a pair / timeframe combination while the bot is running. **Alpha** @@ -135,7 +136,7 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `strategies` | List strategies in strategy directory. **Alpha** | `strategy ` | Get specific Strategy content. **Alpha** | `available_pairs` | List available backtest data. **Alpha** -| `version` | Show version +| `version` | Show version. !!! Warning "Alpha status" Endpoints labeled with *Alpha status* above may change at any time without notice. diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index f3581c38f..18360c418 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -744,9 +744,8 @@ class Telegram(RPC): "*/delete :* `Instantly delete the given trade in the database`\n" "*/performance:* `Show performance of each finished trade grouped by pair`\n" "*/daily :* `Shows profit or loss per day, over the last n days`\n" - "*/count:* `Show number of trades running compared to allowed number of trades`" - "*/locks:* `Show currently locked pairs`" - "\n" + "*/count:* `Show number of active trades compared to allowed number of trades`\n" + "*/locks:* `Show currently locked pairs`\n" "*/balance:* `Show account balance per currency`\n" "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" "*/reload_config:* `Reload configuration file` \n" diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index e9d9bcc75..dc5cd47e7 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -388,9 +388,6 @@ def test_is_pair_locked(default_conf): # Lock until 14:30 lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc) strategy.lock_pair(pair, lock_time) - # Lock is in the past, so we must fake the lock - lock = PairLock.query.filter(PairLock.pair == pair).first() - lock.lock_time = lock_time - timedelta(hours=2) assert not strategy.is_pair_locked(pair) # latest candle is from 14:20, lock goes to 14:30 diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 59b1fa31b..243da3396 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1194,9 +1194,6 @@ def test_PairLock(default_conf): # Lock until 14:30 lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc) PairLock.lock_pair(pair, lock_time) - # Lock is in the past, so we must fake the lock - lock = PairLock.query.filter(PairLock.pair == pair).first() - lock.lock_time = lock_time - timedelta(hours=2) assert not PairLock.is_pair_locked(pair) assert PairLock.is_pair_locked(pair, lock_time + timedelta(minutes=-10)) From 5f63fdd8adb1533322afa5b8f3d67b3a77d70ec8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Oct 2020 19:39:38 +0200 Subject: [PATCH 09/11] Use better lock message --- freqtrade/freqtradebot.py | 4 ++-- freqtrade/rpc/api_server.py | 2 +- freqtrade/rpc/telegram.py | 6 +----- tests/rpc/test_rpc_apiserver.py | 1 - 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e004ed51c..6112a599e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -938,7 +938,7 @@ class FreqtradeBot: stoploss_order=True) # Lock pair for one candle to prevent immediate rebuys self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe']), - reason='auto_lock_1_candle') + reason='Auto lock') self._notify_sell(trade, "stoploss") return True @@ -1265,7 +1265,7 @@ class FreqtradeBot: # Lock pair for one candle to prevent immediate rebuys self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe']), - reason='auto_lock_1_candle') + reason='Auto lock') self._notify_sell(trade, order_type) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 89e0f88c7..be21179ad 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -356,7 +356,7 @@ class ApiServer(RPC): def _locks(self): """ Handler for /locks. - Returns the number of trades running + Returns the currently active locks. """ return jsonify(self._rpc_locks()) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 18360c418..3dcb7ab72 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -15,7 +15,6 @@ from telegram.ext import CallbackContext, CommandHandler, Updater from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ -from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.rpc import RPC, RPCException, RPCMessageType from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -614,10 +613,7 @@ class Telegram(RPC): 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 + Returns the currently active locks """ try: locks = self._rpc_locks() diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 2b4242f5a..34e959875 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -4,7 +4,6 @@ Unit test file for rpc/api_server.py from datetime import datetime, timedelta from pathlib import Path -from time import sleep from unittest.mock import ANY, MagicMock, PropertyMock import pytest From a143f7bc434deb7cac6d37fe43dd66ce2d74a25a Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 21 Oct 2020 19:35:57 +0200 Subject: [PATCH 10/11] Improve pairlock docstrings --- freqtrade/persistence/models.py | 3 +++ freqtrade/strategy/interface.py | 1 + 2 files changed, 4 insertions(+) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index b06aab7df..f1f9a4e67 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -719,6 +719,9 @@ class PairLock(_DECL_BASE): def unlock_pair(pair: str, now: Optional[datetime] = None) -> None: """ Release all locks for this pair. + :param pair: Pair to unlock + :param now: Datetime object (generated via datetime.now(timezone.utc)). + defaults to datetime.utcnow() """ if not now: now = datetime.now(timezone.utc) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 36abfd05a..e6256cafb 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -286,6 +286,7 @@ class IStrategy(ABC): :param pair: Pair to lock :param until: datetime in UTC until the pair should be blocked from opening new trades. Needs to be timezone aware `datetime.now(timezone.utc)` + :param reason: Optional string explaining why the pair was locked. """ PairLock.lock_pair(pair, until, reason) From cf1a7261987b806ac8004ac6cd7247cd0115e1da Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Oct 2020 07:35:25 +0200 Subject: [PATCH 11/11] Rename table to be inline with other table naming --- freqtrade/persistence/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index f1f9a4e67..477a94bad 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -664,7 +664,7 @@ class PairLock(_DECL_BASE): """ Pair Locks database model. """ - __tablename__ = 'pair_lock' + __tablename__ = 'pairlocks' id = Column(Integer, primary_key=True) @@ -673,7 +673,7 @@ class PairLock(_DECL_BASE): # Time the pair was locked (start time) lock_time = Column(DateTime, nullable=False) # Time until the pair is locked (end time) - lock_end_time = Column(DateTime, nullable=False) + lock_end_time = Column(DateTime, nullable=False, index=True) active = Column(Boolean, nullable=False, default=True, index=True)