Modified files for partial trades operation

Signed-off-by: Es Fem <esfem.es@gmail.com>
This commit is contained in:
esfem 2020-10-29 20:39:35 -04:00 committed by Es Fem
parent 4e40c4c95d
commit 4b7d8c4419
3 changed files with 79 additions and 37 deletions

View File

@ -12,17 +12,17 @@ from typing import Any, Dict, List, Optional
import arrow import arrow
from cachetools import TTLCache from cachetools import TTLCache
from freqtrade import __version__, constants, persistence from freqtrade import __version__, constants
from freqtrade.configuration import validate_config_consistency from freqtrade.configuration import validate_config_consistency
from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.converter import order_book_to_dataframe
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.edge import Edge from freqtrade.edge import Edge
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
InvalidOrderException, PricingError) 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.misc import safe_value_fallback, safe_value_fallback2
from freqtrade.pairlist.pairlistmanager import PairListManager 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.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.rpc import RPCManager, RPCMessageType
from freqtrade.state import State from freqtrade.state import State
@ -71,6 +71,8 @@ class FreqtradeBot:
self.wallets = Wallets(self.config, self.exchange) self.wallets = Wallets(self.config, self.exchange)
PairLocks.timeframe = self.config['timeframe']
self.pairlists = PairListManager(self.exchange, self.config) self.pairlists = PairListManager(self.exchange, self.config)
self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists) self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists)
@ -344,6 +346,7 @@ class FreqtradeBot:
whitelist = copy.deepcopy(self.active_pair_whitelist) whitelist = copy.deepcopy(self.active_pair_whitelist)
if not whitelist: if not whitelist:
logger.info("Active pair whitelist is empty.") logger.info("Active pair whitelist is empty.")
return trades_created
else: else:
'''# Remove pairs for currently opened trades from the whitelist '''# Remove pairs for currently opened trades from the whitelist
for trade in Trade.get_open_trades(): for trade in Trade.get_open_trades():
@ -354,6 +357,7 @@ class FreqtradeBot:
if not whitelist: if not whitelist:
logger.info("No currency pair in active pair whitelist, " logger.info("No currency pair in active pair whitelist, "
"but checking to sell open trades.") "but checking to sell open trades.")
return trades_created
else: else:
# Create entity and execute trade for each pair from whitelist # Create entity and execute trade for each pair from whitelist
for pair in whitelist: for pair in whitelist:
@ -1062,8 +1066,8 @@ class FreqtradeBot:
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
stoploss_order=True) stoploss_order=True)
# Lock pair for one candle to prevent immediate rebuys # Lock pair for one candle to prevent immediate rebuys
self.strategy.lock_pair(trade.pair, self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
timeframe_to_next_date(self.config['timeframe'])) reason='Auto lock')
self._notify_sell(trade, "stoploss") self._notify_sell(trade, "stoploss")
return True return True
@ -1395,7 +1399,8 @@ class FreqtradeBot:
Trade.session.flush() Trade.session.flush()
# Lock pair for one candle to prevent immediate rebuys # 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) self._notify_sell(trade, order_type)
@ -1467,7 +1472,8 @@ class FreqtradeBot:
Trade.session.flush() Trade.session.flush()
#Lock pair for one candle to prevent immediate rebuys #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) self._notify_sell(trade, order_type)

View File

@ -7,8 +7,8 @@ from decimal import Decimal
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
import arrow import arrow
from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, String,
String, create_engine, desc, func, inspect) create_engine, desc, func, inspect)
from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.exc import NoSuchModuleError
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Query, relationship from sqlalchemy.orm import Query, relationship
@ -17,6 +17,7 @@ from sqlalchemy.orm.session import sessionmaker
from sqlalchemy.pool import StaticPool from sqlalchemy.pool import StaticPool
from sqlalchemy.sql.schema import UniqueConstraint from sqlalchemy.sql.schema import UniqueConstraint
from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.misc import safe_value_fallback from freqtrade.misc import safe_value_fallback
from freqtrade.persistence.migrations import check_migrate 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 # Copy session attributes to order object too
Order.session = Trade.session Order.session = Trade.session
Order.query = Order.session.query_property() Order.query = Order.session.query_property()
PairLock.session = Trade.session
PairLock.query = PairLock.session.query_property()
previous_tables = inspect(engine).get_table_names() previous_tables = inspect(engine).get_table_names()
_DECL_BASE.metadata.create_all(engine) _DECL_BASE.metadata.create_all(engine)
check_migrate(engine, decl_base=_DECL_BASE, previous_tables=previous_tables) check_migrate(engine, decl_base=_DECL_BASE, previous_tables=previous_tables)
@ -124,8 +128,7 @@ class Order(_DECL_BASE):
filled = Column(Float, nullable=True) filled = Column(Float, nullable=True)
remaining = Column(Float, nullable=True) remaining = Column(Float, nullable=True)
cost = Column(Float, nullable=True) cost = Column(Float, nullable=True)
fee = Column(Float, nullable=True) # OSM fee = Column(Float, nullable=True)
fee_cost = Column(Float, nullable=True) # OSM
order_date = Column(DateTime, nullable=True, default=datetime.utcnow) order_date = Column(DateTime, nullable=True, default=datetime.utcnow)
order_filled_date = Column(DateTime, nullable=True) order_filled_date = Column(DateTime, nullable=True)
order_update_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 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: if filtered_orders:
oobj = filtered_orders[0] oobj = filtered_orders[0]
oobj.update_from_ccxt_object(order) oobj.update_from_ccxt_object(order)
else: else:
logger.warning(f"Did not find order for {order['id']}.") logger.warning(f"Did not find order for {order}.")
@staticmethod @staticmethod
def parse_from_ccxt_object(order: Dict[str, Any], pair: str, side: str) -> 'Order': 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() self.recalc_open_trade_price()
def __repr__(self): 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}, ' return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
f'open_rate={self.open_rate:.8f}, open_since={open_since})') 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, 'fee_close_currency': self.fee_close_currency,
'open_date_hum': arrow.get(self.open_date).humanize(), '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_timestamp': int(self.open_date.replace(tzinfo=timezone.utc).timestamp() * 1000),
'open_rate': self.open_rate, 'open_rate': self.open_rate,
'open_rate_requested': self.open_rate_requested, 'open_rate_requested': self.open_rate_requested,
@ -286,7 +289,7 @@ class Trade(_DECL_BASE):
'close_date_hum': (arrow.get(self.close_date).humanize() 'close_date_hum': (arrow.get(self.close_date).humanize()
if self.close_date else None), 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), if self.close_date else None),
'close_timestamp': int(self.close_date.replace( 'close_timestamp': int(self.close_date.replace(
tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None, 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_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, 'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
'stoploss_order_id': self.stoploss_order_id, '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), if self.stoploss_last_update else None),
'stoploss_last_update_timestamp': int(self.stoploss_last_update.replace( 'stoploss_last_update_timestamp': int(self.stoploss_last_update.replace(
tzinfo=timezone.utc).timestamp() * 1000) if self.stoploss_last_update else None, 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}') raise ValueError(f'Unknown order type: {order_type}')
cleanup_db() 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: def close(self, rate: float) -> None:
""" """
Sets close_rate to the given rate, calculates total profit Sets close_rate to the given rate, calculates total profit
@ -468,7 +509,7 @@ class Trade(_DECL_BASE):
self.sell_order_status = 'closed' self.sell_order_status = 'closed'
self.open_order_id = None self.open_order_id = None
logger.info( logger.info(
'Updated position %s,', 'Updated position %s,',
self self
) )
@ -606,7 +647,6 @@ class Trade(_DECL_BASE):
:trade_open_rate: Actual open price of position. :trade_open_rate: Actual open price of position.
:return: New open rate modified with the order data. :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) return ((order_amount * order_price) + (trade_amount * trade_open_rate)) / (order_amount + trade_amount)
def increase_wallet(self, amount: float, rate: float) -> None: 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) active = Column(Boolean, nullable=False, default=True, index=True)
def __repr__(self): def __repr__(self):
lock_time = self.lock_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('%Y-%m-%d %H:%M:%S') lock_end_time = self.lock_end_time.strftime(DATETIME_PRINT_FORMAT)
return (f'PairLock(id={self.id}, pair={self.pair}, lock_time={lock_time}, ' return (f'PairLock(id={self.id}, pair={self.pair}, lock_time={lock_time}, '
f'lock_end_time={lock_end_time})') f'lock_end_time={lock_end_time})')
@ -783,9 +823,9 @@ class PairLock(_DECL_BASE):
def to_json(self) -> Dict[str, Any]: def to_json(self) -> Dict[str, Any]:
return { return {
'pair': self.pair, '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_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 'lock_end_timestamp': int(self.lock_end_time.replace(tzinfo=timezone.utc
).timestamp() * 1000), ).timestamp() * 1000),
'reason': self.reason, 'reason': self.reason,

View File

@ -17,7 +17,7 @@ from freqtrade.data.dataprovider import DataProvider
from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exceptions import OperationalException, StrategyError
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
from freqtrade.exchange.exchange import timeframe_to_next_date 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.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.wallets import Wallets from freqtrade.wallets import Wallets
@ -134,6 +134,8 @@ class IStrategy(ABC):
# and wallets - access to the current balance. # and wallets - access to the current balance.
dp: Optional[DataProvider] = None dp: Optional[DataProvider] = None
wallets: Optional[Wallets] = None wallets: Optional[Wallets] = None
# container variable for strategy source code
__source__: str = ''
# Definition of plot_config. See plotting documentation for more details. # Definition of plot_config. See plotting documentation for more details.
plot_config: Dict = {} plot_config: Dict = {}
@ -142,7 +144,6 @@ class IStrategy(ABC):
self.config = config self.config = config
# Dict to determine if analysis is necessary # Dict to determine if analysis is necessary
self._last_candle_seen_per_pair: Dict[str, datetime] = {} self._last_candle_seen_per_pair: Dict[str, datetime] = {}
self._pair_locked_until: Dict[str, datetime] = {}
@abstractmethod @abstractmethod
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
@ -287,7 +288,7 @@ class IStrategy(ABC):
""" """
return self.__class__.__name__ 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. Locks pair until a given timestamp happens.
Locked pairs are not analyzed, and are prevented from opening new trades. 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. :param until: datetime in UTC until the pair should be blocked from opening new trades.
Needs to be timezone aware `datetime.now(timezone.utc)` Needs to be timezone aware `datetime.now(timezone.utc)`
""" """
if pair not in self._pair_locked_until or self._pair_locked_until[pair] < until: PairLocks.lock_pair(pair, until, reason)
self._pair_locked_until[pair] = until
def unlock_pair(self, pair: str) -> None: 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. manually from within the strategy, to allow an easy way to unlock pairs.
:param pair: Unlock pair to allow trading again :param pair: Unlock pair to allow trading again
""" """
if pair in self._pair_locked_until: PairLocks.unlock_pair(pair, datetime.now(timezone.utc))
del self._pair_locked_until[pair]
def is_pair_locked(self, pair: str, candle_date: datetime = None) -> bool: 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 :param candle_date: Date of the last candle. Optional, defaults to current date
:returns: locking state of the pair in question. :returns: locking state of the pair in question.
""" """
if pair not in self._pair_locked_until:
return False
if not candle_date: 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: else:
# Locking should happen until a new candle arrives
lock_time = timeframe_to_next_date(self.timeframe, candle_date) lock_time = timeframe_to_next_date(self.timeframe, candle_date)
# lock_time = candle_date + timedelta(minutes=timeframe_to_minutes(self.timeframe)) return PairLocks.is_pair_locked(pair, lock_time)
return self._pair_locked_until[pair] > lock_time
def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """