Merge branch 'develop' into pr/cryptomeisternox/5150
This commit is contained in:
@@ -11,16 +11,17 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency
|
||||
from freqtrade.configuration import TimeRange, validate_config_consistency
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||
from freqtrade.data import history
|
||||
from freqtrade.data.btanalysis import trade_list_to_dataframe
|
||||
from freqtrade.data.converter import trim_dataframes
|
||||
from freqtrade.data.converter import trim_dataframe, trim_dataframes
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.enums import SellType
|
||||
from freqtrade.enums import BacktestState, SellType
|
||||
from freqtrade.exceptions import DependencyException, OperationalException
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
||||
from freqtrade.mixins import LoggingMixin
|
||||
from freqtrade.optimize.bt_progress import BTProgress
|
||||
from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results,
|
||||
store_backtest_stats)
|
||||
from freqtrade.persistence import LocalTrade, PairLocks, Trade
|
||||
@@ -42,6 +43,7 @@ CLOSE_IDX = 3
|
||||
SELL_IDX = 4
|
||||
LOW_IDX = 5
|
||||
HIGH_IDX = 6
|
||||
BUY_TAG_IDX = 7
|
||||
|
||||
|
||||
class Backtesting:
|
||||
@@ -57,9 +59,9 @@ class Backtesting:
|
||||
|
||||
LoggingMixin.show_output = False
|
||||
self.config = config
|
||||
self.results: Optional[Dict[str, Any]] = None
|
||||
|
||||
# Reset keys for backtesting
|
||||
remove_credentials(self.config)
|
||||
config['dry_run'] = True
|
||||
self.strategylist: List[IStrategy] = []
|
||||
self.all_results: Dict[str, Dict] = {}
|
||||
|
||||
@@ -83,7 +85,7 @@ class Backtesting:
|
||||
"configuration or as cli argument `--timeframe 5m`")
|
||||
self.timeframe = str(self.config.get('timeframe'))
|
||||
self.timeframe_min = timeframe_to_minutes(self.timeframe)
|
||||
|
||||
self.init_backtest_detail()
|
||||
self.pairlists = PairListManager(self.exchange, self.config)
|
||||
if 'VolumePairList' in self.pairlists.name_list:
|
||||
raise OperationalException("VolumePairList not allowed for backtesting.")
|
||||
@@ -106,52 +108,79 @@ class Backtesting:
|
||||
else:
|
||||
self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0])
|
||||
|
||||
Trade.use_db = False
|
||||
Trade.reset_trades()
|
||||
PairLocks.timeframe = self.config['timeframe']
|
||||
PairLocks.use_db = False
|
||||
PairLocks.reset_locks()
|
||||
|
||||
self.wallets = Wallets(self.config, self.exchange, log=False)
|
||||
self.timerange = TimeRange.parse_timerange(
|
||||
None if self.config.get('timerange') is None else str(self.config.get('timerange')))
|
||||
|
||||
# Get maximum required startup period
|
||||
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
|
||||
# Add maximum startup candle count to configuration for informative pairs support
|
||||
self.config['startup_candle_count'] = self.required_startup
|
||||
self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe)
|
||||
self.init_backtest()
|
||||
|
||||
def __del__(self):
|
||||
self.cleanup()
|
||||
|
||||
def cleanup(self):
|
||||
LoggingMixin.show_output = True
|
||||
PairLocks.use_db = True
|
||||
Trade.use_db = True
|
||||
|
||||
def init_backtest_detail(self):
|
||||
# Load detail timeframe if specified
|
||||
self.timeframe_detail = str(self.config.get('timeframe_detail', ''))
|
||||
if self.timeframe_detail:
|
||||
self.timeframe_detail_min = timeframe_to_minutes(self.timeframe_detail)
|
||||
if self.timeframe_min <= self.timeframe_detail_min:
|
||||
raise OperationalException(
|
||||
"Detail timeframe must be smaller than strategy timeframe.")
|
||||
|
||||
else:
|
||||
self.timeframe_detail_min = 0
|
||||
self.detail_data: Dict[str, DataFrame] = {}
|
||||
|
||||
def init_backtest(self):
|
||||
|
||||
self.prepare_backtest(False)
|
||||
|
||||
self.wallets = Wallets(self.config, self.exchange, log=False)
|
||||
|
||||
self.progress = BTProgress()
|
||||
self.abort = False
|
||||
|
||||
def _set_strategy(self, strategy: IStrategy):
|
||||
"""
|
||||
Load strategy into backtesting
|
||||
"""
|
||||
self.strategy: IStrategy = strategy
|
||||
strategy.dp = self.dataprovider
|
||||
# Attach Wallets to Strategy baseclass
|
||||
strategy.wallets = self.wallets
|
||||
# Set stoploss_on_exchange to false for backtesting,
|
||||
# since a "perfect" stoploss-sell is assumed anyway
|
||||
# And the regular "stoploss" function would not apply to that case
|
||||
self.strategy.order_types['stoploss_on_exchange'] = False
|
||||
|
||||
def _load_protections(self, strategy: IStrategy):
|
||||
if self.config.get('enable_protections', False):
|
||||
conf = self.config
|
||||
if hasattr(strategy, 'protections'):
|
||||
conf = deepcopy(conf)
|
||||
conf['protections'] = strategy.protections
|
||||
self.protections = ProtectionManager(conf)
|
||||
self.protections = ProtectionManager(self.config, strategy.protections)
|
||||
|
||||
def load_bt_data(self) -> Tuple[Dict[str, DataFrame], TimeRange]:
|
||||
"""
|
||||
Loads backtest data and returns the data combined with the timerange
|
||||
as tuple.
|
||||
"""
|
||||
timerange = TimeRange.parse_timerange(None if self.config.get(
|
||||
'timerange') is None else str(self.config.get('timerange')))
|
||||
self.progress.init_step(BacktestState.DATALOAD, 1)
|
||||
|
||||
data = history.load_data(
|
||||
datadir=self.config['datadir'],
|
||||
pairs=self.pairlists.whitelist,
|
||||
timeframe=self.timeframe,
|
||||
timerange=timerange,
|
||||
timerange=self.timerange,
|
||||
startup_candles=self.required_startup,
|
||||
fail_without_data=True,
|
||||
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
||||
@@ -164,10 +193,28 @@ class Backtesting:
|
||||
f'({(max_date - min_date).days} days).')
|
||||
|
||||
# Adjust startts forward if not enough data is available
|
||||
timerange.adjust_start_if_necessary(timeframe_to_seconds(self.timeframe),
|
||||
self.required_startup, min_date)
|
||||
self.timerange.adjust_start_if_necessary(timeframe_to_seconds(self.timeframe),
|
||||
self.required_startup, min_date)
|
||||
|
||||
return data, timerange
|
||||
self.progress.set_new_value(1)
|
||||
return data, self.timerange
|
||||
|
||||
def load_bt_data_detail(self) -> None:
|
||||
"""
|
||||
Loads backtest detail data (smaller timeframe) if necessary.
|
||||
"""
|
||||
if self.timeframe_detail:
|
||||
self.detail_data = history.load_data(
|
||||
datadir=self.config['datadir'],
|
||||
pairs=self.pairlists.whitelist,
|
||||
timeframe=self.timeframe_detail,
|
||||
timerange=self.timerange,
|
||||
startup_candles=0,
|
||||
fail_without_data=True,
|
||||
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
||||
)
|
||||
else:
|
||||
self.detail_data = {}
|
||||
|
||||
def prepare_backtest(self, enable_protections):
|
||||
"""
|
||||
@@ -180,6 +227,17 @@ class Backtesting:
|
||||
Trade.reset_trades()
|
||||
self.rejected_trades = 0
|
||||
self.dataprovider.clear_cache()
|
||||
if enable_protections:
|
||||
self._load_protections(self.strategy)
|
||||
|
||||
def check_abort(self):
|
||||
"""
|
||||
Check if abort was requested, raise DependencyException if that's the case
|
||||
Only applies to Interactive backtest mode (webserver mode)
|
||||
"""
|
||||
if self.abort:
|
||||
self.abort = False
|
||||
raise DependencyException("Stop requested")
|
||||
|
||||
def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]:
|
||||
"""
|
||||
@@ -189,27 +247,38 @@ class Backtesting:
|
||||
"""
|
||||
# Every change to this headers list must evaluate further usages of the resulting tuple
|
||||
# and eventually change the constants for indexes at the top
|
||||
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high']
|
||||
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high', 'buy_tag']
|
||||
data: Dict = {}
|
||||
self.progress.init_step(BacktestState.CONVERT, len(processed))
|
||||
|
||||
# Create dict with data
|
||||
for pair, pair_data in processed.items():
|
||||
self.check_abort()
|
||||
self.progress.increment()
|
||||
if not pair_data.empty:
|
||||
pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist
|
||||
pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist
|
||||
pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist
|
||||
|
||||
df_analyzed = self.strategy.advise_sell(
|
||||
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy()
|
||||
|
||||
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair}).copy()
|
||||
# Trim startup period from analyzed dataframe
|
||||
df_analyzed = trim_dataframe(df_analyzed, self.timerange,
|
||||
startup_candles=self.required_startup)
|
||||
# To avoid using data from future, we use buy/sell signals shifted
|
||||
# from the previous candle
|
||||
df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1)
|
||||
df_analyzed.loc[:, 'sell'] = df_analyzed.loc[:, 'sell'].shift(1)
|
||||
df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1)
|
||||
|
||||
df_analyzed.drop(df_analyzed.head(1).index, inplace=True)
|
||||
# Update dataprovider cache
|
||||
self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed)
|
||||
|
||||
df_analyzed = df_analyzed.drop(df_analyzed.head(1).index)
|
||||
|
||||
# Convert from Pandas to list for performance reasons
|
||||
# (Looping Pandas is slow.)
|
||||
data[pair] = df_analyzed.values.tolist()
|
||||
data[pair] = df_analyzed[headers].values.tolist()
|
||||
return data
|
||||
|
||||
def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple,
|
||||
@@ -225,6 +294,26 @@ class Backtesting:
|
||||
# sell at open price.
|
||||
return sell_row[OPEN_IDX]
|
||||
|
||||
# Special case: trailing triggers within same candle as trade opened. Assume most
|
||||
# pessimistic price movement, which is moving just enough to arm stoploss and
|
||||
# immediately going down to stop price.
|
||||
if sell.sell_type == SellType.TRAILING_STOP_LOSS and trade_dur == 0:
|
||||
if (
|
||||
not self.strategy.use_custom_stoploss and self.strategy.trailing_stop
|
||||
and self.strategy.trailing_only_offset_is_reached
|
||||
and self.strategy.trailing_stop_positive_offset is not None
|
||||
and self.strategy.trailing_stop_positive
|
||||
):
|
||||
# Worst case: price reaches stop_positive_offset and dives down.
|
||||
stop_rate = (sell_row[OPEN_IDX] *
|
||||
(1 + abs(self.strategy.trailing_stop_positive_offset) -
|
||||
abs(self.strategy.trailing_stop_positive)))
|
||||
else:
|
||||
# Worst case: price ticks tiny bit above open and dives down.
|
||||
stop_rate = sell_row[OPEN_IDX] * (1 - abs(trade.stop_loss_pct))
|
||||
assert stop_rate < sell_row[HIGH_IDX]
|
||||
return stop_rate
|
||||
|
||||
# Set close_rate to stoploss
|
||||
return trade.stop_loss
|
||||
elif sell.sell_type == (SellType.ROI):
|
||||
@@ -258,15 +347,16 @@ class Backtesting:
|
||||
else:
|
||||
return sell_row[OPEN_IDX]
|
||||
|
||||
def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]:
|
||||
|
||||
def _get_sell_trade_entry_for_candle(self, trade: LocalTrade,
|
||||
sell_row: Tuple) -> Optional[LocalTrade]:
|
||||
sell_candle_time = sell_row[DATE_IDX].to_pydatetime()
|
||||
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore
|
||||
sell_row[DATE_IDX].to_pydatetime(), sell_row[BUY_IDX],
|
||||
sell_candle_time, sell_row[BUY_IDX],
|
||||
sell_row[SELL_IDX],
|
||||
low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX])
|
||||
|
||||
if sell.sell_flag:
|
||||
trade.close_date = sell_row[DATE_IDX].to_pydatetime()
|
||||
trade.close_date = sell_candle_time
|
||||
trade.sell_reason = sell.sell_reason
|
||||
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
||||
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
||||
@@ -278,7 +368,7 @@ class Backtesting:
|
||||
rate=closerate,
|
||||
time_in_force=time_in_force,
|
||||
sell_reason=sell.sell_reason,
|
||||
current_time=sell_row[DATE_IDX].to_pydatetime()):
|
||||
current_time=sell_candle_time):
|
||||
return None
|
||||
|
||||
trade.close(closerate, show_msg=False)
|
||||
@@ -286,12 +376,49 @@ class Backtesting:
|
||||
|
||||
return None
|
||||
|
||||
def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]:
|
||||
if self.timeframe_detail and trade.pair in self.detail_data:
|
||||
sell_candle_time = sell_row[DATE_IDX].to_pydatetime()
|
||||
sell_candle_end = sell_candle_time + timedelta(minutes=self.timeframe_min)
|
||||
|
||||
detail_data = self.detail_data[trade.pair]
|
||||
detail_data = detail_data.loc[
|
||||
(detail_data['date'] >= sell_candle_time) &
|
||||
(detail_data['date'] < sell_candle_end)
|
||||
].copy()
|
||||
if len(detail_data) == 0:
|
||||
# Fall back to "regular" data if no detail data was found for this candle
|
||||
return self._get_sell_trade_entry_for_candle(trade, sell_row)
|
||||
detail_data.loc[:, 'buy'] = sell_row[BUY_IDX]
|
||||
detail_data.loc[:, 'sell'] = sell_row[SELL_IDX]
|
||||
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high']
|
||||
for det_row in detail_data[headers].values.tolist():
|
||||
res = self._get_sell_trade_entry_for_candle(trade, det_row)
|
||||
if res:
|
||||
return res
|
||||
|
||||
return None
|
||||
|
||||
else:
|
||||
return self._get_sell_trade_entry_for_candle(trade, sell_row)
|
||||
|
||||
def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]:
|
||||
try:
|
||||
stake_amount = self.wallets.get_trade_stake_amount(pair, None)
|
||||
except DependencyException:
|
||||
return None
|
||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05)
|
||||
|
||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) or 0
|
||||
max_stake_amount = self.wallets.get_available_stake_amount()
|
||||
|
||||
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
|
||||
default_retval=stake_amount)(
|
||||
pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX],
|
||||
proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount)
|
||||
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)
|
||||
|
||||
if not stake_amount:
|
||||
return None
|
||||
|
||||
order_type = self.strategy.order_types['buy']
|
||||
time_in_force = self.strategy.order_time_in_force['sell']
|
||||
@@ -303,6 +430,7 @@ class Backtesting:
|
||||
|
||||
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
|
||||
# Enter trade
|
||||
has_buy_tag = len(row) >= BUY_TAG_IDX + 1
|
||||
trade = LocalTrade(
|
||||
pair=pair,
|
||||
open_rate=row[OPEN_IDX],
|
||||
@@ -312,6 +440,7 @@ class Backtesting:
|
||||
fee_open=self.fee,
|
||||
fee_close=self.fee,
|
||||
is_open=True,
|
||||
buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None,
|
||||
exchange='backtesting',
|
||||
)
|
||||
return trade
|
||||
@@ -368,10 +497,6 @@ class Backtesting:
|
||||
trades: List[LocalTrade] = []
|
||||
self.prepare_backtest(enable_protections)
|
||||
|
||||
# Update dataprovider cache
|
||||
for pair, dataframe in processed.items():
|
||||
self.dataprovider._set_cached_df(pair, self.timeframe, dataframe)
|
||||
|
||||
# Use dict of lists with data for performance
|
||||
# (looping lists is a lot faster than pandas DataFrames)
|
||||
data: Dict = self._get_ohlcv_as_lists(processed)
|
||||
@@ -383,13 +508,18 @@ class Backtesting:
|
||||
open_trades: Dict[str, List[LocalTrade]] = defaultdict(list)
|
||||
open_trade_count = 0
|
||||
|
||||
self.progress.init_step(BacktestState.BACKTEST, int(
|
||||
(end_date - start_date) / timedelta(minutes=self.timeframe_min)))
|
||||
|
||||
# Loop timerange and get candle for each pair at that point in time
|
||||
while tmp <= end_date:
|
||||
open_trade_count_start = open_trade_count
|
||||
|
||||
self.check_abort()
|
||||
for i, pair in enumerate(data):
|
||||
row_index = indexes[pair]
|
||||
try:
|
||||
# Row is treated as "current incomplete candle".
|
||||
# Buy / sell signals are shifted by 1 to compensate for this.
|
||||
row = data[pair][row_index]
|
||||
except IndexError:
|
||||
# missing Data for one pair at the end.
|
||||
@@ -401,8 +531,8 @@ class Backtesting:
|
||||
continue
|
||||
|
||||
row_index += 1
|
||||
self.dataprovider._set_dataframe_max_index(row_index)
|
||||
indexes[pair] = row_index
|
||||
self.dataprovider._set_dataframe_max_index(row_index)
|
||||
|
||||
# without positionstacking, we can only have one open trade per pair.
|
||||
# max_open_trades must be respected
|
||||
@@ -426,10 +556,10 @@ class Backtesting:
|
||||
open_trades[pair].append(trade)
|
||||
LocalTrade.add_bt_trade(trade)
|
||||
|
||||
for trade in open_trades[pair]:
|
||||
for trade in list(open_trades[pair]):
|
||||
# also check the buying candle for sell conditions.
|
||||
trade_entry = self._get_sell_trade_entry(trade, row)
|
||||
# Sell occured
|
||||
# Sell occurred
|
||||
if trade_entry:
|
||||
# logger.debug(f"{pair} - Backtesting sell {trade}")
|
||||
open_trade_count -= 1
|
||||
@@ -442,6 +572,7 @@ class Backtesting:
|
||||
self.protections.global_stop(tmp)
|
||||
|
||||
# Move time one configured time_interval ahead.
|
||||
self.progress.increment()
|
||||
tmp += timedelta(minutes=self.timeframe_min)
|
||||
|
||||
trades += self.handle_left_open(open_trades, data=data)
|
||||
@@ -456,7 +587,10 @@ class Backtesting:
|
||||
'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']),
|
||||
}
|
||||
|
||||
def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange):
|
||||
def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, DataFrame],
|
||||
timerange: TimeRange):
|
||||
self.progress.init_step(BacktestState.ANALYZE, 0)
|
||||
|
||||
logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())
|
||||
backtest_start_time = datetime.now(timezone.utc)
|
||||
self._set_strategy(strat)
|
||||
@@ -473,16 +607,18 @@ class Backtesting:
|
||||
max_open_trades = 0
|
||||
|
||||
# need to reprocess data every time to populate signals
|
||||
preprocessed = self.strategy.ohlcvdata_to_dataframe(data)
|
||||
preprocessed = self.strategy.advise_all_indicators(data)
|
||||
|
||||
# Trim startup period from analyzed dataframe
|
||||
preprocessed = trim_dataframes(preprocessed, timerange, self.required_startup)
|
||||
preprocessed_tmp = trim_dataframes(preprocessed, timerange, self.required_startup)
|
||||
|
||||
if not preprocessed:
|
||||
if not preprocessed_tmp:
|
||||
raise OperationalException(
|
||||
"No data left after adjusting for startup candles.")
|
||||
|
||||
min_date, max_date = history.get_timerange(preprocessed)
|
||||
# Use preprocessed_tmp for date generation (the trimmed dataframe).
|
||||
# Backtesting will re-trim the dataframes after buy/sell signal generation.
|
||||
min_date, max_date = history.get_timerange(preprocessed_tmp)
|
||||
logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} '
|
||||
f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} '
|
||||
f'({(max_date - min_date).days} days).')
|
||||
@@ -512,16 +648,18 @@ class Backtesting:
|
||||
data: Dict[str, Any] = {}
|
||||
|
||||
data, timerange = self.load_bt_data()
|
||||
self.load_bt_data_detail()
|
||||
logger.info("Dataload complete. Calculating indicators")
|
||||
|
||||
for strat in self.strategylist:
|
||||
min_date, max_date = self.backtest_one_strategy(strat, data, timerange)
|
||||
if len(self.strategylist) > 0:
|
||||
stats = generate_backtest_stats(data, self.all_results,
|
||||
min_date=min_date, max_date=max_date)
|
||||
|
||||
if self.config.get('export', False):
|
||||
store_backtest_stats(self.config['exportfilename'], stats)
|
||||
self.results = generate_backtest_stats(data, self.all_results,
|
||||
min_date=min_date, max_date=max_date)
|
||||
|
||||
if self.config.get('export', 'none') == 'trades':
|
||||
store_backtest_stats(self.config['exportfilename'], self.results)
|
||||
|
||||
# Show backtest results
|
||||
show_backtest_results(self.config, stats)
|
||||
show_backtest_results(self.config, self.results)
|
||||
|
33
freqtrade/optimize/bt_progress.py
Normal file
33
freqtrade/optimize/bt_progress.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from freqtrade.enums import BacktestState
|
||||
|
||||
|
||||
class BTProgress:
|
||||
_action: BacktestState = BacktestState.STARTUP
|
||||
_progress: float = 0
|
||||
_max_steps: float = 0
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def init_step(self, action: BacktestState, max_steps: float):
|
||||
self._action = action
|
||||
self._max_steps = max_steps
|
||||
self._proress = 0
|
||||
|
||||
def set_new_value(self, new_value: float):
|
||||
self._progress = new_value
|
||||
|
||||
def increment(self):
|
||||
self._progress += 1
|
||||
|
||||
@property
|
||||
def progress(self):
|
||||
"""
|
||||
Get progress as ratio, capped to be between 0 and 1 (to avoid small calculation errors).
|
||||
"""
|
||||
return max(min(round(self._progress / self._max_steps, 5)
|
||||
if self._max_steps > 0 else 0, 1), 0)
|
||||
|
||||
@property
|
||||
def action(self):
|
||||
return str(self._action)
|
@@ -7,7 +7,8 @@ import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency
|
||||
from freqtrade.configuration import TimeRange, validate_config_consistency
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.edge import Edge
|
||||
from freqtrade.optimize.optimize_reports import generate_edge_table
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
@@ -28,11 +29,12 @@ class EdgeCli:
|
||||
def __init__(self, config: Dict[str, Any]) -> None:
|
||||
self.config = config
|
||||
|
||||
# Reset keys for edge
|
||||
remove_credentials(self.config)
|
||||
# Ensure using dry-run
|
||||
self.config['dry_run'] = True
|
||||
self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
||||
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
||||
self.strategy = StrategyResolver.load_strategy(self.config)
|
||||
self.strategy.dp = DataProvider(config, None)
|
||||
|
||||
validate_config_consistency(self.config)
|
||||
|
||||
|
@@ -12,7 +12,6 @@ from math import ceil
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import numpy as np
|
||||
import progressbar
|
||||
import rapidjson
|
||||
from colorama import Fore, Style
|
||||
@@ -20,18 +19,19 @@ from colorama import init as colorama_init
|
||||
from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN
|
||||
from freqtrade.data.converter import trim_dataframes
|
||||
from freqtrade.data.history import get_timerange
|
||||
from freqtrade.misc import file_dump_json, plural
|
||||
from freqtrade.exceptions import OperationalException
|
||||
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_tools import HyperoptTools
|
||||
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, HyperOptResolver
|
||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
|
||||
|
||||
|
||||
# Suppress scikit-learn FutureWarnings from skopt
|
||||
@@ -67,6 +67,7 @@ class Hyperopt:
|
||||
def __init__(self, config: Dict[str, Any]) -> None:
|
||||
self.buy_space: List[Dimension] = []
|
||||
self.sell_space: List[Dimension] = []
|
||||
self.protection_space: List[Dimension] = []
|
||||
self.roi_space: List[Dimension] = []
|
||||
self.stoploss_space: List[Dimension] = []
|
||||
self.trailing_space: List[Dimension] = []
|
||||
@@ -79,7 +80,10 @@ class Hyperopt:
|
||||
if not self.config.get('hyperopt'):
|
||||
self.custom_hyperopt = HyperOptAuto(self.config)
|
||||
else:
|
||||
self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config)
|
||||
raise OperationalException(
|
||||
"Using separate Hyperopt files has been removed in 2021.9. Please convert "
|
||||
"your existing Hyperopt file to the new Hyperoptable strategy interface")
|
||||
|
||||
self.backtesting._set_strategy(self.backtesting.strategylist[0])
|
||||
self.custom_hyperopt.strategy = self.backtesting.strategy
|
||||
|
||||
@@ -100,17 +104,6 @@ class Hyperopt:
|
||||
self.num_epochs_saved = 0
|
||||
self.current_best_epoch: Optional[Dict[str, Any]] = None
|
||||
|
||||
# Populate functions here (hasattr is slow so should not be run during "regular" operations)
|
||||
if hasattr(self.custom_hyperopt, 'populate_indicators'):
|
||||
self.backtesting.strategy.advise_indicators = ( # type: ignore
|
||||
self.custom_hyperopt.populate_indicators) # type: ignore
|
||||
if hasattr(self.custom_hyperopt, 'populate_buy_trend'):
|
||||
self.backtesting.strategy.advise_buy = ( # type: ignore
|
||||
self.custom_hyperopt.populate_buy_trend) # type: ignore
|
||||
if hasattr(self.custom_hyperopt, 'populate_sell_trend'):
|
||||
self.backtesting.strategy.advise_sell = ( # type: ignore
|
||||
self.custom_hyperopt.populate_sell_trend) # type: ignore
|
||||
|
||||
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
|
||||
if self.config.get('use_max_market_positions', True):
|
||||
self.max_open_trades = self.config['max_open_trades']
|
||||
@@ -163,13 +156,9 @@ class Hyperopt:
|
||||
While not a valid json object - this allows appending easily.
|
||||
:param epoch: result dictionary for this epoch.
|
||||
"""
|
||||
def default_parser(x):
|
||||
if isinstance(x, np.integer):
|
||||
return int(x)
|
||||
return str(x)
|
||||
|
||||
epoch[FTHYPT_FILEVERSION] = 2
|
||||
with self.results_file.open('a') as f:
|
||||
rapidjson.dump(epoch, f, default=default_parser,
|
||||
rapidjson.dump(epoch, f, default=hyperopt_serializer,
|
||||
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN)
|
||||
f.write("\n")
|
||||
|
||||
@@ -191,6 +180,8 @@ class Hyperopt:
|
||||
result['buy'] = {p.name: params.get(p.name) for p in self.buy_space}
|
||||
if HyperoptTools.has_space(self.config, 'sell'):
|
||||
result['sell'] = {p.name: params.get(p.name) for p in self.sell_space}
|
||||
if HyperoptTools.has_space(self.config, 'protection'):
|
||||
result['protection'] = {p.name: params.get(p.name) for p in self.protection_space}
|
||||
if HyperoptTools.has_space(self.config, 'roi'):
|
||||
result['roi'] = {str(k): v for k, v in
|
||||
self.custom_hyperopt.generate_roi_table(params).items()}
|
||||
@@ -201,6 +192,25 @@ class Hyperopt:
|
||||
|
||||
return result
|
||||
|
||||
def _get_no_optimize_details(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get non-optimized parameters
|
||||
"""
|
||||
result: Dict[str, Any] = {}
|
||||
strategy = self.backtesting.strategy
|
||||
if not HyperoptTools.has_space(self.config, 'roi'):
|
||||
result['roi'] = {str(k): v for k, v in strategy.minimal_roi.items()}
|
||||
if not HyperoptTools.has_space(self.config, 'stoploss'):
|
||||
result['stoploss'] = {'stoploss': strategy.stoploss}
|
||||
if not HyperoptTools.has_space(self.config, 'trailing'):
|
||||
result['trailing'] = {
|
||||
'trailing_stop': strategy.trailing_stop,
|
||||
'trailing_stop_positive': strategy.trailing_stop_positive,
|
||||
'trailing_stop_positive_offset': strategy.trailing_stop_positive_offset,
|
||||
'trailing_only_offset_is_reached': strategy.trailing_only_offset_is_reached,
|
||||
}
|
||||
return result
|
||||
|
||||
def print_results(self, results) -> None:
|
||||
"""
|
||||
Log results if it is better than any previous evaluation
|
||||
@@ -222,10 +232,16 @@ class Hyperopt:
|
||||
"""
|
||||
Assign the dimensions in the hyperoptimization space.
|
||||
"""
|
||||
if HyperoptTools.has_space(self.config, 'protection'):
|
||||
# Protections can only be optimized when using the Parameter interface
|
||||
logger.debug("Hyperopt has 'protection' space")
|
||||
# Enable Protections if protection space is selected.
|
||||
self.config['enable_protections'] = True
|
||||
self.protection_space = self.custom_hyperopt.protection_space()
|
||||
|
||||
if HyperoptTools.has_space(self.config, 'buy'):
|
||||
logger.debug("Hyperopt has 'buy' space")
|
||||
self.buy_space = self.custom_hyperopt.indicator_space()
|
||||
self.buy_space = self.custom_hyperopt.buy_indicator_space()
|
||||
|
||||
if HyperoptTools.has_space(self.config, 'sell'):
|
||||
logger.debug("Hyperopt has 'sell' space")
|
||||
@@ -242,30 +258,42 @@ 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.roi_space +
|
||||
self.stoploss_space + self.trailing_space)
|
||||
|
||||
self.dimensions = (self.buy_space + self.sell_space + self.protection_space
|
||||
+ self.roi_space + self.stoploss_space + self.trailing_space)
|
||||
|
||||
def assign_params(self, params_dict: Dict, category: str) -> None:
|
||||
"""
|
||||
Assign hyperoptable parameters
|
||||
"""
|
||||
for attr_name, attr in self.backtesting.strategy.enumerate_parameters(category):
|
||||
if attr.optimize:
|
||||
# noinspection PyProtectedMember
|
||||
attr.value = params_dict[attr_name]
|
||||
|
||||
def generate_optimizer(self, raw_params: List[Any], iteration=None) -> Dict:
|
||||
"""
|
||||
Used Optimize function. Called once per epoch to optimize whatever is configured.
|
||||
Used Optimize function.
|
||||
Called once per epoch to optimize whatever is configured.
|
||||
Keep this function as optimized as possible!
|
||||
"""
|
||||
backtest_start_time = datetime.now(timezone.utc)
|
||||
params_dict = self._get_params_dict(self.dimensions, raw_params)
|
||||
|
||||
# Apply parameters
|
||||
if HyperoptTools.has_space(self.config, 'buy'):
|
||||
self.assign_params(params_dict, 'buy')
|
||||
|
||||
if HyperoptTools.has_space(self.config, 'sell'):
|
||||
self.assign_params(params_dict, 'sell')
|
||||
|
||||
if HyperoptTools.has_space(self.config, 'protection'):
|
||||
self.assign_params(params_dict, 'protection')
|
||||
|
||||
if HyperoptTools.has_space(self.config, 'roi'):
|
||||
self.backtesting.strategy.minimal_roi = ( # type: ignore
|
||||
self.custom_hyperopt.generate_roi_table(params_dict))
|
||||
|
||||
if HyperoptTools.has_space(self.config, 'buy'):
|
||||
self.backtesting.strategy.advise_buy = ( # type: ignore
|
||||
self.custom_hyperopt.buy_strategy_generator(params_dict))
|
||||
|
||||
if HyperoptTools.has_space(self.config, 'sell'):
|
||||
self.backtesting.strategy.advise_sell = ( # type: ignore
|
||||
self.custom_hyperopt.sell_strategy_generator(params_dict))
|
||||
|
||||
if HyperoptTools.has_space(self.config, 'stoploss'):
|
||||
self.backtesting.strategy.stoploss = params_dict['stoploss']
|
||||
|
||||
@@ -310,7 +338,8 @@ class Hyperopt:
|
||||
results_explanation = HyperoptTools.format_results_explanation_string(
|
||||
strat_stats, self.config['stake_currency'])
|
||||
|
||||
not_optimized = self.backtesting.strategy.get_params_dict()
|
||||
not_optimized = self.backtesting.strategy.get_no_optimize_params()
|
||||
not_optimized = deep_merge_dicts(not_optimized, self._get_no_optimize_details())
|
||||
|
||||
trade_count = strat_stats['total_trades']
|
||||
total_profit = strat_stats['profit_total']
|
||||
@@ -324,7 +353,8 @@ class Hyperopt:
|
||||
loss = self.calculate_loss(results=backtesting_results['results'],
|
||||
trade_count=trade_count,
|
||||
min_date=min_date, max_date=max_date,
|
||||
config=self.config, processed=processed)
|
||||
config=self.config, processed=processed,
|
||||
backtest_stats=strat_stats)
|
||||
return {
|
||||
'loss': loss,
|
||||
'params_dict': params_dict,
|
||||
@@ -336,10 +366,20 @@ class Hyperopt:
|
||||
}
|
||||
|
||||
def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer:
|
||||
estimator = self.custom_hyperopt.generate_estimator()
|
||||
|
||||
acq_optimizer = "sampling"
|
||||
if isinstance(estimator, str):
|
||||
if estimator not in ("GP", "RF", "ET", "GBRT"):
|
||||
raise OperationalException(f"Estimator {estimator} not supported.")
|
||||
else:
|
||||
acq_optimizer = "auto"
|
||||
|
||||
logger.info(f"Using estimator {estimator}.")
|
||||
return Optimizer(
|
||||
dimensions,
|
||||
base_estimator="ET",
|
||||
acq_optimizer="auto",
|
||||
base_estimator=estimator,
|
||||
acq_optimizer=acq_optimizer,
|
||||
n_initial_points=INITIAL_POINTS,
|
||||
acq_optimizer_kwargs={'n_jobs': cpu_count},
|
||||
random_state=self.random_state,
|
||||
@@ -357,18 +397,17 @@ class Hyperopt:
|
||||
data, timerange = self.backtesting.load_bt_data()
|
||||
logger.info("Dataload complete. Calculating indicators")
|
||||
|
||||
preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data)
|
||||
preprocessed = self.backtesting.strategy.advise_all_indicators(data)
|
||||
|
||||
# Trim startup period from analyzed dataframe
|
||||
# Trim startup period from analyzed dataframe to get correct dates for output.
|
||||
processed = trim_dataframes(preprocessed, timerange, self.backtesting.required_startup)
|
||||
|
||||
self.min_date, self.max_date = get_timerange(processed)
|
||||
|
||||
logger.info(f'Hyperopting with data from {self.min_date.strftime(DATETIME_PRINT_FORMAT)} '
|
||||
f'up to {self.max_date.strftime(DATETIME_PRINT_FORMAT)} '
|
||||
f'({(self.max_date - self.min_date).days} days)..')
|
||||
|
||||
dump(processed, self.data_pickle_file)
|
||||
# Store non-trimmed data - will be trimmed after signal generation.
|
||||
dump(preprocessed, self.data_pickle_file)
|
||||
|
||||
def start(self) -> None:
|
||||
self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None))
|
||||
@@ -423,9 +462,9 @@ class Hyperopt:
|
||||
' [', progressbar.ETA(), ', ', progressbar.Timer(), ']',
|
||||
]
|
||||
with progressbar.ProgressBar(
|
||||
max_value=self.total_epochs, redirect_stdout=False, redirect_stderr=False,
|
||||
widgets=widgets
|
||||
) as pbar:
|
||||
max_value=self.total_epochs, redirect_stdout=False, redirect_stderr=False,
|
||||
widgets=widgets
|
||||
) as pbar:
|
||||
EVALS = ceil(self.total_epochs / jobs)
|
||||
for i in range(EVALS):
|
||||
# Correct the number of epochs to be processed for the last
|
||||
@@ -469,6 +508,11 @@ class Hyperopt:
|
||||
f"saved to '{self.results_file}'.")
|
||||
|
||||
if self.current_best_epoch:
|
||||
HyperoptTools.try_export_params(
|
||||
self.config,
|
||||
self.backtesting.strategy.get_strategy_name(),
|
||||
self.current_best_epoch)
|
||||
|
||||
HyperoptTools.show_epoch_details(self.current_best_epoch, self.total_epochs,
|
||||
self.print_json)
|
||||
else:
|
||||
|
@@ -3,16 +3,32 @@ 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 Any, Callable, Dict, List
|
||||
from typing import Callable, Dict, List
|
||||
|
||||
from pandas import DataFrame
|
||||
from freqtrade.exceptions import OperationalException
|
||||
|
||||
|
||||
with suppress(ImportError):
|
||||
from skopt.space import Dimension
|
||||
|
||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
||||
from freqtrade.optimize.hyperopt_interface import EstimatorType, IHyperOpt
|
||||
|
||||
|
||||
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):
|
||||
@@ -22,26 +38,6 @@ class HyperOptAuto(IHyperOpt):
|
||||
sell_indicator_space methods, but other hyperopt methods can be overridden as well.
|
||||
"""
|
||||
|
||||
def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable:
|
||||
def populate_buy_trend(dataframe: DataFrame, metadata: dict):
|
||||
for attr_name, attr in self.strategy.enumerate_parameters('buy'):
|
||||
if attr.optimize:
|
||||
# noinspection PyProtectedMember
|
||||
attr.value = params[attr_name]
|
||||
return self.strategy.populate_buy_trend(dataframe, metadata)
|
||||
|
||||
return populate_buy_trend
|
||||
|
||||
def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable:
|
||||
def populate_sell_trend(dataframe: DataFrame, metadata: dict):
|
||||
for attr_name, attr in self.strategy.enumerate_parameters('sell'):
|
||||
if attr.optimize:
|
||||
# noinspection PyProtectedMember
|
||||
attr.value = params[attr_name]
|
||||
return self.strategy.populate_sell_trend(dataframe, metadata)
|
||||
|
||||
return populate_sell_trend
|
||||
|
||||
def _get_func(self, name) -> Callable:
|
||||
"""
|
||||
Return a function defined in Strategy.HyperOpt class, or one defined in super() class.
|
||||
@@ -60,18 +56,25 @@ class HyperOptAuto(IHyperOpt):
|
||||
if attr.optimize:
|
||||
yield attr.get_space(attr_name)
|
||||
|
||||
def _get_indicator_space(self, category, fallback_method_name):
|
||||
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:
|
||||
return self._get_func(fallback_method_name)()
|
||||
_format_exception_message(
|
||||
category,
|
||||
self.config.get("hyperopt_ignore_missing_space", False))
|
||||
return []
|
||||
|
||||
def indicator_space(self) -> List['Dimension']:
|
||||
return self._get_indicator_space('buy', 'indicator_space')
|
||||
def buy_indicator_space(self) -> List['Dimension']:
|
||||
return self._get_indicator_space('buy')
|
||||
|
||||
def sell_indicator_space(self) -> List['Dimension']:
|
||||
return self._get_indicator_space('sell', 'sell_indicator_space')
|
||||
return self._get_indicator_space('sell')
|
||||
|
||||
def protection_space(self) -> List['Dimension']:
|
||||
return self._get_indicator_space('protection')
|
||||
|
||||
def generate_roi_table(self, params: Dict) -> Dict[int, float]:
|
||||
return self._get_func('generate_roi_table')(params)
|
||||
@@ -87,3 +90,6 @@ class HyperOptAuto(IHyperOpt):
|
||||
|
||||
def trailing_space(self) -> List['Dimension']:
|
||||
return self._get_func('trailing_space')()
|
||||
|
||||
def generate_estimator(self) -> EstimatorType:
|
||||
return self._get_func('generate_estimator')()
|
||||
|
128
freqtrade/optimize/hyperopt_epoch_filters.py
Normal file
128
freqtrade/optimize/hyperopt_epoch_filters.py
Normal file
@@ -0,0 +1,128 @@
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def hyperopt_filter_epochs(epochs: List, filteroptions: dict, log: bool = True) -> List:
|
||||
"""
|
||||
Filter our items from the list of hyperopt results
|
||||
"""
|
||||
if filteroptions['only_best']:
|
||||
epochs = [x for x in epochs if x['is_best']]
|
||||
if filteroptions['only_profitable']:
|
||||
epochs = [x for x in epochs
|
||||
if x['results_metrics'].get('profit_total', 0) > 0]
|
||||
|
||||
epochs = _hyperopt_filter_epochs_trade_count(epochs, filteroptions)
|
||||
|
||||
epochs = _hyperopt_filter_epochs_duration(epochs, filteroptions)
|
||||
|
||||
epochs = _hyperopt_filter_epochs_profit(epochs, filteroptions)
|
||||
|
||||
epochs = _hyperopt_filter_epochs_objective(epochs, filteroptions)
|
||||
if log:
|
||||
logger.info(f"{len(epochs)} " +
|
||||
("best " if filteroptions['only_best'] else "") +
|
||||
("profitable " if filteroptions['only_profitable'] else "") +
|
||||
"epochs found.")
|
||||
return epochs
|
||||
|
||||
|
||||
def _hyperopt_filter_epochs_trade(epochs: List, trade_count: int):
|
||||
"""
|
||||
Filter epochs with trade-counts > trades
|
||||
"""
|
||||
return [
|
||||
x for x in epochs if x['results_metrics'].get('total_trades', 0) > trade_count
|
||||
]
|
||||
|
||||
|
||||
def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> List:
|
||||
|
||||
if filteroptions['filter_min_trades'] > 0:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, filteroptions['filter_min_trades'])
|
||||
|
||||
if filteroptions['filter_max_trades'] > 0:
|
||||
epochs = [
|
||||
x for x in epochs
|
||||
if x['results_metrics'].get('total_trades') < filteroptions['filter_max_trades']
|
||||
]
|
||||
return epochs
|
||||
|
||||
|
||||
def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List:
|
||||
|
||||
def get_duration_value(x):
|
||||
# Duration in minutes ...
|
||||
if 'holding_avg_s' in x['results_metrics']:
|
||||
avg = x['results_metrics']['holding_avg_s']
|
||||
return avg // 60
|
||||
raise OperationalException(
|
||||
"Holding-average not available. Please omit the filter on average time, "
|
||||
"or rerun hyperopt with this version")
|
||||
|
||||
if filteroptions['filter_min_avg_time'] is not None:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||
epochs = [
|
||||
x for x in epochs
|
||||
if get_duration_value(x) > filteroptions['filter_min_avg_time']
|
||||
]
|
||||
if filteroptions['filter_max_avg_time'] is not None:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||
epochs = [
|
||||
x for x in epochs
|
||||
if get_duration_value(x) < filteroptions['filter_max_avg_time']
|
||||
]
|
||||
|
||||
return epochs
|
||||
|
||||
|
||||
def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List:
|
||||
|
||||
if filteroptions['filter_min_avg_profit'] is not None:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||
epochs = [
|
||||
x for x in epochs
|
||||
if x['results_metrics'].get('profit_mean', 0) * 100
|
||||
> filteroptions['filter_min_avg_profit']
|
||||
]
|
||||
if filteroptions['filter_max_avg_profit'] is not None:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||
epochs = [
|
||||
x for x in epochs
|
||||
if x['results_metrics'].get('profit_mean', 0) * 100
|
||||
< filteroptions['filter_max_avg_profit']
|
||||
]
|
||||
if filteroptions['filter_min_total_profit'] is not None:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||
epochs = [
|
||||
x for x in epochs
|
||||
if x['results_metrics'].get('profit_total_abs', 0)
|
||||
> filteroptions['filter_min_total_profit']
|
||||
]
|
||||
if filteroptions['filter_max_total_profit'] is not None:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||
epochs = [
|
||||
x for x in epochs
|
||||
if x['results_metrics'].get('profit_total_abs', 0)
|
||||
< filteroptions['filter_max_total_profit']
|
||||
]
|
||||
return epochs
|
||||
|
||||
|
||||
def _hyperopt_filter_epochs_objective(epochs: List, filteroptions: dict) -> List:
|
||||
|
||||
if filteroptions['filter_min_objective'] is not None:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||
|
||||
epochs = [x for x in epochs if x['loss'] < filteroptions['filter_min_objective']]
|
||||
if filteroptions['filter_max_objective'] is not None:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||
|
||||
epochs = [x for x in epochs if x['loss'] > filteroptions['filter_max_objective']]
|
||||
|
||||
return epochs
|
@@ -5,11 +5,11 @@ This module defines the interface to apply for hyperopt
|
||||
import logging
|
||||
import math
|
||||
from abc import ABC
|
||||
from typing import Any, Callable, Dict, List
|
||||
from typing import Dict, List, Union
|
||||
|
||||
from sklearn.base import RegressorMixin
|
||||
from skopt.space import Categorical, Dimension, Integer
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
from freqtrade.misc import round_dict
|
||||
from freqtrade.optimize.space import SKDecimal
|
||||
@@ -18,12 +18,7 @@ from freqtrade.strategy import IStrategy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _format_exception_message(method: str, space: str) -> str:
|
||||
return (f"The '{space}' space is included into the hyperoptimization "
|
||||
f"but {method}() method is not found in your "
|
||||
f"custom Hyperopt class. You should either implement this "
|
||||
f"method or remove the '{space}' space from hyperoptimization.")
|
||||
EstimatorType = Union[RegressorMixin, str]
|
||||
|
||||
|
||||
class IHyperOpt(ABC):
|
||||
@@ -45,29 +40,13 @@ class IHyperOpt(ABC):
|
||||
IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED
|
||||
IHyperOpt.timeframe = str(config['timeframe'])
|
||||
|
||||
def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable:
|
||||
def generate_estimator(self) -> EstimatorType:
|
||||
"""
|
||||
Create a buy strategy generator.
|
||||
Return base_estimator.
|
||||
Can be any of "GP", "RF", "ET", "GBRT" or an instance of a class
|
||||
inheriting from RegressorMixin (from sklearn).
|
||||
"""
|
||||
raise OperationalException(_format_exception_message('buy_strategy_generator', 'buy'))
|
||||
|
||||
def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable:
|
||||
"""
|
||||
Create a sell strategy generator.
|
||||
"""
|
||||
raise OperationalException(_format_exception_message('sell_strategy_generator', 'sell'))
|
||||
|
||||
def indicator_space(self) -> List[Dimension]:
|
||||
"""
|
||||
Create an indicator space.
|
||||
"""
|
||||
raise OperationalException(_format_exception_message('indicator_space', 'buy'))
|
||||
|
||||
def sell_indicator_space(self) -> List[Dimension]:
|
||||
"""
|
||||
Create a sell indicator space.
|
||||
"""
|
||||
raise OperationalException(_format_exception_message('sell_indicator_space', 'sell'))
|
||||
return 'ET'
|
||||
|
||||
def generate_roi_table(self, params: Dict) -> Dict[int, float]:
|
||||
"""
|
||||
|
64
freqtrade/optimize/hyperopt_loss_calmar.py
Normal file
64
freqtrade/optimize/hyperopt_loss_calmar.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
CalmarHyperOptLoss
|
||||
|
||||
This module defines the alternative HyperOptLoss class which can be used for
|
||||
Hyperoptimization.
|
||||
"""
|
||||
from datetime import datetime
|
||||
from math import sqrt as msqrt
|
||||
from typing import Any, Dict
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.data.btanalysis import calculate_max_drawdown
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
|
||||
class CalmarHyperOptLoss(IHyperOptLoss):
|
||||
"""
|
||||
Defines the loss function for hyperopt.
|
||||
|
||||
This implementation uses the Calmar Ratio calculation.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(
|
||||
results: DataFrame,
|
||||
trade_count: int,
|
||||
min_date: datetime,
|
||||
max_date: datetime,
|
||||
config: Dict,
|
||||
processed: Dict[str, DataFrame],
|
||||
backtest_stats: Dict[str, Any],
|
||||
*args,
|
||||
**kwargs
|
||||
) -> float:
|
||||
"""
|
||||
Objective function, returns smaller number for more optimal results.
|
||||
|
||||
Uses Calmar Ratio calculation.
|
||||
"""
|
||||
total_profit = backtest_stats["profit_total"]
|
||||
days_period = (max_date - min_date).days
|
||||
|
||||
# adding slippage of 0.1% per trade
|
||||
total_profit = total_profit - 0.0005
|
||||
expected_returns_mean = total_profit.sum() / days_period * 100
|
||||
|
||||
# calculate max drawdown
|
||||
try:
|
||||
_, _, _, high_val, low_val = calculate_max_drawdown(
|
||||
results, value_col="profit_abs"
|
||||
)
|
||||
max_drawdown = (high_val - low_val) / high_val
|
||||
except ValueError:
|
||||
max_drawdown = 0
|
||||
|
||||
if max_drawdown != 0:
|
||||
calmar_ratio = expected_returns_mean / max_drawdown * msqrt(365)
|
||||
else:
|
||||
# Define high (negative) calmar ratio to be clear that this is NOT optimal.
|
||||
calmar_ratio = -20.0
|
||||
|
||||
# print(expected_returns_mean, max_drawdown, calmar_ratio)
|
||||
return -calmar_ratio
|
@@ -5,7 +5,7 @@ This module defines the interface for the loss-function for hyperopt
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
from typing import Dict
|
||||
from typing import Any, Dict
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
@@ -22,6 +22,7 @@ class IHyperOptLoss(ABC):
|
||||
def hyperopt_loss_function(results: DataFrame, trade_count: int,
|
||||
min_date: datetime, max_date: datetime,
|
||||
config: Dict, processed: Dict[str, DataFrame],
|
||||
backtest_stats: Dict[str, Any],
|
||||
*args, **kwargs) -> float:
|
||||
"""
|
||||
Objective function, returns smaller number for better results
|
||||
|
41
freqtrade/optimize/hyperopt_loss_max_drawdown.py
Normal file
41
freqtrade/optimize/hyperopt_loss_max_drawdown.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
MaxDrawDownHyperOptLoss
|
||||
|
||||
This module defines the alternative HyperOptLoss class which can be used for
|
||||
Hyperoptimization.
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.data.btanalysis import calculate_max_drawdown
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
|
||||
class MaxDrawDownHyperOptLoss(IHyperOptLoss):
|
||||
|
||||
"""
|
||||
Defines the loss function for hyperopt.
|
||||
|
||||
This implementation optimizes for max draw down and profit
|
||||
Less max drawdown more profit -> Lower return value
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(results: DataFrame, trade_count: int,
|
||||
min_date: datetime, max_date: datetime,
|
||||
*args, **kwargs) -> float:
|
||||
|
||||
"""
|
||||
Objective function.
|
||||
|
||||
Uses profit ratio weighted max_drawdown when drawdown is available.
|
||||
Otherwise directly optimizes profit ratio.
|
||||
"""
|
||||
total_profit = results['profit_abs'].sum()
|
||||
try:
|
||||
max_drawdown = calculate_max_drawdown(results, value_col='profit_abs')
|
||||
except ValueError:
|
||||
# No losing trade, therefore no drawdown.
|
||||
return -total_profit
|
||||
return -total_profit / max_drawdown[0]
|
250
freqtrade/optimize/hyperopt_tools.py
Normal file → Executable file
250
freqtrade/optimize/hyperopt_tools.py
Normal file → Executable file
@@ -1,75 +1,160 @@
|
||||
|
||||
import io
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import rapidjson
|
||||
import tabulate
|
||||
from colorama import Fore, Style
|
||||
from pandas import isna, json_normalize
|
||||
|
||||
from freqtrade.constants import FTHYPT_FILEVERSION, USERPATH_STRATEGIES
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import round_coin_value, round_dict
|
||||
from freqtrade.misc import deep_merge_dicts, round_coin_value, round_dict, safe_value_fallback2
|
||||
from freqtrade.optimize.hyperopt_epoch_filters import hyperopt_filter_epochs
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
NON_OPT_PARAM_APPENDIX = " # value loaded from strategy"
|
||||
|
||||
|
||||
def hyperopt_serializer(x):
|
||||
if isinstance(x, np.integer):
|
||||
return int(x)
|
||||
if isinstance(x, np.bool_):
|
||||
return bool(x)
|
||||
|
||||
return str(x)
|
||||
|
||||
|
||||
class HyperoptTools():
|
||||
|
||||
@staticmethod
|
||||
def get_strategy_filename(config: Dict, strategy_name: str) -> Optional[Path]:
|
||||
"""
|
||||
Get Strategy-location (filename) from strategy_name
|
||||
"""
|
||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||
directory = Path(config.get('strategy_path', config['user_data_dir'] / USERPATH_STRATEGIES))
|
||||
strategy_objs = StrategyResolver.search_all_objects(directory, False)
|
||||
strategies = [s for s in strategy_objs if s['name'] == strategy_name]
|
||||
if strategies:
|
||||
strategy = strategies[0]
|
||||
|
||||
return Path(strategy['location'])
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def export_params(params, strategy_name: str, filename: Path):
|
||||
"""
|
||||
Generate files
|
||||
"""
|
||||
final_params = deepcopy(params['params_not_optimized'])
|
||||
final_params = deep_merge_dicts(params['params_details'], final_params)
|
||||
final_params = {
|
||||
'strategy_name': strategy_name,
|
||||
'params': final_params,
|
||||
'ft_stratparam_v': 1,
|
||||
'export_time': datetime.now(timezone.utc),
|
||||
}
|
||||
logger.info(f"Dumping parameters to {filename}")
|
||||
with filename.open('w') as f:
|
||||
rapidjson.dump(final_params, f, indent=2,
|
||||
default=hyperopt_serializer,
|
||||
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def try_export_params(config: Dict[str, Any], strategy_name: str, params: Dict):
|
||||
if params.get(FTHYPT_FILEVERSION, 1) >= 2 and not config.get('disableparamexport', False):
|
||||
# Export parameters ...
|
||||
fn = HyperoptTools.get_strategy_filename(config, strategy_name)
|
||||
if fn:
|
||||
HyperoptTools.export_params(params, strategy_name, fn.with_suffix('.json'))
|
||||
else:
|
||||
logger.warning("Strategy not found, not exporting parameter file.")
|
||||
|
||||
@staticmethod
|
||||
def has_space(config: Dict[str, Any], space: str) -> bool:
|
||||
"""
|
||||
Tell if the space value is contained in the configuration
|
||||
"""
|
||||
# The 'trailing' space is not included in the 'default' set of spaces
|
||||
if space == 'trailing':
|
||||
# 'trailing' and 'protection spaces are not included in the 'default' set of spaces
|
||||
if space in ('trailing', 'protection'):
|
||||
return any(s in config['spaces'] for s in [space, 'all'])
|
||||
else:
|
||||
return any(s in config['spaces'] for s in [space, 'all', 'default'])
|
||||
|
||||
@staticmethod
|
||||
def _read_results_pickle(results_file: Path) -> List:
|
||||
def _read_results(results_file: Path, batch_size: int = 10) -> Iterator[List[Any]]:
|
||||
"""
|
||||
Read hyperopt results from pickle file
|
||||
LEGACY method - new files are written as json and cannot be read with this method.
|
||||
"""
|
||||
from joblib import load
|
||||
|
||||
logger.info(f"Reading pickled epochs from '{results_file}'")
|
||||
data = load(results_file)
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def _read_results(results_file: Path) -> List:
|
||||
"""
|
||||
Read hyperopt results from file
|
||||
Stream hyperopt results from file
|
||||
"""
|
||||
import rapidjson
|
||||
logger.info(f"Reading epochs from '{results_file}'")
|
||||
with results_file.open('r') as f:
|
||||
data = [rapidjson.loads(line) for line in f]
|
||||
return data
|
||||
data = []
|
||||
for line in f:
|
||||
data += [rapidjson.loads(line)]
|
||||
if len(data) >= batch_size:
|
||||
yield data
|
||||
data = []
|
||||
yield data
|
||||
|
||||
@staticmethod
|
||||
def load_previous_results(results_file: Path) -> List:
|
||||
"""
|
||||
Load data for epochs from the file if we have one
|
||||
"""
|
||||
epochs: List = []
|
||||
def _test_hyperopt_results_exist(results_file) -> bool:
|
||||
if results_file.is_file() and results_file.stat().st_size > 0:
|
||||
if results_file.suffix == '.pickle':
|
||||
epochs = HyperoptTools._read_results_pickle(results_file)
|
||||
else:
|
||||
epochs = HyperoptTools._read_results(results_file)
|
||||
# Detection of some old format, without 'is_best' field saved
|
||||
if epochs[0].get('is_best') is None:
|
||||
raise OperationalException(
|
||||
"Legacy hyperopt results are no longer supported."
|
||||
"Please rerun hyperopt or use an older version to load this file."
|
||||
)
|
||||
return True
|
||||
else:
|
||||
# No file found.
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def load_filtered_results(results_file: Path, config: Dict[str, Any]) -> Tuple[List, int]:
|
||||
filteroptions = {
|
||||
'only_best': config.get('hyperopt_list_best', False),
|
||||
'only_profitable': config.get('hyperopt_list_profitable', False),
|
||||
'filter_min_trades': config.get('hyperopt_list_min_trades', 0),
|
||||
'filter_max_trades': config.get('hyperopt_list_max_trades', 0),
|
||||
'filter_min_avg_time': config.get('hyperopt_list_min_avg_time', None),
|
||||
'filter_max_avg_time': config.get('hyperopt_list_max_avg_time', None),
|
||||
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
|
||||
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
|
||||
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
|
||||
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None),
|
||||
'filter_min_objective': config.get('hyperopt_list_min_objective', None),
|
||||
'filter_max_objective': config.get('hyperopt_list_max_objective', None),
|
||||
}
|
||||
if not HyperoptTools._test_hyperopt_results_exist(results_file):
|
||||
# No file found.
|
||||
return [], 0
|
||||
|
||||
epochs = []
|
||||
total_epochs = 0
|
||||
for epochs_tmp in HyperoptTools._read_results(results_file):
|
||||
if total_epochs == 0 and epochs_tmp[0].get('is_best') is None:
|
||||
raise OperationalException(
|
||||
"The file with HyperoptTools results is incompatible with this version "
|
||||
"of Freqtrade and cannot be loaded.")
|
||||
logger.info(f"Loaded {len(epochs)} previous evaluations from disk.")
|
||||
return epochs
|
||||
total_epochs += len(epochs_tmp)
|
||||
epochs += hyperopt_filter_epochs(epochs_tmp, filteroptions, log=False)
|
||||
|
||||
logger.info(f"Loaded {total_epochs} previous evaluations from disk.")
|
||||
|
||||
# Final filter run ...
|
||||
epochs = hyperopt_filter_epochs(epochs, filteroptions, log=True)
|
||||
|
||||
return epochs, total_epochs
|
||||
|
||||
@staticmethod
|
||||
def show_epoch_details(results, total_epochs: int, print_json: bool,
|
||||
@@ -90,8 +175,8 @@ class HyperoptTools():
|
||||
|
||||
if print_json:
|
||||
result_dict: Dict = {}
|
||||
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']:
|
||||
HyperoptTools._params_update_for_json(result_dict, params, s)
|
||||
for s in ['buy', 'sell', 'protection', 'roi', 'stoploss', 'trailing']:
|
||||
HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s)
|
||||
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
|
||||
|
||||
else:
|
||||
@@ -99,44 +184,64 @@ class HyperoptTools():
|
||||
non_optimized)
|
||||
HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:",
|
||||
non_optimized)
|
||||
HyperoptTools._params_pretty_print(params, 'roi', "ROI table:")
|
||||
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:")
|
||||
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:")
|
||||
HyperoptTools._params_pretty_print(params, 'protection',
|
||||
"Protection hyperspace params:", non_optimized)
|
||||
HyperoptTools._params_pretty_print(params, 'roi', "ROI table:", non_optimized)
|
||||
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:", non_optimized)
|
||||
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:", non_optimized)
|
||||
|
||||
@staticmethod
|
||||
def _params_update_for_json(result_dict, params, space: str) -> None:
|
||||
if space in params:
|
||||
def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None:
|
||||
if (space in params) or (space in non_optimized):
|
||||
space_params = HyperoptTools._space_params(params, space)
|
||||
space_non_optimized = HyperoptTools._space_params(non_optimized, space)
|
||||
all_space_params = space_params
|
||||
|
||||
# Merge non optimized params if there are any
|
||||
if len(space_non_optimized) > 0:
|
||||
all_space_params = {**space_params, **space_non_optimized}
|
||||
|
||||
if space in ['buy', 'sell']:
|
||||
result_dict.setdefault('params', {}).update(space_params)
|
||||
result_dict.setdefault('params', {}).update(all_space_params)
|
||||
elif space == 'roi':
|
||||
# Convert keys in min_roi dict to strings because
|
||||
# rapidjson cannot dump dicts with integer keys...
|
||||
result_dict['minimal_roi'] = {str(k): v for k, v in space_params.items()}
|
||||
result_dict['minimal_roi'] = {str(k): v for k, v in all_space_params.items()}
|
||||
else: # 'stoploss', 'trailing'
|
||||
result_dict.update(space_params)
|
||||
result_dict.update(all_space_params)
|
||||
|
||||
@staticmethod
|
||||
def _params_pretty_print(params, space: str, header: str, non_optimized={}) -> None:
|
||||
if space in params or space in non_optimized:
|
||||
space_params = HyperoptTools._space_params(params, space, 5)
|
||||
no_params = HyperoptTools._space_params(non_optimized, space, 5)
|
||||
appendix = ''
|
||||
if not space_params and not no_params:
|
||||
# No parameters - don't print
|
||||
return
|
||||
if not space_params:
|
||||
# Not optimized parameters - append string
|
||||
appendix = NON_OPT_PARAM_APPENDIX
|
||||
|
||||
result = f"\n# {header}\n"
|
||||
if space == 'stoploss':
|
||||
result += f"stoploss = {space_params.get('stoploss')}"
|
||||
elif space == 'roi':
|
||||
if space == "stoploss":
|
||||
stoploss = safe_value_fallback2(space_params, no_params, space, space)
|
||||
result += (f"stoploss = {stoploss}{appendix}")
|
||||
|
||||
elif space == "roi":
|
||||
result = result[:-1] + f'{appendix}\n'
|
||||
minimal_roi_result = rapidjson.dumps({
|
||||
str(k): v for k, v in space_params.items()
|
||||
str(k): v for k, v in (space_params or no_params).items()
|
||||
}, default=str, indent=4, number_mode=rapidjson.NM_NATIVE)
|
||||
result += f"minimal_roi = {minimal_roi_result}"
|
||||
elif space == 'trailing':
|
||||
|
||||
for k, v in space_params.items():
|
||||
result += f'{k} = {v}\n'
|
||||
elif space == "trailing":
|
||||
for k, v in (space_params or no_params).items():
|
||||
result += f"{k} = {v}{appendix}\n"
|
||||
|
||||
else:
|
||||
no_params = HyperoptTools._space_params(non_optimized, space, 5)
|
||||
# Buy / sell parameters
|
||||
|
||||
result += f"{space}_params = {HyperoptTools._pprint(space_params, no_params)}"
|
||||
result += f"{space}_params = {HyperoptTools._pprint_dict(space_params, no_params)}"
|
||||
|
||||
result = result.replace("\n", "\n ")
|
||||
print(result)
|
||||
@@ -150,7 +255,7 @@ class HyperoptTools():
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def _pprint(params, non_optimized, indent: int = 4):
|
||||
def _pprint_dict(params, non_optimized, indent: int = 4):
|
||||
"""
|
||||
Pretty-print hyperopt results (based on 2 dicts - with add. comment)
|
||||
"""
|
||||
@@ -162,7 +267,7 @@ class HyperoptTools():
|
||||
result += " " * indent + f'"{k}": '
|
||||
result += f'"{param}",' if isinstance(param, str) else f'{param},'
|
||||
if k in non_optimized:
|
||||
result += " # value loaded from strategy"
|
||||
result += NON_OPT_PARAM_APPENDIX
|
||||
result += "\n"
|
||||
result += '}'
|
||||
return result
|
||||
@@ -194,8 +299,8 @@ class HyperoptTools():
|
||||
f"Objective: {results['loss']:.5f}")
|
||||
|
||||
@staticmethod
|
||||
def prepare_trials_columns(trials, legacy_mode: bool, has_drawdown: bool) -> str:
|
||||
|
||||
def prepare_trials_columns(trials: pd.DataFrame, legacy_mode: bool,
|
||||
has_drawdown: bool) -> pd.DataFrame:
|
||||
trials['Best'] = ''
|
||||
|
||||
if 'results_metrics.winsdrawslosses' not in trials.columns:
|
||||
@@ -331,8 +436,7 @@ class HyperoptTools():
|
||||
return table
|
||||
|
||||
@staticmethod
|
||||
def export_csv_file(config: dict, results: list, total_epochs: int, highlight_best: bool,
|
||||
csv_file: str) -> None:
|
||||
def export_csv_file(config: dict, results: list, csv_file: str) -> None:
|
||||
"""
|
||||
Log result to csv-file
|
||||
"""
|
||||
@@ -354,21 +458,14 @@ class HyperoptTools():
|
||||
trials['Best'] = ''
|
||||
trials['Stake currency'] = config['stake_currency']
|
||||
|
||||
if 'results_metrics.total_trades' in trials:
|
||||
base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades',
|
||||
'results_metrics.profit_mean', 'results_metrics.profit_median',
|
||||
'results_metrics.profit_total',
|
||||
'Stake currency',
|
||||
'results_metrics.profit_total_abs', 'results_metrics.holding_avg',
|
||||
'loss', 'is_initial_point', 'is_best']
|
||||
perc_multi = 100
|
||||
else:
|
||||
perc_multi = 1
|
||||
base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count',
|
||||
'results_metrics.avg_profit', 'results_metrics.median_profit',
|
||||
'results_metrics.total_profit',
|
||||
'Stake currency', 'results_metrics.profit', 'results_metrics.duration',
|
||||
'loss', 'is_initial_point', 'is_best']
|
||||
base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades',
|
||||
'results_metrics.profit_mean', 'results_metrics.profit_median',
|
||||
'results_metrics.profit_total',
|
||||
'Stake currency',
|
||||
'results_metrics.profit_total_abs', 'results_metrics.holding_avg',
|
||||
'loss', 'is_initial_point', 'is_best']
|
||||
perc_multi = 100
|
||||
|
||||
param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()]
|
||||
trials = trials[base_metrics + param_metrics]
|
||||
|
||||
@@ -396,11 +493,6 @@ class HyperoptTools():
|
||||
trials['Avg profit'] = trials['Avg profit'].apply(
|
||||
lambda x: f'{x * perc_multi:,.2f}%' if not isna(x) else ""
|
||||
)
|
||||
if perc_multi == 1:
|
||||
trials['Avg duration'] = trials['Avg duration'].apply(
|
||||
lambda x: f'{x:,.1f} m' if isinstance(
|
||||
x, float) else f"{x.total_seconds() // 60:,.1f} m" if not isna(x) else ""
|
||||
)
|
||||
trials['Objective'] = trials['Objective'].apply(
|
||||
lambda x: f'{x:,.5f}' if x != 100000 else ""
|
||||
)
|
||||
|
@@ -4,7 +4,7 @@ from pathlib import Path
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
from numpy import int64
|
||||
from pandas import DataFrame
|
||||
from pandas import DataFrame, to_datetime
|
||||
from tabulate import tabulate
|
||||
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT
|
||||
@@ -21,7 +21,7 @@ def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> N
|
||||
Stores backtest results
|
||||
:param recordfilename: Path object, which can either be a filename or a directory.
|
||||
Filenames will be appended with a timestamp right before the suffix
|
||||
while for diectories, <directory>/backtest-result-<datetime>.json will be used as filename
|
||||
while for directories, <directory>/backtest-result-<datetime>.json will be used as filename
|
||||
:param stats: Dataframe containing the backtesting statistics
|
||||
"""
|
||||
if recordfilename.is_dir():
|
||||
@@ -31,7 +31,7 @@ def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> N
|
||||
filename = Path.joinpath(
|
||||
recordfilename.parent,
|
||||
f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}'
|
||||
).with_suffix(recordfilename.suffix)
|
||||
).with_suffix(recordfilename.suffix)
|
||||
file_dump_json(filename, stats)
|
||||
|
||||
latest_filename = Path.joinpath(filename.parent, LAST_BT_RESULT_FN)
|
||||
@@ -173,7 +173,7 @@ def generate_strategy_comparison(all_results: Dict) -> List[Dict]:
|
||||
for strategy, results in all_results.items():
|
||||
tabular_data.append(_generate_result_line(
|
||||
results['results'], results['config']['dry_run_wallet'], strategy)
|
||||
)
|
||||
)
|
||||
try:
|
||||
max_drawdown_per, _, _, _, _ = calculate_max_drawdown(results['results'],
|
||||
value_col='profit_ratio')
|
||||
@@ -189,7 +189,6 @@ def generate_strategy_comparison(all_results: Dict) -> List[Dict]:
|
||||
|
||||
|
||||
def generate_edge_table(results: dict) -> str:
|
||||
|
||||
floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', 'd', 'd')
|
||||
tabular_data = []
|
||||
headers = ['Pair', 'Stoploss', 'Win Rate', 'Risk Reward Ratio',
|
||||
@@ -214,6 +213,41 @@ def generate_edge_table(results: dict) -> str:
|
||||
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
|
||||
|
||||
|
||||
def _get_resample_from_period(period: str) -> str:
|
||||
if period == 'day':
|
||||
return '1d'
|
||||
if period == 'week':
|
||||
return '1w'
|
||||
if period == 'month':
|
||||
return '1M'
|
||||
raise ValueError(f"Period {period} is not supported.")
|
||||
|
||||
|
||||
def generate_periodic_breakdown_stats(trade_list: List, period: str) -> List[Dict[str, Any]]:
|
||||
results = DataFrame.from_records(trade_list)
|
||||
if len(results) == 0:
|
||||
return []
|
||||
results['close_date'] = to_datetime(results['close_date'], utc=True)
|
||||
resample_period = _get_resample_from_period(period)
|
||||
resampled = results.resample(resample_period, on='close_date')
|
||||
stats = []
|
||||
for name, day in resampled:
|
||||
profit_abs = day['profit_abs'].sum().round(10)
|
||||
wins = sum(day['profit_abs'] > 0)
|
||||
draws = sum(day['profit_abs'] == 0)
|
||||
loses = sum(day['profit_abs'] < 0)
|
||||
stats.append(
|
||||
{
|
||||
'date': name.strftime('%d/%m/%Y'),
|
||||
'profit_abs': profit_abs,
|
||||
'wins': wins,
|
||||
'draws': draws,
|
||||
'loses': loses
|
||||
}
|
||||
)
|
||||
return stats
|
||||
|
||||
|
||||
def generate_trading_stats(results: DataFrame) -> Dict[str, Any]:
|
||||
""" Generate overall trade statistics """
|
||||
if len(results) == 0:
|
||||
@@ -229,8 +263,6 @@ def generate_trading_stats(results: DataFrame) -> Dict[str, Any]:
|
||||
winning_trades = results.loc[results['profit_ratio'] > 0]
|
||||
draw_trades = results.loc[results['profit_ratio'] == 0]
|
||||
losing_trades = results.loc[results['profit_ratio'] < 0]
|
||||
zero_duration_trades = len(results.loc[(results['trade_duration'] == 0) &
|
||||
(results['sell_reason'] == 'trailing_stop_loss')])
|
||||
|
||||
holding_avg = (timedelta(minutes=round(results['trade_duration'].mean()))
|
||||
if not results.empty else timedelta())
|
||||
@@ -249,7 +281,6 @@ def generate_trading_stats(results: DataFrame) -> Dict[str, Any]:
|
||||
'winner_holding_avg_s': winner_holding_avg.total_seconds(),
|
||||
'loser_holding_avg': loser_holding_avg,
|
||||
'loser_holding_avg_s': loser_holding_avg.total_seconds(),
|
||||
'zero_duration_trades': zero_duration_trades,
|
||||
}
|
||||
|
||||
|
||||
@@ -264,6 +295,7 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
|
||||
'winning_days': 0,
|
||||
'draw_days': 0,
|
||||
'losing_days': 0,
|
||||
'daily_profit_list': [],
|
||||
}
|
||||
daily_profit_rel = results.resample('1d', on='close_date')['profit_ratio'].sum()
|
||||
daily_profit = results.resample('1d', on='close_date')['profit_abs'].sum().round(10)
|
||||
@@ -274,6 +306,7 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
|
||||
winning_days = sum(daily_profit > 0)
|
||||
draw_days = sum(daily_profit == 0)
|
||||
losing_days = sum(daily_profit < 0)
|
||||
daily_profit_list = [(str(idx.date()), val) for idx, val in daily_profit.iteritems()]
|
||||
|
||||
return {
|
||||
'backtest_best_day': best_rel,
|
||||
@@ -283,6 +316,7 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
|
||||
'winning_days': winning_days,
|
||||
'draw_days': draw_days,
|
||||
'losing_days': losing_days,
|
||||
'daily_profit': daily_profit_list,
|
||||
}
|
||||
|
||||
|
||||
@@ -300,7 +334,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
|
||||
:param min_date: Backtest start date
|
||||
:param max_date: Backtest end date
|
||||
:param market_change: float indicating the market change
|
||||
:return: Dictionary containing results per strategy and a stratgy summary.
|
||||
:return: Dictionary containing results per strategy and a strategy summary.
|
||||
"""
|
||||
results: Dict[str, DataFrame] = content['results']
|
||||
if not isinstance(results, DataFrame):
|
||||
@@ -325,10 +359,11 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
|
||||
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
|
||||
worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'],
|
||||
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
|
||||
results['open_timestamp'] = results['open_date'].astype(int64) // 1e6
|
||||
results['close_timestamp'] = results['close_date'].astype(int64) // 1e6
|
||||
if not results.empty:
|
||||
results['open_timestamp'] = results['open_date'].view(int64) // 1e6
|
||||
results['close_timestamp'] = results['close_date'].view(int64) // 1e6
|
||||
|
||||
backtest_days = (max_date - min_date).days
|
||||
backtest_days = (max_date - min_date).days or 1
|
||||
strat_stats = {
|
||||
'trades': results.to_dict(orient='records'),
|
||||
'locks': [lock.to_json() for lock in content['locks']],
|
||||
@@ -337,6 +372,8 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
|
||||
'results_per_pair': pair_results,
|
||||
'sell_reason_summary': sell_reason_stats,
|
||||
'left_open_trades': left_open_results,
|
||||
# 'days_breakdown_stats': days_breakdown_stats,
|
||||
|
||||
'total_trades': len(results),
|
||||
'total_volume': float(results['stake_amount'].sum()),
|
||||
'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0,
|
||||
@@ -353,7 +390,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
|
||||
'backtest_run_start_ts': content['backtest_start_time'],
|
||||
'backtest_run_end_ts': content['backtest_end_time'],
|
||||
|
||||
'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0,
|
||||
'trades_per_day': round(len(results) / backtest_days, 2),
|
||||
'market_change': market_change,
|
||||
'pairlist': list(btdata.keys()),
|
||||
'stake_amount': config['stake_amount'],
|
||||
@@ -367,6 +404,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
|
||||
'max_open_trades_setting': (config['max_open_trades']
|
||||
if config['max_open_trades'] != float('inf') else -1),
|
||||
'timeframe': config['timeframe'],
|
||||
'timeframe_detail': config.get('timeframe_detail', ''),
|
||||
'timerange': config.get('timerange', ''),
|
||||
'enable_protections': config.get('enable_protections', False),
|
||||
'strategy_name': strategy,
|
||||
@@ -378,10 +416,10 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
|
||||
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False),
|
||||
'use_custom_stoploss': config.get('use_custom_stoploss', False),
|
||||
'minimal_roi': config['minimal_roi'],
|
||||
'use_sell_signal': config['ask_strategy']['use_sell_signal'],
|
||||
'sell_profit_only': config['ask_strategy']['sell_profit_only'],
|
||||
'sell_profit_offset': config['ask_strategy']['sell_profit_offset'],
|
||||
'ignore_roi_if_buy_signal': config['ask_strategy']['ignore_roi_if_buy_signal'],
|
||||
'use_sell_signal': config['use_sell_signal'],
|
||||
'sell_profit_only': config['sell_profit_only'],
|
||||
'sell_profit_offset': config['sell_profit_offset'],
|
||||
'ignore_roi_if_buy_signal': config['ignore_roi_if_buy_signal'],
|
||||
**daily_stats,
|
||||
**trade_stats
|
||||
}
|
||||
@@ -436,7 +474,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
|
||||
{ Strategy: {'results: results, 'config: config}}.
|
||||
:param min_date: Backtest start date
|
||||
:param max_date: Backtest end date
|
||||
:return: Dictionary containing results per strategy and a stratgy summary.
|
||||
:return: Dictionary containing results per strategy and a strategy summary.
|
||||
"""
|
||||
result: Dict[str, Any] = {'strategy': {}}
|
||||
market_change = calculate_market_change(btdata, 'close')
|
||||
@@ -504,12 +542,33 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren
|
||||
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
|
||||
|
||||
|
||||
def text_table_periodic_breakdown(days_breakdown_stats: List[Dict[str, Any]],
|
||||
stake_currency: str, period: str) -> str:
|
||||
"""
|
||||
Generate small table with Backtest results by days
|
||||
:param days_breakdown_stats: Days breakdown metrics
|
||||
:param stake_currency: Stakecurrency used
|
||||
:return: pretty printed table with tabulate as string
|
||||
"""
|
||||
headers = [
|
||||
period.capitalize(),
|
||||
f'Tot Profit {stake_currency}',
|
||||
'Wins',
|
||||
'Draws',
|
||||
'Losses',
|
||||
]
|
||||
output = [[
|
||||
d['date'], round_coin_value(d['profit_abs'], stake_currency, False),
|
||||
d['wins'], d['draws'], d['loses'],
|
||||
] for d in days_breakdown_stats]
|
||||
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
|
||||
|
||||
|
||||
def text_table_strategy(strategy_results, stake_currency: str) -> str:
|
||||
"""
|
||||
Generate summary table per strategy
|
||||
:param strategy_results: Dict of <Strategyname: DataFrame> containing results for all strategies
|
||||
:param stake_currency: stake-currency - used to correctly name headers
|
||||
:param max_open_trades: Maximum allowed open trades used for backtest
|
||||
:param all_results: Dict of <Strategyname: DataFrame> containing results for all strategies
|
||||
:return: pretty printed table with tabulate as string
|
||||
"""
|
||||
floatfmt = _get_line_floatfmt(stake_currency)
|
||||
@@ -543,28 +602,23 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||
# Newly added fields should be ignored if they are missing in strat_results. hyperopt-show
|
||||
# command stores these results and newer version of freqtrade must be able to handle old
|
||||
# results with missing new fields.
|
||||
zero_duration_trades = '--'
|
||||
|
||||
if 'zero_duration_trades' in strat_results:
|
||||
zero_duration_trades_per = \
|
||||
100.0 / strat_results['total_trades'] * strat_results['zero_duration_trades']
|
||||
zero_duration_trades = f'{zero_duration_trades_per:.2f}% ' \
|
||||
f'({strat_results["zero_duration_trades"]})'
|
||||
|
||||
metrics = [
|
||||
('Backtesting from', strat_results['backtest_start']),
|
||||
('Backtesting to', strat_results['backtest_end']),
|
||||
('Max open trades', strat_results['max_open_trades']),
|
||||
('', ''), # Empty line to improve readability
|
||||
('Total trades', strat_results['total_trades']),
|
||||
('Total/Daily Avg Trades',
|
||||
f"{strat_results['total_trades']} / {strat_results['trades_per_day']}"),
|
||||
('Starting balance', round_coin_value(strat_results['starting_balance'],
|
||||
strat_results['stake_currency'])),
|
||||
('Final balance', round_coin_value(strat_results['final_balance'],
|
||||
strat_results['stake_currency'])),
|
||||
('Absolute profit ', round_coin_value(strat_results['profit_total_abs'],
|
||||
strat_results['stake_currency'])),
|
||||
('Total profit %', f"{round(strat_results['profit_total'] * 100, 2):}%"),
|
||||
('Total profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"),
|
||||
('Trades per day', strat_results['trades_per_day']),
|
||||
('Avg. daily profit %',
|
||||
f"{round(strat_results['profit_total'] / strat_results['backtest_days'] * 100, 2)}%"),
|
||||
('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'],
|
||||
strat_results['stake_currency'])),
|
||||
('Total trade volume', round_coin_value(strat_results['total_volume'],
|
||||
@@ -586,7 +640,6 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||
f"{strat_results['draw_days']} / {strat_results['losing_days']}"),
|
||||
('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"),
|
||||
('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"),
|
||||
('Zero Duration Trades', zero_duration_trades),
|
||||
('Rejected Buy signals', strat_results.get('rejected_signals', 'N/A')),
|
||||
('', ''), # Empty line to improve readability
|
||||
|
||||
@@ -613,7 +666,7 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||
strat_results['stake_currency'])
|
||||
stake_amount = round_coin_value(
|
||||
strat_results['stake_amount'], strat_results['stake_currency']
|
||||
) if strat_results['stake_amount'] != UNLIMITED_STAKE_AMOUNT else 'unlimited'
|
||||
) if strat_results['stake_amount'] != UNLIMITED_STAKE_AMOUNT else 'unlimited'
|
||||
|
||||
message = ("No trades made. "
|
||||
f"Your starting balance was {start_balance}, "
|
||||
@@ -622,7 +675,8 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||
return message
|
||||
|
||||
|
||||
def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: str):
|
||||
def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: str,
|
||||
backtest_breakdown=[]):
|
||||
"""
|
||||
Print results for one strategy
|
||||
"""
|
||||
@@ -644,6 +698,15 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
|
||||
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
|
||||
print(table)
|
||||
|
||||
for period in backtest_breakdown:
|
||||
days_breakdown_stats = generate_periodic_breakdown_stats(
|
||||
trade_list=results['trades'], period=period)
|
||||
table = text_table_periodic_breakdown(days_breakdown_stats=days_breakdown_stats,
|
||||
stake_currency=stake_currency, period=period)
|
||||
if isinstance(table, str) and len(table) > 0:
|
||||
print(f' {period.upper()} BREAKDOWN '.center(len(table.splitlines()[0]), '='))
|
||||
print(table)
|
||||
|
||||
table = text_table_add_metrics(results)
|
||||
if isinstance(table, str) and len(table) > 0:
|
||||
print(' SUMMARY METRICS '.center(len(table.splitlines()[0]), '='))
|
||||
@@ -658,12 +721,16 @@ def show_backtest_results(config: Dict, backtest_stats: Dict):
|
||||
stake_currency = config['stake_currency']
|
||||
|
||||
for strategy, results in backtest_stats['strategy'].items():
|
||||
show_backtest_result(strategy, results, stake_currency)
|
||||
show_backtest_result(
|
||||
strategy, results, stake_currency,
|
||||
config.get('backtest_breakdown', []))
|
||||
|
||||
if len(backtest_stats['strategy']) > 1:
|
||||
# Print Strategy summary table
|
||||
|
||||
table = text_table_strategy(backtest_stats['strategy_comparison'], stake_currency)
|
||||
print(f"{results['backtest_start']} -> {results['backtest_end']} |"
|
||||
f" Max open trades : {results['max_open_trades']}")
|
||||
print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '='))
|
||||
print(table)
|
||||
print('=' * len(table.splitlines()[0]))
|
||||
|
@@ -7,11 +7,15 @@ class SKDecimal(Integer):
|
||||
def __init__(self, low, high, decimals=3, prior="uniform", base=10, transform=None,
|
||||
name=None, dtype=np.int64):
|
||||
self.decimals = decimals
|
||||
_low = int(low * pow(10, self.decimals))
|
||||
_high = int(high * pow(10, self.decimals))
|
||||
|
||||
self.pow_dot_one = pow(0.1, self.decimals)
|
||||
self.pow_ten = pow(10, self.decimals)
|
||||
|
||||
_low = int(low * self.pow_ten)
|
||||
_high = int(high * self.pow_ten)
|
||||
# trunc to precision to avoid points out of space
|
||||
self.low_orig = round(_low * pow(0.1, self.decimals), self.decimals)
|
||||
self.high_orig = round(_high * pow(0.1, self.decimals), self.decimals)
|
||||
self.low_orig = round(_low * self.pow_dot_one, self.decimals)
|
||||
self.high_orig = round(_high * self.pow_dot_one, self.decimals)
|
||||
|
||||
super().__init__(_low, _high, prior, base, transform, name, dtype)
|
||||
|
||||
@@ -25,9 +29,9 @@ class SKDecimal(Integer):
|
||||
return self.low_orig <= point <= self.high_orig
|
||||
|
||||
def transform(self, Xt):
|
||||
aa = [int(x * pow(10, self.decimals)) for x in Xt]
|
||||
return super().transform(aa)
|
||||
return super().transform([int(v * self.pow_ten) for v in Xt])
|
||||
|
||||
def inverse_transform(self, Xt):
|
||||
res = super().inverse_transform(Xt)
|
||||
return [round(x * pow(0.1, self.decimals), self.decimals) for x in res]
|
||||
# equivalent to [round(x * pow(0.1, self.decimals), self.decimals) for x in res]
|
||||
return [int(v) / self.pow_ten for v in res]
|
||||
|
Reference in New Issue
Block a user