Merge branch 'feat/short' into funding-fee-backtesting
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
}
|
||||
|
||||
@@ -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']}")
|
||||
|
||||
|
||||
@@ -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: {}')
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.",
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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__,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
10
freqtrade/vendor/qtpylib/indicators.py
vendored
10
freqtrade/vendor/qtpylib/indicators.py
vendored
@@ -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))
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user