Merge branch 'develop' into data_handler
This commit is contained in:
@@ -9,6 +9,7 @@ from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, NamedTuple, Optional
|
||||
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import (TimeRange, remove_credentials,
|
||||
@@ -25,7 +26,7 @@ from freqtrade.optimize.optimize_reports import (
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.state import RunMode
|
||||
from freqtrade.strategy.interface import IStrategy, SellType
|
||||
from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -150,7 +151,7 @@ class Backtesting:
|
||||
logger.info(f'Dumping backtest results to {recordfilename}')
|
||||
file_dump_json(recordfilename, records)
|
||||
|
||||
def _get_ticker_list(self, processed) -> Dict[str, DataFrame]:
|
||||
def _get_ticker_list(self, processed: Dict) -> Dict[str, DataFrame]:
|
||||
"""
|
||||
Helper function to convert a processed tickerlist into a list for performance reasons.
|
||||
|
||||
@@ -177,7 +178,8 @@ class Backtesting:
|
||||
ticker[pair] = [x for x in ticker_data.itertuples()]
|
||||
return ticker
|
||||
|
||||
def _get_close_rate(self, sell_row, trade: Trade, sell, trade_dur) -> float:
|
||||
def _get_close_rate(self, sell_row, trade: Trade, sell: SellCheckTuple,
|
||||
trade_dur: int) -> float:
|
||||
"""
|
||||
Get close rate for backtesting result
|
||||
"""
|
||||
@@ -282,7 +284,7 @@ class Backtesting:
|
||||
return None
|
||||
|
||||
def backtest(self, processed: Dict, stake_amount: float,
|
||||
start_date, end_date,
|
||||
start_date: arrow.Arrow, end_date: arrow.Arrow,
|
||||
max_open_trades: int = 0, position_stacking: bool = False) -> DataFrame:
|
||||
"""
|
||||
Implement backtesting functionality
|
||||
@@ -406,12 +408,12 @@ class Backtesting:
|
||||
)
|
||||
# Execute backtest and print results
|
||||
all_results[self.strategy.get_strategy_name()] = self.backtest(
|
||||
processed=preprocessed,
|
||||
stake_amount=self.config['stake_amount'],
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=max_open_trades,
|
||||
position_stacking=position_stacking,
|
||||
processed=preprocessed,
|
||||
stake_amount=self.config['stake_amount'],
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=max_open_trades,
|
||||
position_stacking=position_stacking,
|
||||
)
|
||||
|
||||
for strategy, results in all_results.items():
|
||||
@@ -428,7 +430,10 @@ class Backtesting:
|
||||
results=results))
|
||||
|
||||
print(' SELL REASON STATS '.center(133, '='))
|
||||
print(generate_text_table_sell_reason(data, results))
|
||||
print(generate_text_table_sell_reason(data,
|
||||
stake_currency=self.config['stake_currency'],
|
||||
max_open_trades=self.config['max_open_trades'],
|
||||
results=results))
|
||||
|
||||
print(' LEFT OPEN TRADES REPORT '.center(133, '='))
|
||||
print(generate_text_table(data,
|
||||
@@ -438,7 +443,7 @@ class Backtesting:
|
||||
print()
|
||||
if len(all_results) > 1:
|
||||
# Print Strategy summary table
|
||||
print(' Strategy Summary '.center(133, '='))
|
||||
print(' STRATEGY SUMMARY '.center(133, '='))
|
||||
print(generate_text_table_strategy(self.config['stake_currency'],
|
||||
self.config['max_open_trades'],
|
||||
all_results=all_results))
|
||||
|
@@ -60,6 +60,7 @@ class Hyperopt:
|
||||
hyperopt = Hyperopt(config)
|
||||
hyperopt.start()
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]) -> None:
|
||||
self.config = config
|
||||
|
||||
@@ -91,13 +92,13 @@ class Hyperopt:
|
||||
# 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 = \
|
||||
self.custom_hyperopt.populate_indicators # type: ignore
|
||||
self.custom_hyperopt.populate_indicators # type: ignore
|
||||
if hasattr(self.custom_hyperopt, 'populate_buy_trend'):
|
||||
self.backtesting.strategy.advise_buy = \
|
||||
self.custom_hyperopt.populate_buy_trend # type: ignore
|
||||
self.custom_hyperopt.populate_buy_trend # type: ignore
|
||||
if hasattr(self.custom_hyperopt, 'populate_sell_trend'):
|
||||
self.backtesting.strategy.advise_sell = \
|
||||
self.custom_hyperopt.populate_sell_trend # 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):
|
||||
@@ -118,11 +119,11 @@ class Hyperopt:
|
||||
self.print_json = self.config.get('print_json', False)
|
||||
|
||||
@staticmethod
|
||||
def get_lock_filename(config) -> str:
|
||||
def get_lock_filename(config: Dict[str, Any]) -> str:
|
||||
|
||||
return str(config['user_data_dir'] / 'hyperopt.lock')
|
||||
|
||||
def clean_hyperopt(self):
|
||||
def clean_hyperopt(self) -> None:
|
||||
"""
|
||||
Remove hyperopt pickle files to restart hyperopt.
|
||||
"""
|
||||
@@ -159,7 +160,7 @@ class Hyperopt:
|
||||
f"saved to '{self.trials_file}'.")
|
||||
|
||||
@staticmethod
|
||||
def _read_trials(trials_file) -> List:
|
||||
def _read_trials(trials_file: Path) -> List:
|
||||
"""
|
||||
Read hyperopt trials file
|
||||
"""
|
||||
@@ -190,7 +191,7 @@ class Hyperopt:
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def print_epoch_details(results, total_epochs, print_json: bool,
|
||||
def print_epoch_details(results, total_epochs: int, print_json: bool,
|
||||
no_header: bool = False, header_str: str = None) -> None:
|
||||
"""
|
||||
Display details of the hyperopt result
|
||||
@@ -219,7 +220,7 @@ class Hyperopt:
|
||||
Hyperopt._params_pretty_print(params, 'trailing', "Trailing stop:")
|
||||
|
||||
@staticmethod
|
||||
def _params_update_for_json(result_dict, params, space: str):
|
||||
def _params_update_for_json(result_dict, params, space: str) -> None:
|
||||
if space in params:
|
||||
space_params = Hyperopt._space_params(params, space)
|
||||
if space in ['buy', 'sell']:
|
||||
@@ -236,7 +237,7 @@ class Hyperopt:
|
||||
result_dict.update(space_params)
|
||||
|
||||
@staticmethod
|
||||
def _params_pretty_print(params, space: str, header: str):
|
||||
def _params_pretty_print(params, space: str, header: str) -> None:
|
||||
if space in params:
|
||||
space_params = Hyperopt._space_params(params, space, 5)
|
||||
if space == 'stoploss':
|
||||
@@ -252,7 +253,7 @@ class Hyperopt:
|
||||
return round_dict(d, r) if r else d
|
||||
|
||||
@staticmethod
|
||||
def is_best_loss(results, current_best_loss) -> bool:
|
||||
def is_best_loss(results, current_best_loss: float) -> bool:
|
||||
return results['loss'] < current_best_loss
|
||||
|
||||
def print_results(self, results) -> None:
|
||||
@@ -346,15 +347,15 @@ class Hyperopt:
|
||||
|
||||
if self.has_space('roi'):
|
||||
self.backtesting.strategy.minimal_roi = \
|
||||
self.custom_hyperopt.generate_roi_table(params_dict)
|
||||
self.custom_hyperopt.generate_roi_table(params_dict)
|
||||
|
||||
if self.has_space('buy'):
|
||||
self.backtesting.strategy.advise_buy = \
|
||||
self.custom_hyperopt.buy_strategy_generator(params_dict)
|
||||
self.custom_hyperopt.buy_strategy_generator(params_dict)
|
||||
|
||||
if self.has_space('sell'):
|
||||
self.backtesting.strategy.advise_sell = \
|
||||
self.custom_hyperopt.sell_strategy_generator(params_dict)
|
||||
self.custom_hyperopt.sell_strategy_generator(params_dict)
|
||||
|
||||
if self.has_space('stoploss'):
|
||||
self.backtesting.strategy.stoploss = params_dict['stoploss']
|
||||
@@ -373,12 +374,12 @@ class Hyperopt:
|
||||
min_date, max_date = get_timerange(processed)
|
||||
|
||||
backtesting_results = self.backtesting.backtest(
|
||||
processed=processed,
|
||||
stake_amount=self.config['stake_amount'],
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=self.max_open_trades,
|
||||
position_stacking=self.position_stacking,
|
||||
processed=processed,
|
||||
stake_amount=self.config['stake_amount'],
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=self.max_open_trades,
|
||||
position_stacking=self.position_stacking,
|
||||
)
|
||||
return self._get_results_dict(backtesting_results, min_date, max_date,
|
||||
params_dict, params_details)
|
||||
@@ -439,7 +440,7 @@ class Hyperopt:
|
||||
random_state=self.random_state,
|
||||
)
|
||||
|
||||
def fix_optimizer_models_list(self):
|
||||
def fix_optimizer_models_list(self) -> None:
|
||||
"""
|
||||
WORKAROUND: Since skopt is not actively supported, this resolves problems with skopt
|
||||
memory usage, see also: https://github.com/scikit-optimize/scikit-optimize/pull/746
|
||||
@@ -461,7 +462,7 @@ class Hyperopt:
|
||||
wrap_non_picklable_objects(self.generate_optimizer))(v, i) for v in asked)
|
||||
|
||||
@staticmethod
|
||||
def load_previous_results(trials_file) -> List:
|
||||
def load_previous_results(trials_file: Path) -> List:
|
||||
"""
|
||||
Load data for epochs from the file if we have one
|
||||
"""
|
||||
@@ -470,8 +471,8 @@ class Hyperopt:
|
||||
trials = Hyperopt._read_trials(trials_file)
|
||||
if trials[0].get('is_best') is None:
|
||||
raise OperationalException(
|
||||
"The file with Hyperopt results is incompatible with this version "
|
||||
"of Freqtrade and cannot be loaded.")
|
||||
"The file with Hyperopt results is incompatible with this version "
|
||||
"of Freqtrade and cannot be loaded.")
|
||||
logger.info(f"Loaded {len(trials)} previous evaluations from disk.")
|
||||
return trials
|
||||
|
||||
|
@@ -207,7 +207,7 @@ class IHyperOpt(ABC):
|
||||
# so this intermediate parameter is used as the value of the difference between
|
||||
# them. The value of the 'trailing_stop_positive_offset' is constructed in the
|
||||
# generate_trailing_params() method.
|
||||
# # This is similar to the hyperspace dimensions used for constructing the ROI tables.
|
||||
# This is similar to the hyperspace dimensions used for constructing the ROI tables.
|
||||
Real(0.001, 0.1, name='trailing_stop_positive_offset_p1'),
|
||||
|
||||
Categorical([True, False], name='trailing_only_offset_is_reached'),
|
||||
|
@@ -28,18 +28,19 @@ class SharpeHyperOptLoss(IHyperOptLoss):
|
||||
|
||||
Uses Sharpe Ratio calculation.
|
||||
"""
|
||||
total_profit = results.profit_percent
|
||||
total_profit = results["profit_percent"]
|
||||
days_period = (max_date - min_date).days
|
||||
|
||||
# adding slippage of 0.1% per trade
|
||||
total_profit = total_profit - 0.0005
|
||||
expected_yearly_return = total_profit.sum() / days_period
|
||||
expected_returns_mean = total_profit.sum() / days_period
|
||||
up_stdev = np.std(total_profit)
|
||||
|
||||
if (np.std(total_profit) != 0.):
|
||||
sharp_ratio = expected_yearly_return / np.std(total_profit) * np.sqrt(365)
|
||||
sharp_ratio = expected_returns_mean / up_stdev * np.sqrt(365)
|
||||
else:
|
||||
# Define high (negative) sharpe ratio to be clear that this is NOT optimal.
|
||||
sharp_ratio = -20.
|
||||
|
||||
# print(expected_yearly_return, np.std(total_profit), sharp_ratio)
|
||||
# print(expected_returns_mean, up_stdev, sharp_ratio)
|
||||
return -sharp_ratio
|
||||
|
61
freqtrade/optimize/hyperopt_loss_sharpe_daily.py
Normal file
61
freqtrade/optimize/hyperopt_loss_sharpe_daily.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
SharpeHyperOptLossDaily
|
||||
|
||||
This module defines the alternative HyperOptLoss class which can be used for
|
||||
Hyperoptimization.
|
||||
"""
|
||||
import math
|
||||
from datetime import datetime
|
||||
|
||||
from pandas import DataFrame, date_range
|
||||
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
|
||||
class SharpeHyperOptLossDaily(IHyperOptLoss):
|
||||
"""
|
||||
Defines the loss function for hyperopt.
|
||||
|
||||
This implementation uses the Sharpe Ratio calculation.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(results: DataFrame, trade_count: int,
|
||||
min_date: datetime, max_date: datetime,
|
||||
*args, **kwargs) -> float:
|
||||
"""
|
||||
Objective function, returns smaller number for more optimal results.
|
||||
|
||||
Uses Sharpe Ratio calculation.
|
||||
"""
|
||||
resample_freq = '1D'
|
||||
slippage_per_trade_ratio = 0.0005
|
||||
days_in_year = 365
|
||||
annual_risk_free_rate = 0.0
|
||||
risk_free_rate = annual_risk_free_rate / days_in_year
|
||||
|
||||
# apply slippage per trade to profit_percent
|
||||
results.loc[:, 'profit_percent_after_slippage'] = \
|
||||
results['profit_percent'] - slippage_per_trade_ratio
|
||||
|
||||
# create the index within the min_date and end max_date
|
||||
t_index = date_range(start=min_date, end=max_date, freq=resample_freq)
|
||||
|
||||
sum_daily = (
|
||||
results.resample(resample_freq, on='close_time').agg(
|
||||
{"profit_percent_after_slippage": sum}).reindex(t_index).fillna(0)
|
||||
)
|
||||
|
||||
total_profit = sum_daily["profit_percent_after_slippage"] - risk_free_rate
|
||||
expected_returns_mean = total_profit.mean()
|
||||
up_stdev = total_profit.std()
|
||||
|
||||
if (up_stdev != 0.):
|
||||
sharp_ratio = expected_returns_mean / up_stdev * math.sqrt(days_in_year)
|
||||
else:
|
||||
# Define high (negative) sharpe ratio to be clear that this is NOT optimal.
|
||||
sharp_ratio = -20.
|
||||
|
||||
# print(t_index, sum_daily, total_profit)
|
||||
# print(risk_free_rate, expected_returns_mean, up_stdev, sharp_ratio)
|
||||
return -sharp_ratio
|
@@ -19,9 +19,18 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra
|
||||
|
||||
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f')
|
||||
tabular_data = []
|
||||
headers = ['pair', 'buy count', 'avg profit %', 'cum profit %',
|
||||
f'tot profit {stake_currency}', 'tot profit %', 'avg duration',
|
||||
'profit', 'loss']
|
||||
headers = [
|
||||
'Pair',
|
||||
'Buys',
|
||||
'Avg Profit %',
|
||||
'Cum Profit %',
|
||||
f'Tot Profit {stake_currency}',
|
||||
'Tot Profit %',
|
||||
'Avg Duration',
|
||||
'Wins',
|
||||
'Draws',
|
||||
'Losses'
|
||||
]
|
||||
for pair in data:
|
||||
result = results[results.pair == pair]
|
||||
if skip_nan and result.profit_abs.isnull().all():
|
||||
@@ -37,6 +46,7 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra
|
||||
str(timedelta(
|
||||
minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00',
|
||||
len(result[result.profit_abs > 0]),
|
||||
len(result[result.profit_abs == 0]),
|
||||
len(result[result.profit_abs < 0])
|
||||
])
|
||||
|
||||
@@ -51,6 +61,7 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra
|
||||
str(timedelta(
|
||||
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
|
||||
len(results[results.profit_abs > 0]),
|
||||
len(results[results.profit_abs == 0]),
|
||||
len(results[results.profit_abs < 0])
|
||||
])
|
||||
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||
@@ -58,7 +69,9 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra
|
||||
floatfmt=floatfmt, tablefmt="pipe") # type: ignore
|
||||
|
||||
|
||||
def generate_text_table_sell_reason(data: Dict[str, Dict], results: DataFrame) -> str:
|
||||
def generate_text_table_sell_reason(
|
||||
data: Dict[str, Dict], stake_currency: str, max_open_trades: int, results: DataFrame
|
||||
) -> str:
|
||||
"""
|
||||
Generate small table outlining Backtest results
|
||||
:param data: Dict of <pair: dataframe> containing data that was used during backtesting.
|
||||
@@ -66,13 +79,39 @@ def generate_text_table_sell_reason(data: Dict[str, Dict], results: DataFrame) -
|
||||
:return: pretty printed table with tabulate as string
|
||||
"""
|
||||
tabular_data = []
|
||||
headers = ['Sell Reason', 'Count', 'Profit', 'Loss', 'Profit %']
|
||||
headers = [
|
||||
"Sell Reason",
|
||||
"Sells",
|
||||
"Wins",
|
||||
"Draws",
|
||||
"Losses",
|
||||
"Avg Profit %",
|
||||
"Cum Profit %",
|
||||
f"Tot Profit {stake_currency}",
|
||||
"Tot Profit %",
|
||||
]
|
||||
for reason, count in results['sell_reason'].value_counts().iteritems():
|
||||
result = results.loc[results['sell_reason'] == reason]
|
||||
profit = len(result[result['profit_abs'] >= 0])
|
||||
wins = len(result[result['profit_abs'] > 0])
|
||||
draws = len(result[result['profit_abs'] == 0])
|
||||
loss = len(result[result['profit_abs'] < 0])
|
||||
profit_mean = round(result['profit_percent'].mean() * 100.0, 2)
|
||||
tabular_data.append([reason.value, count, profit, loss, profit_mean])
|
||||
profit_sum = round(result["profit_percent"].sum() * 100.0, 2)
|
||||
profit_tot = result['profit_abs'].sum()
|
||||
profit_percent_tot = round(result['profit_percent'].sum() * 100.0 / max_open_trades, 2)
|
||||
tabular_data.append(
|
||||
[
|
||||
reason.value,
|
||||
count,
|
||||
wins,
|
||||
draws,
|
||||
loss,
|
||||
profit_mean,
|
||||
profit_sum,
|
||||
profit_tot,
|
||||
profit_percent_tot,
|
||||
]
|
||||
)
|
||||
return tabulate(tabular_data, headers=headers, tablefmt="pipe")
|
||||
|
||||
|
||||
@@ -88,9 +127,9 @@ def generate_text_table_strategy(stake_currency: str, max_open_trades: str,
|
||||
|
||||
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f')
|
||||
tabular_data = []
|
||||
headers = ['Strategy', 'buy count', 'avg profit %', 'cum profit %',
|
||||
f'tot profit {stake_currency}', 'tot profit %', 'avg duration',
|
||||
'profit', 'loss']
|
||||
headers = ['Strategy', 'Buys', 'Avg Profit %', 'Cum Profit %',
|
||||
f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration',
|
||||
'Wins', 'Draws', 'Losses']
|
||||
for strategy, results in all_results.items():
|
||||
tabular_data.append([
|
||||
strategy,
|
||||
@@ -102,6 +141,7 @@ def generate_text_table_strategy(stake_currency: str, max_open_trades: str,
|
||||
str(timedelta(
|
||||
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
|
||||
len(results[results.profit_abs > 0]),
|
||||
len(results[results.profit_abs == 0]),
|
||||
len(results[results.profit_abs < 0])
|
||||
])
|
||||
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||
@@ -113,9 +153,9 @@ def generate_edge_table(results: dict) -> str:
|
||||
|
||||
floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', '.d')
|
||||
tabular_data = []
|
||||
headers = ['pair', 'stoploss', 'win rate', 'risk reward ratio',
|
||||
'required risk reward', 'expectancy', 'total number of trades',
|
||||
'average duration (min)']
|
||||
headers = ['Pair', 'Stoploss', 'Win Rate', 'Risk Reward Ratio',
|
||||
'Required Risk Reward', 'Expectancy', 'Total Number of Trades',
|
||||
'Average Duration (min)']
|
||||
|
||||
for result in results.items():
|
||||
if result[1].nb_trades > 0:
|
||||
|
Reference in New Issue
Block a user