diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 270998749..f1a7c689a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -4,7 +4,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade() import copy import logging import traceback -from datetime import datetime +from datetime import datetime, timezone from math import isclose from threading import Lock from typing import Any, Dict, List, Optional @@ -22,7 +22,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.pairlist.pairlistmanager import PairListManager -from freqtrade.persistence import Order, Trade +from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.state import State @@ -57,8 +57,8 @@ class FreqtradeBot: # Cache values for 1800 to avoid frequent polling of the exchange for prices # Caching only applies to RPC methods, so prices for open trades are still # refreshed once every iteration. - self._sell_rate_cache = TTLCache(maxsize=100, ttl=1800) - self._buy_rate_cache = TTLCache(maxsize=100, ttl=1800) + self._sell_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800) + self._buy_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800) self.strategy: IStrategy = StrategyResolver.load_strategy(self.config) @@ -67,7 +67,7 @@ class FreqtradeBot: self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) - persistence.init(self.config.get('db_url', None), clean_open_orders=self.config['dry_run']) + init_db(self.config.get('db_url', None), clean_open_orders=self.config['dry_run']) self.wallets = Wallets(self.config, self.exchange) @@ -122,7 +122,7 @@ class FreqtradeBot: self.check_for_open_trades() self.rpc.cleanup() - persistence.cleanup() + cleanup_db() def startup(self) -> None: """ @@ -734,7 +734,7 @@ class FreqtradeBot: return False amount = stake_amount / buy_limit_requested - order_type = self.strategy.order_types['buy'] + order_type = self.strategy.order_types['partial_buy'] if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=amount, rate=buy_limit_requested, @@ -801,10 +801,10 @@ class FreqtradeBot: open_rate=buy_limit_filled_price, open_rate_requested=buy_limit_requested, open_date=datetime.utcnow(), - exchange=_bot.exchange.id, + exchange=self.exchange.id, open_order_id=order_id, - strategy=_bot.strategy.get_strategy_name(), - timeframe=timeframe_to_minutes(_bot.config['timeframe']) + strategy=self.strategy.get_strategy_name(), + timeframe=timeframe_to_minutes(self.config['timeframe']) ) trade.orders.append(order_obj) # Update fees if order is closed @@ -1062,8 +1062,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, datetime.now(timezone.utc), + reason='Auto lock') self._notify_sell(trade, "stoploss") return True diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 5d2bf6c91..2c89bdd86 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -27,7 +27,7 @@ _DECL_BASE: Any = declarative_base() _SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls' -def init(db_url: str, clean_open_orders: bool = False) -> None: +def init_db(db_url: str, clean_open_orders: bool = False) -> None: """ Initializes this module with the given config, registers all known command handlers @@ -70,7 +70,7 @@ def init(db_url: str, clean_open_orders: bool = False) -> None: clean_dry_run_db() -def cleanup() -> None: +def cleanup_db() -> None: """ Flushes all pending operations to disk. :return: None @@ -404,7 +404,7 @@ class Trade(_DECL_BASE): self.close(order['average']) else: raise ValueError(f'Unknown order type: {order_type}') - cleanup() + cleanup_db() def partial_update(self, order: Dict) -> None: """ @@ -442,7 +442,7 @@ class Trade(_DECL_BASE): self.close(order['average']) else: raise ValueError(f'Unknown order type: {order_type}') - cleanup() + cleanup_db() def close(self, rate: float) -> None: """ @@ -738,3 +738,56 @@ 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('%Y-%m-%d %H:%M:%S') + lock_end_time = self.lock_end_time.strftime('%Y-%m-%d %H:%M:%S') + return (f'PairLock(id={self.id}, pair={self.pair}, lock_time={lock_time}, ' + f'lock_end_time={lock_end_time})') + + @staticmethod + def query_pair_locks(pair: Optional[str], now: datetime) -> Query: + """ + 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)). + """ + + filters = [PairLock.lock_end_time > now, + # Only active locks + PairLock.active.is_(True), ] + if pair: + filters.append(PairLock.pair == pair) + return PairLock.query.filter( + *filters + ) + + def to_json(self) -> Dict[str, Any]: + return { + 'pair': self.pair, + '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/strategy/interface.py b/freqtrade/strategy/interface.py index a1a4be2c3..08afd4189 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 PairLocks, Trade from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -287,7 +287,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. @@ -297,8 +297,8 @@ 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 + PairLocks.lock_pair(pair, until, reason) + def unlock_pair(self, pair: str) -> None: """ @@ -307,8 +307,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] + PairLocks.unlock_pair(pair, datetime.now(timezone.utc)) def is_pair_locked(self, pair: str, candle_date: datetime = None) -> bool: """ @@ -368,6 +367,8 @@ class IStrategy(ABC): logger.debug("Skipping TA Analysis for already analyzed candle") dataframe['buy'] = 0 dataframe['sell'] = 0 + dataframe['partial_buy'] = PartialTradeTuple(0, 0) + dataframe['partial_sell'] = PartialTradeTuple(0, 0) # Other Defs in strategy that want to be called every loop here # twitter_sell = self.watch_twitter_feed(dataframe, metadata) @@ -464,9 +465,16 @@ class IStrategy(ABC): ) return False, False, PartialTradeTuple(False,0), PartialTradeTuple(False,0) + # Check if partials are set in strategy and set to 0 if not + if latest.partial_buy and latest.partial_sell: + if type(latest.partial_buy) is not PartialTradeTuple: + latest.partial_buy = PartialTradeTuple(0, 0) + if type(latest.partial_sell) is not PartialTradeTuple: + latest.partial_sell = PartialTradeTuple(0, 0) + (buy, sell, partial_buy, partial_sell) = \ latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1,\ - latest[SignalType.PARTIAL_BUY] == 1, latest[SignalType.PARTIAL_SELL == 1] + latest[SignalType.PARTIAL_BUY.value], latest[SignalType.PARTIAL_SELL.value] logger.debug('trigger: %s (pair=%s) buy=%s sell=%s partial_buy = %s partial_sell = %s', latest['date'], pair, str(buy), str(sell), str(partial_buy), str(partial_sell)) return buy, sell, partial_buy, partial_sell diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index ea0e234ec..50bfe8a11 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -47,3 +47,4 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, dataframe = dataframe.ffill() return dataframe + diff --git a/user_data/backtest_results/.gitkeep b/user_data/backtest_results/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/user_data/data/.gitkeep b/user_data/data/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/user_data/hyperopts/.gitkeep b/user_data/hyperopts/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/user_data/logs/.gitkeep b/user_data/logs/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/user_data/notebooks/.gitkeep b/user_data/notebooks/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/user_data/strategies/.gitkeep b/user_data/strategies/.gitkeep deleted file mode 100644 index e69de29bb..000000000