Modified files for partial trades operation

This commit is contained in:
Oscar Martinez 2020-10-29 09:35:51 -04:00
parent 684de9c7d0
commit 520a597f83
4 changed files with 410 additions and 154 deletions

View File

@ -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, timezone
from datetime import datetime
from math import isclose
from threading import Lock
from typing import Any, Dict, List, Optional
@ -12,25 +12,24 @@ from typing import Any, Dict, List, Optional
import arrow
from cachetools import TTLCache
from freqtrade import __version__, constants
from freqtrade import __version__, constants, persistence
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
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, PairLocks, Trade, cleanup_db, init_db
from freqtrade.persistence import Order, Trade
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.rpc import RPCManager, RPCMessageType
from freqtrade.state import State
from freqtrade.strategy.interface import IStrategy, SellType
from freqtrade.strategy.interface import IStrategy, SellType, PartialTradeTuple
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.wallets import Wallets
logger = logging.getLogger(__name__)
@ -58,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 = TTLCache(maxsize=100, ttl=1800)
self._buy_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800)
self._sell_rate_cache = TTLCache(maxsize=100, ttl=1800)
self._buy_rate_cache = TTLCache(maxsize=100, ttl=1800)
self.strategy: IStrategy = StrategyResolver.load_strategy(self.config)
@ -68,12 +67,10 @@ class FreqtradeBot:
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
init_db(self.config.get('db_url', None), clean_open_orders=self.config['dry_run'])
persistence.init(self.config.get('db_url', None), clean_open_orders=self.config['dry_run'])
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)
@ -125,7 +122,7 @@ class FreqtradeBot:
self.check_for_open_trades()
self.rpc.cleanup()
cleanup_db()
persistence.cleanup()
def startup(self) -> None:
"""
@ -347,17 +344,17 @@ class FreqtradeBot:
whitelist = copy.deepcopy(self.active_pair_whitelist)
if not whitelist:
logger.info("Active pair whitelist is empty.")
return trades_created
# Remove pairs for currently opened trades from the whitelist
else:
'''# Remove pairs for currently opened trades from the whitelist
for trade in Trade.get_open_trades():
if trade.pair in whitelist:
whitelist.remove(trade.pair)
logger.debug('Ignoring %s in pair whitelist', trade.pair)
logger.debug('Ignoring %s in pair whitelist', trade.pair)'''
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:
try:
@ -553,9 +550,9 @@ class FreqtradeBot:
return False
# running get_signal on historical data fetched
(buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df)
(buy, sell, partial_buy, partial_sell) = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df)
if buy and not sell:
if buy or partial_buy.flag and not sell and not partial_sell.flag:
stake_amount = self.get_trade_stake_amount(pair)
if not stake_amount:
logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.")
@ -568,11 +565,19 @@ class FreqtradeBot:
if ((bid_check_dom.get('enabled', False)) and
(bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
if self._check_depth_of_market_buy(pair, bid_check_dom):
if partial_buy.flag:
logger.info(f'Executing Partial Buy for {pair}.')
return self.execute_partial_buy(pair, partial_buy.amount)
else:
logger.info(f'Executing Buy for {pair}.')
return self.execute_buy(pair, stake_amount)
else:
return False
if partial_buy.flag:
logger.info(f'Executing Partial Buy for {pair}')
return self.execute_partial_buy(pair, partial_buy.amount)
else:
logger.info(f'Executing Buy for {pair}')
return self.execute_buy(pair, stake_amount)
else:
@ -704,6 +709,122 @@ class FreqtradeBot:
return True
def execute_partial_buy(self, pair: str, stake_amount: float, price: Optional[float] = None) -> bool:
"""
Executes a limit buy for the given pair
:param pair: pair for which we want to create a LIMIT_BUY
:return: True if a buy order is created, false if it fails.
"""
time_in_force = self.strategy.order_time_in_force['buy']
if price:
buy_limit_requested = price
else:
# Calculate price
buy_limit_requested = self.get_buy_rate(pair, True)
# in case you try to buy more than available
#stake_amount = self._safe_buy_amount(pair, stake_amount)
min_stake_amount = self._get_min_pair_stake_amount(pair, buy_limit_requested)
if min_stake_amount is not None and min_stake_amount > stake_amount:
logger.warning(
f"Can't open a new trade for {pair}: stake amount "
f"is too small ({stake_amount} < {min_stake_amount})"
)
return False
amount = stake_amount / buy_limit_requested
order_type = self.strategy.order_types['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,
time_in_force=time_in_force):
logger.info(f"User requested abortion of buying {pair}")
return False
amount = self.exchange.amount_to_precision(pair, amount)
order = self.exchange.buy(pair=pair, ordertype=order_type,
amount=amount, rate=buy_limit_requested,
time_in_force=time_in_force)
order_obj = Order.parse_from_ccxt_object(order, pair, 'buy')
order_id = order['id']
order_status = order.get('status', None)
# we assume the order is executed at the price requested
buy_limit_filled_price = buy_limit_requested
amount_requested = amount
if order_status == 'expired' or order_status == 'rejected':
order_tif = self.strategy.order_time_in_force['buy']
# return false if the order is not filled
if float(order['filled']) == 0:
logger.warning('Buy %s order with time in force %s for %s is %s by %s.'
' zero amount is fulfilled.',
order_tif, order_type, pair, order_status, self.exchange.name)
return False
else:
# the order is partially fulfilled
# in case of IOC orders we can check immediately
# if the order is fulfilled fully or partially
logger.warning('Buy %s order with time in force %s for %s is %s by %s.'
' %s amount fulfilled out of %s (%s remaining which is canceled).',
order_tif, order_type, pair, order_status, self.exchange.name,
order['filled'], order['amount'], order['remaining']
)
stake_amount = order['cost']
amount = safe_value_fallback(order, 'filled', 'amount')
buy_limit_filled_price = safe_value_fallback(order, 'average', 'price')
# in case of FOK the order may be filled immediately and fully
elif order_status == 'closed':
stake_amount = order['cost']
amount = safe_value_fallback(order, 'filled', 'amount')
buy_limit_filled_price = safe_value_fallback(order, 'average', 'price')
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
order['fee'] = fee
for trade in Trade.get_open_trades():
if trade.pair == pair:
break
if len(Trade.get_open_trades()) == 0:
trade = Trade(
pair=pair,
stake_amount=stake_amount,
amount=amount,
amount_requested=amount_requested,
fee_open=fee,
fee_close=fee,
open_rate=buy_limit_filled_price,
open_rate_requested=buy_limit_requested,
open_date=datetime.utcnow(),
exchange=_bot.exchange.id,
open_order_id=order_id,
strategy=_bot.strategy.get_strategy_name(),
timeframe=timeframe_to_minutes(_bot.config['timeframe'])
)
trade.orders.append(order_obj)
# Update fees if order is closed
if order_status == 'closed':
self.update_trade_state(trade, order_id, order)
Trade.session.add(trade)
else:
trade.open_order_id = order_id
trade.orders.append(order_obj)
self.update_trade_state(trade, order_id, order)
Trade.session.flush()
# Updating wallets
self.wallets.update()
self._notify_buy(trade, order_type)
return True
def _notify_buy(self, trade: Trade, order_type: str) -> None:
"""
Sends rpc notification when a buy occured.
@ -834,7 +955,8 @@ class FreqtradeBot:
logger.debug('Handling %s ...', trade)
(buy, sell) = (False, False)
(buy, sell, partial_buy, partial_sell) = (False, False, PartialTradeTuple(flag=False, amount=0),
PartialTradeTuple(flag=False, amount=0))
config_ask_strategy = self.config.get('ask_strategy', {})
@ -843,7 +965,8 @@ class FreqtradeBot:
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
self.strategy.timeframe)
(buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe, analyzed_df)
(buy, sell, partial_buy, partial_sell) = self.strategy.get_signal(trade.pair,
self.strategy.timeframe, analyzed_df)
if config_ask_strategy.get('use_order_book', False):
order_book_min = config_ask_strategy.get('order_book_min', 1)
@ -868,13 +991,13 @@ class FreqtradeBot:
# resulting in outdated RPC messages
self._sell_rate_cache[trade.pair] = sell_rate
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
if self._check_and_execute_sell(trade, sell_rate, buy, sell, partial_buy, partial_sell):
return True
else:
logger.debug('checking sell')
sell_rate = self.get_sell_rate(trade.pair, True)
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
if self._check_and_execute_sell(trade, sell_rate, buy, sell, partial_buy, partial_sell):
return True
logger.debug('Found no sell signal for %s.', trade)
@ -939,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, datetime.now(timezone.utc),
reason='Auto lock')
self.strategy.lock_pair(trade.pair,
timeframe_to_next_date(self.config['timeframe']))
self._notify_sell(trade, "stoploss")
return True
@ -1004,19 +1127,25 @@ class FreqtradeBot:
f"for pair {trade.pair}.")
def _check_and_execute_sell(self, trade: Trade, sell_rate: float,
buy: bool, sell: bool) -> bool:
buy: bool, sell: bool, partial_buy: PartialTradeTuple,
partial_sell: PartialTradeTuple) -> bool:
"""
Check and execute sell
"""
should_sell = self.strategy.should_sell(
trade, sell_rate, datetime.utcnow(), buy, sell,
trade, sell_rate, datetime.utcnow(), buy, sell, partial_buy, partial_sell,
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
)
if should_sell.sell_flag:
if should_sell.sell_type == SellType.PARTIAL_SELL_SIGNAL:
logger.info(f'Executing Partial Sell for {trade.pair}. Reason: {should_sell.sell_type}')
self.execute_partial_sell(trade, partial_sell.amount, sell_rate, should_sell.sell_type)
else:
logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}')
self.execute_sell(trade, sell_rate, should_sell.sell_type)
return True
return False
def _check_timed_out(self, side: str, order: dict) -> bool:
@ -1266,8 +1395,79 @@ class FreqtradeBot:
Trade.session.flush()
# Lock pair for one candle to prevent immediate rebuys
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
reason='Auto lock')
self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe']))
self._notify_sell(trade, order_type)
return True
def execute_partial_sell(self, trade: Trade, amount: float, limit: float, sell_reason: SellType) -> bool:
"""
Executes a limit sell for the given trade and limit
:param trade: Trade instance
:param limit: limit rate for the sell order
:param sellreason: Reason the sell was triggered
:return: True if it succeeds (supported) False (not supported)
:OSM metodo modificado para aceptar ventas parciales
"""
#TODO: Add a custom partial sell notification call
sell_type = 'sell'
# First cancelling stoploss on exchange ...
if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
try:
self.exchange.cancel_stoploss_order(trade.stoploss_order_id, trade.pair)
except InvalidOrderException:
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
order_type = self.strategy.order_types[sell_type]
if sell_reason == SellType.EMERGENCY_SELL:
# Emergency sells (default to market!)
order_type = self.strategy.order_types.get("emergencysell", "market")
if amount > trade.amount:
amount = trade.amount
amount = self._safe_sell_amount(trade.pair, amount)
time_in_force = self.strategy.order_time_in_force['sell']
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit,
time_in_force=time_in_force,
sell_reason=sell_reason.value):
logger.info(f"User requested abortion of selling {trade.pair}")
return False
try:
# Execute sell and update trade record
order = self.exchange.sell(pair=trade.pair,
ordertype=order_type,
amount=amount, rate=limit,
time_in_force=time_in_force
)
except InsufficientFundsError as e:
logger.warning(f"Unable to place order {e}.")
# Try to figure out what went wrong
self.handle_insufficient_funds(trade)
return False
order_obj = Order.parse_from_ccxt_object(order, trade.pair, 'sell')
trade.orders.append(order_obj)
trade.open_order_id = order['id']
trade.close_rate_requested = limit
trade.sell_reason = sell_reason.value
# In case of market sell orders the order can be closed immediately
if order.get('status', 'unknown') == 'closed':
self.update_trade_state(trade, trade.open_order_id, order)
# force trade to contine opened, if amount >= trade amount close trade
if amount < trade.amount:
trade.is_open = True
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._notify_sell(trade, order_type)
@ -1393,7 +1593,7 @@ class FreqtradeBot:
abs_tol=constants.MATH_CLOSE_PREC):
order['amount'] = new_amount
order.pop('filled', None)
trade.recalc_open_trade_price()
except DependencyException as exception:
logger.warning("Could not update trade amount: %s", exception)
@ -1404,7 +1604,7 @@ class FreqtradeBot:
trade.update(order)
# Updating wallets when order is closed
if not trade.is_open:
if order['status'] == 'closed':
self.wallets.update()
return False

View File

@ -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,20 +17,17 @@ 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
logger = logging.getLogger(__name__)
_DECL_BASE: Any = declarative_base()
_SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
def init_db(db_url: str, clean_open_orders: bool = False) -> None:
def init(db_url: str, clean_open_orders: bool = False) -> None:
"""
Initializes this module with the given config,
registers all known command handlers
@ -64,9 +61,6 @@ 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)
@ -76,7 +70,7 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None:
clean_dry_run_db()
def cleanup_db() -> None:
def cleanup() -> None:
"""
Flushes all pending operations to disk.
:return: None
@ -130,6 +124,8 @@ 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
order_date = Column(DateTime, nullable=True, default=datetime.utcnow)
order_filled_date = Column(DateTime, nullable=True)
order_update_date = Column(DateTime, nullable=True)
@ -156,6 +152,7 @@ class Order(_DECL_BASE):
self.filled = order.get('filled', self.filled)
self.remaining = order.get('remaining', self.remaining)
self.cost = order.get('cost', self.cost)
self.fee = order.get('fee', self.fee)
if 'timestamp' in order and order['timestamp'] is not None:
self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc)
@ -171,12 +168,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.get('id')]
filtered_orders = [o for o in orders if o.order_id == order['id']]
if filtered_orders:
oobj = filtered_orders[0]
oobj.update_from_ccxt_object(order)
else:
logger.warning(f"Did not find order for {order}.")
logger.warning(f"Did not find order for {order['id']}.")
@staticmethod
def parse_from_ccxt_object(order: Dict[str, Any], pair: str, side: str) -> 'Order':
@ -255,7 +252,7 @@ class Trade(_DECL_BASE):
self.recalc_open_trade_price()
def __repr__(self):
open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed'
open_since = self.open_date.strftime('%Y-%m-%d %H:%M:%S') 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})')
@ -281,7 +278,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(DATETIME_PRINT_FORMAT),
'open_date': self.open_date.strftime("%Y-%m-%d %H:%M:%S"),
'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 +286,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(DATETIME_PRINT_FORMAT)
'close_date': (self.close_date.strftime("%Y-%m-%d %H:%M:%S")
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 +302,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(DATETIME_PRINT_FORMAT)
'stoploss_last_update': (self.stoploss_last_update.strftime("%Y-%m-%d %H:%M:%S")
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,
@ -382,6 +379,10 @@ class Trade(_DECL_BASE):
return
logger.info('Updating trade (id=%s) ...', self.id)
# to be able to partially buy or sell
if order['amount'] < self.amount:
self.partial_update(order)
return
if order_type in ('market', 'limit') and order['side'] == 'buy':
# Update open rate and actual amount
@ -403,7 +404,45 @@ class Trade(_DECL_BASE):
self.close(order['average'])
else:
raise ValueError(f'Unknown order type: {order_type}')
cleanup_db()
cleanup()
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()
def close(self, rate: float) -> None:
"""
@ -422,6 +461,17 @@ class Trade(_DECL_BASE):
self
)
def partial_close(self, rate: float) -> None:
""" modified close() to keep trade opened
"""
self.is_open = True
self.sell_order_status = 'closed'
self.open_order_id = None
logger.info(
'Updated position %s,',
self
)
def update_fee(self, fee_cost: float, fee_currency: Optional[str], fee_rate: Optional[float],
side: str) -> None:
"""
@ -499,6 +549,8 @@ class Trade(_DECL_BASE):
fee: Optional[float] = None) -> float:
"""
Calculate the absolute profit in stake currency between Close and Open trade
Modified to stop using open_tride_price and use open_rate instead,
which be actualized if partial buys.
:param fee: fee to use on the close rate (optional).
If rate is not set self.fee will be used
:param rate: close rate to compare with (optional).
@ -509,13 +561,15 @@ class Trade(_DECL_BASE):
rate=(rate or self.close_rate),
fee=(fee or self.fee_close)
)
profit = close_trade_price - self.open_trade_price
profit = close_trade_price - self.open_rate * self.amount
return float(f"{profit:.8f}")
def calc_profit_ratio(self, rate: Optional[float] = None,
fee: Optional[float] = None) -> float:
"""
Calculates the profit as ratio (including fee).
Modified to stop using open_tride_price and use open_rate instead,
which be actualized if partial buys.
:param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used
:param fee: fee to use on the close rate (optional).
@ -525,7 +579,7 @@ class Trade(_DECL_BASE):
rate=(rate or self.close_rate),
fee=(fee or self.fee_close)
)
profit_ratio = (close_trade_price / self.open_trade_price) - 1
profit_ratio = (close_trade_price / (self.open_rate * self.amount)) - 1
return float(f"{profit_ratio:.8f}")
def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]:
@ -543,6 +597,32 @@ class Trade(_DECL_BASE):
else:
return None
def average_open_rate(self, order_amount, order_price, trade_amount, trade_open_rate):
"""
Calculates average entry price when increase an open position with a partial buy
:param order_amount: Amount in base coin to buy
:param order_price: rate at the time of order buy
:trade_amount: Actual amount of the open position. (befor adding new order amount)
: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:
sell_trade = Decimal(amount) * Decimal(rate)
fees = sell_trade * Decimal(self.fee_close)
close_trade_price = float(sell_trade - fees)
self.stake_amount = Decimal(self.stake_amount or 0) + Decimal(close_trade_price)
def decrease_wallet(self, amount: float, rate: float) -> None:
buy_trade = Decimal(amount * rate)
fees = buy_trade * Decimal(self.fee_open)
open_trade_price = float(buy_trade + fees)
self.stake_amount = Decimal(self.stake_amount or 0) - Decimal(open_trade_price)
@staticmethod
def get_trades(trade_filter=None) -> Query:
"""
@ -600,8 +680,8 @@ class Trade(_DECL_BASE):
Calculates total invested amount in open trades
in stake currency
"""
total_open_stake_amount = Trade.session.query(func.sum(Trade.stake_amount))\
.filter(Trade.is_open.is_(True))\
total_open_stake_amount = Trade.session.query(func.sum(Trade.stake_amount)) \
.filter(Trade.is_open.is_(True)) \
.scalar()
return total_open_stake_amount or 0
@ -614,7 +694,7 @@ class Trade(_DECL_BASE):
Trade.pair,
func.sum(Trade.close_profit).label('profit_sum'),
func.count(Trade.pair).label('count')
).filter(Trade.is_open.is_(False))\
).filter(Trade.is_open.is_(False)) \
.group_by(Trade.pair) \
.order_by(desc('profit_sum')) \
.all()
@ -658,56 +738,3 @@ 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 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(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,
}

View File

@ -17,11 +17,10 @@ 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 PairLocks, Trade
from freqtrade.persistence import Trade
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.wallets import Wallets
logger = logging.getLogger(__name__)
@ -31,6 +30,8 @@ class SignalType(Enum):
"""
BUY = "buy"
SELL = "sell"
PARTIAL_BUY = "partial_buy"
PARTIAL_SELL = "partial_sell"
class SellType(Enum):
@ -42,6 +43,7 @@ class SellType(Enum):
STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange"
TRAILING_STOP_LOSS = "trailing_stop_loss"
SELL_SIGNAL = "sell_signal"
PARTIAL_SELL_SIGNAL = "partial_sell_signal"
FORCE_SELL = "force_sell"
EMERGENCY_SELL = "emergency_sell"
NONE = ""
@ -58,6 +60,13 @@ class SellCheckTuple(NamedTuple):
sell_flag: bool
sell_type: SellType
class PartialTradeTuple(NamedTuple):
"""
NamedTuple for partial trade + amount
"""
flag: bool
amount: float
class IStrategy(ABC):
"""
@ -101,6 +110,8 @@ class IStrategy(ABC):
'stoploss': 'limit',
'stoploss_on_exchange': False,
'stoploss_on_exchange_interval': 60,
'partial_buy': 'limit',
'partial_sell': 'limit',
}
# Optional time in force
@ -123,8 +134,6 @@ 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 = {}
@ -133,6 +142,7 @@ 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:
@ -277,7 +287,7 @@ class IStrategy(ABC):
"""
return self.__class__.__name__
def lock_pair(self, pair: str, until: datetime, reason: str = None) -> None:
def lock_pair(self, pair: str, until: datetime) -> None:
"""
Locks pair until a given timestamp happens.
Locked pairs are not analyzed, and are prevented from opening new trades.
@ -286,9 +296,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.
"""
PairLocks.lock_pair(pair, until, reason)
if pair not in self._pair_locked_until or self._pair_locked_until[pair] < until:
self._pair_locked_until[pair] = until
def unlock_pair(self, pair: str) -> None:
"""
@ -297,7 +307,8 @@ class IStrategy(ABC):
manually from within the strategy, to allow an easy way to unlock pairs.
:param pair: Unlock pair to allow trading again
"""
PairLocks.unlock_pair(pair, datetime.now(timezone.utc))
if pair in self._pair_locked_until:
del self._pair_locked_until[pair]
def is_pair_locked(self, pair: str, candle_date: datetime = None) -> bool:
"""
@ -309,13 +320,15 @@ 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:
# Simple call ...
return PairLocks.is_pair_locked(pair, candle_date)
return self._pair_locked_until[pair] >= datetime.now(timezone.utc)
else:
# Locking should happen until a new candle arrives
lock_time = timeframe_to_next_date(self.timeframe, candle_date)
return PairLocks.is_pair_locked(pair, lock_time)
# lock_time = candle_date + timedelta(minutes=timeframe_to_minutes(self.timeframe))
return self._pair_locked_until[pair] > lock_time
def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
@ -422,18 +435,19 @@ class IStrategy(ABC):
else:
raise StrategyError(f"Dataframe returned from strategy has mismatching {message}.")
def get_signal(self, pair: str, timeframe: str, dataframe: DataFrame) -> Tuple[bool, bool]:
def get_signal(self, pair: str, timeframe: str, dataframe: DataFrame) \
-> Tuple[bool, bool, PartialTradeTuple, PartialTradeTuple]:
"""
Calculates current signal based based on the buy / sell columns of the dataframe.
Calculates current signal based based on the buy / sell / partial_buy / partial_sell columns of the dataframe.
Used by Bot to get the signal to buy or sell
:param pair: pair in format ANT/BTC
:param timeframe: timeframe to use
:param dataframe: Analyzed dataframe to get signal from.
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
:return: (Buy, Sell,partial_buy, partial_sell) A bool-tuple indicating buy/sell signal
"""
if not isinstance(dataframe, DataFrame) or dataframe.empty:
logger.warning(f'Empty candle (OHLCV) data for pair {pair}')
return False, False
return False, False, PartialTradeTuple(False,0), PartialTradeTuple(False,0)
latest_date = dataframe['date'].max()
latest = dataframe.loc[dataframe['date'] == latest_date].iloc[-1]
@ -448,19 +462,22 @@ class IStrategy(ABC):
'Outdated history for pair %s. Last tick is %s minutes old',
pair, int((arrow.utcnow() - latest_date).total_seconds() // 60)
)
return False, False
return False, False, PartialTradeTuple(False,0), PartialTradeTuple(False,0)
(buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1
logger.debug('trigger: %s (pair=%s) buy=%s sell=%s',
latest['date'], pair, str(buy), str(sell))
return buy, sell
(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]
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
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
sell: bool, low: float = None, high: float = None,
force_stoploss: float = 0) -> SellCheckTuple:
sell: bool, partial_buy: PartialTradeTuple, partial_sell: PartialTradeTuple,
low: float = None, high: float = None, force_stoploss: float = 0) -> SellCheckTuple:
"""
This function evaluates if one of the conditions required to trigger a sell
has been reached, which can either be a stop-loss, ROI or sell-signal.
Modified to support partial trades
:param low: Only used during backtesting to simulate stoploss
:param high: Only used during backtesting, to simulate ROI
:param force_stoploss: Externally provided stoploss
@ -510,6 +527,11 @@ class IStrategy(ABC):
f"sell_type=SellType.SELL_SIGNAL")
return SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)
if partial_sell.flag and not partial_buy.flag and not buy and config_ask_strategy.get('use_sell_signal', True):
logger.debug(f"{trade.pair} - Partial Sell signal received. partial_sell_flag=True, "
f"sell_type=SellType.PARTIAL_SELL_SIGNAL")
return SellCheckTuple(sell_flag=True, sell_type=SellType.PARTIAL_SELL_SIGNAL)
# This one is noisy, commented out...
# logger.debug(f"{trade.pair} - No sell signal. sell_flag=False")
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)

View File

@ -10,7 +10,6 @@ import arrow
from freqtrade.exchange import Exchange
from freqtrade.persistence import Trade
logger = logging.getLogger(__name__)
@ -56,6 +55,7 @@ class Wallets:
def _update_dry(self) -> None:
"""
Update from database in dry-run mode
- Modified to support partial trades considering trade.stake_amount as wallet value (Beta)
- Apply apply profits of closed trades on top of stake amount
- Subtract currently tied up stake_amount in open trades
- update balances for currencies currently in trades
@ -65,16 +65,15 @@ class Wallets:
closed_trades = Trade.get_trades(Trade.is_open.is_(False)).all()
open_trades = Trade.get_trades(Trade.is_open.is_(True)).all()
tot_profit = sum([trade.calc_profit() for trade in closed_trades])
tot_in_trades = sum([trade.stake_amount for trade in open_trades])
current_stake = self.start_cap + tot_profit - tot_in_trades
current_stake = self.start_cap + tot_profit
_wallets[self._config['stake_currency']] = Wallet(
self._config['stake_currency'],
current_stake,
0,
current_stake
)
for trade in open_trades:
curr = self._exchange.get_pair_base_currency(trade.pair)
_wallets[curr] = Wallet(
@ -83,8 +82,16 @@ class Wallets:
0,
trade.amount
)
current_stake += trade.stake_amount
_wallets[self._config['stake_currency']] = Wallet(
self._config['stake_currency'],
current_stake,
0,
current_stake
)
self._wallets = _wallets
def _update_live(self) -> None:
balances = self._exchange.get_balances()