Merge branch 'feat/short' into funding-fee-backtesting

This commit is contained in:
Sam Germain
2021-10-20 08:21:12 -06:00
48 changed files with 1765 additions and 907 deletions

View File

@@ -31,7 +31,8 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
"epochs", "spaces", "print_all",
"print_colorized", "print_json", "hyperopt_jobs",
"hyperopt_random_state", "hyperopt_min_trades",
"hyperopt_loss", "disableparamexport"]
"hyperopt_loss", "disableparamexport",
"hyperopt_ignore_missing_space"]
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
@@ -62,9 +63,9 @@ ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "d
ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"]
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "timerange",
"download_trades", "exchange", "timeframes", "erase", "dataformat_ohlcv",
"dataformat_trades"]
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "include_inactive",
"timerange", "download_trades", "exchange", "timeframes",
"erase", "dataformat_ohlcv", "dataformat_trades"]
ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
"db_url", "trade_source", "export", "exportfilename",

View File

@@ -355,6 +355,11 @@ AVAILABLE_CLI_OPTIONS = {
type=check_int_positive,
metavar='INT',
),
"include_inactive": Arg(
'--include-inactive-pairs',
help='Also download data from inactive pairs.',
action='store_true',
),
"new_pairs_days": Arg(
'--new-pairs-days',
help='Download data of new pairs for given number of days. Default: `%(default)s`.',
@@ -558,4 +563,10 @@ AVAILABLE_CLI_OPTIONS = {
help='Do not print epoch details header.',
action='store_true',
),
"hyperopt_ignore_missing_space": Arg(
"--ignore-missing-spaces", "--ignore-unparameterized-spaces",
help=("Suppress errors for any requested Hyperopt spaces "
"that do not contain any parameters."),
action="store_true",
),
}

View File

@@ -11,6 +11,7 @@ from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_oh
from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes
from freqtrade.exchange.exchange import market_is_active
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.resolvers import ExchangeResolver
@@ -47,11 +48,13 @@ def start_download_data(args: Dict[str, Any]) -> None:
# Init exchange
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
markets = [p for p, m in exchange.markets.items() if market_is_active(m)
or config.get('include_inactive')]
expanded_pairs = expand_pairlist(config['pairs'], markets)
# Manual validations of relevant settings
if not config['exchange'].get('skip_pair_validation', False):
exchange.validate_pairs(config['pairs'])
expanded_pairs = expand_pairlist(config['pairs'], list(exchange.markets))
exchange.validate_pairs(expanded_pairs)
logger.info(f"About to download pairs: {expanded_pairs}, "
f"intervals: {config['timeframes']} to {config['datadir']}")

View File

@@ -369,6 +369,9 @@ class Configuration:
self._args_to_config(config, argname='hyperopt_show_no_header',
logstring='Parameter --no-header detected: {}')
self._args_to_config(config, argname="hyperopt_ignore_missing_space",
logstring="Paramter --ignore-missing-space detected: {}")
def _process_plot_options(self, config: Dict[str, Any]) -> None:
self._args_to_config(config, argname='pairs',
@@ -404,6 +407,9 @@ class Configuration:
self._args_to_config(config, argname='days',
logstring='Detected --days: {}')
self._args_to_config(config, argname='include_inactive',
logstring='Detected --include-inactive-pairs: {}')
self._args_to_config(config, argname='download_trades',
logstring='Detected --dl-trades: {}')

View File

@@ -39,6 +39,8 @@ DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume']
# Don't modify sequence of DEFAULT_TRADES_COLUMNS
# it has wide consequences for stored trades files
DEFAULT_TRADES_COLUMNS = ['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost']
TRADING_MODES = ['spot', 'margin', 'futures']
COLLATERAL_TYPES = ['cross', 'isolated']
LAST_BT_RESULT_FN = '.last_result.json'
FTHYPT_FILEVERSION = 'fthypt_fileversion'
@@ -146,6 +148,8 @@ CONF_SCHEMA = {
'sell_profit_offset': {'type': 'number'},
'ignore_roi_if_buy_signal': {'type': 'boolean'},
'ignore_buying_expired_candle_after': {'type': 'number'},
'trading_mode': {'type': 'string', 'enum': TRADING_MODES},
'collateral_type': {'type': 'string', 'enum': COLLATERAL_TYPES},
'bot_name': {'type': 'string'},
'unfilledtimeout': {
'type': 'object',
@@ -193,7 +197,7 @@ CONF_SCHEMA = {
'required': ['price_side']
},
'custom_price_max_distance_ratio': {
'type': 'number', 'minimum': 0.0
'type': 'number', 'minimum': 0.0
},
'order_types': {
'type': 'object',

View File

@@ -5,15 +5,21 @@ class RPCMessageType(Enum):
STATUS = 'status'
WARNING = 'warning'
STARTUP = 'startup'
BUY = 'buy'
BUY_FILL = 'buy_fill'
BUY_CANCEL = 'buy_cancel'
SELL = 'sell'
SELL_FILL = 'sell_fill'
SELL_CANCEL = 'sell_cancel'
PROTECTION_TRIGGER = 'protection_trigger'
PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global'
SHORT = 'short'
SHORT_FILL = 'short_fill'
SHORT_CANCEL = 'short_cancel'
def __repr__(self):
return self.value

View File

@@ -16,8 +16,6 @@ API_FETCH_ORDER_RETRY_COUNT = 5
BAD_EXCHANGES = {
"bitmex": "Various reasons.",
"bitstamp": "Does not provide history. "
"Details in https://github.com/freqtrade/freqtrade/issues/1983",
"phemex": "Does not provide history. ",
"poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders.",
}

View File

@@ -805,8 +805,14 @@ class Exchange:
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
self._lev_prep(pair, leverage)
order = self._api.create_order(pair, ordertype, side,
amount, rate_for_order, params)
order = self._api.create_order(
pair,
ordertype,
side,
amount,
rate_for_order,
params
)
self._log_exchange_response('create_order', order)
return order

View File

@@ -7,7 +7,7 @@ import traceback
from datetime import datetime, time, timezone
from math import isclose
from threading import Lock
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Tuple
import arrow
from schedule import Scheduler
@@ -17,7 +17,8 @@ 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.enums import RPCMessageType, SellType, State, TradingMode
from freqtrade.enums import (Collateral, RPCMessageType, SellType, SignalDirection, State,
TradingMode)
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
InvalidOrderException, PricingError)
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
@@ -101,14 +102,19 @@ class FreqtradeBot(LoggingMixin):
initial_state = self.config.get('initial_state')
self.state = State[initial_state.upper()] if initial_state else State.STOPPED
# Protect sell-logic from forcesell and vice versa
# Protect exit-logic from forcesell and vice versa
self._exit_lock = Lock()
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
self.trading_mode: TradingMode = TradingMode.SPOT
self.collateral_type: Optional[Collateral] = None
if 'trading_mode' in self.config:
self.trading_mode = TradingMode(self.config['trading_mode'])
else:
self.trading_mode = TradingMode.SPOT
if 'collateral_type' in self.config:
self.collateral_type = Collateral(self.config['collateral_type'])
self._schedule = Scheduler()
if self.trading_mode == TradingMode.FUTURES:
@@ -194,7 +200,7 @@ class FreqtradeBot(LoggingMixin):
# Protect from collisions with forceexit.
# Without this, freqtrade my try to recreate stoploss_on_exchange orders
# while selling is in process, since telegram messages arrive in an different thread.
# while exiting is in process, since telegram messages arrive in an different thread.
with self._exit_lock:
trades = Trade.get_open_trades()
# First process current opened trades (positions)
@@ -305,21 +311,26 @@ class FreqtradeBot(LoggingMixin):
trades: List[Trade] = Trade.get_closed_trades_without_assigned_fees()
for trade in trades:
if not trade.is_open and not trade.fee_updated('sell'):
if not trade.is_open and not trade.fee_updated(trade.exit_side):
# Get sell fee
order = trade.select_order('sell', False)
order = trade.select_order(trade.exit_side, False)
if order:
logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.")
logger.info(
f"Updating {trade.exit_side}-fee on trade {trade}"
f"for order {order.order_id}."
)
self.update_trade_state(trade, order.order_id,
stoploss_order=order.ft_order_side == 'stoploss')
trades: List[Trade] = Trade.get_open_trades_without_assigned_fees()
for trade in trades:
if trade.is_open and not trade.fee_updated('buy'):
order = trade.select_order('buy', False)
if trade.is_open and not trade.fee_updated(trade.enter_side):
order = trade.select_order(trade.enter_side, False)
if order:
logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.")
logger.info(
f"Updating {trade.enter_side}-fee on trade {trade}"
f"for order {order.order_id}."
)
self.update_trade_state(trade, order.order_id)
def handle_insufficient_funds(self, trade: Trade):
@@ -327,8 +338,8 @@ class FreqtradeBot(LoggingMixin):
Determine if we ever opened a exiting order for this trade.
If not, try update entering fees - otherwise "refind" the open order we obviously lost.
"""
sell_order = trade.select_order('sell', None)
if sell_order:
exit_order = trade.select_order(trade.exit_side, None)
if exit_order:
self.refind_lost_order(trade)
else:
self.reupdate_enter_order_fees(trade)
@@ -338,10 +349,11 @@ class FreqtradeBot(LoggingMixin):
Get buy order from database, and try to reupdate.
Handles trades where the initial fee-update did not work.
"""
logger.info(f"Trying to reupdate buy fees for {trade}")
order = trade.select_order('buy', False)
logger.info(f"Trying to reupdate {trade.enter_side} fees for {trade}")
order = trade.select_order(trade.enter_side, False)
if order:
logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.")
logger.info(
f"Updating {trade.enter_side}-fee on trade {trade} for order {order.order_id}.")
self.update_trade_state(trade, order.order_id)
def refind_lost_order(self, trade):
@@ -357,7 +369,7 @@ class FreqtradeBot(LoggingMixin):
if not order.ft_is_open:
logger.debug(f"Order {order} is no longer open.")
continue
if order.ft_order_side == 'buy':
if order.ft_order_side == trade.enter_side:
# Skip buy side - this is handled by reupdate_enter_order_fees
continue
try:
@@ -367,7 +379,7 @@ class FreqtradeBot(LoggingMixin):
if fo and fo['status'] == 'open':
# Assume this as the open stoploss order
trade.stoploss_order_id = order.order_id
elif order.ft_order_side == 'sell':
elif order.ft_order_side == trade.exit_side:
if fo and fo['status'] == 'open':
# Assume this as the open order
trade.open_order_id = order.order_id
@@ -456,7 +468,9 @@ class FreqtradeBot(LoggingMixin):
# running get_signal on historical data fetched
(signal, enter_tag) = self.strategy.get_entry_signal(
pair, self.strategy.timeframe, analyzed_df
pair,
self.strategy.timeframe,
analyzed_df
)
if signal:
@@ -465,19 +479,31 @@ class FreqtradeBot(LoggingMixin):
bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {})
if ((bid_check_dom.get('enabled', False)) and
(bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
# TODO-lev: Does the below need to be adjusted for shorts?
if self._check_depth_of_market_buy(pair, bid_check_dom):
# TODO-lev: pass in "enter" as side.
return self.execute_entry(pair, stake_amount, enter_tag=enter_tag)
if self._check_depth_of_market(pair, bid_check_dom, side=signal):
return self.execute_entry(
pair,
stake_amount,
enter_tag=enter_tag,
is_short=(signal == SignalDirection.SHORT)
)
else:
return False
return self.execute_entry(pair, stake_amount, enter_tag=enter_tag)
return self.execute_entry(
pair,
stake_amount,
enter_tag=enter_tag,
is_short=(signal == SignalDirection.SHORT)
)
else:
return False
def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool:
def _check_depth_of_market(
self,
pair: str,
conf: Dict,
side: SignalDirection
) -> bool:
"""
Checks depth of market before executing a buy
"""
@@ -487,9 +513,17 @@ class FreqtradeBot(LoggingMixin):
order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks'])
order_book_bids = order_book_data_frame['b_size'].sum()
order_book_asks = order_book_data_frame['a_size'].sum()
bids_ask_delta = order_book_bids / order_book_asks
enter_side = order_book_bids if side == SignalDirection.LONG else order_book_asks
exit_side = order_book_asks if side == SignalDirection.LONG else order_book_bids
bids_ask_delta = enter_side / exit_side
bids = f"Bids: {order_book_bids}"
asks = f"Asks: {order_book_asks}"
delta = f"Delta: {bids_ask_delta}"
logger.info(
f"Bids: {order_book_bids}, Asks: {order_book_asks}, Delta: {bids_ask_delta}, "
f"{bids}, {asks}, {delta}, Direction: {side.value}"
f"Bid Price: {order_book['bids'][0][0]}, Ask Price: {order_book['asks'][0][0]}, "
f"Immediate Bid Quantity: {order_book['bids'][0][1]}, "
f"Immediate Ask Quantity: {order_book['asks'][0][1]}."
@@ -501,21 +535,65 @@ class FreqtradeBot(LoggingMixin):
logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
return False
def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None,
forcebuy: bool = False, enter_tag: Optional[str] = None) -> bool:
def leverage_prep(
self,
pair: str,
open_rate: float,
amount: float,
leverage: float,
is_short: bool
) -> Tuple[float, Optional[float]]:
interest_rate = 0.0
isolated_liq = None
# TODO-lev: Uncomment once liq and interest merged in
# if TradingMode == TradingMode.MARGIN:
# interest_rate = self.exchange.get_interest_rate(
# pair=pair,
# open_rate=open_rate,
# is_short=is_short
# )
# if self.collateral_type == Collateral.ISOLATED:
# isolated_liq = liquidation_price(
# exchange_name=self.exchange.name,
# trading_mode=self.trading_mode,
# open_rate=open_rate,
# amount=amount,
# leverage=leverage,
# is_short=is_short
# )
return interest_rate, isolated_liq
def execute_entry(
self,
pair: str,
stake_amount: float,
price: Optional[float] = None,
forcebuy: bool = False,
leverage: float = 1.0,
is_short: bool = False,
enter_tag: Optional[str] = None
) -> bool:
"""
Executes a limit buy for the given pair
:param pair: pair for which we want to create a LIMIT_BUY
:param stake_amount: amount of stake-currency for the pair
:param leverage: amount of leverage applied to this trade
:return: True if a buy order is created, false if it fails.
"""
time_in_force = self.strategy.order_time_in_force['buy']
[side, name] = ['sell', 'Short'] if is_short else ['buy', 'Long']
if price:
enter_limit_requested = price
else:
# Calculate price
proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side="buy")
proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side=side)
custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
default_retval=proposed_enter_rate)(
pair=pair, current_time=datetime.now(timezone.utc),
@@ -524,10 +602,14 @@ class FreqtradeBot(LoggingMixin):
enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate)
if not enter_limit_requested:
raise PricingError('Could not determine buy price.')
raise PricingError(f'Could not determine {side} price.')
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, enter_limit_requested,
self.strategy.stoploss)
min_stake_amount = self.exchange.get_min_pair_stake_amount(
pair,
enter_limit_requested,
self.strategy.stoploss,
leverage=leverage
)
if not self.edge:
max_stake_amount = self.wallets.get_available_stake_amount()
@@ -543,10 +625,12 @@ class FreqtradeBot(LoggingMixin):
if not stake_amount:
return False
logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: "
f"{stake_amount} ...")
logger.info(
f"{name} signal found: about create a new trade for {pair} with stake_amount: "
f"{stake_amount} ..."
)
amount = stake_amount / enter_limit_requested
amount = (stake_amount / enter_limit_requested) * leverage
order_type = self.strategy.order_types['buy']
if forcebuy:
# Forcebuy can define a different ordertype
@@ -558,15 +642,21 @@ class FreqtradeBot(LoggingMixin):
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
time_in_force=time_in_force, current_time=datetime.now(timezone.utc),
side='long'
side='short' if is_short else 'long'
):
logger.info(f"User requested abortion of buying {pair}")
return False
amount = self.exchange.amount_to_precision(pair, amount)
order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy",
amount=amount, rate=enter_limit_requested,
time_in_force=time_in_force)
order_obj = Order.parse_from_ccxt_object(order, pair, 'buy')
order = self.exchange.create_order(
pair=pair,
ordertype=order_type,
side=side,
amount=amount,
rate=enter_limit_requested,
time_in_force=time_in_force,
leverage=leverage
)
order_obj = Order.parse_from_ccxt_object(order, pair, side)
order_id = order['id']
order_status = order.get('status', None)
@@ -579,17 +669,17 @@ class FreqtradeBot(LoggingMixin):
# 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.'
logger.warning('%s %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)
name, 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.'
logger.warning('%s %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,
name, order_tif, order_type, pair, order_status, self.exchange.name,
order['filled'], order['amount'], order['remaining']
)
stake_amount = order['cost']
@@ -602,6 +692,14 @@ class FreqtradeBot(LoggingMixin):
amount = safe_value_fallback(order, 'filled', 'amount')
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price')
interest_rate, isolated_liq = self.leverage_prep(
leverage=leverage,
pair=pair,
amount=amount,
open_rate=enter_limit_filled_price,
is_short=is_short
)
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
open_date = datetime.now(timezone.utc)
@@ -627,6 +725,10 @@ class FreqtradeBot(LoggingMixin):
# TODO-lev: compatibility layer for buy_tag (!)
buy_tag=enter_tag,
timeframe=timeframe_to_minutes(self.config['timeframe']),
leverage=leverage,
is_short=is_short,
interest_rate=interest_rate,
isolated_liq=isolated_liq,
trading_mode=self.trading_mode,
funding_fees=funding_fees
)
@@ -652,7 +754,7 @@ class FreqtradeBot(LoggingMixin):
"""
msg = {
'trade_id': trade.id,
'type': RPCMessageType.BUY,
'type': RPCMessageType.SHORT if trade.is_short else RPCMessageType.BUY,
'buy_tag': trade.buy_tag,
'exchange': self.exchange.name.capitalize(),
'pair': trade.pair,
@@ -673,11 +775,11 @@ class FreqtradeBot(LoggingMixin):
"""
Sends rpc notification when a entry order cancel occurred.
"""
current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="buy")
current_rate = self.exchange.get_rate(trade.pair, refresh=False, side=trade.enter_side)
msg_type = RPCMessageType.SHORT_CANCEL if trade.is_short else RPCMessageType.BUY_CANCEL
msg = {
'trade_id': trade.id,
'type': RPCMessageType.BUY_CANCEL,
'type': msg_type,
'buy_tag': trade.buy_tag,
'exchange': self.exchange.name.capitalize(),
'pair': trade.pair,
@@ -696,9 +798,10 @@ class FreqtradeBot(LoggingMixin):
self.rpc.send_msg(msg)
def _notify_enter_fill(self, trade: Trade) -> None:
msg_type = RPCMessageType.SHORT_FILL if trade.is_short else RPCMessageType.BUY_FILL
msg = {
'trade_id': trade.id,
'type': RPCMessageType.BUY_FILL,
'type': msg_type,
'buy_tag': trade.buy_tag,
'exchange': self.exchange.name.capitalize(),
'pair': trade.pair,
@@ -752,6 +855,7 @@ class FreqtradeBot(LoggingMixin):
logger.debug('Handling %s ...', trade)
(enter, exit_) = (False, False)
exit_signal_type = "exit_short" if trade.is_short else "exit_long"
# TODO-lev: change to use_exit_signal, ignore_roi_if_enter_signal
if (self.config.get('use_sell_signal', True) or
@@ -762,15 +866,16 @@ class FreqtradeBot(LoggingMixin):
(enter, exit_) = self.strategy.get_exit_signal(
trade.pair,
self.strategy.timeframe,
analyzed_df, is_short=trade.is_short
analyzed_df,
is_short=trade.is_short
)
# TODO-lev: side should depend on trade side.
exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell")
logger.debug('checking exit')
exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side=trade.exit_side)
if self._check_and_execute_exit(trade, exit_rate, enter, exit_):
return True
logger.debug('Found no sell signal for %s.', trade)
logger.debug(f'Found no {exit_signal_type} signal for %s.', trade)
return False
def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool:
@@ -855,7 +960,10 @@ class FreqtradeBot(LoggingMixin):
# If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange
if not stoploss_order:
stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss
stop_price = trade.open_rate * (1 + stoploss)
if trade.is_short:
stop_price = trade.open_rate * (1 - stoploss)
else:
stop_price = trade.open_rate * (1 + stoploss)
if self.create_stoploss_order(trade=trade, stop_price=stop_price):
trade.stoploss_last_update = datetime.utcnow()
@@ -880,11 +988,11 @@ class FreqtradeBot(LoggingMixin):
# if trailing stoploss is enabled we check if stoploss value has changed
# in which case we cancel stoploss order and put another one with new
# value immediately
self.handle_trailing_stoploss_on_exchange(trade, stoploss_order, side=trade.exit_side)
self.handle_trailing_stoploss_on_exchange(trade, stoploss_order)
return False
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict, side: str) -> None:
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> None:
"""
Check to see if stoploss on exchange should be updated
in case of trailing stoploss on exchange
@@ -892,7 +1000,7 @@ class FreqtradeBot(LoggingMixin):
:param order: Current on exchange stoploss order
:return: None
"""
if self.exchange.stoploss_adjust(trade.stop_loss, order, side):
if self.exchange.stoploss_adjust(trade.stop_loss, order, side=trade.exit_side):
# we check if the update is necessary
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:
@@ -918,7 +1026,11 @@ class FreqtradeBot(LoggingMixin):
Check and execute trade exit
"""
should_exit: SellCheckTuple = self.strategy.should_exit(
trade, exit_rate, datetime.now(timezone.utc), enter=enter, exit_=exit_,
trade,
exit_rate,
datetime.now(timezone.utc),
enter=enter,
exit_=exit_,
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
)
@@ -959,24 +1071,23 @@ class FreqtradeBot(LoggingMixin):
continue
fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order)
is_entering = order['side'] == trade.enter_side
not_closed = order['status'] == 'open' or fully_cancelled
side = trade.enter_side if is_entering else trade.exit_side
timed_out = self._check_timed_out(side, order)
time_method = 'check_sell_timeout' if order['side'] == 'sell' else 'check_buy_timeout'
if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and (
fully_cancelled
or self._check_timed_out('buy', order)
or strategy_safe_wrapper(self.strategy.check_buy_timeout,
default_retval=False)(pair=trade.pair,
trade=trade,
order=order))):
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and (
fully_cancelled
or self._check_timed_out('sell', order)
or strategy_safe_wrapper(self.strategy.check_sell_timeout,
default_retval=False)(pair=trade.pair,
trade=trade,
order=order))):
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
if not_closed and (fully_cancelled or timed_out or (
strategy_safe_wrapper(getattr(self.strategy, time_method), default_retval=False)(
pair=trade.pair,
trade=trade,
order=order
)
)):
if is_entering:
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
else:
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
def cancel_all_open_orders(self) -> None:
"""
@@ -991,10 +1102,10 @@ class FreqtradeBot(LoggingMixin):
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
continue
if order['side'] == 'buy':
if order['side'] == trade.enter_side:
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
elif order['side'] == 'sell':
elif order['side'] == trade.exit_side:
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
Trade.commit()
@@ -1016,7 +1127,7 @@ class FreqtradeBot(LoggingMixin):
if filled_val > 0 and filled_stake < minstake:
logger.warning(
f"Order {trade.open_order_id} for {trade.pair} not cancelled, "
f"as the filled amount of {filled_val} would result in an unsellable trade.")
f"as the filled amount of {filled_val} would result in an unexitable trade.")
return False
corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
trade.amount)
@@ -1031,12 +1142,16 @@ class FreqtradeBot(LoggingMixin):
corder = order
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
logger.info('Buy order %s for %s.', reason, trade)
side = trade.enter_side.capitalize()
logger.info('%s order %s for %s.', side, reason, trade)
# Using filled to determine the filled amount
filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled')
if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC):
logger.info('Buy order fully cancelled. Removing %s from database.', trade)
logger.info(
'%s order fully cancelled. Removing %s from database.',
side, trade
)
# if trade is not partially completed, just delete the trade
trade.delete()
was_trade_fully_canceled = True
@@ -1054,11 +1169,11 @@ class FreqtradeBot(LoggingMixin):
self.update_trade_state(trade, trade.open_order_id, corder)
trade.open_order_id = None
logger.info('Partial buy order timeout for %s.', trade)
logger.info('Partial %s order timeout for %s.', trade.enter_side, trade)
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
self.wallets.update()
self._notify_enter_cancel(trade, order_type=self.strategy.order_types['buy'],
self._notify_enter_cancel(trade, order_type=self.strategy.order_types[trade.enter_side],
reason=reason)
return was_trade_fully_canceled
@@ -1076,12 +1191,13 @@ class FreqtradeBot(LoggingMixin):
trade.amount)
trade.update_order(co)
except InvalidOrderException:
logger.exception(f"Could not cancel sell order {trade.open_order_id}")
logger.exception(
f"Could not cancel {trade.exit_side} order {trade.open_order_id}")
return 'error cancelling order'
logger.info('Sell order %s for %s.', reason, trade)
logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade)
else:
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
logger.info('Sell order %s for %s.', reason, trade)
logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade)
trade.update_order(order)
trade.close_rate = None
@@ -1098,7 +1214,7 @@ class FreqtradeBot(LoggingMixin):
self.wallets.update()
self._notify_exit_cancel(
trade,
order_type=self.strategy.order_types['sell'],
order_type=self.strategy.order_types[trade.exit_side],
reason=reason
)
return reason
@@ -1129,7 +1245,12 @@ class FreqtradeBot(LoggingMixin):
raise DependencyException(
f"Not enough amount to exit trade. Trade-amount: {amount}, Wallet: {wallet_amount}")
def execute_trade_exit(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool:
def execute_trade_exit(
self,
trade: Trade,
limit: float,
sell_reason: SellCheckTuple, # TODO-lev update to exit_reason
) -> bool:
"""
Executes a trade exit for the given trade and limit
:param trade: Trade instance
@@ -1137,13 +1258,13 @@ class FreqtradeBot(LoggingMixin):
:param sell_reason: Reason the sell was triggered
:return: True if it succeeds (supported) False (not supported)
"""
sell_type = 'sell' # TODO-lev: Update to exit
exit_type = 'sell' # TODO-lev: Update to exit
if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
sell_type = 'stoploss'
exit_type = 'stoploss'
# if stoploss is on exchange and we are on dry_run mode,
# we consider the sell price stop price
if self.config['dry_run'] and sell_type == 'stoploss' \
if self.config['dry_run'] and exit_type == 'stoploss' \
and self.strategy.order_types['stoploss_on_exchange']:
limit = trade.stop_loss
@@ -1167,7 +1288,7 @@ class FreqtradeBot(LoggingMixin):
except InvalidOrderException:
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
order_type = self.strategy.order_types[sell_type]
order_type = self.strategy.order_types[exit_type]
if sell_reason.sell_type == SellType.EMERGENCY_SELL:
# Emergency sells (default to market!)
order_type = self.strategy.order_types.get("emergencysell", "market")
@@ -1177,7 +1298,7 @@ class FreqtradeBot(LoggingMixin):
order_type = self.strategy.order_types.get("forcesell", order_type)
amount = self._safe_exit_amount(trade.pair, trade.amount)
time_in_force = self.strategy.order_time_in_force['sell']
time_in_force = self.strategy.order_time_in_force['sell'] # TODO-lev update to exit
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,
@@ -1191,7 +1312,7 @@ class FreqtradeBot(LoggingMixin):
order = self.exchange.create_order(
pair=trade.pair,
ordertype=order_type,
side="sell",
side=trade.exit_side,
amount=amount,
rate=limit,
time_in_force=time_in_force
@@ -1202,7 +1323,7 @@ class FreqtradeBot(LoggingMixin):
self.handle_insufficient_funds(trade)
return False
order_obj = Order.parse_from_ccxt_object(order, trade.pair, 'sell')
order_obj = Order.parse_from_ccxt_object(order, trade.pair, trade.exit_side)
trade.orders.append(order_obj)
trade.open_order_id = order['id']
@@ -1230,7 +1351,7 @@ class FreqtradeBot(LoggingMixin):
profit_trade = trade.calc_profit(rate=profit_rate)
# Use cached rates here - it was updated seconds ago.
current_rate = self.exchange.get_rate(
trade.pair, refresh=False, side="sell") if not fill else None
trade.pair, refresh=False, side=trade.exit_side) if not fill else None
profit_ratio = trade.calc_profit_ratio(profit_rate)
gain = "profit" if profit_ratio > 0 else "loss"
@@ -1275,7 +1396,7 @@ class FreqtradeBot(LoggingMixin):
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
profit_trade = trade.calc_profit(rate=profit_rate)
current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="sell")
current_rate = self.exchange.get_rate(trade.pair, refresh=False, side=trade.exit_side)
profit_ratio = trade.calc_profit_ratio(profit_rate)
gain = "profit" if profit_ratio > 0 else "loss"
@@ -1390,7 +1511,7 @@ class FreqtradeBot(LoggingMixin):
self.wallets.update()
if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount:
# Eat into dust if we own more than base currency
# TODO-lev: won't be in "base"(quote) currency for shorts
# TODO-lev: won't be in base currency for shorts
logger.info(f"Fee amount for {trade} was in base currency - "
f"Eating Fee {fee_abs} into dust.")
elif fee_abs != 0:

View File

@@ -45,7 +45,7 @@ progressbar.streams.wrap_stdout()
logger = logging.getLogger(__name__)
INITIAL_POINTS = 5
INITIAL_POINTS = 30
# Keep no more than SKOPT_MODEL_QUEUE_SIZE models
# in the skopt model queue, to optimize memory consumption
@@ -258,6 +258,7 @@ class Hyperopt:
if HyperoptTools.has_space(self.config, 'trailing'):
logger.debug("Hyperopt has 'trailing' space")
self.trailing_space = self.custom_hyperopt.trailing_space()
self.dimensions = (self.buy_space + self.sell_space + self.protection_space
+ self.roi_space + self.stoploss_space + self.trailing_space)

View File

@@ -3,6 +3,7 @@ HyperOptAuto class.
This module implements a convenience auto-hyperopt class, which can be used together with strategies
that implement IHyperStrategy interface.
"""
import logging
from contextlib import suppress
from typing import Callable, Dict, List
@@ -15,12 +16,19 @@ with suppress(ImportError):
from freqtrade.optimize.hyperopt_interface import EstimatorType, IHyperOpt
def _format_exception_message(space: str) -> str:
raise OperationalException(
f"The '{space}' space is included into the hyperoptimization "
f"but no parameter for this space was not found in your Strategy. "
f"Please make sure to have parameters for this space enabled for optimization "
f"or remove the '{space}' space from hyperoptimization.")
logger = logging.getLogger(__name__)
def _format_exception_message(space: str, ignore_missing_space: bool) -> None:
msg = (f"The '{space}' space is included into the hyperoptimization "
f"but no parameter for this space was not found in your Strategy. "
)
if ignore_missing_space:
logger.warning(msg + "This space will be ignored.")
else:
raise OperationalException(
msg + f"Please make sure to have parameters for this space enabled for optimization "
f"or remove the '{space}' space from hyperoptimization.")
class HyperOptAuto(IHyperOpt):
@@ -48,13 +56,16 @@ class HyperOptAuto(IHyperOpt):
if attr.optimize:
yield attr.get_space(attr_name)
def _get_indicator_space(self, category):
def _get_indicator_space(self, category) -> List:
# TODO: is this necessary, or can we call "generate_space" directly?
indicator_space = list(self._generate_indicator_space(category))
if len(indicator_space) > 0:
return indicator_space
else:
_format_exception_message(category)
_format_exception_message(
category,
self.config.get("hyperopt_ignore_missing_space", False))
return []
def buy_indicator_space(self) -> List['Dimension']:
return self._get_indicator_space('buy')

View File

@@ -506,7 +506,6 @@ class LocalTrade():
lower_stop = new_loss < self.stop_loss
# stop losses only walk up, never down!,
# TODO-lev
# ? But adding more to a leveraged trade would create a lower liquidation price,
# ? decreasing the minimum stoploss
if (higher_stop and not self.is_short) or (lower_stop and self.is_short):

View File

@@ -21,6 +21,7 @@ class PerformanceFilter(IPairList):
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._minutes = pairlistconfig.get('minutes', 0)
self._min_profit = pairlistconfig.get('min_profit', None)
@property
def needstickers(self) -> bool:
@@ -68,6 +69,14 @@ class PerformanceFilter(IPairList):
sorted_df = list_df.merge(performance, on='pair', how='left')\
.fillna(0).sort_values(by=['count', 'pair'], ascending=True)\
.sort_values(by=['profit'], ascending=False)
if self._min_profit is not None:
removed = sorted_df[sorted_df['profit'] < self._min_profit]
for _, row in removed.iterrows():
self.log_once(
f"Removing pair {row['pair']} since {row['profit']} is "
f"below {self._min_profit}", logger.info)
sorted_df = sorted_df[sorted_df['profit'] >= self._min_profit]
pairlist = sorted_df['pair'].tolist()
return pairlist

View File

@@ -4,9 +4,9 @@ Static Pair List provider
Provides pair white list as it configured in config
"""
import logging
from copy import deepcopy
from typing import Any, Dict, List
from freqtrade.exceptions import OperationalException
from freqtrade.plugins.pairlist.IPairList import IPairList
@@ -20,10 +20,6 @@ class StaticPairList(IPairList):
pairlist_pos: int) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
if self._pairlist_pos != 0:
raise OperationalException(f"{self.name} can only be used in the first position "
"in the list of Pairlist Handlers.")
self._allow_inactive = self._pairlistconfig.get('allow_inactive', False)
@property
@@ -64,4 +60,8 @@ class StaticPairList(IPairList):
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
:return: new whitelist
"""
return pairlist
pairlist_ = deepcopy(pairlist)
for pair in self._config['exchange']['pair_whitelist']:
if pair not in pairlist_:
pairlist_.append(pair)
return pairlist_

View File

@@ -1,5 +1,6 @@
from typing import Any, Dict, Optional
from typing import Any, Dict, Iterator, Optional
from freqtrade.persistence import Trade
from freqtrade.rpc.rpc import RPC, RPCException
from .webserver import ApiServer
@@ -11,10 +12,12 @@ def get_rpc_optional() -> Optional[RPC]:
return None
def get_rpc() -> Optional[RPC]:
def get_rpc() -> Optional[Iterator[RPC]]:
_rpc = get_rpc_optional()
if _rpc:
return _rpc
Trade.query.session.rollback()
yield _rpc
Trade.query.session.rollback()
else:
raise RPCException('Bot is not in the correct state')

View File

@@ -25,6 +25,7 @@ from freqtrade.constants import DUST_PER_COIN
from freqtrade.enums import RPCMessageType
from freqtrade.exceptions import OperationalException
from freqtrade.misc import chunks, plural, round_coin_value
from freqtrade.persistence import Trade
from freqtrade.rpc import RPC, RPCException, RPCHandler
@@ -59,7 +60,8 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
update.message.chat_id
)
return wrapper
# Rollback session to avoid getting data stored in a transaction.
Trade.query.session.rollback()
logger.debug(
'Executing handler: %s for chat_id: %s',
command_handler.__name__,

View File

@@ -840,28 +840,32 @@ class IStrategy(ABC, HyperStrategyMixin):
else:
logger.warning("CustomStoploss function did not return valid stoploss")
if self.trailing_stop and trade.stop_loss < (low or current_rate):
sl_lower_long = (trade.stop_loss < (low or current_rate) and not trade.is_short)
sl_higher_short = (trade.stop_loss > (high or current_rate) and trade.is_short)
if self.trailing_stop and (sl_lower_long or sl_higher_short):
# trailing stoploss handling
sl_offset = self.trailing_stop_positive_offset
# Make sure current_profit is calculated using high for backtesting.
# TODO-lev: Check this function - high / low usage must be inversed for short trades!
high_profit = current_profit if not high else trade.calc_profit_ratio(high)
bound = low if trade.is_short else high
bound_profit = current_profit if not bound else trade.calc_profit_ratio(bound)
# Don't update stoploss if trailing_only_offset_is_reached is true.
if not (self.trailing_only_offset_is_reached and high_profit < sl_offset):
if not (self.trailing_only_offset_is_reached and bound_profit < sl_offset):
# Specific handling for trailing_stop_positive
if self.trailing_stop_positive is not None and high_profit > sl_offset:
if self.trailing_stop_positive is not None and bound_profit > sl_offset:
stop_loss_value = self.trailing_stop_positive
logger.debug(f"{trade.pair} - Using positive stoploss: {stop_loss_value} "
f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%")
trade.adjust_stop_loss(high or current_rate, stop_loss_value)
trade.adjust_stop_loss(bound or current_rate, stop_loss_value)
sl_higher_short = (trade.stop_loss >= (low or current_rate) and not trade.is_short)
sl_lower_long = ((trade.stop_loss <= (high or current_rate) and trade.is_short))
# evaluate if the stoploss was hit if stoploss is not on exchange
# in Dry-Run, this handles stoploss logic as well, as the logic will not be different to
# regular stoploss handling.
if ((trade.stop_loss >= (low or current_rate)) and
if ((sl_higher_short or sl_lower_long) and
(not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])):
sell_type = SellType.STOP_LOSS
@@ -870,12 +874,18 @@ class IStrategy(ABC, HyperStrategyMixin):
if trade.initial_stop_loss != trade.stop_loss:
sell_type = SellType.TRAILING_STOP_LOSS
logger.debug(
f"{trade.pair} - HIT STOP: current price at {(low or current_rate):.6f}, "
f"{trade.pair} - HIT STOP: current price at "
f"{((high if trade.is_short else low) or current_rate):.6f}, "
f"stoploss is {trade.stop_loss:.6f}, "
f"initial stoploss was at {trade.initial_stop_loss:.6f}, "
f"trade opened at {trade.open_rate:.6f}")
new_stoploss = (
trade.stop_loss + trade.initial_stop_loss
if trade.is_short else
trade.stop_loss - trade.initial_stop_loss
)
logger.debug(f"{trade.pair} - Trailing stop saved "
f"{trade.stop_loss - trade.initial_stop_loss:.6f}")
f"{new_stoploss:.6f}")
return SellCheckTuple(sell_type=sell_type)

View File

@@ -58,6 +58,8 @@ class SampleStrategy(IStrategy):
# Hyperoptable parameters
buy_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True)
sell_rsi = IntParameter(low=50, high=100, default=70, space='sell', optimize=True, load=True)
short_rsi = IntParameter(low=51, high=100, default=70, space='sell', optimize=True, load=True)
exit_short_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True)
# Optimal timeframe for the strategy.
timeframe = '5m'
@@ -354,6 +356,16 @@ class SampleStrategy(IStrategy):
),
'enter_long'] = 1
dataframe.loc[
(
# Signal: RSI crosses above 70
(qtpylib.crossed_above(dataframe['rsi'], self.short_rsi.value)) &
(dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
'enter_short'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
@@ -371,5 +383,18 @@ class SampleStrategy(IStrategy):
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
'exit_long'] = 1
dataframe.loc[
(
# Signal: RSI crosses above 30
(qtpylib.crossed_above(dataframe['rsi'], self.exit_short_rsi.value)) &
# Guard: tema below BB middle
(dataframe['tema'] <= dataframe['bb_middleband']) &
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
'exit_short'] = 1
return dataframe

View File

@@ -339,11 +339,13 @@ def vwap(bars):
(input can be pandas series or numpy array)
bars are usually mid [ (h+l)/2 ] or typical [ (h+l+c)/3 ]
"""
typical = ((bars['high'] + bars['low'] + bars['close']) / 3).values
volume = bars['volume'].values
raise ValueError("using `qtpylib.vwap` facilitates lookahead bias. Please use "
"`qtpylib.rolling_vwap` instead, which calculates vwap in a rolling manner.")
# typical = ((bars['high'] + bars['low'] + bars['close']) / 3).values
# volume = bars['volume'].values
return pd.Series(index=bars.index,
data=np.cumsum(volume * typical) / np.cumsum(volume))
# return pd.Series(index=bars.index,
# data=np.cumsum(volume * typical) / np.cumsum(volume))
# ---------------------------------------------