From 4b7d8c4419d610ea28a6ceace74f130cccef6c91 Mon Sep 17 00:00:00 2001 From: esfem Date: Thu, 29 Oct 2020 20:39:35 -0400 Subject: [PATCH] Modified files for partial trades operation Signed-off-by: Es Fem --- freqtrade/freqtradebot.py | 20 +++++---- freqtrade/persistence/models.py | 72 +++++++++++++++++++++++++-------- freqtrade/strategy/interface.py | 24 +++++------ 3 files changed, 79 insertions(+), 37 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 166f180d1..31e7450eb 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -12,17 +12,17 @@ from typing import Any, Dict, List, Optional import arrow from cachetools import TTLCache -from freqtrade import __version__, constants, persistence +from freqtrade import __version__, constants from freqtrade.configuration import validate_config_consistency from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, PricingError) -from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date +from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.pairlist.pairlistmanager import PairListManager -from freqtrade.persistence import Order,Trade, cleanup_db, init_db +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 @@ -71,6 +71,8 @@ class FreqtradeBot: self.wallets = Wallets(self.config, self.exchange) + PairLocks.timeframe = self.config['timeframe'] + self.pairlists = PairListManager(self.exchange, self.config) self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists) @@ -344,6 +346,7 @@ class FreqtradeBot: whitelist = copy.deepcopy(self.active_pair_whitelist) if not whitelist: logger.info("Active pair whitelist is empty.") + return trades_created else: '''# Remove pairs for currently opened trades from the whitelist for trade in Trade.get_open_trades(): @@ -354,6 +357,7 @@ class FreqtradeBot: if not whitelist: logger.info("No currency pair in active pair whitelist, " "but checking to sell open trades.") + return trades_created else: # Create entity and execute trade for each pair from whitelist for pair in whitelist: @@ -1062,8 +1066,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 @@ -1395,7 +1399,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, datetime.now(timezone.utc), + reason='Auto lock') self._notify_sell(trade, order_type) @@ -1467,7 +1472,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, datetime.now(timezone.utc), + reason='Auto lock') self._notify_sell(trade, order_type) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 2c89bdd86..642d00e85 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -7,8 +7,8 @@ from decimal import Decimal from typing import Any, Dict, List, Optional import arrow -from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, - String, create_engine, desc, func, inspect) +from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, String, + create_engine, desc, func, inspect) from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import Query, relationship @@ -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 @@ -61,6 +62,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) @@ -124,8 +128,7 @@ class Order(_DECL_BASE): filled = Column(Float, nullable=True) remaining = Column(Float, nullable=True) cost = Column(Float, nullable=True) - fee = Column(Float, nullable=True) # OSM - fee_cost = Column(Float, nullable=True) # OSM + fee = Column(Float, nullable=True) order_date = Column(DateTime, nullable=True, default=datetime.utcnow) order_filled_date = Column(DateTime, nullable=True) order_update_date = Column(DateTime, nullable=True) @@ -168,12 +171,12 @@ class Order(_DECL_BASE): """ Get all non-closed orders - useful when trying to batch-update orders """ - filtered_orders = [o for o in orders if o.order_id == order['id']] + filtered_orders = [o for o in orders if o.order_id == order.get('id')] if filtered_orders: oobj = filtered_orders[0] oobj.update_from_ccxt_object(order) else: - logger.warning(f"Did not find order for {order['id']}.") + logger.warning(f"Did not find order for {order}.") @staticmethod def parse_from_ccxt_object(order: Dict[str, Any], pair: str, side: str) -> 'Order': @@ -252,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})') @@ -278,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, @@ -286,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, @@ -302,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, @@ -444,6 +447,44 @@ class Trade(_DECL_BASE): raise ValueError(f'Unknown order type: {order_type}') cleanup_db() + def partial_update(self, order: Dict) -> None: + """ + Updates this entity with amount and actual open/close rates, + modified to support multiple orders keeping the trade opened + :param order: order retrieved by exchange.fetch_order() + :return: None + + """ + order_type = order['type'] + + if order_type in ('market', 'limit') and order['side'] == 'buy': + # Update open rate and actual amount + self.open_rate = self.average_open_rate(order['filled'], + safe_value_fallback(order, 'average', 'price'), + self.amount, self.open_rate) + self.amount = Decimal(self.amount or 0) + Decimal(order['filled']) + self.decrease_wallet(self, Decimal(order['filled']), self.open_rate) + if self.is_open and order['filled'] != 0: + logger.info(f'{order_type.upper()}_Partial BUY has been fulfilled for {self}.') + self.open_order_id = None + + elif order_type in ('market', 'limit') and order['side'] == 'sell': + self.amount = (Decimal(self.amount or 0) - Decimal(order['filled'])) + if self.is_open and order['filled'] != 0: + logger.info(f'{order_type.upper()}_Partial SELL has been fulfilled for {self}.') + self.partial_close(self, safe_value_fallback(order, 'average', 'price')) + self.increase_wallet(self, Decimal(order['filled']), order['price']) + + elif order_type in ('stop_loss_limit', 'stop-loss', 'stop'): + self.stoploss_order_id = None + self.close_rate_requested = self.stop_loss + if self.is_open: + logger.info(f'{order_type.upper()} is hit for {self}.') + self.close(order['average']) + else: + raise ValueError(f'Unknown order type: {order_type}') + cleanup_db() + def close(self, rate: float) -> None: """ Sets close_rate to the given rate, calculates total profit @@ -468,7 +509,7 @@ class Trade(_DECL_BASE): self.sell_order_status = 'closed' self.open_order_id = None logger.info( - 'Updated position %s,', + 'Updated position %s,', self ) @@ -606,7 +647,6 @@ class Trade(_DECL_BASE): :trade_open_rate: Actual open price of position. :return: New open rate modified with the order data. """ - #((order['amount'] * order['price']) + (self.amount * trade.open_rate)) / (order['amount'] + trade.amount) return ((order_amount * order_price) + (trade_amount * trade_open_rate)) / (order_amount + trade_amount) def increase_wallet(self, amount: float, rate: float) -> None: @@ -758,8 +798,8 @@ class PairLock(_DECL_BASE): 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') + 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})') @@ -783,9 +823,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/strategy/interface.py b/freqtrade/strategy/interface.py index 6cc26bd1c..c2e6f4d96 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 @@ -134,6 +134,8 @@ class IStrategy(ABC): # and wallets - access to the current balance. dp: Optional[DataProvider] = None wallets: Optional[Wallets] = None + # container variable for strategy source code + __source__: str = '' # Definition of plot_config. See plotting documentation for more details. plot_config: Dict = {} @@ -142,7 +144,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: @@ -287,7 +288,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,9 +298,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)` """ - 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: """ @@ -308,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: """ @@ -321,15 +319,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 PairLocks.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 PairLocks.is_pair_locked(pair, lock_time) def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """