Skip to content

Commit

Permalink
added daily sharpe ratio hyperopt loss method, ty @djacky (#2826)
Browse files Browse the repository at this point in the history
* more consistent backtesting tables and labels

* added rounding to Tot Profit % on Sell Reasosn table to be consistent with other percentiles on table.

* added daily sharpe ratio hyperopt loss method, ty @djacky

* removed commented code

* removed unused profit_abs

* added proper slippage to each trade

* replaced use of old value total_profit

* Align quotes in same area

* added daily sharpe ratio test and modified hyperopt_loss_sharpe_daily

* fixed some more line alignments

* updated docs to include SharpeHyperOptLossDaily

* Update dockerfile to 3.8.1

* Run tests against 3.8

* added daily sharpe ratio hyperopt loss method, ty @djacky

* removed commented code

* removed unused profit_abs

* added proper slippage to each trade

* replaced use of old value total_profit

* added daily sharpe ratio test and modified hyperopt_loss_sharpe_daily

* updated docs to include SharpeHyperOptLossDaily

* docs fixes

* missed one fix

* fixed standard deviation line

* fixed to bracket notation

* fixed to bracket notation

* fixed syntax error

* better readability, kept np.sqrt(365) which results in  annualized sharpe ratio

* fixed method arguments indentation

* updated commented out debug print line

* renamed after slippage profit_percent so it wont affect _calculate_results_metrics()

* Reworked to fill leading and trailing days

* No need for np; make flake happy

* Fix risk free rate

Co-authored-by: Matthias <xmatthias@outlook.com>
Co-authored-by: hroff-1902 <47309513+hroff-1902@users.noreply.github.com>
  • Loading branch information
3 people committed Feb 6, 2020
1 parent b5ee4f1 commit 9639ffb
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 21 deletions.
4 changes: 2 additions & 2 deletions docs/bot-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,8 +337,8 @@ optional arguments:
generate completely different results, since the
target for optimization is different. Built-in
Hyperopt-loss-functions are: DefaultHyperOptLoss,
OnlyProfitHyperOptLoss, SharpeHyperOptLoss (default:
`DefaultHyperOptLoss`).
OnlyProfitHyperOptLoss, SharpeHyperOptLoss,
SharpeHyperOptLossDaily (default: `DefaultHyperOptLoss`).
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
Expand Down
38 changes: 21 additions & 17 deletions docs/hyperopt.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ Rarely you may also need to override:
!!! Tip "Quickly optimize ROI, stoploss and trailing stoploss"
You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything (i.e. without creation of a "complete" Hyperopt class with dimensions, parameters, triggers and guards, as described in this document) from the default hyperopt template by relying on your strategy to do most of the calculations.

``` python
```python
# Have a working strategy at hand.
freqtrade new-hyperopt --hyperopt EmptyHyperopt

freqtrade hyperopt --hyperopt EmptyHyperopt --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100
```
```

### 1. Install a Custom Hyperopt File

Expand All @@ -75,8 +75,8 @@ Copy the file `user_data/hyperopts/sample_hyperopt.py` into `user_data/hyperopts

There are two places you need to change in your hyperopt file to add a new buy hyperopt for testing:

- Inside `indicator_space()` - the parameters hyperopt shall be optimizing.
- Inside `populate_buy_trend()` - applying the parameters.
* Inside `indicator_space()` - the parameters hyperopt shall be optimizing.
* Inside `populate_buy_trend()` - applying the parameters.

There you have two different types of indicators: 1. `guards` and 2. `triggers`.

Expand Down Expand Up @@ -141,7 +141,7 @@ one we call `trigger` and use it to decide which buy trigger we want to use.

So let's write the buy strategy using these values:

``` python
```python
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
conditions = []
# GUARDS AND TRENDS
Expand Down Expand Up @@ -192,6 +192,7 @@ Currently, the following loss functions are builtin:
* `DefaultHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function)
* `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration)
* `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on the trade returns)
* `SharpeHyperOptLossDaily` (optimizes Sharpe Ratio calculated on daily trade returns)

Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation.

Expand All @@ -206,7 +207,7 @@ We strongly recommend to use `screen` or `tmux` to prevent any connection loss.
freqtrade hyperopt --config config.json --hyperopt <hyperoptname> -e 5000 --spaces all
```

Use `<hyperoptname>` as the name of the custom hyperopt used.
Use `<hyperoptname>` as the name of the custom hyperopt used.

The `-e` option will set how many evaluations hyperopt will do. We recommend
running at least several thousand evaluations.
Expand Down Expand Up @@ -265,23 +266,23 @@ The default Hyperopt Search Space, used when no `--space` command line option is

### Position stacking and disabling max market positions

In some situations, you may need to run Hyperopt (and Backtesting) with the
In some situations, you may need to run Hyperopt (and Backtesting) with the
`--eps`/`--enable-position-staking` and `--dmmp`/`--disable-max-market-positions` arguments.

By default, hyperopt emulates the behavior of the Freqtrade Live Run/Dry Run, where only one
open trade is allowed for every traded pair. The total number of trades open for all pairs
open trade is allowed for every traded pair. The total number of trades open for all pairs
is also limited by the `max_open_trades` setting. During Hyperopt/Backtesting this may lead to
some potential trades to be hidden (or masked) by previosly open trades.

The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times,
while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades`
while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades`
during Hyperopt/Backtesting (which is equal to setting `max_open_trades` to a very high
number).

!!! Note
Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality.

You can also enable position stacking in the configuration file by explicitly setting
You can also enable position stacking in the configuration file by explicitly setting
`"position_stacking"=true`.

### Reproducible results
Expand Down Expand Up @@ -323,7 +324,7 @@ method, what those values match to.

So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block:

``` python
```python
(dataframe['rsi'] < 29.0)
```

Expand Down Expand Up @@ -372,18 +373,19 @@ In order to use this best ROI table found by Hyperopt in backtesting and for liv
118: 0
}
```

As stated in the comment, you can also use it as the value of the `minimal_roi` setting in the configuration file.

#### Default ROI Search Space

If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the ticker_interval used. By default the values vary in the following ranges (for some of the most used ticker intervals, values are rounded to 5 digits after the decimal point):

| # step | 1m | | 5m | | 1h | | 1d | |
|---|---|---|---|---|---|---|---|---|
| 1 | 0 | 0.01161...0.11992 | 0 | 0.03...0.31 | 0 | 0.06883...0.71124 | 0 | 0.12178...1.25835 |
| 2 | 2...8 | 0.00774...0.04255 | 10...40 | 0.02...0.11 | 120...480 | 0.04589...0.25238 | 2880...11520 | 0.08118...0.44651 |
| 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 |
| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 |
| # step | 1m | | 5m | | 1h | | 1d | |
| ------ | ------ | ----------------- | -------- | ----------- | ---------- | ----------------- | ------------ | ----------------- |
| 1 | 0 | 0.01161...0.11992 | 0 | 0.03...0.31 | 0 | 0.06883...0.71124 | 0 | 0.12178...1.25835 |
| 2 | 2...8 | 0.00774...0.04255 | 10...40 | 0.02...0.11 | 120...480 | 0.04589...0.25238 | 2880...11520 | 0.08118...0.44651 |
| 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 |
| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 |

These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the ticker interval used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the ticker interval used.

Expand Down Expand Up @@ -416,6 +418,7 @@ In order to use this best stoploss value found by Hyperopt in backtesting and fo
# This attribute will be overridden if the config file contains "stoploss"
stoploss = -0.27996
```

As stated in the comment, you can also use it as the value of the `stoploss` setting in the configuration file.

#### Default Stoploss Search Space
Expand Down Expand Up @@ -452,6 +455,7 @@ In order to use these best trailing stop parameters found by Hyperopt in backtes
trailing_stop_positive_offset = 0.06038
trailing_only_offset_is_reached = True
```

As stated in the comment, you can also use it as the values of the corresponding settings in the configuration file.

#### Default Trailing Stop Search Space
Expand Down
2 changes: 1 addition & 1 deletion freqtrade/commands/cli_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ def __init__(self, *args, **kwargs):
help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). '
'Different functions can generate completely different results, '
'since the target for optimization is different. Built-in Hyperopt-loss-functions are: '
'DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss.'
'DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, SharpeHyperOptLossDaily.'
'(default: `%(default)s`).',
metavar='NAME',
default=constants.DEFAULT_HYPEROPT_LOSS,
Expand Down
61 changes: 61 additions & 0 deletions freqtrade/optimize/hyperopt_loss_sharpe_daily.py
Original file line number Diff line number Diff line change
@@ -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
26 changes: 25 additions & 1 deletion tests/optimize/test_hyperopt.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,13 @@ def hyperopt_results():
'profit_percent': [-0.1, 0.2, 0.3],
'profit_abs': [-0.2, 0.4, 0.6],
'trade_duration': [10, 30, 10],
'sell_reason': [SellType.STOP_LOSS, SellType.ROI, SellType.ROI]
'sell_reason': [SellType.STOP_LOSS, SellType.ROI, SellType.ROI],
'close_time':
[
datetime(2019, 1, 1, 9, 26, 3, 478039),
datetime(2019, 2, 1, 9, 26, 3, 478039),
datetime(2019, 3, 1, 9, 26, 3, 478039)
]
}
)

Expand Down Expand Up @@ -336,6 +342,24 @@ def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> N
assert under > correct


def test_sharpe_loss_daily_prefers_higher_profits(default_conf, 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

default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'})
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results),
datetime(2019, 1, 1), datetime(2019, 5, 1))
over = hl.hyperopt_loss_function(results_over, len(hyperopt_results),
datetime(2019, 1, 1), datetime(2019, 5, 1))
under = hl.hyperopt_loss_function(results_under, len(hyperopt_results),
datetime(2019, 1, 1), datetime(2019, 5, 1))
assert over < correct
assert under > correct


def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None:
results_over = hyperopt_results.copy()
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
Expand Down

0 comments on commit 9639ffb

Please sign in to comment.