Merge branch 'develop' into data_handler

This commit is contained in:
Matthias
2020-02-09 15:16:43 +01:00
74 changed files with 1078 additions and 408 deletions

View File

@@ -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))

View File

@@ -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

View File

@@ -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'),

View File

@@ -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

View 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

View File

@@ -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: