Merge branch 'feat/short' into fs_fix
This commit is contained in:
@@ -6,7 +6,7 @@ from jsonschema import Draft4Validator, validators
|
||||
from jsonschema.exceptions import ValidationError, best_match
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.enums import RunMode, TradingMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None:
|
||||
_validate_protections(conf)
|
||||
_validate_unlimited_amount(conf)
|
||||
_validate_ask_orderbook(conf)
|
||||
validate_migrated_strategy_settings(conf)
|
||||
|
||||
# validate configuration before returning
|
||||
logger.info('Validating configuration ...')
|
||||
@@ -207,3 +208,24 @@ def _validate_ask_orderbook(conf: Dict[str, Any]) -> None:
|
||||
"Please use `order_book_top` instead of `order_book_min` and `order_book_max` "
|
||||
"for your `ask_strategy` configuration."
|
||||
)
|
||||
|
||||
|
||||
def validate_migrated_strategy_settings(conf: Dict[str, Any]) -> None:
|
||||
|
||||
_validate_time_in_force(conf)
|
||||
|
||||
|
||||
def _validate_time_in_force(conf: Dict[str, Any]) -> None:
|
||||
|
||||
time_in_force = conf.get('order_time_in_force', {})
|
||||
if 'buy' in time_in_force or 'sell' in time_in_force:
|
||||
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
|
||||
raise OperationalException(
|
||||
"Please migrate your time_in_force settings to use 'entry' and 'exit'.")
|
||||
else:
|
||||
logger.warning(
|
||||
"DEPRECATED: Using 'buy' and 'sell' for time_in_force is deprecated."
|
||||
"Please migrate your time_in_force settings to use 'entry' and 'exit'."
|
||||
)
|
||||
time_in_force['entry'] = time_in_force.pop('buy')
|
||||
time_in_force['exit'] = time_in_force.pop('sell')
|
||||
|
@@ -19,7 +19,7 @@ DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
|
||||
DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite'
|
||||
UNLIMITED_STAKE_AMOUNT = 'unlimited'
|
||||
DEFAULT_AMOUNT_RESERVE_PERCENT = 0.05
|
||||
REQUIRED_ORDERTIF = ['buy', 'sell']
|
||||
REQUIRED_ORDERTIF = ['entry', 'exit']
|
||||
REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
||||
ORDERBOOK_SIDES = ['ask', 'bid']
|
||||
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
||||
@@ -233,10 +233,10 @@ CONF_SCHEMA = {
|
||||
'order_time_in_force': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'buy': {'type': 'string', 'enum': ORDERTIF_POSSIBILITIES},
|
||||
'sell': {'type': 'string', 'enum': ORDERTIF_POSSIBILITIES}
|
||||
'entry': {'type': 'string', 'enum': ORDERTIF_POSSIBILITIES},
|
||||
'exit': {'type': 'string', 'enum': ORDERTIF_POSSIBILITIES}
|
||||
},
|
||||
'required': ['buy', 'sell']
|
||||
'required': REQUIRED_ORDERTIF
|
||||
},
|
||||
'exchange': {'$ref': '#/definitions/exchange'},
|
||||
'edge': {'$ref': '#/definitions/edge'},
|
||||
|
@@ -595,7 +595,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
: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']
|
||||
time_in_force = self.strategy.order_time_in_force['entry']
|
||||
|
||||
[side, name] = ['sell', 'Short'] if is_short else ['buy', 'Long']
|
||||
trade_side = 'short' if is_short else 'long'
|
||||
@@ -659,13 +659,12 @@ class FreqtradeBot(LoggingMixin):
|
||||
amount_requested = amount
|
||||
|
||||
if order_status == 'expired' or order_status == 'rejected':
|
||||
order_tif = self.strategy.order_time_in_force['buy']
|
||||
|
||||
# return false if the order is not filled
|
||||
if float(order['filled']) == 0:
|
||||
logger.warning('%s %s order with time in force %s for %s is %s by %s.'
|
||||
' zero amount is fulfilled.',
|
||||
name, order_tif, order_type, pair, order_status, self.exchange.name)
|
||||
logger.warning(f'{name} {time_in_force} order with time in force {order_type} '
|
||||
f'for {pair} is {order_status} by {self.exchange.name}.'
|
||||
' zero amount is fulfilled.')
|
||||
return False
|
||||
else:
|
||||
# the order is partially fulfilled
|
||||
@@ -673,8 +672,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
# if the order is fulfilled fully or partially
|
||||
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).',
|
||||
name, order_tif, order_type, pair, order_status, self.exchange.name,
|
||||
order['filled'], order['amount'], order['remaining']
|
||||
name, time_in_force, order_type, pair, order_status,
|
||||
self.exchange.name, order['filled'], order['amount'],
|
||||
order['remaining']
|
||||
)
|
||||
stake_amount = order['cost']
|
||||
amount = safe_value_fallback(order, 'filled', 'amount')
|
||||
@@ -725,7 +725,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
leverage=leverage,
|
||||
is_short=is_short,
|
||||
interest_rate=interest_rate,
|
||||
isolated_liq=isolated_liq,
|
||||
liquidation_price=isolated_liq,
|
||||
trading_mode=self.trading_mode,
|
||||
funding_fees=funding_fees
|
||||
)
|
||||
@@ -1382,7 +1382,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
order_type = self.strategy.order_types.get("emergencysell", "market")
|
||||
|
||||
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['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,
|
||||
|
@@ -506,7 +506,7 @@ class Backtesting:
|
||||
# freqtrade does not support this in live, and the order would fill immediately
|
||||
closerate = max(closerate, sell_row[LOW_IDX])
|
||||
# Confirm trade exit:
|
||||
time_in_force = self.strategy.order_time_in_force['sell']
|
||||
time_in_force = self.strategy.order_time_in_force['exit']
|
||||
|
||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
|
||||
pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount,
|
||||
@@ -642,7 +642,7 @@ class Backtesting:
|
||||
# If not pos adjust, trade is None
|
||||
return trade
|
||||
order_type = self.strategy.order_types['buy']
|
||||
time_in_force = self.strategy.order_time_in_force['buy']
|
||||
time_in_force = self.strategy.order_time_in_force['entry']
|
||||
|
||||
if not pos_adjust:
|
||||
max_leverage = self.exchange.get_max_leverage(pair, stake_amount)
|
||||
|
@@ -82,7 +82,8 @@ def migrate_trades_and_orders_table(
|
||||
|
||||
# Leverage Properties
|
||||
leverage = get_column_def(cols, 'leverage', '1.0')
|
||||
isolated_liq = get_column_def(cols, 'isolated_liq', 'null')
|
||||
liquidation_price = get_column_def(cols, 'liquidation_price',
|
||||
get_column_def(cols, 'isolated_liq', 'null'))
|
||||
# sqlite does not support literals for booleans
|
||||
is_short = get_column_def(cols, 'is_short', '0')
|
||||
|
||||
@@ -137,7 +138,7 @@ def migrate_trades_and_orders_table(
|
||||
stoploss_order_id, stoploss_last_update,
|
||||
max_rate, min_rate, sell_reason, sell_order_status, strategy, enter_tag,
|
||||
timeframe, open_trade_value, close_profit_abs,
|
||||
trading_mode, leverage, isolated_liq, is_short,
|
||||
trading_mode, leverage, liquidation_price, is_short,
|
||||
interest_rate, funding_fees
|
||||
)
|
||||
select id, lower(exchange), pair,
|
||||
@@ -155,7 +156,7 @@ def migrate_trades_and_orders_table(
|
||||
{sell_order_status} sell_order_status,
|
||||
{strategy} strategy, {enter_tag} enter_tag, {timeframe} timeframe,
|
||||
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs,
|
||||
{trading_mode} trading_mode, {leverage} leverage, {isolated_liq} isolated_liq,
|
||||
{trading_mode} trading_mode, {leverage} leverage, {liquidation_price} liquidation_price,
|
||||
{is_short} is_short, {interest_rate} interest_rate,
|
||||
{funding_fees} funding_fees
|
||||
from {trade_back_name}
|
||||
@@ -233,10 +234,9 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
|
||||
|
||||
# Check if migration necessary
|
||||
# Migrates both trades and orders table!
|
||||
# if not has_column(cols, 'buy_tag'):
|
||||
if ('orders' not in previous_tables
|
||||
or not has_column(cols_orders, 'ft_fee_base')
|
||||
or not has_column(cols_orders, 'leverage')):
|
||||
# if ('orders' not in previous_tables
|
||||
# or not has_column(cols_orders, 'leverage')):
|
||||
if not has_column(cols, 'liquidation_price'):
|
||||
logger.info(f"Running database migration for trades - "
|
||||
f"backup: {table_back_name}, {order_table_bak_name}")
|
||||
migrate_trades_and_orders_table(
|
||||
|
@@ -329,7 +329,7 @@ class LocalTrade():
|
||||
trading_mode: TradingMode = TradingMode.SPOT
|
||||
|
||||
# Leverage trading properties
|
||||
isolated_liq: Optional[float] = None
|
||||
liquidation_price: Optional[float] = None
|
||||
is_short: bool = False
|
||||
leverage: float = 1.0
|
||||
|
||||
@@ -483,7 +483,7 @@ class LocalTrade():
|
||||
|
||||
'leverage': self.leverage,
|
||||
'interest_rate': self.interest_rate,
|
||||
'isolated_liq': self.isolated_liq,
|
||||
'liquidation_price': self.liquidation_price,
|
||||
'is_short': self.is_short,
|
||||
'trading_mode': self.trading_mode,
|
||||
'funding_fees': self.funding_fees,
|
||||
@@ -507,25 +507,25 @@ class LocalTrade():
|
||||
self.max_rate = max(current_price, self.max_rate or self.open_rate)
|
||||
self.min_rate = min(current_price_low, self.min_rate or self.open_rate)
|
||||
|
||||
def set_isolated_liq(self, isolated_liq: Optional[float]):
|
||||
def set_isolated_liq(self, liquidation_price: Optional[float]):
|
||||
"""
|
||||
Method you should use to set self.liquidation price.
|
||||
Assures stop_loss is not passed the liquidation price
|
||||
"""
|
||||
if not isolated_liq:
|
||||
if not liquidation_price:
|
||||
return
|
||||
self.isolated_liq = isolated_liq
|
||||
self.liquidation_price = liquidation_price
|
||||
|
||||
def _set_stop_loss(self, stop_loss: float, percent: float):
|
||||
"""
|
||||
Method you should use to set self.stop_loss.
|
||||
Assures stop_loss is not passed the liquidation price
|
||||
"""
|
||||
if self.isolated_liq is not None:
|
||||
if self.liquidation_price is not None:
|
||||
if self.is_short:
|
||||
sl = min(stop_loss, self.isolated_liq)
|
||||
sl = min(stop_loss, self.liquidation_price)
|
||||
else:
|
||||
sl = max(stop_loss, self.isolated_liq)
|
||||
sl = max(stop_loss, self.liquidation_price)
|
||||
else:
|
||||
sl = stop_loss
|
||||
|
||||
@@ -553,13 +553,13 @@ class LocalTrade():
|
||||
if self.is_short:
|
||||
new_loss = float(current_price * (1 + abs(stoploss / leverage)))
|
||||
# If trading with leverage, don't set the stoploss below the liquidation price
|
||||
if self.isolated_liq:
|
||||
new_loss = min(self.isolated_liq, new_loss)
|
||||
if self.liquidation_price:
|
||||
new_loss = min(self.liquidation_price, new_loss)
|
||||
else:
|
||||
new_loss = float(current_price * (1 - abs(stoploss / leverage)))
|
||||
# If trading with leverage, don't set the stoploss below the liquidation price
|
||||
if self.isolated_liq:
|
||||
new_loss = max(self.isolated_liq, new_loss)
|
||||
if self.liquidation_price:
|
||||
new_loss = max(self.liquidation_price, new_loss)
|
||||
|
||||
# no stop loss assigned yet
|
||||
if self.initial_stop_loss_pct is None:
|
||||
@@ -1093,7 +1093,7 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
# Leverage trading properties
|
||||
leverage = Column(Float, nullable=True, default=1.0)
|
||||
is_short = Column(Boolean, nullable=False, default=False)
|
||||
isolated_liq = Column(Float, nullable=True)
|
||||
liquidation_price = Column(Float, nullable=True)
|
||||
|
||||
# Margin Trading Properties
|
||||
interest_rate = Column(Float, nullable=False, default=0.0)
|
||||
|
@@ -10,6 +10,7 @@ from inspect import getfullargspec
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from freqtrade.configuration.config_validation import validate_migrated_strategy_settings
|
||||
from freqtrade.constants import REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES, USERPATH_STRATEGIES
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.resolvers import IResolver
|
||||
@@ -160,10 +161,12 @@ class StrategyResolver(IResolver):
|
||||
|
||||
@staticmethod
|
||||
def _strategy_sanity_validations(strategy):
|
||||
# Ensure necessary migrations are performed first.
|
||||
validate_migrated_strategy_settings(strategy.config)
|
||||
|
||||
if not all(k in strategy.order_types for k in REQUIRED_ORDERTYPES):
|
||||
raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. "
|
||||
f"Order-types mapping is incomplete.")
|
||||
|
||||
if not all(k in strategy.order_time_in_force for k in REQUIRED_ORDERTIF):
|
||||
raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. "
|
||||
f"Order-time-in-force mapping is incomplete.")
|
||||
|
@@ -96,8 +96,8 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
|
||||
# Optional time in force
|
||||
order_time_in_force: Dict = {
|
||||
'buy': 'gtc',
|
||||
'sell': 'gtc',
|
||||
'entry': 'gtc',
|
||||
'exit': 'gtc',
|
||||
}
|
||||
|
||||
# run "populate_indicators" only for new candle
|
||||
|
@@ -83,8 +83,8 @@ class {{ strategy }}(IStrategy):
|
||||
|
||||
# Optional order time in force.
|
||||
order_time_in_force = {
|
||||
'buy': 'gtc',
|
||||
'sell': 'gtc'
|
||||
'entry': 'gtc',
|
||||
'exit': 'gtc'
|
||||
}
|
||||
{{ plot_config | indent(4) }}
|
||||
|
||||
|
@@ -84,8 +84,8 @@ class SampleShortStrategy(IStrategy):
|
||||
|
||||
# Optional order time in force.
|
||||
order_time_in_force = {
|
||||
'buy': 'gtc',
|
||||
'sell': 'gtc'
|
||||
'entry': 'gtc',
|
||||
'exit': 'gtc'
|
||||
}
|
||||
|
||||
plot_config = {
|
||||
|
@@ -85,8 +85,8 @@ class SampleStrategy(IStrategy):
|
||||
|
||||
# Optional order time in force.
|
||||
order_time_in_force = {
|
||||
'buy': 'gtc',
|
||||
'sell': 'gtc'
|
||||
'entry': 'gtc',
|
||||
'exit': 'gtc'
|
||||
}
|
||||
|
||||
plot_config = {
|
||||
|
Reference in New Issue
Block a user