From 55e8092cbf8b5a025f90a08f786e854e321ea1eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jul 2019 22:52:33 +0200 Subject: [PATCH] Add sharpe ratio as loss function --- docs/hyperopt.md | 14 ++++++++ freqtrade/configuration/arguments.py | 2 +- freqtrade/optimize/hyperopt.py | 4 ++- freqtrade/optimize/hyperopt_loss.py | 27 ++++++++++++++++ freqtrade/tests/optimize/test_hyperopt.py | 39 ++++++++++++++++------- 5 files changed, 73 insertions(+), 13 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index cb344abf3..b341ec669 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -151,6 +151,20 @@ The above setup expects to find ADX, RSI and Bollinger Bands in the populated in When you want to test an indicator that isn't used by the bot currently, remember to add it to the `populate_indicators()` method in `hyperopt.py`. +## Loss-functions + +Each hyperparameter tuning requires a target. This is usually defined as a function, which get's closer to 0 for increasing values. + +By default, freqtrade uses a loss function we call `legacy` - since it's been with freqtrade since the beginning and optimizes for short trade duration. + +This can be configured by using the `--loss ` argument. +Possible options are: + +* `legacy` - The default option, optimizing for short trades and few losses. +* `sharpe` - using the sharpe-ratio to determine the quality of results +* `custom` - Custom defined loss-function [see next section](#using-a-custom-loss-function) + + ### Using a custom loss function To use a custom loss function, make sure that the function `hyperopt_loss_custom` is defined in your custom hyperopt class. diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 3e9629fbb..b6deb2451 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -235,7 +235,7 @@ AVAILABLE_CLI_OPTIONS = { help='Define the loss-function to use for hyperopt.' 'Possibilities are `legacy`, and `custom` (providing a custom loss-function).' 'Default: `%(default)s`.', - choices=['legacy', 'custom'], + choices=['legacy', 'sharpe', 'custom'], default='legacy', ), # List exchanges diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index cc9d9299c..929debc86 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -23,7 +23,7 @@ from freqtrade.configuration import Arguments from freqtrade.data.history import load_data, get_timeframe from freqtrade.optimize.backtesting import Backtesting from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver -from freqtrade.optimize.hyperopt_loss import hyperopt_loss_legacy +from freqtrade.optimize.hyperopt_loss import hyperopt_loss_legacy, hyperopt_loss_sharpe logger = logging.getLogger(__name__) @@ -74,6 +74,8 @@ class Hyperopt(Backtesting): # Assign loss function if self.config.get('loss_function', 'legacy') == 'legacy': self.calculate_loss = hyperopt_loss_legacy + elif self.config.get('loss_function', 'sharpe') == 'sharpe': + self.calculate_loss = hyperopt_loss_sharpe elif (self.config['loss_function'] == 'custom' and hasattr(self.custom_hyperopt, 'hyperopt_loss_custom')): self.calculate_loss = self.custom_hyperopt.hyperopt_loss_custom # type: ignore diff --git a/freqtrade/optimize/hyperopt_loss.py b/freqtrade/optimize/hyperopt_loss.py index d80febed5..20194ecb0 100644 --- a/freqtrade/optimize/hyperopt_loss.py +++ b/freqtrade/optimize/hyperopt_loss.py @@ -1,4 +1,7 @@ +from datetime import datetime from math import exp + +import numpy as np from pandas import DataFrame # Define some constants: @@ -35,3 +38,27 @@ def hyperopt_loss_legacy(results: DataFrame, trade_count: int, duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) result = trade_loss + profit_loss + duration_loss return result + + +def hyperopt_loss_sharpe(results: DataFrame, trade_count: int, + min_date: datetime, max_date: datetime, *args, **kwargs) -> float: + """ + Objective function, returns smaller number for more optimal results + Using sharpe ratio calculation + """ + 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 + + if (np.std(total_profit) != 0.): + sharp_ratio = expected_yearly_return / np.std(total_profit) * np.sqrt(365) + else: + sharp_ratio = 1. + + # print(expected_yearly_return, np.std(total_profit), sharp_ratio) + + # Negate sharp-ratio so lower is better (??) + return -sharp_ratio diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 889d8cb44..88d7de39c 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -15,6 +15,7 @@ from freqtrade.optimize import setup_configuration, start_hyperopt from freqtrade.optimize.default_hyperopt import DefaultHyperOpts from freqtrade.optimize.hyperopt import (HYPEROPT_LOCKFILE, TICKERDATA_PICKLE, Hyperopt) +from freqtrade.optimize.hyperopt_loss import hyperopt_loss_legacy, hyperopt_loss_sharpe from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode from freqtrade.strategy.interface import SellType @@ -273,32 +274,48 @@ def test_start_filelock(mocker, default_conf, caplog) -> None: ) -def test_loss_calculation_prefer_correct_trade_count(hyperopt, hyperopt_results) -> None: - correct = hyperopt.calculate_loss(hyperopt_results, hyperopt.target_trades) - over = hyperopt.calculate_loss(hyperopt_results, hyperopt.target_trades + 100) - under = hyperopt.calculate_loss(hyperopt_results, hyperopt.target_trades - 100) +def test_loss_calculation_prefer_correct_trade_count(hyperopt_results) -> None: + correct = hyperopt_loss_legacy(hyperopt_results, 600) + over = hyperopt_loss_legacy(hyperopt_results, 600 + 100) + under = hyperopt_loss_legacy(hyperopt_results, 600 - 100) assert over > correct assert under > correct -def test_loss_calculation_prefer_shorter_trades(hyperopt, hyperopt_results) -> None: +def test_loss_calculation_prefer_shorter_trades(hyperopt_results) -> None: resultsb = hyperopt_results.copy() resultsb['trade_duration'][1] = 20 - longer = hyperopt.calculate_loss(hyperopt_results, 100) - shorter = hyperopt.calculate_loss(resultsb, 100) + longer = hyperopt_loss_legacy(hyperopt_results, 100) + shorter = hyperopt_loss_legacy(resultsb, 100) assert shorter < longer -def test_loss_calculation_has_limited_profit(hyperopt, hyperopt_results) -> None: +def test_loss_calculation_has_limited_profit(hyperopt_results) -> None: results_over = hyperopt_results.copy() results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 results_under = hyperopt_results.copy() results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - correct = hyperopt.calculate_loss(hyperopt_results, hyperopt.target_trades) - over = hyperopt.calculate_loss(results_over, hyperopt.target_trades) - under = hyperopt.calculate_loss(results_under, hyperopt.target_trades) + correct = hyperopt_loss_legacy(hyperopt_results, 600) + over = hyperopt_loss_legacy(results_over, 600) + under = hyperopt_loss_legacy(results_under, 600) + assert over < correct + assert under > correct + + +def test_sharpe_loss_prefers_higher_profits(hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + correct = hyperopt_loss_sharpe(hyperopt_results, len( + hyperopt_results), datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hyperopt_loss_sharpe(results_over, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hyperopt_loss_sharpe(results_under, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) assert over < correct assert under > correct