Merge branch 'develop' into pr/mkavinkumar1/6545
This commit is contained in:
@@ -19,9 +19,9 @@ def start_convert_db(args: Dict[str, Any]) -> None:
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
init_db(config['db_url'], False)
|
||||
init_db(config['db_url'])
|
||||
session_target = Trade._session
|
||||
init_db(config['db_url_from'], False)
|
||||
init_db(config['db_url_from'])
|
||||
logger.info("Starting db migration.")
|
||||
|
||||
trade_count = 0
|
||||
|
||||
@@ -212,7 +212,7 @@ def start_show_trades(args: Dict[str, Any]) -> None:
|
||||
raise OperationalException("--db-url is required for this command.")
|
||||
|
||||
logger.info(f'Using DB: "{parse_db_uri_for_logging(config["db_url"])}"')
|
||||
init_db(config['db_url'], clean_open_orders=False)
|
||||
init_db(config['db_url'])
|
||||
tfilter = []
|
||||
|
||||
if config.get('trade_ids'):
|
||||
|
||||
@@ -27,7 +27,7 @@ def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool:
|
||||
return True
|
||||
logger.info("Checking exchange...")
|
||||
|
||||
exchange = config.get('exchange', {}).get('name').lower()
|
||||
exchange = config.get('exchange', {}).get('name', '').lower()
|
||||
if not exchange:
|
||||
raise OperationalException(
|
||||
f'This command requires a configured exchange. You should either use '
|
||||
|
||||
@@ -490,7 +490,8 @@ class Configuration:
|
||||
if not pairs_file.exists():
|
||||
raise OperationalException(f'No pairs file found with path "{pairs_file}".')
|
||||
config['pairs'] = load_file(pairs_file)
|
||||
config['pairs'].sort()
|
||||
if isinstance(config['pairs'], list):
|
||||
config['pairs'].sort()
|
||||
return
|
||||
|
||||
if 'config' in self.args and self.args['config']:
|
||||
@@ -501,5 +502,5 @@ class Configuration:
|
||||
pairs_file = config['datadir'] / 'pairs.json'
|
||||
if pairs_file.exists():
|
||||
config['pairs'] = load_file(pairs_file)
|
||||
if 'pairs' in config:
|
||||
if 'pairs' in config and isinstance(config['pairs'], list):
|
||||
config['pairs'].sort()
|
||||
|
||||
@@ -113,7 +113,7 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
|
||||
process_removed_setting(config, 'experimental', 'ignore_roi_if_buy_signal',
|
||||
None, 'ignore_roi_if_entry_signal')
|
||||
|
||||
process_removed_setting(config, 'ask_strategy', 'use_sell_signal', None, 'exit_sell_signal')
|
||||
process_removed_setting(config, 'ask_strategy', 'use_sell_signal', None, 'use_exit_signal')
|
||||
process_removed_setting(config, 'ask_strategy', 'sell_profit_only', None, 'exit_profit_only')
|
||||
process_removed_setting(config, 'ask_strategy', 'sell_profit_offset',
|
||||
None, 'exit_profit_offset')
|
||||
|
||||
@@ -15,7 +15,7 @@ def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> Pat
|
||||
folder = Path(datadir) if datadir else Path(f"{config['user_data_dir']}/data")
|
||||
if not datadir:
|
||||
# set datadir
|
||||
exchange_name = config.get('exchange', {}).get('name').lower()
|
||||
exchange_name = config.get('exchange', {}).get('name', '').lower()
|
||||
folder = folder.joinpath(exchange_name)
|
||||
|
||||
if not folder.is_dir():
|
||||
|
||||
@@ -302,12 +302,12 @@ CONF_SCHEMA = {
|
||||
'exit_fill': {
|
||||
'type': 'string',
|
||||
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||
'default': 'off'
|
||||
'default': 'on'
|
||||
},
|
||||
'protection_trigger': {
|
||||
'type': 'string',
|
||||
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||
'default': 'off'
|
||||
'default': 'on'
|
||||
},
|
||||
'protection_trigger_global': {
|
||||
'type': 'string',
|
||||
|
||||
@@ -353,7 +353,7 @@ def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataF
|
||||
Can also serve as protection to load the correct result.
|
||||
:return: Dataframe containing Trades
|
||||
"""
|
||||
init_db(db_url, clean_open_orders=False)
|
||||
init_db(db_url)
|
||||
|
||||
filters = []
|
||||
if strategy:
|
||||
|
||||
@@ -68,7 +68,8 @@ def load_data(datadir: Path,
|
||||
startup_candles: int = 0,
|
||||
fail_without_data: bool = False,
|
||||
data_format: str = 'json',
|
||||
candle_type: CandleType = CandleType.SPOT
|
||||
candle_type: CandleType = CandleType.SPOT,
|
||||
user_futures_funding_rate: int = None,
|
||||
) -> Dict[str, DataFrame]:
|
||||
"""
|
||||
Load ohlcv history data for a list of pairs.
|
||||
@@ -100,6 +101,10 @@ def load_data(datadir: Path,
|
||||
)
|
||||
if not hist.empty:
|
||||
result[pair] = hist
|
||||
else:
|
||||
if candle_type is CandleType.FUNDING_RATE and user_futures_funding_rate is not None:
|
||||
logger.warn(f"{pair} using user specified [{user_futures_funding_rate}]")
|
||||
result[pair] = DataFrame(columns=["open", "close", "high", "low", "volume"])
|
||||
|
||||
if fail_without_data and not result:
|
||||
raise OperationalException("No data found. Terminating.")
|
||||
@@ -277,6 +282,7 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
|
||||
pairs_not_available = []
|
||||
data_handler = get_datahandler(datadir, data_format)
|
||||
candle_type = CandleType.get_default(trading_mode)
|
||||
process = ''
|
||||
for idx, pair in enumerate(pairs, start=1):
|
||||
if pair not in exchange.markets:
|
||||
pairs_not_available.append(pair)
|
||||
|
||||
@@ -15,3 +15,9 @@ class ExitCheckTuple:
|
||||
@property
|
||||
def exit_flag(self):
|
||||
return self.exit_type != ExitType.NONE
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.exit_type == other.exit_type and self.exit_reason == other.exit_reason
|
||||
|
||||
def __repr__(self):
|
||||
return f"ExitCheckTuple({self.exit_type}, {self.exit_reason})"
|
||||
|
||||
@@ -57,7 +57,7 @@ class Binance(Exchange):
|
||||
(side == "buy" and stop_loss < float(order['info']['stopPrice']))
|
||||
)
|
||||
|
||||
def get_tickers(self, symbols: List[str] = None, cached: bool = False) -> Dict:
|
||||
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
|
||||
tickers = super().get_tickers(symbols=symbols, cached=cached)
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
# Binance's future result has no bid/ask values.
|
||||
@@ -95,7 +95,7 @@ class Binance(Exchange):
|
||||
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||
since_ms: int, candle_type: CandleType,
|
||||
is_new_pair: bool = False, raise_: bool = False,
|
||||
until_ms: int = None
|
||||
until_ms: Optional[int] = None
|
||||
) -> Tuple[str, str, str, List]:
|
||||
"""
|
||||
Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ import asyncio
|
||||
import logging
|
||||
import time
|
||||
from functools import wraps
|
||||
from typing import Any, Callable, Optional, TypeVar, cast, overload
|
||||
|
||||
from freqtrade.exceptions import DDosProtection, RetryableOrderError, TemporaryError
|
||||
from freqtrade.mixins import LoggingMixin
|
||||
@@ -11,6 +12,14 @@ logger = logging.getLogger(__name__)
|
||||
__logging_mixin = None
|
||||
|
||||
|
||||
def _reset_logging_mixin():
|
||||
"""
|
||||
Reset global logging mixin - used in tests only.
|
||||
"""
|
||||
global __logging_mixin
|
||||
__logging_mixin = LoggingMixin(logger)
|
||||
|
||||
|
||||
def _get_logging_mixin():
|
||||
# Logging-mixin to cache kucoin responses
|
||||
# Only to be used in retrier
|
||||
@@ -133,8 +142,22 @@ def retrier_async(f):
|
||||
return wrapper
|
||||
|
||||
|
||||
def retrier(_func=None, retries=API_RETRY_COUNT):
|
||||
def decorator(f):
|
||||
F = TypeVar('F', bound=Callable[..., Any])
|
||||
|
||||
|
||||
# Type shenanigans
|
||||
@overload
|
||||
def retrier(_func: F) -> F:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def retrier(*, retries=API_RETRY_COUNT) -> Callable[[F], F]:
|
||||
...
|
||||
|
||||
|
||||
def retrier(_func: Optional[F] = None, *, retries=API_RETRY_COUNT):
|
||||
def decorator(f: F) -> F:
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
count = kwargs.pop('count', retries)
|
||||
@@ -155,7 +178,7 @@ def retrier(_func=None, retries=API_RETRY_COUNT):
|
||||
else:
|
||||
logger.warning(msg + 'Giving up.')
|
||||
raise ex
|
||||
return wrapper
|
||||
return cast(F, wrapper)
|
||||
# Support both @retrier and @retrier(retries=2) syntax
|
||||
if _func is None:
|
||||
return decorator
|
||||
|
||||
@@ -92,8 +92,8 @@ class Exchange:
|
||||
it does basic validation whether the specified exchange and pairs are valid.
|
||||
:return: None
|
||||
"""
|
||||
self._api: ccxt.Exchange = None
|
||||
self._api_async: ccxt_async.Exchange = None
|
||||
self._api: ccxt.Exchange
|
||||
self._api_async: ccxt_async.Exchange
|
||||
self._markets: Dict = {}
|
||||
self._trading_fees: Dict[str, Any] = {}
|
||||
self._leverage_tiers: Dict[str, List[Dict]] = {}
|
||||
@@ -291,7 +291,7 @@ class Exchange:
|
||||
return self._markets
|
||||
|
||||
@property
|
||||
def precisionMode(self) -> str:
|
||||
def precisionMode(self) -> int:
|
||||
"""exchange ccxt precisionMode"""
|
||||
return self._api.precisionMode
|
||||
|
||||
@@ -322,7 +322,7 @@ class Exchange:
|
||||
return int(self._ft_has.get('ohlcv_candle_limit_per_timeframe', {}).get(
|
||||
timeframe, self._ft_has.get('ohlcv_candle_limit')))
|
||||
|
||||
def get_markets(self, base_currencies: List[str] = None, quote_currencies: List[str] = None,
|
||||
def get_markets(self, base_currencies: List[str] = [], quote_currencies: List[str] = [],
|
||||
spot_only: bool = False, margin_only: bool = False, futures_only: bool = False,
|
||||
tradable_only: bool = True,
|
||||
active_only: bool = False) -> Dict[str, Any]:
|
||||
@@ -953,6 +953,12 @@ class Exchange:
|
||||
order = self.check_dry_limit_order_filled(order)
|
||||
return order
|
||||
except KeyError as e:
|
||||
from freqtrade.persistence import Order
|
||||
order = Order.order_by_id(order_id)
|
||||
if order:
|
||||
ccxt_order = order.to_ccxt_object()
|
||||
self._dry_run_open_orders[order_id] = ccxt_order
|
||||
return ccxt_order
|
||||
# Gracefully handle errors with dry-run orders.
|
||||
raise InvalidOrderException(
|
||||
f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e
|
||||
@@ -1158,7 +1164,7 @@ class Exchange:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
||||
def fetch_order(self, order_id: str, pair: str, params={}) -> Dict:
|
||||
def fetch_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
return self.fetch_dry_run_order(order_id)
|
||||
try:
|
||||
@@ -1180,8 +1186,8 @@ class Exchange:
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
# Assign method to fetch_stoploss_order to allow easy overriding in other classes
|
||||
fetch_stoploss_order = fetch_order
|
||||
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||
return self.fetch_order(order_id, pair, params)
|
||||
|
||||
def fetch_order_or_stoploss_order(self, order_id: str, pair: str,
|
||||
stoploss_order: bool = False) -> Dict:
|
||||
@@ -1206,7 +1212,7 @@ class Exchange:
|
||||
and order.get('filled') == 0.0)
|
||||
|
||||
@retrier
|
||||
def cancel_order(self, order_id: str, pair: str, params={}) -> Dict:
|
||||
def cancel_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
try:
|
||||
order = self.fetch_dry_run_order(order_id)
|
||||
@@ -1232,8 +1238,8 @@ class Exchange:
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
# Assign method to cancel_stoploss_order to allow easy overriding in other classes
|
||||
cancel_stoploss_order = cancel_order
|
||||
def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||
return self.cancel_order(order_id, pair, params)
|
||||
|
||||
def is_cancel_order_result_suitable(self, corder) -> bool:
|
||||
if not isinstance(corder, dict):
|
||||
@@ -1345,7 +1351,7 @@ class Exchange:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def fetch_bids_asks(self, symbols: List[str] = None, cached: bool = False) -> Dict:
|
||||
def fetch_bids_asks(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
|
||||
"""
|
||||
:param cached: Allow cached result
|
||||
:return: fetch_tickers result
|
||||
@@ -1373,7 +1379,7 @@ class Exchange:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def get_tickers(self, symbols: List[str] = None, cached: bool = False) -> Dict:
|
||||
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
|
||||
"""
|
||||
:param cached: Allow cached result
|
||||
:return: fetch_tickers result
|
||||
@@ -1744,7 +1750,7 @@ class Exchange:
|
||||
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||
since_ms: int, candle_type: CandleType,
|
||||
is_new_pair: bool = False, raise_: bool = False,
|
||||
until_ms: int = None
|
||||
until_ms: Optional[int] = None
|
||||
) -> Tuple[str, str, str, List]:
|
||||
"""
|
||||
Download historic ohlcv
|
||||
@@ -1805,7 +1811,7 @@ class Exchange:
|
||||
|
||||
def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
|
||||
since_ms: Optional[int] = None, cache: bool = True,
|
||||
drop_incomplete: bool = None
|
||||
drop_incomplete: Optional[bool] = None
|
||||
) -> Dict[PairWithTimeframe, DataFrame]:
|
||||
"""
|
||||
Refresh in-memory OHLCV asynchronously and set `_klines` with the result
|
||||
@@ -2445,14 +2451,35 @@ class Exchange:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def combine_funding_and_mark(funding_rates: DataFrame, mark_rates: DataFrame) -> DataFrame:
|
||||
def combine_funding_and_mark(funding_rates: DataFrame, mark_rates: DataFrame,
|
||||
futures_funding_rate: Optional[int] = None) -> DataFrame:
|
||||
"""
|
||||
Combine funding-rates and mark-rates dataframes
|
||||
:param funding_rates: Dataframe containing Funding rates (Type FUNDING_RATE)
|
||||
:param mark_rates: Dataframe containing Mark rates (Type mark_ohlcv_price)
|
||||
:param futures_funding_rate: Fake funding rate to use if funding_rates are not available
|
||||
"""
|
||||
if futures_funding_rate is None:
|
||||
return mark_rates.merge(
|
||||
funding_rates, on='date', how="inner", suffixes=["_mark", "_fund"])
|
||||
else:
|
||||
if len(funding_rates) == 0:
|
||||
# No funding rate candles - full fillup with fallback variable
|
||||
mark_rates['open_fund'] = futures_funding_rate
|
||||
return mark_rates.rename(
|
||||
columns={'open': 'open_mark',
|
||||
'close': 'close_mark',
|
||||
'high': 'high_mark',
|
||||
'low': 'low_mark',
|
||||
'volume': 'volume_mark'})
|
||||
|
||||
return funding_rates.merge(mark_rates, on='date', how="inner", suffixes=["_fund", "_mark"])
|
||||
else:
|
||||
# Fill up missing funding_rate candles with fallback value
|
||||
combined = mark_rates.merge(
|
||||
funding_rates, on='date', how="outer", suffixes=["_mark", "_fund"]
|
||||
)
|
||||
combined['open_fund'] = combined['open_fund'].fillna(futures_funding_rate)
|
||||
return combined
|
||||
|
||||
def calculate_funding_fees(
|
||||
self,
|
||||
|
||||
@@ -104,7 +104,7 @@ class Ftx(Exchange):
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
||||
def fetch_stoploss_order(self, order_id: str, pair: str) -> Dict:
|
||||
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
return self.fetch_dry_run_order(order_id)
|
||||
|
||||
@@ -145,7 +145,7 @@ class Ftx(Exchange):
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def cancel_stoploss_order(self, order_id: str, pair: str) -> Dict:
|
||||
def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
return {}
|
||||
try:
|
||||
|
||||
@@ -71,14 +71,14 @@ class Gateio(Exchange):
|
||||
}
|
||||
return trades
|
||||
|
||||
def fetch_stoploss_order(self, order_id: str, pair: str, params={}) -> Dict:
|
||||
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||
return self.fetch_order(
|
||||
order_id=order_id,
|
||||
pair=pair,
|
||||
params={'stop': True}
|
||||
)
|
||||
|
||||
def cancel_stoploss_order(self, order_id: str, pair: str, params={}) -> Dict:
|
||||
def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||
return self.cancel_order(
|
||||
order_id=order_id,
|
||||
pair=pair,
|
||||
|
||||
@@ -45,7 +45,7 @@ class Kraken(Exchange):
|
||||
return (parent_check and
|
||||
market.get('darkpool', False) is False)
|
||||
|
||||
def get_tickers(self, symbols: List[str] = None, cached: bool = False) -> Dict:
|
||||
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
|
||||
# Only fetch tickers for current stake currency
|
||||
# Otherwise the request for kraken becomes too large.
|
||||
symbols = list(self.get_markets(quote_currencies=[self._config['stake_currency']]))
|
||||
|
||||
@@ -68,7 +68,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
||||
|
||||
init_db(self.config.get('db_url', None), clean_open_orders=self.config['dry_run'])
|
||||
init_db(self.config.get('db_url', None))
|
||||
|
||||
self.wallets = Wallets(self.config, self.exchange)
|
||||
|
||||
@@ -124,7 +124,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
self._schedule.every().day.at(t).do(update)
|
||||
self.last_process = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
||||
|
||||
self.strategy.bot_start()
|
||||
self.strategy.ft_bot_start()
|
||||
|
||||
def notify_status(self, msg: str) -> None:
|
||||
"""
|
||||
@@ -300,7 +300,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair,
|
||||
order.ft_order_side == 'stoploss')
|
||||
|
||||
self.update_trade_state(order.trade, order.order_id, fo)
|
||||
self.update_trade_state(order.trade, order.order_id, fo,
|
||||
stoploss_order=(order.ft_order_side == 'stoploss'))
|
||||
|
||||
except ExchangeError as e:
|
||||
|
||||
@@ -1043,7 +1044,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Lock pair for one candle to prevent immediate rebuys
|
||||
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
||||
reason='Auto lock')
|
||||
self._notify_exit(trade, "stoploss")
|
||||
self._notify_exit(trade, "stoploss", True)
|
||||
return True
|
||||
|
||||
if trade.open_order_id or not trade.is_open:
|
||||
@@ -1130,7 +1131,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
"""
|
||||
Check and execute trade exit
|
||||
"""
|
||||
should_exit: ExitCheckTuple = self.strategy.should_exit(
|
||||
exits: List[ExitCheckTuple] = self.strategy.should_exit(
|
||||
trade,
|
||||
exit_rate,
|
||||
datetime.now(timezone.utc),
|
||||
@@ -1138,12 +1139,13 @@ class FreqtradeBot(LoggingMixin):
|
||||
exit_=exit_,
|
||||
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
|
||||
)
|
||||
|
||||
if should_exit.exit_flag:
|
||||
logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.exit_type}'
|
||||
f'Tag: {exit_tag if exit_tag is not None else "None"}')
|
||||
self.execute_trade_exit(trade, exit_rate, should_exit, exit_tag=exit_tag)
|
||||
return True
|
||||
for should_exit in exits:
|
||||
if should_exit.exit_flag:
|
||||
logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.exit_type}'
|
||||
f'{f" Tag: {exit_tag}" if exit_tag is not None else ""}')
|
||||
exited = self.execute_trade_exit(trade, exit_rate, should_exit, exit_tag=exit_tag)
|
||||
if exited:
|
||||
return True
|
||||
return False
|
||||
|
||||
def manage_open_orders(self) -> None:
|
||||
@@ -1433,7 +1435,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
:param trade: Trade instance
|
||||
:param limit: limit rate for the sell order
|
||||
:param exit_check: CheckTuple with signal and reason
|
||||
:return: True if it succeeds (supported) False (not supported)
|
||||
:return: True if it succeeds False
|
||||
"""
|
||||
trade.funding_fees = self.exchange.get_funding_fees(
|
||||
pair=trade.pair,
|
||||
@@ -1481,7 +1483,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
time_in_force=time_in_force, exit_reason=exit_reason,
|
||||
sell_reason=exit_reason, # sellreason -> compatibility
|
||||
current_time=datetime.now(timezone.utc)):
|
||||
logger.info(f"User requested abortion of exiting {trade.pair}")
|
||||
logger.info(f"User requested abortion of {trade.pair} exit.")
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -1708,7 +1710,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
if send_msg and not stoploss_order and not trade.open_order_id:
|
||||
self._notify_exit(trade, '', True, sub_trade=sub_trade, order=order_obj)
|
||||
self.handle_protections(trade.pair, trade.trade_direction)
|
||||
elif send_msg and not trade.open_order_id:
|
||||
elif send_msg and not trade.open_order_id and not stoploss_order:
|
||||
# Enter fill
|
||||
self._notify_enter(trade, order_obj, fill=True, sub_trade=sub_trade)
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ class Backtesting:
|
||||
# since a "perfect" stoploss-exit is assumed anyway
|
||||
# And the regular "stoploss" function would not apply to that case
|
||||
self.strategy.order_types['stoploss_on_exchange'] = False
|
||||
self.strategy.bot_start()
|
||||
self.strategy.ft_bot_start()
|
||||
|
||||
def _load_protections(self, strategy: IStrategy):
|
||||
if self.config.get('enable_protections', False):
|
||||
@@ -275,8 +275,12 @@ class Backtesting:
|
||||
if pair not in self.exchange._leverage_tiers:
|
||||
unavailable_pairs.append(pair)
|
||||
continue
|
||||
self.futures_data[pair] = funding_rates_dict[pair].merge(
|
||||
mark_rates_dict[pair], on='date', how="inner", suffixes=["_fund", "_mark"])
|
||||
|
||||
self.futures_data[pair] = self.exchange.combine_funding_and_mark(
|
||||
funding_rates=funding_rates_dict[pair],
|
||||
mark_rates=mark_rates_dict[pair],
|
||||
futures_funding_rate=self.config.get('futures_funding_rate', None),
|
||||
)
|
||||
|
||||
if unavailable_pairs:
|
||||
raise OperationalException(
|
||||
@@ -497,7 +501,8 @@ class Backtesting:
|
||||
stake_available = self.wallets.get_available_stake_amount()
|
||||
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
|
||||
default_retval=None)(
|
||||
trade=trade, current_time=row[DATE_IDX].to_pydatetime(), current_rate=current_rate,
|
||||
trade=trade, # type: ignore[arg-type]
|
||||
current_time=row[DATE_IDX].to_pydatetime(), current_rate=current_rate,
|
||||
current_profit=current_profit, min_stake=min_stake,
|
||||
max_stake=min(max_stake, stake_available),
|
||||
current_entry_rate=current_rate, current_exit_rate=current_rate,
|
||||
@@ -540,15 +545,23 @@ class Backtesting:
|
||||
if check_adjust_entry:
|
||||
trade = self._get_adjust_trade_entry_for_candle(trade, row)
|
||||
|
||||
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
|
||||
enter = row[SHORT_IDX] if trade.is_short else row[LONG_IDX]
|
||||
exit_sig = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX]
|
||||
exit_ = self.strategy.should_exit(
|
||||
trade, row[OPEN_IDX], exit_candle_time, # type: ignore
|
||||
exits = self.strategy.should_exit(
|
||||
trade, row[OPEN_IDX], row[DATE_IDX].to_pydatetime(), # type: ignore
|
||||
enter=enter, exit_=exit_sig,
|
||||
low=row[LOW_IDX], high=row[HIGH_IDX]
|
||||
)
|
||||
for exit_ in exits:
|
||||
t = self._get_exit_for_signal(trade, row, exit_)
|
||||
if t:
|
||||
return t
|
||||
return None
|
||||
|
||||
def _get_exit_for_signal(self, trade: LocalTrade, row: Tuple,
|
||||
exit_: ExitCheckTuple) -> Optional[LocalTrade]:
|
||||
|
||||
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
|
||||
if exit_.exit_flag:
|
||||
trade.close_date = exit_candle_time
|
||||
exit_reason = exit_.exit_reason
|
||||
@@ -575,7 +588,8 @@ class Backtesting:
|
||||
if order_type == 'limit':
|
||||
close_rate = strategy_safe_wrapper(self.strategy.custom_exit_price,
|
||||
default_retval=close_rate)(
|
||||
pair=trade.pair, trade=trade,
|
||||
pair=trade.pair,
|
||||
trade=trade, # type: ignore[arg-type]
|
||||
current_time=exit_candle_time,
|
||||
proposed_rate=close_rate, current_profit=current_profit,
|
||||
exit_tag=exit_reason)
|
||||
@@ -589,7 +603,10 @@ class Backtesting:
|
||||
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,
|
||||
pair=trade.pair,
|
||||
trade=trade, # type: ignore[arg-type]
|
||||
order_type='limit',
|
||||
amount=trade.amount,
|
||||
rate=close_rate,
|
||||
time_in_force=time_in_force,
|
||||
sell_reason=exit_reason, # deprecated
|
||||
@@ -694,7 +711,7 @@ class Backtesting:
|
||||
return self._get_exit_trade_entry_for_candle(trade, row)
|
||||
|
||||
def get_valid_price_and_stake(
|
||||
self, pair: str, row: Tuple, propose_rate: float, stake_amount: Optional[float],
|
||||
self, pair: str, row: Tuple, propose_rate: float, stake_amount: float,
|
||||
direction: LongShort, current_time: datetime, entry_tag: Optional[str],
|
||||
trade: Optional[LocalTrade], order_type: str
|
||||
) -> Tuple[float, float, float, float]:
|
||||
@@ -768,8 +785,9 @@ class Backtesting:
|
||||
order_type = self.strategy.order_types['entry']
|
||||
pos_adjust = trade is not None and requested_rate is None
|
||||
|
||||
stake_amount_ = stake_amount or (trade.stake_amount if trade else 0.0)
|
||||
propose_rate, stake_amount, leverage, min_stake_amount = self.get_valid_price_and_stake(
|
||||
pair, row, row[OPEN_IDX], stake_amount, direction, current_time, entry_tag, trade,
|
||||
pair, row, row[OPEN_IDX], stake_amount_, direction, current_time, entry_tag, trade,
|
||||
order_type
|
||||
)
|
||||
|
||||
@@ -939,7 +957,9 @@ class Backtesting:
|
||||
Check if current analyzed order has to be canceled.
|
||||
Returns True if the trade should be Deleted (initial order was canceled).
|
||||
"""
|
||||
timedout = self.strategy.ft_check_timed_out(trade, order, current_time)
|
||||
timedout = self.strategy.ft_check_timed_out(
|
||||
trade, # type: ignore[arg-type]
|
||||
order, current_time)
|
||||
if timedout:
|
||||
if order.side == trade.entry_side:
|
||||
self.timedout_entry_orders += 1
|
||||
@@ -968,7 +988,8 @@ class Backtesting:
|
||||
if order.side == trade.entry_side and current_time > order.order_date_utc:
|
||||
requested_rate = strategy_safe_wrapper(self.strategy.adjust_entry_price,
|
||||
default_retval=order.price)(
|
||||
trade=trade, order=order, pair=trade.pair, current_time=current_time,
|
||||
trade=trade, # type: ignore[arg-type]
|
||||
order=order, pair=trade.pair, current_time=current_time,
|
||||
proposed_rate=row[OPEN_IDX], current_order_rate=order.price,
|
||||
entry_tag=trade.enter_tag, side=trade.trade_direction
|
||||
) # default value is current order price
|
||||
|
||||
@@ -44,7 +44,7 @@ class EdgeCli:
|
||||
|
||||
self.edge._timerange = TimeRange.parse_timerange(None if self.config.get(
|
||||
'timerange') is None else str(self.config.get('timerange')))
|
||||
self.strategy.bot_start()
|
||||
self.strategy.ft_bot_start()
|
||||
|
||||
def start(self) -> None:
|
||||
result = self.edge.calculate(self.config['exchange']['pair_whitelist'])
|
||||
|
||||
@@ -27,8 +27,7 @@ from freqtrade.misc import deep_merge_dicts, file_dump_json, plural
|
||||
from freqtrade.optimize.backtesting import Backtesting
|
||||
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
|
||||
from freqtrade.optimize.hyperopt_auto import HyperOptAuto
|
||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401
|
||||
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401
|
||||
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss
|
||||
from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer
|
||||
from freqtrade.optimize.optimize_reports import generate_strategy_stats
|
||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
|
||||
@@ -62,7 +61,6 @@ class Hyperopt:
|
||||
hyperopt = Hyperopt(config)
|
||||
hyperopt.start()
|
||||
"""
|
||||
custom_hyperopt: IHyperOpt
|
||||
|
||||
def __init__(self, config: Dict[str, Any]) -> None:
|
||||
self.buy_space: List[Dimension] = []
|
||||
@@ -77,6 +75,7 @@ class Hyperopt:
|
||||
|
||||
self.backtesting = Backtesting(self.config)
|
||||
self.pairlist = self.backtesting.pairlists.whitelist
|
||||
self.custom_hyperopt: HyperOptAuto
|
||||
|
||||
if not self.config.get('hyperopt'):
|
||||
self.custom_hyperopt = HyperOptAuto(self.config)
|
||||
@@ -88,7 +87,8 @@ class Hyperopt:
|
||||
self.backtesting._set_strategy(self.backtesting.strategylist[0])
|
||||
self.custom_hyperopt.strategy = self.backtesting.strategy
|
||||
|
||||
self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config)
|
||||
self.custom_hyperoptloss: IHyperOptLoss = HyperOptLossResolver.load_hyperoptloss(
|
||||
self.config)
|
||||
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
|
||||
time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
strategy = str(self.config['strategy'])
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# flake8: noqa: F401
|
||||
|
||||
from freqtrade.persistence.models import clean_dry_run_db, cleanup_db, init_db
|
||||
from freqtrade.persistence.models import cleanup_db, init_db
|
||||
from freqtrade.persistence.pairlock_middleware import PairLocks
|
||||
from freqtrade.persistence.trade_model import LocalTrade, Order, Trade
|
||||
|
||||
@@ -21,14 +21,12 @@ logger = logging.getLogger(__name__)
|
||||
_SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
|
||||
|
||||
|
||||
def init_db(db_url: str, clean_open_orders: bool = False) -> None:
|
||||
def init_db(db_url: str) -> None:
|
||||
"""
|
||||
Initializes this module with the given config,
|
||||
registers all known command handlers
|
||||
and starts polling for message updates
|
||||
:param db_url: Database to use
|
||||
:param clean_open_orders: Remove open orders from the database.
|
||||
Useful for dry-run or if all orders have been reset on the exchange.
|
||||
:return: None
|
||||
"""
|
||||
kwargs = {}
|
||||
@@ -64,10 +62,6 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None:
|
||||
_DECL_BASE.metadata.create_all(engine)
|
||||
check_migrate(engine, decl_base=_DECL_BASE, previous_tables=previous_tables)
|
||||
|
||||
# Clean dry_run DB if the db is not in-memory
|
||||
if clean_open_orders and db_url != 'sqlite://':
|
||||
clean_dry_run_db()
|
||||
|
||||
|
||||
def cleanup_db() -> None:
|
||||
"""
|
||||
@@ -75,15 +69,3 @@ def cleanup_db() -> None:
|
||||
:return: None
|
||||
"""
|
||||
Trade.commit()
|
||||
|
||||
|
||||
def clean_dry_run_db() -> None:
|
||||
"""
|
||||
Remove open_order_id from a Dry_run DB
|
||||
:return: None
|
||||
"""
|
||||
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
||||
# Check we are updating only a dry_run order not a prod one
|
||||
if 'dry_run' in trade.open_order_id:
|
||||
trade.open_order_id = None
|
||||
Trade.commit()
|
||||
|
||||
@@ -120,6 +120,25 @@ class Order(_DECL_BASE):
|
||||
self.order_filled_date = datetime.now(timezone.utc)
|
||||
self.order_update_date = datetime.now(timezone.utc)
|
||||
|
||||
def to_ccxt_object(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'id': self.order_id,
|
||||
'symbol': self.ft_pair,
|
||||
'price': self.price,
|
||||
'average': self.average,
|
||||
'amount': self.amount,
|
||||
'cost': self.cost,
|
||||
'type': self.order_type,
|
||||
'side': self.ft_order_side,
|
||||
'filled': self.filled,
|
||||
'remaining': self.remaining,
|
||||
'datetime': self.order_date_utc.strftime('%Y-%m-%dT%H:%M:%S.%f'),
|
||||
'timestamp': int(self.order_date_utc.timestamp() * 1000),
|
||||
'status': self.status,
|
||||
'fee': None,
|
||||
'info': {},
|
||||
}
|
||||
|
||||
def to_json(self, entry_side: str) -> Dict[str, Any]:
|
||||
return {
|
||||
'pair': self.ft_pair,
|
||||
@@ -192,6 +211,14 @@ class Order(_DECL_BASE):
|
||||
"""
|
||||
return Order.query.filter(Order.ft_is_open.is_(True)).all()
|
||||
|
||||
@staticmethod
|
||||
def order_by_id(order_id: str) -> Optional['Order']:
|
||||
"""
|
||||
Retrieve order based on order_id
|
||||
:return: Order or None
|
||||
"""
|
||||
return Order.query.filter(Order.order_id == order_id).first()
|
||||
|
||||
|
||||
class LocalTrade():
|
||||
"""
|
||||
@@ -892,8 +919,8 @@ class LocalTrade():
|
||||
return o
|
||||
return None
|
||||
|
||||
def select_order(
|
||||
self, order_side: str = None, is_open: Optional[bool] = None) -> Optional[Order]:
|
||||
def select_order(self, order_side: Optional[str] = None,
|
||||
is_open: Optional[bool] = None) -> Optional[Order]:
|
||||
"""
|
||||
Finds latest order for this orderside and status
|
||||
:param order_side: ft_order_side of the order (either 'buy', 'sell' or 'stoploss')
|
||||
|
||||
@@ -633,7 +633,7 @@ def load_and_plot_trades(config: Dict[str, Any]):
|
||||
|
||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
|
||||
IStrategy.dp = DataProvider(config, exchange)
|
||||
strategy.bot_start()
|
||||
strategy.ft_bot_start()
|
||||
strategy.bot_loop_start()
|
||||
plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count)
|
||||
timerange = plot_elements['timerange']
|
||||
|
||||
@@ -50,7 +50,7 @@ class SpreadFilter(IPairList):
|
||||
:param ticker: ticker dict as returned from ccxt.fetch_tickers()
|
||||
:return: True if the pair can stay, false if it should be removed
|
||||
"""
|
||||
if 'bid' in ticker and 'ask' in ticker and ticker['ask']:
|
||||
if 'bid' in ticker and 'ask' in ticker and ticker['ask'] and ticker['bid']:
|
||||
spread = 1 - ticker['bid'] / ticker['ask']
|
||||
if spread > self._max_spread_ratio:
|
||||
self.log_once(f"Removed {pair} from whitelist, because spread "
|
||||
|
||||
@@ -256,6 +256,7 @@ class TradeSchema(BaseModel):
|
||||
|
||||
leverage: Optional[float]
|
||||
interest_rate: Optional[float]
|
||||
liquidation_price: Optional[float]
|
||||
funding_fees: Optional[float]
|
||||
trading_mode: Optional[TradingMode]
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# flake8: noqa: F401
|
||||
from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date,
|
||||
timeframe_to_prev_date, timeframe_to_seconds)
|
||||
from freqtrade.strategy.hyper import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||
IntParameter, RealParameter)
|
||||
from freqtrade.strategy.informative_decorator import informative
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
from freqtrade.strategy.parameters import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||
IntParameter, RealParameter)
|
||||
from freqtrade.strategy.strategy_helper import (merge_informative_pair, stoploss_from_absolute,
|
||||
stoploss_from_open)
|
||||
|
||||
@@ -3,295 +3,19 @@ IHyperStrategy interface, hyperoptable Parameter class.
|
||||
This module defines a base class for auto-hyperoptable strategies.
|
||||
"""
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple, Union
|
||||
|
||||
from freqtrade.misc import deep_merge_dicts, json_load
|
||||
from freqtrade.optimize.hyperopt_tools import HyperoptTools
|
||||
|
||||
|
||||
with suppress(ImportError):
|
||||
from skopt.space import Integer, Real, Categorical
|
||||
from freqtrade.optimize.space import SKDecimal
|
||||
from typing import Any, Dict, Iterator, List, Tuple
|
||||
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import deep_merge_dicts, json_load
|
||||
from freqtrade.optimize.hyperopt_tools import HyperoptTools
|
||||
from freqtrade.strategy.parameters import BaseParameter
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseParameter(ABC):
|
||||
"""
|
||||
Defines a parameter that can be optimized by hyperopt.
|
||||
"""
|
||||
category: Optional[str]
|
||||
default: Any
|
||||
value: Any
|
||||
in_space: bool = False
|
||||
name: str
|
||||
|
||||
def __init__(self, *, default: Any, space: Optional[str] = None,
|
||||
optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable parameter.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter field
|
||||
name is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.(Integer|Real|Categorical).
|
||||
"""
|
||||
if 'name' in kwargs:
|
||||
raise OperationalException(
|
||||
'Name is determined by parameter field name and can not be specified manually.')
|
||||
self.category = space
|
||||
self._space_params = kwargs
|
||||
self.value = default
|
||||
self.optimize = optimize
|
||||
self.load = load
|
||||
|
||||
def __repr__(self):
|
||||
return f'{self.__class__.__name__}({self.value})'
|
||||
|
||||
@abstractmethod
|
||||
def get_space(self, name: str) -> Union['Integer', 'Real', 'SKDecimal', 'Categorical']:
|
||||
"""
|
||||
Get-space - will be used by Hyperopt to get the hyperopt Space
|
||||
"""
|
||||
|
||||
|
||||
class NumericParameter(BaseParameter):
|
||||
""" Internal parameter used for Numeric purposes """
|
||||
float_or_int = Union[int, float]
|
||||
default: float_or_int
|
||||
value: float_or_int
|
||||
|
||||
def __init__(self, low: Union[float_or_int, Sequence[float_or_int]],
|
||||
high: Optional[float_or_int] = None, *, default: float_or_int,
|
||||
space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable numeric parameter.
|
||||
Cannot be instantiated, but provides the validation for other numeric parameters
|
||||
:param low: Lower end (inclusive) of optimization space or [low, high].
|
||||
:param high: Upper end (inclusive) of optimization space.
|
||||
Must be none of entire range is passed first parameter.
|
||||
:param default: A default value.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter fieldname is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.*.
|
||||
"""
|
||||
if high is not None and isinstance(low, Sequence):
|
||||
raise OperationalException(f'{self.__class__.__name__} space invalid.')
|
||||
if high is None or isinstance(low, Sequence):
|
||||
if not isinstance(low, Sequence) or len(low) != 2:
|
||||
raise OperationalException(f'{self.__class__.__name__} space must be [low, high]')
|
||||
self.low, self.high = low
|
||||
else:
|
||||
self.low = low
|
||||
self.high = high
|
||||
|
||||
super().__init__(default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
||||
|
||||
|
||||
class IntParameter(NumericParameter):
|
||||
default: int
|
||||
value: int
|
||||
|
||||
def __init__(self, low: Union[int, Sequence[int]], high: Optional[int] = None, *, default: int,
|
||||
space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable integer parameter.
|
||||
:param low: Lower end (inclusive) of optimization space or [low, high].
|
||||
:param high: Upper end (inclusive) of optimization space.
|
||||
Must be none of entire range is passed first parameter.
|
||||
:param default: A default value.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter fieldname is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.Integer.
|
||||
"""
|
||||
|
||||
super().__init__(low=low, high=high, default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
||||
|
||||
def get_space(self, name: str) -> 'Integer':
|
||||
"""
|
||||
Create skopt optimization space.
|
||||
:param name: A name of parameter field.
|
||||
"""
|
||||
return Integer(low=self.low, high=self.high, name=name, **self._space_params)
|
||||
|
||||
@property
|
||||
def range(self):
|
||||
"""
|
||||
Get each value in this space as list.
|
||||
Returns a List from low to high (inclusive) in Hyperopt mode.
|
||||
Returns a List with 1 item (`value`) in "non-hyperopt" mode, to avoid
|
||||
calculating 100ds of indicators.
|
||||
"""
|
||||
if self.in_space and self.optimize:
|
||||
# Scikit-optimize ranges are "inclusive", while python's "range" is exclusive
|
||||
return range(self.low, self.high + 1)
|
||||
else:
|
||||
return range(self.value, self.value + 1)
|
||||
|
||||
|
||||
class RealParameter(NumericParameter):
|
||||
default: float
|
||||
value: float
|
||||
|
||||
def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *,
|
||||
default: float, space: Optional[str] = None, optimize: bool = True,
|
||||
load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable floating point parameter with unlimited precision.
|
||||
:param low: Lower end (inclusive) of optimization space or [low, high].
|
||||
:param high: Upper end (inclusive) of optimization space.
|
||||
Must be none if entire range is passed first parameter.
|
||||
:param default: A default value.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter fieldname is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.Real.
|
||||
"""
|
||||
super().__init__(low=low, high=high, default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
||||
|
||||
def get_space(self, name: str) -> 'Real':
|
||||
"""
|
||||
Create skopt optimization space.
|
||||
:param name: A name of parameter field.
|
||||
"""
|
||||
return Real(low=self.low, high=self.high, name=name, **self._space_params)
|
||||
|
||||
|
||||
class DecimalParameter(NumericParameter):
|
||||
default: float
|
||||
value: float
|
||||
|
||||
def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *,
|
||||
default: float, decimals: int = 3, space: Optional[str] = None,
|
||||
optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable decimal parameter with a limited precision.
|
||||
:param low: Lower end (inclusive) of optimization space or [low, high].
|
||||
:param high: Upper end (inclusive) of optimization space.
|
||||
Must be none if entire range is passed first parameter.
|
||||
:param default: A default value.
|
||||
:param decimals: A number of decimals after floating point to be included in testing.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter fieldname is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.Integer.
|
||||
"""
|
||||
self._decimals = decimals
|
||||
default = round(default, self._decimals)
|
||||
|
||||
super().__init__(low=low, high=high, default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
||||
|
||||
def get_space(self, name: str) -> 'SKDecimal':
|
||||
"""
|
||||
Create skopt optimization space.
|
||||
:param name: A name of parameter field.
|
||||
"""
|
||||
return SKDecimal(low=self.low, high=self.high, decimals=self._decimals, name=name,
|
||||
**self._space_params)
|
||||
|
||||
@property
|
||||
def range(self):
|
||||
"""
|
||||
Get each value in this space as list.
|
||||
Returns a List from low to high (inclusive) in Hyperopt mode.
|
||||
Returns a List with 1 item (`value`) in "non-hyperopt" mode, to avoid
|
||||
calculating 100ds of indicators.
|
||||
"""
|
||||
if self.in_space and self.optimize:
|
||||
low = int(self.low * pow(10, self._decimals))
|
||||
high = int(self.high * pow(10, self._decimals)) + 1
|
||||
return [round(n * pow(0.1, self._decimals), self._decimals) for n in range(low, high)]
|
||||
else:
|
||||
return [self.value]
|
||||
|
||||
|
||||
class CategoricalParameter(BaseParameter):
|
||||
default: Any
|
||||
value: Any
|
||||
opt_range: Sequence[Any]
|
||||
|
||||
def __init__(self, categories: Sequence[Any], *, default: Optional[Any] = None,
|
||||
space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable parameter.
|
||||
:param categories: Optimization space, [a, b, ...].
|
||||
:param default: A default value. If not specified, first item from specified space will be
|
||||
used.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter field
|
||||
name is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.Categorical.
|
||||
"""
|
||||
if len(categories) < 2:
|
||||
raise OperationalException(
|
||||
'CategoricalParameter space must be [a, b, ...] (at least two parameters)')
|
||||
self.opt_range = categories
|
||||
super().__init__(default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
||||
|
||||
def get_space(self, name: str) -> 'Categorical':
|
||||
"""
|
||||
Create skopt optimization space.
|
||||
:param name: A name of parameter field.
|
||||
"""
|
||||
return Categorical(self.opt_range, name=name, **self._space_params)
|
||||
|
||||
@property
|
||||
def range(self):
|
||||
"""
|
||||
Get each value in this space as list.
|
||||
Returns a List of categories in Hyperopt mode.
|
||||
Returns a List with 1 item (`value`) in "non-hyperopt" mode, to avoid
|
||||
calculating 100ds of indicators.
|
||||
"""
|
||||
if self.in_space and self.optimize:
|
||||
return self.opt_range
|
||||
else:
|
||||
return [self.value]
|
||||
|
||||
|
||||
class BooleanParameter(CategoricalParameter):
|
||||
|
||||
def __init__(self, *, default: Optional[Any] = None,
|
||||
space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable Boolean Parameter.
|
||||
It's a shortcut to `CategoricalParameter([True, False])`.
|
||||
:param default: A default value. If not specified, first item from specified space will be
|
||||
used.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter field
|
||||
name is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.Categorical.
|
||||
"""
|
||||
|
||||
categories = [True, False]
|
||||
super().__init__(categories=categories, default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
||||
|
||||
|
||||
class HyperStrategyMixin:
|
||||
"""
|
||||
A helper base class which allows HyperOptAuto class to reuse implementations of buy/sell
|
||||
@@ -345,7 +69,7 @@ class HyperStrategyMixin:
|
||||
@classmethod
|
||||
def detect_all_parameters(cls) -> Dict:
|
||||
""" Detect all parameters and return them as a list"""
|
||||
params: Dict = {
|
||||
params: Dict[str, Any] = {
|
||||
'buy': list(cls.detect_parameters('buy')),
|
||||
'sell': list(cls.detect_parameters('sell')),
|
||||
'protection': list(cls.detect_parameters('protection')),
|
||||
@@ -424,7 +148,7 @@ class HyperStrategyMixin:
|
||||
"""
|
||||
Returns list of Parameters that are not part of the current optimize job
|
||||
"""
|
||||
params = {
|
||||
params: Dict[str, Dict] = {
|
||||
'buy': {},
|
||||
'sell': {},
|
||||
'protection': {},
|
||||
|
||||
@@ -16,7 +16,7 @@ from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, SignalDirecti
|
||||
SignalType, TradingMode)
|
||||
from freqtrade.exceptions import OperationalException, StrategyError
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds
|
||||
from freqtrade.persistence import LocalTrade, Order, PairLocks, Trade
|
||||
from freqtrade.persistence import Order, PairLocks, Trade
|
||||
from freqtrade.strategy.hyper import HyperStrategyMixin
|
||||
from freqtrade.strategy.informative_decorator import (InformativeData, PopulateIndicators,
|
||||
_create_and_merge_informative_pair,
|
||||
@@ -82,7 +82,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
}
|
||||
|
||||
# run "populate_indicators" only for new candle
|
||||
process_only_new_candles: bool = False
|
||||
process_only_new_candles: bool = True
|
||||
|
||||
use_exit_signal: bool
|
||||
exit_profit_only: bool
|
||||
@@ -144,6 +144,13 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
informative_data.candle_type = config['candle_type_def']
|
||||
self._ft_informative.append((informative_data, cls_method))
|
||||
|
||||
def ft_bot_start(self, **kwargs) -> None:
|
||||
"""
|
||||
Strategy init - runs after dataprovider has been added.
|
||||
Must call bot_start()
|
||||
"""
|
||||
strategy_safe_wrapper(self.bot_start)()
|
||||
|
||||
@abstractmethod
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
@@ -429,7 +436,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
return self.custom_sell(pair, trade, current_time, current_rate, current_profit, **kwargs)
|
||||
|
||||
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_stake: float, min_stake: float, max_stake: float,
|
||||
proposed_stake: float, min_stake: Optional[float], max_stake: float,
|
||||
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||
"""
|
||||
Customize stake size for each new trade.
|
||||
@@ -447,8 +454,9 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
return proposed_stake
|
||||
|
||||
def adjust_trade_position(self, trade: Trade, current_time: datetime,
|
||||
current_rate: float, current_profit: float, min_stake: float,
|
||||
max_stake: float, **kwargs) -> Optional[float]:
|
||||
current_rate: float, current_profit: float,
|
||||
min_stake: Optional[float], max_stake: float,
|
||||
**kwargs) -> Optional[float]:
|
||||
"""
|
||||
Custom trade adjustment logic, returning the stake amount that a trade should be increased.
|
||||
This means extra buy orders with additional fees.
|
||||
@@ -878,16 +886,16 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
def should_exit(self, trade: Trade, rate: float, current_time: datetime, *,
|
||||
enter: bool, exit_: bool,
|
||||
low: float = None, high: float = None,
|
||||
force_stoploss: float = 0) -> ExitCheckTuple:
|
||||
force_stoploss: float = 0) -> List[ExitCheckTuple]:
|
||||
"""
|
||||
This function evaluates if one of the conditions required to trigger an exit order
|
||||
has been reached, which can either be a stop-loss, ROI or exit-signal.
|
||||
:param low: Only used during backtesting to simulate (long)stoploss/(short)ROI
|
||||
:param high: Only used during backtesting, to simulate (short)stoploss/(long)ROI
|
||||
:param force_stoploss: Externally provided stoploss
|
||||
:return: True if trade should be exited, False otherwise
|
||||
:return: List of exit reasons - or empty list.
|
||||
"""
|
||||
|
||||
exits: List[ExitCheckTuple] = []
|
||||
current_rate = rate
|
||||
current_profit = trade.calc_profit_ratio(current_rate)
|
||||
|
||||
@@ -917,19 +925,20 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
if exit_ and not enter:
|
||||
exit_signal = ExitType.EXIT_SIGNAL
|
||||
else:
|
||||
custom_reason = strategy_safe_wrapper(self.custom_exit, default_retval=False)(
|
||||
reason_cust = strategy_safe_wrapper(self.custom_exit, default_retval=False)(
|
||||
pair=trade.pair, trade=trade, current_time=current_time,
|
||||
current_rate=current_rate, current_profit=current_profit)
|
||||
if custom_reason:
|
||||
if reason_cust:
|
||||
exit_signal = ExitType.CUSTOM_EXIT
|
||||
if isinstance(custom_reason, str):
|
||||
if len(custom_reason) > CUSTOM_EXIT_MAX_LENGTH:
|
||||
if isinstance(reason_cust, str):
|
||||
custom_reason = reason_cust
|
||||
if len(reason_cust) > CUSTOM_EXIT_MAX_LENGTH:
|
||||
logger.warning(f'Custom exit reason returned from '
|
||||
f'custom_exit is too long and was trimmed'
|
||||
f'to {CUSTOM_EXIT_MAX_LENGTH} characters.')
|
||||
custom_reason = custom_reason[:CUSTOM_EXIT_MAX_LENGTH]
|
||||
custom_reason = reason_cust[:CUSTOM_EXIT_MAX_LENGTH]
|
||||
else:
|
||||
custom_reason = None
|
||||
custom_reason = ''
|
||||
if (
|
||||
exit_signal == ExitType.CUSTOM_EXIT
|
||||
or (exit_signal == ExitType.EXIT_SIGNAL
|
||||
@@ -938,24 +947,29 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
logger.debug(f"{trade.pair} - Sell signal received. "
|
||||
f"exit_type=ExitType.{exit_signal.name}" +
|
||||
(f", custom_reason={custom_reason}" if custom_reason else ""))
|
||||
return ExitCheckTuple(exit_type=exit_signal, exit_reason=custom_reason)
|
||||
exits.append(ExitCheckTuple(exit_type=exit_signal, exit_reason=custom_reason))
|
||||
|
||||
# Sequence:
|
||||
# Exit-signal
|
||||
# ROI (if not stoploss)
|
||||
# Stoploss
|
||||
if roi_reached and stoplossflag.exit_type != ExitType.STOP_LOSS:
|
||||
logger.debug(f"{trade.pair} - Required profit reached. exit_type=ExitType.ROI")
|
||||
return ExitCheckTuple(exit_type=ExitType.ROI)
|
||||
# ROI
|
||||
# Trailing stoploss
|
||||
|
||||
if stoplossflag.exit_flag:
|
||||
if stoplossflag.exit_type == ExitType.STOP_LOSS:
|
||||
|
||||
logger.debug(f"{trade.pair} - Stoploss hit. exit_type={stoplossflag.exit_type}")
|
||||
return stoplossflag
|
||||
exits.append(stoplossflag)
|
||||
|
||||
# This one is noisy, commented out...
|
||||
# logger.debug(f"{trade.pair} - No exit signal.")
|
||||
return ExitCheckTuple(exit_type=ExitType.NONE)
|
||||
if roi_reached:
|
||||
logger.debug(f"{trade.pair} - Required profit reached. exit_type=ExitType.ROI")
|
||||
exits.append(ExitCheckTuple(exit_type=ExitType.ROI))
|
||||
|
||||
if stoplossflag.exit_type == ExitType.TRAILING_STOP_LOSS:
|
||||
|
||||
logger.debug(f"{trade.pair} - Trailing stoploss hit.")
|
||||
exits.append(stoplossflag)
|
||||
|
||||
return exits
|
||||
|
||||
def stop_loss_reached(self, current_rate: float, trade: Trade,
|
||||
current_time: datetime, current_profit: float,
|
||||
@@ -1070,7 +1084,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
else:
|
||||
return current_profit > roi
|
||||
|
||||
def ft_check_timed_out(self, trade: LocalTrade, order: Order,
|
||||
def ft_check_timed_out(self, trade: Trade, order: Order,
|
||||
current_time: datetime) -> bool:
|
||||
"""
|
||||
FT Internal method.
|
||||
|
||||
289
freqtrade/strategy/parameters.py
Normal file
289
freqtrade/strategy/parameters.py
Normal file
@@ -0,0 +1,289 @@
|
||||
"""
|
||||
IHyperStrategy interface, hyperoptable Parameter class.
|
||||
This module defines a base class for auto-hyperoptable strategies.
|
||||
"""
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import suppress
|
||||
from typing import Any, Optional, Sequence, Union
|
||||
|
||||
|
||||
with suppress(ImportError):
|
||||
from skopt.space import Integer, Real, Categorical
|
||||
from freqtrade.optimize.space import SKDecimal
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseParameter(ABC):
|
||||
"""
|
||||
Defines a parameter that can be optimized by hyperopt.
|
||||
"""
|
||||
category: Optional[str]
|
||||
default: Any
|
||||
value: Any
|
||||
in_space: bool = False
|
||||
name: str
|
||||
|
||||
def __init__(self, *, default: Any, space: Optional[str] = None,
|
||||
optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable parameter.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter field
|
||||
name is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.(Integer|Real|Categorical).
|
||||
"""
|
||||
if 'name' in kwargs:
|
||||
raise OperationalException(
|
||||
'Name is determined by parameter field name and can not be specified manually.')
|
||||
self.category = space
|
||||
self._space_params = kwargs
|
||||
self.value = default
|
||||
self.optimize = optimize
|
||||
self.load = load
|
||||
|
||||
def __repr__(self):
|
||||
return f'{self.__class__.__name__}({self.value})'
|
||||
|
||||
@abstractmethod
|
||||
def get_space(self, name: str) -> Union['Integer', 'Real', 'SKDecimal', 'Categorical']:
|
||||
"""
|
||||
Get-space - will be used by Hyperopt to get the hyperopt Space
|
||||
"""
|
||||
|
||||
|
||||
class NumericParameter(BaseParameter):
|
||||
""" Internal parameter used for Numeric purposes """
|
||||
float_or_int = Union[int, float]
|
||||
default: float_or_int
|
||||
value: float_or_int
|
||||
|
||||
def __init__(self, low: Union[float_or_int, Sequence[float_or_int]],
|
||||
high: Optional[float_or_int] = None, *, default: float_or_int,
|
||||
space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable numeric parameter.
|
||||
Cannot be instantiated, but provides the validation for other numeric parameters
|
||||
:param low: Lower end (inclusive) of optimization space or [low, high].
|
||||
:param high: Upper end (inclusive) of optimization space.
|
||||
Must be none of entire range is passed first parameter.
|
||||
:param default: A default value.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter fieldname is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.*.
|
||||
"""
|
||||
if high is not None and isinstance(low, Sequence):
|
||||
raise OperationalException(f'{self.__class__.__name__} space invalid.')
|
||||
if high is None or isinstance(low, Sequence):
|
||||
if not isinstance(low, Sequence) or len(low) != 2:
|
||||
raise OperationalException(f'{self.__class__.__name__} space must be [low, high]')
|
||||
self.low, self.high = low
|
||||
else:
|
||||
self.low = low
|
||||
self.high = high
|
||||
|
||||
super().__init__(default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
||||
|
||||
|
||||
class IntParameter(NumericParameter):
|
||||
default: int
|
||||
value: int
|
||||
low: int
|
||||
high: int
|
||||
|
||||
def __init__(self, low: Union[int, Sequence[int]], high: Optional[int] = None, *, default: int,
|
||||
space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable integer parameter.
|
||||
:param low: Lower end (inclusive) of optimization space or [low, high].
|
||||
:param high: Upper end (inclusive) of optimization space.
|
||||
Must be none of entire range is passed first parameter.
|
||||
:param default: A default value.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter fieldname is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.Integer.
|
||||
"""
|
||||
|
||||
super().__init__(low=low, high=high, default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
||||
|
||||
def get_space(self, name: str) -> 'Integer':
|
||||
"""
|
||||
Create skopt optimization space.
|
||||
:param name: A name of parameter field.
|
||||
"""
|
||||
return Integer(low=self.low, high=self.high, name=name, **self._space_params)
|
||||
|
||||
@property
|
||||
def range(self):
|
||||
"""
|
||||
Get each value in this space as list.
|
||||
Returns a List from low to high (inclusive) in Hyperopt mode.
|
||||
Returns a List with 1 item (`value`) in "non-hyperopt" mode, to avoid
|
||||
calculating 100ds of indicators.
|
||||
"""
|
||||
if self.in_space and self.optimize:
|
||||
# Scikit-optimize ranges are "inclusive", while python's "range" is exclusive
|
||||
return range(self.low, self.high + 1)
|
||||
else:
|
||||
return range(self.value, self.value + 1)
|
||||
|
||||
|
||||
class RealParameter(NumericParameter):
|
||||
default: float
|
||||
value: float
|
||||
|
||||
def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *,
|
||||
default: float, space: Optional[str] = None, optimize: bool = True,
|
||||
load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable floating point parameter with unlimited precision.
|
||||
:param low: Lower end (inclusive) of optimization space or [low, high].
|
||||
:param high: Upper end (inclusive) of optimization space.
|
||||
Must be none if entire range is passed first parameter.
|
||||
:param default: A default value.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter fieldname is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.Real.
|
||||
"""
|
||||
super().__init__(low=low, high=high, default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
||||
|
||||
def get_space(self, name: str) -> 'Real':
|
||||
"""
|
||||
Create skopt optimization space.
|
||||
:param name: A name of parameter field.
|
||||
"""
|
||||
return Real(low=self.low, high=self.high, name=name, **self._space_params)
|
||||
|
||||
|
||||
class DecimalParameter(NumericParameter):
|
||||
default: float
|
||||
value: float
|
||||
|
||||
def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *,
|
||||
default: float, decimals: int = 3, space: Optional[str] = None,
|
||||
optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable decimal parameter with a limited precision.
|
||||
:param low: Lower end (inclusive) of optimization space or [low, high].
|
||||
:param high: Upper end (inclusive) of optimization space.
|
||||
Must be none if entire range is passed first parameter.
|
||||
:param default: A default value.
|
||||
:param decimals: A number of decimals after floating point to be included in testing.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter fieldname is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.Integer.
|
||||
"""
|
||||
self._decimals = decimals
|
||||
default = round(default, self._decimals)
|
||||
|
||||
super().__init__(low=low, high=high, default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
||||
|
||||
def get_space(self, name: str) -> 'SKDecimal':
|
||||
"""
|
||||
Create skopt optimization space.
|
||||
:param name: A name of parameter field.
|
||||
"""
|
||||
return SKDecimal(low=self.low, high=self.high, decimals=self._decimals, name=name,
|
||||
**self._space_params)
|
||||
|
||||
@property
|
||||
def range(self):
|
||||
"""
|
||||
Get each value in this space as list.
|
||||
Returns a List from low to high (inclusive) in Hyperopt mode.
|
||||
Returns a List with 1 item (`value`) in "non-hyperopt" mode, to avoid
|
||||
calculating 100ds of indicators.
|
||||
"""
|
||||
if self.in_space and self.optimize:
|
||||
low = int(self.low * pow(10, self._decimals))
|
||||
high = int(self.high * pow(10, self._decimals)) + 1
|
||||
return [round(n * pow(0.1, self._decimals), self._decimals) for n in range(low, high)]
|
||||
else:
|
||||
return [self.value]
|
||||
|
||||
|
||||
class CategoricalParameter(BaseParameter):
|
||||
default: Any
|
||||
value: Any
|
||||
opt_range: Sequence[Any]
|
||||
|
||||
def __init__(self, categories: Sequence[Any], *, default: Optional[Any] = None,
|
||||
space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable parameter.
|
||||
:param categories: Optimization space, [a, b, ...].
|
||||
:param default: A default value. If not specified, first item from specified space will be
|
||||
used.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter field
|
||||
name is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.Categorical.
|
||||
"""
|
||||
if len(categories) < 2:
|
||||
raise OperationalException(
|
||||
'CategoricalParameter space must be [a, b, ...] (at least two parameters)')
|
||||
self.opt_range = categories
|
||||
super().__init__(default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
||||
|
||||
def get_space(self, name: str) -> 'Categorical':
|
||||
"""
|
||||
Create skopt optimization space.
|
||||
:param name: A name of parameter field.
|
||||
"""
|
||||
return Categorical(self.opt_range, name=name, **self._space_params)
|
||||
|
||||
@property
|
||||
def range(self):
|
||||
"""
|
||||
Get each value in this space as list.
|
||||
Returns a List of categories in Hyperopt mode.
|
||||
Returns a List with 1 item (`value`) in "non-hyperopt" mode, to avoid
|
||||
calculating 100ds of indicators.
|
||||
"""
|
||||
if self.in_space and self.optimize:
|
||||
return self.opt_range
|
||||
else:
|
||||
return [self.value]
|
||||
|
||||
|
||||
class BooleanParameter(CategoricalParameter):
|
||||
|
||||
def __init__(self, *, default: Optional[Any] = None,
|
||||
space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable Boolean Parameter.
|
||||
It's a shortcut to `CategoricalParameter([True, False])`.
|
||||
:param default: A default value. If not specified, first item from specified space will be
|
||||
used.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter field
|
||||
name is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.Categorical.
|
||||
"""
|
||||
|
||||
categories = [True, False]
|
||||
super().__init__(categories=categories, default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
||||
@@ -1,5 +1,7 @@
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from functools import wraps
|
||||
from typing import Any, Callable, TypeVar, cast
|
||||
|
||||
from freqtrade.exceptions import StrategyError
|
||||
|
||||
@@ -7,12 +9,16 @@ from freqtrade.exceptions import StrategyError
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def strategy_safe_wrapper(f, message: str = "", default_retval=None, supress_error=False):
|
||||
F = TypeVar('F', bound=Callable[..., Any])
|
||||
|
||||
|
||||
def strategy_safe_wrapper(f: F, message: str = "", default_retval=None, supress_error=False) -> F:
|
||||
"""
|
||||
Wrapper around user-provided methods and functions.
|
||||
Caches all exceptions and returns either the default_retval (if it's not None) or raises
|
||||
a StrategyError exception, which then needs to be handled by the calling method.
|
||||
"""
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
if 'trade' in kwargs:
|
||||
@@ -37,4 +43,4 @@ def strategy_safe_wrapper(f, message: str = "", default_retval=None, supress_err
|
||||
raise StrategyError(str(error)) from error
|
||||
return default_retval
|
||||
|
||||
return wrapper
|
||||
return cast(F, wrapper)
|
||||
|
||||
@@ -6,7 +6,7 @@ import numpy as np # noqa
|
||||
import pandas as pd # noqa
|
||||
from pandas import DataFrame # noqa
|
||||
from datetime import datetime # noqa
|
||||
from typing import Optional # noqa
|
||||
from typing import Optional, Union # noqa
|
||||
|
||||
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||
IStrategy, IntParameter)
|
||||
@@ -64,7 +64,7 @@ class {{ strategy }}(IStrategy):
|
||||
# trailing_stop_positive_offset = 0.0 # Disabled / not configured
|
||||
|
||||
# Run "populate_indicators()" only for new candle.
|
||||
process_only_new_candles = False
|
||||
process_only_new_candles = True
|
||||
|
||||
# These values can be overridden in the config.
|
||||
use_exit_signal = True
|
||||
|
||||
@@ -62,7 +62,7 @@ class SampleStrategy(IStrategy):
|
||||
timeframe = '5m'
|
||||
|
||||
# Run "populate_indicators()" only for new candle.
|
||||
process_only_new_candles = False
|
||||
process_only_new_candles = True
|
||||
|
||||
# These values can be overridden in the config.
|
||||
use_exit_signal = True
|
||||
|
||||
@@ -13,7 +13,7 @@ def bot_loop_start(self, **kwargs) -> None:
|
||||
pass
|
||||
|
||||
def custom_entry_price(self, pair: str, current_time: 'datetime', proposed_rate: float,
|
||||
entry_tag: Optional[str], **kwargs) -> float:
|
||||
entry_tag: 'Optional[str]', side: str, **kwargs) -> float:
|
||||
"""
|
||||
Custom entry price logic, returning the new entry price.
|
||||
|
||||
@@ -80,8 +80,8 @@ def custom_exit_price(self, pair: str, trade: 'Trade',
|
||||
return proposed_rate
|
||||
|
||||
def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: float,
|
||||
proposed_stake: float, min_stake: float, max_stake: float,
|
||||
side: str, entry_tag: Optional[str], **kwargs) -> float:
|
||||
proposed_stake: float, min_stake: Optional[float], max_stake: float,
|
||||
entry_tag: 'Optional[str]', side: str, **kwargs) -> float:
|
||||
"""
|
||||
Customize stake size for each new trade.
|
||||
|
||||
@@ -244,8 +244,8 @@ def check_exit_timeout(self, pair: str, trade: 'Trade', order: 'Order',
|
||||
return False
|
||||
|
||||
def adjust_trade_position(self, trade: 'Trade', current_time: 'datetime',
|
||||
current_rate: float, current_profit: float, min_stake: float,
|
||||
max_stake: float, **kwargs) -> Optional[float]:
|
||||
current_rate: float, current_profit: float, min_stake: Optional[float],
|
||||
max_stake: float, **kwargs) -> 'Optional[float]':
|
||||
"""
|
||||
Custom trade adjustment logic, returning the stake amount that a trade should be increased.
|
||||
This means extra buy orders with additional fees.
|
||||
|
||||
Reference in New Issue
Block a user