Modified files for partial trades operation
This commit is contained in:
parent
684de9c7d0
commit
520a597f83
@ -4,7 +4,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade()
|
|||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime
|
||||||
from math import isclose
|
from math import isclose
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
@ -12,25 +12,24 @@ from typing import Any, Dict, List, Optional
|
|||||||
import arrow
|
import arrow
|
||||||
from cachetools import TTLCache
|
from cachetools import TTLCache
|
||||||
|
|
||||||
from freqtrade import __version__, constants
|
from freqtrade import __version__, constants, persistence
|
||||||
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
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date
|
||||||
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, PairLocks, Trade, cleanup_db, init_db
|
from freqtrade.persistence import Order, Trade
|
||||||
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
|
||||||
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.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||||
from freqtrade.wallets import Wallets
|
from freqtrade.wallets import Wallets
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -58,8 +57,8 @@ class FreqtradeBot:
|
|||||||
# Cache values for 1800 to avoid frequent polling of the exchange for prices
|
# 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
|
# Caching only applies to RPC methods, so prices for open trades are still
|
||||||
# refreshed once every iteration.
|
# refreshed once every iteration.
|
||||||
self._sell_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800)
|
self._sell_rate_cache = TTLCache(maxsize=100, ttl=1800)
|
||||||
self._buy_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800)
|
self._buy_rate_cache = TTLCache(maxsize=100, ttl=1800)
|
||||||
|
|
||||||
self.strategy: IStrategy = StrategyResolver.load_strategy(self.config)
|
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)
|
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)
|
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)
|
||||||
@ -125,7 +122,7 @@ class FreqtradeBot:
|
|||||||
self.check_for_open_trades()
|
self.check_for_open_trades()
|
||||||
|
|
||||||
self.rpc.cleanup()
|
self.rpc.cleanup()
|
||||||
cleanup_db()
|
persistence.cleanup()
|
||||||
|
|
||||||
def startup(self) -> None:
|
def startup(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -347,17 +344,17 @@ 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:
|
||||||
# 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():
|
||||||
if trade.pair in whitelist:
|
if trade.pair in whitelist:
|
||||||
whitelist.remove(trade.pair)
|
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:
|
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:
|
||||||
# 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:
|
||||||
try:
|
try:
|
||||||
@ -553,9 +550,9 @@ class FreqtradeBot:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# running get_signal on historical data fetched
|
# 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)
|
stake_amount = self.get_trade_stake_amount(pair)
|
||||||
if not stake_amount:
|
if not stake_amount:
|
||||||
logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.")
|
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
|
if ((bid_check_dom.get('enabled', False)) and
|
||||||
(bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
|
(bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
|
||||||
if self._check_depth_of_market_buy(pair, bid_check_dom):
|
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}.')
|
logger.info(f'Executing Buy for {pair}.')
|
||||||
return self.execute_buy(pair, stake_amount)
|
return self.execute_buy(pair, stake_amount)
|
||||||
else:
|
else:
|
||||||
return False
|
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}')
|
logger.info(f'Executing Buy for {pair}')
|
||||||
return self.execute_buy(pair, stake_amount)
|
return self.execute_buy(pair, stake_amount)
|
||||||
else:
|
else:
|
||||||
@ -704,6 +709,122 @@ class FreqtradeBot:
|
|||||||
|
|
||||||
return True
|
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:
|
def _notify_buy(self, trade: Trade, order_type: str) -> None:
|
||||||
"""
|
"""
|
||||||
Sends rpc notification when a buy occured.
|
Sends rpc notification when a buy occured.
|
||||||
@ -834,7 +955,8 @@ class FreqtradeBot:
|
|||||||
|
|
||||||
logger.debug('Handling %s ...', trade)
|
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', {})
|
config_ask_strategy = self.config.get('ask_strategy', {})
|
||||||
|
|
||||||
@ -843,7 +965,8 @@ class FreqtradeBot:
|
|||||||
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
|
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
|
||||||
self.strategy.timeframe)
|
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):
|
if config_ask_strategy.get('use_order_book', False):
|
||||||
order_book_min = config_ask_strategy.get('order_book_min', 1)
|
order_book_min = config_ask_strategy.get('order_book_min', 1)
|
||||||
@ -868,13 +991,13 @@ class FreqtradeBot:
|
|||||||
# resulting in outdated RPC messages
|
# resulting in outdated RPC messages
|
||||||
self._sell_rate_cache[trade.pair] = sell_rate
|
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
|
return True
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.debug('checking sell')
|
logger.debug('checking sell')
|
||||||
sell_rate = self.get_sell_rate(trade.pair, True)
|
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
|
return True
|
||||||
|
|
||||||
logger.debug('Found no sell signal for %s.', trade)
|
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,
|
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, datetime.now(timezone.utc),
|
self.strategy.lock_pair(trade.pair,
|
||||||
reason='Auto lock')
|
timeframe_to_next_date(self.config['timeframe']))
|
||||||
self._notify_sell(trade, "stoploss")
|
self._notify_sell(trade, "stoploss")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -1004,19 +1127,25 @@ class FreqtradeBot:
|
|||||||
f"for pair {trade.pair}.")
|
f"for pair {trade.pair}.")
|
||||||
|
|
||||||
def _check_and_execute_sell(self, trade: Trade, sell_rate: float,
|
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
|
Check and execute sell
|
||||||
"""
|
"""
|
||||||
should_sell = self.strategy.should_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
|
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
|
||||||
)
|
)
|
||||||
|
|
||||||
if should_sell.sell_flag:
|
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}')
|
logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}')
|
||||||
self.execute_sell(trade, sell_rate, should_sell.sell_type)
|
self.execute_sell(trade, sell_rate, should_sell.sell_type)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _check_timed_out(self, side: str, order: dict) -> bool:
|
def _check_timed_out(self, side: str, order: dict) -> bool:
|
||||||
@ -1266,8 +1395,79 @@ 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, datetime.now(timezone.utc),
|
self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe']))
|
||||||
reason='Auto lock')
|
|
||||||
|
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)
|
self._notify_sell(trade, order_type)
|
||||||
|
|
||||||
@ -1393,7 +1593,7 @@ class FreqtradeBot:
|
|||||||
abs_tol=constants.MATH_CLOSE_PREC):
|
abs_tol=constants.MATH_CLOSE_PREC):
|
||||||
order['amount'] = new_amount
|
order['amount'] = new_amount
|
||||||
order.pop('filled', None)
|
order.pop('filled', None)
|
||||||
trade.recalc_open_trade_price()
|
|
||||||
except DependencyException as exception:
|
except DependencyException as exception:
|
||||||
logger.warning("Could not update trade amount: %s", exception)
|
logger.warning("Could not update trade amount: %s", exception)
|
||||||
|
|
||||||
@ -1404,7 +1604,7 @@ class FreqtradeBot:
|
|||||||
trade.update(order)
|
trade.update(order)
|
||||||
|
|
||||||
# Updating wallets when order is closed
|
# Updating wallets when order is closed
|
||||||
if not trade.is_open:
|
if order['status'] == 'closed':
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -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, String,
|
from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer,
|
||||||
create_engine, desc, func, inspect)
|
String, 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,20 +17,17 @@ 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
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
_DECL_BASE: Any = declarative_base()
|
_DECL_BASE: Any = declarative_base()
|
||||||
_SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
|
_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,
|
Initializes this module with the given config,
|
||||||
registers all known command handlers
|
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
|
# 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)
|
||||||
@ -76,7 +70,7 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None:
|
|||||||
clean_dry_run_db()
|
clean_dry_run_db()
|
||||||
|
|
||||||
|
|
||||||
def cleanup_db() -> None:
|
def cleanup() -> None:
|
||||||
"""
|
"""
|
||||||
Flushes all pending operations to disk.
|
Flushes all pending operations to disk.
|
||||||
:return: None
|
:return: None
|
||||||
@ -130,6 +124,8 @@ 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_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)
|
||||||
@ -156,6 +152,7 @@ class Order(_DECL_BASE):
|
|||||||
self.filled = order.get('filled', self.filled)
|
self.filled = order.get('filled', self.filled)
|
||||||
self.remaining = order.get('remaining', self.remaining)
|
self.remaining = order.get('remaining', self.remaining)
|
||||||
self.cost = order.get('cost', self.cost)
|
self.cost = order.get('cost', self.cost)
|
||||||
|
self.fee = order.get('fee', self.fee)
|
||||||
if 'timestamp' in order and order['timestamp'] is not None:
|
if 'timestamp' in order and order['timestamp'] is not None:
|
||||||
self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc)
|
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
|
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:
|
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}.")
|
logger.warning(f"Did not find order for {order['id']}.")
|
||||||
|
|
||||||
@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':
|
||||||
@ -255,7 +252,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(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}, '
|
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})')
|
||||||
@ -281,7 +278,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(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_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,
|
||||||
@ -289,7 +286,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(DATETIME_PRINT_FORMAT)
|
'close_date': (self.close_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
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,
|
||||||
@ -305,7 +302,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(DATETIME_PRINT_FORMAT)
|
'stoploss_last_update': (self.stoploss_last_update.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
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,
|
||||||
@ -382,6 +379,10 @@ class Trade(_DECL_BASE):
|
|||||||
return
|
return
|
||||||
|
|
||||||
logger.info('Updating trade (id=%s) ...', self.id)
|
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':
|
if order_type in ('market', 'limit') and order['side'] == 'buy':
|
||||||
# Update open rate and actual amount
|
# Update open rate and actual amount
|
||||||
@ -403,7 +404,45 @@ class Trade(_DECL_BASE):
|
|||||||
self.close(order['average'])
|
self.close(order['average'])
|
||||||
else:
|
else:
|
||||||
raise ValueError(f'Unknown order type: {order_type}')
|
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:
|
def close(self, rate: float) -> None:
|
||||||
"""
|
"""
|
||||||
@ -422,6 +461,17 @@ class Trade(_DECL_BASE):
|
|||||||
self
|
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],
|
def update_fee(self, fee_cost: float, fee_currency: Optional[str], fee_rate: Optional[float],
|
||||||
side: str) -> None:
|
side: str) -> None:
|
||||||
"""
|
"""
|
||||||
@ -499,6 +549,8 @@ class Trade(_DECL_BASE):
|
|||||||
fee: Optional[float] = None) -> float:
|
fee: Optional[float] = None) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate the absolute profit in stake currency between Close and Open trade
|
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).
|
:param fee: fee to use on the close rate (optional).
|
||||||
If rate is not set self.fee will be used
|
If rate is not set self.fee will be used
|
||||||
:param rate: close rate to compare with (optional).
|
:param rate: close rate to compare with (optional).
|
||||||
@ -509,13 +561,15 @@ class Trade(_DECL_BASE):
|
|||||||
rate=(rate or self.close_rate),
|
rate=(rate or self.close_rate),
|
||||||
fee=(fee or self.fee_close)
|
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}")
|
return float(f"{profit:.8f}")
|
||||||
|
|
||||||
def calc_profit_ratio(self, rate: Optional[float] = None,
|
def calc_profit_ratio(self, rate: Optional[float] = None,
|
||||||
fee: Optional[float] = None) -> float:
|
fee: Optional[float] = None) -> float:
|
||||||
"""
|
"""
|
||||||
Calculates the profit as ratio (including fee).
|
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).
|
:param rate: rate to compare with (optional).
|
||||||
If rate is not set self.close_rate will be used
|
If rate is not set self.close_rate will be used
|
||||||
:param fee: fee to use on the close rate (optional).
|
:param fee: fee to use on the close rate (optional).
|
||||||
@ -525,7 +579,7 @@ class Trade(_DECL_BASE):
|
|||||||
rate=(rate or self.close_rate),
|
rate=(rate or self.close_rate),
|
||||||
fee=(fee or self.fee_close)
|
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}")
|
return float(f"{profit_ratio:.8f}")
|
||||||
|
|
||||||
def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]:
|
def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]:
|
||||||
@ -543,6 +597,32 @@ class Trade(_DECL_BASE):
|
|||||||
else:
|
else:
|
||||||
return None
|
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
|
@staticmethod
|
||||||
def get_trades(trade_filter=None) -> Query:
|
def get_trades(trade_filter=None) -> Query:
|
||||||
"""
|
"""
|
||||||
@ -600,8 +680,8 @@ class Trade(_DECL_BASE):
|
|||||||
Calculates total invested amount in open trades
|
Calculates total invested amount in open trades
|
||||||
in stake currency
|
in stake currency
|
||||||
"""
|
"""
|
||||||
total_open_stake_amount = Trade.session.query(func.sum(Trade.stake_amount))\
|
total_open_stake_amount = Trade.session.query(func.sum(Trade.stake_amount)) \
|
||||||
.filter(Trade.is_open.is_(True))\
|
.filter(Trade.is_open.is_(True)) \
|
||||||
.scalar()
|
.scalar()
|
||||||
return total_open_stake_amount or 0
|
return total_open_stake_amount or 0
|
||||||
|
|
||||||
@ -614,7 +694,7 @@ class Trade(_DECL_BASE):
|
|||||||
Trade.pair,
|
Trade.pair,
|
||||||
func.sum(Trade.close_profit).label('profit_sum'),
|
func.sum(Trade.close_profit).label('profit_sum'),
|
||||||
func.count(Trade.pair).label('count')
|
func.count(Trade.pair).label('count')
|
||||||
).filter(Trade.is_open.is_(False))\
|
).filter(Trade.is_open.is_(False)) \
|
||||||
.group_by(Trade.pair) \
|
.group_by(Trade.pair) \
|
||||||
.order_by(desc('profit_sum')) \
|
.order_by(desc('profit_sum')) \
|
||||||
.all()
|
.all()
|
||||||
@ -658,56 +738,3 @@ class Trade(_DECL_BASE):
|
|||||||
trade.stop_loss = None
|
trade.stop_loss = None
|
||||||
trade.adjust_stop_loss(trade.open_rate, desired_stoploss)
|
trade.adjust_stop_loss(trade.open_rate, desired_stoploss)
|
||||||
logger.info(f"New stoploss: {trade.stop_loss}.")
|
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,
|
|
||||||
}
|
|
||||||
|
@ -17,11 +17,10 @@ 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 PairLocks, Trade
|
from freqtrade.persistence import 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
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -31,6 +30,8 @@ class SignalType(Enum):
|
|||||||
"""
|
"""
|
||||||
BUY = "buy"
|
BUY = "buy"
|
||||||
SELL = "sell"
|
SELL = "sell"
|
||||||
|
PARTIAL_BUY = "partial_buy"
|
||||||
|
PARTIAL_SELL = "partial_sell"
|
||||||
|
|
||||||
|
|
||||||
class SellType(Enum):
|
class SellType(Enum):
|
||||||
@ -42,6 +43,7 @@ class SellType(Enum):
|
|||||||
STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange"
|
STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange"
|
||||||
TRAILING_STOP_LOSS = "trailing_stop_loss"
|
TRAILING_STOP_LOSS = "trailing_stop_loss"
|
||||||
SELL_SIGNAL = "sell_signal"
|
SELL_SIGNAL = "sell_signal"
|
||||||
|
PARTIAL_SELL_SIGNAL = "partial_sell_signal"
|
||||||
FORCE_SELL = "force_sell"
|
FORCE_SELL = "force_sell"
|
||||||
EMERGENCY_SELL = "emergency_sell"
|
EMERGENCY_SELL = "emergency_sell"
|
||||||
NONE = ""
|
NONE = ""
|
||||||
@ -58,6 +60,13 @@ class SellCheckTuple(NamedTuple):
|
|||||||
sell_flag: bool
|
sell_flag: bool
|
||||||
sell_type: SellType
|
sell_type: SellType
|
||||||
|
|
||||||
|
class PartialTradeTuple(NamedTuple):
|
||||||
|
"""
|
||||||
|
NamedTuple for partial trade + amount
|
||||||
|
"""
|
||||||
|
flag: bool
|
||||||
|
amount: float
|
||||||
|
|
||||||
|
|
||||||
class IStrategy(ABC):
|
class IStrategy(ABC):
|
||||||
"""
|
"""
|
||||||
@ -101,6 +110,8 @@ class IStrategy(ABC):
|
|||||||
'stoploss': 'limit',
|
'stoploss': 'limit',
|
||||||
'stoploss_on_exchange': False,
|
'stoploss_on_exchange': False,
|
||||||
'stoploss_on_exchange_interval': 60,
|
'stoploss_on_exchange_interval': 60,
|
||||||
|
'partial_buy': 'limit',
|
||||||
|
'partial_sell': 'limit',
|
||||||
}
|
}
|
||||||
|
|
||||||
# Optional time in force
|
# Optional time in force
|
||||||
@ -123,8 +134,6 @@ 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 = {}
|
||||||
@ -133,6 +142,7 @@ 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:
|
||||||
@ -277,7 +287,7 @@ class IStrategy(ABC):
|
|||||||
"""
|
"""
|
||||||
return self.__class__.__name__
|
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.
|
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.
|
||||||
@ -286,9 +296,9 @@ class IStrategy(ABC):
|
|||||||
:param pair: Pair to lock
|
:param pair: Pair to lock
|
||||||
: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)`
|
||||||
: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:
|
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.
|
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
|
||||||
"""
|
"""
|
||||||
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:
|
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
|
: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:
|
||||||
# Simple call ...
|
return self._pair_locked_until[pair] >= datetime.now(timezone.utc)
|
||||||
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)
|
||||||
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:
|
def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
@ -422,18 +435,19 @@ class IStrategy(ABC):
|
|||||||
else:
|
else:
|
||||||
raise StrategyError(f"Dataframe returned from strategy has mismatching {message}.")
|
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
|
Used by Bot to get the signal to buy or sell
|
||||||
:param pair: pair in format ANT/BTC
|
:param pair: pair in format ANT/BTC
|
||||||
:param timeframe: timeframe to use
|
:param timeframe: timeframe to use
|
||||||
:param dataframe: Analyzed dataframe to get signal from.
|
: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:
|
if not isinstance(dataframe, DataFrame) or dataframe.empty:
|
||||||
logger.warning(f'Empty candle (OHLCV) data for pair {pair}')
|
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_date = dataframe['date'].max()
|
||||||
latest = dataframe.loc[dataframe['date'] == latest_date].iloc[-1]
|
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',
|
'Outdated history for pair %s. Last tick is %s minutes old',
|
||||||
pair, int((arrow.utcnow() - latest_date).total_seconds() // 60)
|
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
|
(buy, sell, partial_buy, partial_sell) = \
|
||||||
logger.debug('trigger: %s (pair=%s) buy=%s sell=%s',
|
latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1,\
|
||||||
latest['date'], pair, str(buy), str(sell))
|
latest[SignalType.PARTIAL_BUY] == 1, latest[SignalType.PARTIAL_SELL == 1]
|
||||||
return buy, sell
|
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,
|
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
|
||||||
sell: bool, low: float = None, high: float = None,
|
sell: bool, partial_buy: PartialTradeTuple, partial_sell: PartialTradeTuple,
|
||||||
force_stoploss: float = 0) -> SellCheckTuple:
|
low: float = None, high: float = None, force_stoploss: float = 0) -> SellCheckTuple:
|
||||||
"""
|
"""
|
||||||
This function evaluates if one of the conditions required to trigger a sell
|
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.
|
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 low: Only used during backtesting to simulate stoploss
|
||||||
:param high: Only used during backtesting, to simulate ROI
|
:param high: Only used during backtesting, to simulate ROI
|
||||||
:param force_stoploss: Externally provided stoploss
|
:param force_stoploss: Externally provided stoploss
|
||||||
@ -510,6 +527,11 @@ class IStrategy(ABC):
|
|||||||
f"sell_type=SellType.SELL_SIGNAL")
|
f"sell_type=SellType.SELL_SIGNAL")
|
||||||
return SellCheckTuple(sell_flag=True, 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...
|
# This one is noisy, commented out...
|
||||||
# logger.debug(f"{trade.pair} - No sell signal. sell_flag=False")
|
# logger.debug(f"{trade.pair} - No sell signal. sell_flag=False")
|
||||||
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
||||||
|
@ -10,7 +10,6 @@ import arrow
|
|||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -56,6 +55,7 @@ class Wallets:
|
|||||||
def _update_dry(self) -> None:
|
def _update_dry(self) -> None:
|
||||||
"""
|
"""
|
||||||
Update from database in dry-run mode
|
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
|
- Apply apply profits of closed trades on top of stake amount
|
||||||
- Subtract currently tied up stake_amount in open trades
|
- Subtract currently tied up stake_amount in open trades
|
||||||
- update balances for currencies currently in 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()
|
closed_trades = Trade.get_trades(Trade.is_open.is_(False)).all()
|
||||||
open_trades = Trade.get_trades(Trade.is_open.is_(True)).all()
|
open_trades = Trade.get_trades(Trade.is_open.is_(True)).all()
|
||||||
tot_profit = sum([trade.calc_profit() for trade in closed_trades])
|
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(
|
_wallets[self._config['stake_currency']] = Wallet(
|
||||||
self._config['stake_currency'],
|
self._config['stake_currency'],
|
||||||
current_stake,
|
current_stake,
|
||||||
0,
|
0,
|
||||||
current_stake
|
current_stake
|
||||||
)
|
)
|
||||||
|
|
||||||
for trade in open_trades:
|
for trade in open_trades:
|
||||||
curr = self._exchange.get_pair_base_currency(trade.pair)
|
curr = self._exchange.get_pair_base_currency(trade.pair)
|
||||||
_wallets[curr] = Wallet(
|
_wallets[curr] = Wallet(
|
||||||
@ -83,8 +82,16 @@ class Wallets:
|
|||||||
0,
|
0,
|
||||||
trade.amount
|
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
|
self._wallets = _wallets
|
||||||
|
|
||||||
|
|
||||||
def _update_live(self) -> None:
|
def _update_live(self) -> None:
|
||||||
balances = self._exchange.get_balances()
|
balances = self._exchange.get_balances()
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user