Merge pull request #3879 from freqtrade/persist_pairlocks
Persist pairlocks
This commit is contained in:
commit
71410a5a1e
@ -109,24 +109,25 @@ python3 scripts/rest_client.py --config rest_config.json <command> [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 <trade_id>` | 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 <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`).
|
||||
| `forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`).
|
||||
| `forcebuy <pair> [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 <n>` | 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 <n>` | 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 <command> [optional par
|
||||
| `strategies` | List strategies in strategy directory. **Alpha**
|
||||
| `strategy <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.
|
||||
|
@ -693,15 +693,15 @@ Locked pairs will show the message `Pair <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.
|
||||
|
@ -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')
|
||||
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')
|
||||
|
||||
self._notify_sell(trade, order_type)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
@ -63,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)
|
||||
@ -251,7 +255,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})')
|
||||
@ -277,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,
|
||||
@ -285,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,
|
||||
@ -301,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,
|
||||
@ -654,3 +658,105 @@ 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__ = 'pairlocks'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
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, index=True)
|
||||
|
||||
active = Column(Boolean, nullable=False, default=True, index=True)
|
||||
|
||||
def __repr__(self):
|
||||
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: Optional[str], now: Optional[datetime] = None) -> List['PairLock']:
|
||||
"""
|
||||
Get all locks for this pair
|
||||
: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_end_time) >= now,
|
||||
# Only active locks
|
||||
PairLock.active.is_(True), ]
|
||||
if pair:
|
||||
filters.append(PairLock.pair == pair)
|
||||
return PairLock.query.filter(
|
||||
*filters
|
||||
).all()
|
||||
|
||||
@staticmethod
|
||||
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)
|
||||
|
||||
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.now(timezone.utc)).
|
||||
defaults to datetime.utcnow()
|
||||
"""
|
||||
if not now:
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
return PairLock.query.filter(
|
||||
PairLock.pair == pair,
|
||||
func.datetime(PairLock.lock_end_time) >= now,
|
||||
# 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.strftime(DATETIME_PRINT_FORMAT),
|
||||
'lock_timestamp': int(self.lock_time.replace(tzinfo=timezone.utc).timestamp() * 1000),
|
||||
'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,
|
||||
'active': self.active,
|
||||
}
|
||||
|
@ -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 currently active locks.
|
||||
"""
|
||||
return jsonify(self._rpc_locks())
|
||||
|
||||
@require_login
|
||||
@rpc_catch_errors
|
||||
def _daily(self):
|
||||
|
@ -13,13 +13,13 @@ 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
|
||||
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,
|
||||
@ -638,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]
|
||||
|
@ -100,6 +100,7 @@ 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 +609,26 @@ 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 currently active locks
|
||||
"""
|
||||
try:
|
||||
locks = self._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))
|
||||
|
||||
@authorized_only
|
||||
def _whitelist(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
@ -719,8 +740,8 @@ class Telegram(RPC):
|
||||
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\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"
|
||||
"*/count:* `Show number of trades running 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"
|
||||
"*/balance:* `Show account balance per currency`\n"
|
||||
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
|
||||
"*/reload_config:* `Reload configuration file` \n"
|
||||
|
@ -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.
|
||||
@ -287,9 +286,9 @@ 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.
|
||||
"""
|
||||
if pair not in self._pair_locked_until or self._pair_locked_until[pair] < until:
|
||||
self._pair_locked_until[pair] = until
|
||||
PairLock.lock_pair(pair, until, reason)
|
||||
|
||||
def unlock_pair(self, pair: str) -> None:
|
||||
"""
|
||||
@ -298,8 +297,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 +309,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:
|
||||
"""
|
||||
|
@ -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.
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
Unit test file for rpc/api_server.py
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from unittest.mock import ANY, MagicMock, PropertyMock
|
||||
|
||||
@ -12,7 +12,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 +328,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))
|
||||
|
@ -18,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
|
||||
@ -75,8 +75,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 +1024,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(
|
||||
|
@ -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,7 @@ 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 ...
|
||||
|
||||
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))
|
||||
|
@ -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.
|
||||
|
@ -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,49 @@ 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)
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
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])
|
||||
|
Loading…
Reference in New Issue
Block a user