Merge pull request #6715 from nicolaspapp/feat/relative-drawdown
Add relative drawdown
This commit is contained in:
commit
88c8fe5570
@ -287,9 +287,9 @@ A backtesting result will look like that:
|
|||||||
| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 |
|
| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 |
|
||||||
| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 |
|
| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 |
|
||||||
| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
|
| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
|
||||||
================ SUMMARY METRICS ===============
|
================== SUMMARY METRICS ==================
|
||||||
| Metric | Value |
|
| Metric | Value |
|
||||||
|------------------------+---------------------|
|
|-----------------------------+---------------------|
|
||||||
| Backtesting from | 2019-01-01 00:00:00 |
|
| Backtesting from | 2019-01-01 00:00:00 |
|
||||||
| Backtesting to | 2019-05-01 00:00:00 |
|
| Backtesting to | 2019-05-01 00:00:00 |
|
||||||
| Max open trades | 3 |
|
| Max open trades | 3 |
|
||||||
@ -300,10 +300,15 @@ A backtesting result will look like that:
|
|||||||
| Absolute profit | 0.00762792 BTC |
|
| Absolute profit | 0.00762792 BTC |
|
||||||
| Total profit % | 76.2% |
|
| Total profit % | 76.2% |
|
||||||
| CAGR % | 460.87% |
|
| CAGR % | 460.87% |
|
||||||
| Trades per day | 3.575 |
|
|
||||||
| Avg. stake amount | 0.001 BTC |
|
| Avg. stake amount | 0.001 BTC |
|
||||||
| Total trade volume | 0.429 BTC |
|
| Total trade volume | 0.429 BTC |
|
||||||
| | |
|
| | |
|
||||||
|
| Long / Short | 352 / 77 |
|
||||||
|
| Total profit Long % | 1250.58% |
|
||||||
|
| Total profit Short % | -15.02% |
|
||||||
|
| Absolute profit Long | 0.00838792 BTC |
|
||||||
|
| Absolute profit Short | -0.00076 BTC |
|
||||||
|
| | |
|
||||||
| Best Pair | LSK/BTC 26.26% |
|
| Best Pair | LSK/BTC 26.26% |
|
||||||
| Worst Pair | ZEC/BTC -10.18% |
|
| Worst Pair | ZEC/BTC -10.18% |
|
||||||
| Best Trade | LSK/BTC 4.25% |
|
| Best Trade | LSK/BTC 4.25% |
|
||||||
@ -318,14 +323,15 @@ A backtesting result will look like that:
|
|||||||
| | |
|
| | |
|
||||||
| Min balance | 0.00945123 BTC |
|
| Min balance | 0.00945123 BTC |
|
||||||
| Max balance | 0.01846651 BTC |
|
| Max balance | 0.01846651 BTC |
|
||||||
| Drawdown (Account) | 13.33% |
|
| Max % of account underwater | 25.19% |
|
||||||
|
| Absolute Drawdown (Account) | 13.33% |
|
||||||
| Drawdown | 0.0015 BTC |
|
| Drawdown | 0.0015 BTC |
|
||||||
| Drawdown high | 0.0013 BTC |
|
| Drawdown high | 0.0013 BTC |
|
||||||
| Drawdown low | -0.0002 BTC |
|
| Drawdown low | -0.0002 BTC |
|
||||||
| Drawdown Start | 2019-02-15 14:10:00 |
|
| Drawdown Start | 2019-02-15 14:10:00 |
|
||||||
| Drawdown End | 2019-04-11 18:15:00 |
|
| Drawdown End | 2019-04-11 18:15:00 |
|
||||||
| Market change | -5.88% |
|
| Market change | -5.88% |
|
||||||
===============================================
|
=====================================================
|
||||||
```
|
```
|
||||||
|
|
||||||
### Backtesting report table
|
### Backtesting report table
|
||||||
@ -377,9 +383,9 @@ The last element of the backtest report is the summary metrics table.
|
|||||||
It contains some useful key metrics about performance of your strategy on backtesting data.
|
It contains some useful key metrics about performance of your strategy on backtesting data.
|
||||||
|
|
||||||
```
|
```
|
||||||
================ SUMMARY METRICS ===============
|
================== SUMMARY METRICS ==================
|
||||||
| Metric | Value |
|
| Metric | Value |
|
||||||
|------------------------+---------------------|
|
|-----------------------------+---------------------|
|
||||||
| Backtesting from | 2019-01-01 00:00:00 |
|
| Backtesting from | 2019-01-01 00:00:00 |
|
||||||
| Backtesting to | 2019-05-01 00:00:00 |
|
| Backtesting to | 2019-05-01 00:00:00 |
|
||||||
| Max open trades | 3 |
|
| Max open trades | 3 |
|
||||||
@ -413,14 +419,15 @@ It contains some useful key metrics about performance of your strategy on backte
|
|||||||
| | |
|
| | |
|
||||||
| Min balance | 0.00945123 BTC |
|
| Min balance | 0.00945123 BTC |
|
||||||
| Max balance | 0.01846651 BTC |
|
| Max balance | 0.01846651 BTC |
|
||||||
| Drawdown (Account) | 13.33% |
|
| Max % of account underwater | 25.19% |
|
||||||
|
| Absolute Drawdown (Account) | 13.33% |
|
||||||
| Drawdown | 0.0015 BTC |
|
| Drawdown | 0.0015 BTC |
|
||||||
| Drawdown high | 0.0013 BTC |
|
| Drawdown high | 0.0013 BTC |
|
||||||
| Drawdown low | -0.0002 BTC |
|
| Drawdown low | -0.0002 BTC |
|
||||||
| Drawdown Start | 2019-02-15 14:10:00 |
|
| Drawdown Start | 2019-02-15 14:10:00 |
|
||||||
| Drawdown End | 2019-04-11 18:15:00 |
|
| Drawdown End | 2019-04-11 18:15:00 |
|
||||||
| Market change | -5.88% |
|
| Market change | -5.88% |
|
||||||
================================================
|
=====================================================
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -441,7 +448,9 @@ It contains some useful key metrics about performance of your strategy on backte
|
|||||||
- `Rejected Entry signals`: Trade entry signals that could not be acted upon due to `max_open_trades` being reached.
|
- `Rejected Entry signals`: Trade entry signals that could not be acted upon due to `max_open_trades` being reached.
|
||||||
- `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used).
|
- `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used).
|
||||||
- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period.
|
- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period.
|
||||||
- `Drawdown (Account)`: Maximum Account Drawdown experienced. Calculated as $(Absolute Drawdown) / (DrawdownHigh + startingBalance)$.
|
- `Max % of account underwater`: Maximum percentage your account has decreased from the top since the simulation started.
|
||||||
|
Calculated as the maximum of `(Max Balance - Current Balance) / (Max Balance)`.
|
||||||
|
- `Absolute Drawdown (Account)`: Maximum Account Drawdown experienced. Calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`.
|
||||||
- `Drawdown`: Maximum, absolute drawdown experienced. Difference between Drawdown High and Subsequent Low point.
|
- `Drawdown`: Maximum, absolute drawdown experienced. Difference between Drawdown High and Subsequent Low point.
|
||||||
- `Drawdown high` / `Drawdown low`: Profit at the beginning and end of the largest drawdown period. A negative low value means initial capital lost.
|
- `Drawdown high` / `Drawdown low`: Profit at the beginning and end of the largest drawdown period. A negative low value means initial capital lost.
|
||||||
- `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
|
- `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
|
||||||
|
@ -116,7 +116,9 @@ optional arguments:
|
|||||||
ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss,
|
ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss,
|
||||||
SharpeHyperOptLoss, SharpeHyperOptLossDaily,
|
SharpeHyperOptLoss, SharpeHyperOptLossDaily,
|
||||||
SortinoHyperOptLoss, SortinoHyperOptLossDaily,
|
SortinoHyperOptLoss, SortinoHyperOptLossDaily,
|
||||||
CalmarHyperOptLoss, MaxDrawDownHyperOptLoss, ProfitDrawDownHyperOptLoss
|
CalmarHyperOptLoss, MaxDrawDownHyperOptLoss,
|
||||||
|
MaxDrawDownRelativeHyperOptLoss,
|
||||||
|
ProfitDrawDownHyperOptLoss
|
||||||
--disable-param-export
|
--disable-param-export
|
||||||
Disable automatic hyperopt parameter export.
|
Disable automatic hyperopt parameter export.
|
||||||
--ignore-missing-spaces, --ignore-unparameterized-spaces
|
--ignore-missing-spaces, --ignore-unparameterized-spaces
|
||||||
@ -563,7 +565,8 @@ Currently, the following loss functions are builtin:
|
|||||||
* `SharpeHyperOptLossDaily` - optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation.
|
* `SharpeHyperOptLossDaily` - optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation.
|
||||||
* `SortinoHyperOptLoss` - optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation.
|
* `SortinoHyperOptLoss` - optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation.
|
||||||
* `SortinoHyperOptLossDaily` - optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation.
|
* `SortinoHyperOptLossDaily` - optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation.
|
||||||
* `MaxDrawDownHyperOptLoss` - Optimizes Maximum drawdown.
|
* `MaxDrawDownHyperOptLoss` - Optimizes Maximum absolute drawdown.
|
||||||
|
* `MaxDrawDownRelativeHyperOptLoss` - Optimizes both maximum absolute drawdown while also adjusting for maximum relative drawdown.
|
||||||
* `CalmarHyperOptLoss` - Optimizes Calmar Ratio calculated on trade returns relative to max drawdown.
|
* `CalmarHyperOptLoss` - Optimizes Calmar Ratio calculated on trade returns relative to max drawdown.
|
||||||
* `ProfitDrawDownHyperOptLoss` - Optimizes by max Profit & min Drawdown objective. `DRAWDOWN_MULT` variable within the hyperoptloss file can be adjusted to be stricter or more flexible on drawdown purposes.
|
* `ProfitDrawDownHyperOptLoss` - Optimizes by max Profit & min Drawdown objective. `DRAWDOWN_MULT` variable within the hyperoptloss file can be adjusted to be stricter or more flexible on drawdown purposes.
|
||||||
|
|
||||||
|
@ -28,7 +28,8 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
|
|||||||
'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily',
|
'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily',
|
||||||
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily',
|
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily',
|
||||||
'CalmarHyperOptLoss',
|
'CalmarHyperOptLoss',
|
||||||
'MaxDrawDownHyperOptLoss', 'ProfitDrawDownHyperOptLoss']
|
'MaxDrawDownHyperOptLoss', 'MaxDrawDownRelativeHyperOptLoss',
|
||||||
|
'ProfitDrawDownHyperOptLoss']
|
||||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
|
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
|
||||||
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
|
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
|
||||||
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
|
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
|
||||||
|
@ -72,18 +72,28 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
|
|||||||
return df
|
return df
|
||||||
|
|
||||||
|
|
||||||
def _calc_drawdown_series(profit_results: pd.DataFrame, *, date_col: str, value_col: str
|
def _calc_drawdown_series(profit_results: pd.DataFrame, *, date_col: str, value_col: str,
|
||||||
) -> pd.DataFrame:
|
starting_balance: float) -> pd.DataFrame:
|
||||||
max_drawdown_df = pd.DataFrame()
|
max_drawdown_df = pd.DataFrame()
|
||||||
max_drawdown_df['cumulative'] = profit_results[value_col].cumsum()
|
max_drawdown_df['cumulative'] = profit_results[value_col].cumsum()
|
||||||
max_drawdown_df['high_value'] = max_drawdown_df['cumulative'].cummax()
|
max_drawdown_df['high_value'] = max_drawdown_df['cumulative'].cummax()
|
||||||
max_drawdown_df['drawdown'] = max_drawdown_df['cumulative'] - max_drawdown_df['high_value']
|
max_drawdown_df['drawdown'] = max_drawdown_df['cumulative'] - max_drawdown_df['high_value']
|
||||||
max_drawdown_df['date'] = profit_results.loc[:, date_col]
|
max_drawdown_df['date'] = profit_results.loc[:, date_col]
|
||||||
|
if starting_balance:
|
||||||
|
cumulative_balance = starting_balance + max_drawdown_df['cumulative']
|
||||||
|
max_balance = starting_balance + max_drawdown_df['high_value']
|
||||||
|
max_drawdown_df['drawdown_relative'] = ((max_balance - cumulative_balance) / max_balance)
|
||||||
|
else:
|
||||||
|
# NOTE: This is not completely accurate,
|
||||||
|
# but might good enough if starting_balance is not available
|
||||||
|
max_drawdown_df['drawdown_relative'] = (
|
||||||
|
(max_drawdown_df['high_value'] - max_drawdown_df['cumulative'])
|
||||||
|
/ max_drawdown_df['high_value'])
|
||||||
return max_drawdown_df
|
return max_drawdown_df
|
||||||
|
|
||||||
|
|
||||||
def calculate_underwater(trades: pd.DataFrame, *, date_col: str = 'close_date',
|
def calculate_underwater(trades: pd.DataFrame, *, date_col: str = 'close_date',
|
||||||
value_col: str = 'profit_ratio'
|
value_col: str = 'profit_ratio', starting_balance: float = 0.0
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Calculate max drawdown and the corresponding close dates
|
Calculate max drawdown and the corresponding close dates
|
||||||
@ -97,13 +107,18 @@ def calculate_underwater(trades: pd.DataFrame, *, date_col: str = 'close_date',
|
|||||||
if len(trades) == 0:
|
if len(trades) == 0:
|
||||||
raise ValueError("Trade dataframe empty.")
|
raise ValueError("Trade dataframe empty.")
|
||||||
profit_results = trades.sort_values(date_col).reset_index(drop=True)
|
profit_results = trades.sort_values(date_col).reset_index(drop=True)
|
||||||
max_drawdown_df = _calc_drawdown_series(profit_results, date_col=date_col, value_col=value_col)
|
max_drawdown_df = _calc_drawdown_series(
|
||||||
|
profit_results,
|
||||||
|
date_col=date_col,
|
||||||
|
value_col=value_col,
|
||||||
|
starting_balance=starting_balance)
|
||||||
|
|
||||||
return max_drawdown_df
|
return max_drawdown_df
|
||||||
|
|
||||||
|
|
||||||
def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date',
|
def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date',
|
||||||
value_col: str = 'profit_abs', starting_balance: float = 0
|
value_col: str = 'profit_abs', starting_balance: float = 0,
|
||||||
|
relative: bool = False
|
||||||
) -> Tuple[float, pd.Timestamp, pd.Timestamp, float, float, float]:
|
) -> Tuple[float, pd.Timestamp, pd.Timestamp, float, float, float]:
|
||||||
"""
|
"""
|
||||||
Calculate max drawdown and the corresponding close dates
|
Calculate max drawdown and the corresponding close dates
|
||||||
@ -119,9 +134,15 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date'
|
|||||||
if len(trades) == 0:
|
if len(trades) == 0:
|
||||||
raise ValueError("Trade dataframe empty.")
|
raise ValueError("Trade dataframe empty.")
|
||||||
profit_results = trades.sort_values(date_col).reset_index(drop=True)
|
profit_results = trades.sort_values(date_col).reset_index(drop=True)
|
||||||
max_drawdown_df = _calc_drawdown_series(profit_results, date_col=date_col, value_col=value_col)
|
max_drawdown_df = _calc_drawdown_series(
|
||||||
|
profit_results,
|
||||||
|
date_col=date_col,
|
||||||
|
value_col=value_col,
|
||||||
|
starting_balance=starting_balance
|
||||||
|
)
|
||||||
|
|
||||||
idxmin = max_drawdown_df['drawdown'].idxmin()
|
idxmin = max_drawdown_df['drawdown_relative'].idxmax() if relative \
|
||||||
|
else max_drawdown_df['drawdown'].idxmin()
|
||||||
if idxmin == 0:
|
if idxmin == 0:
|
||||||
raise ValueError("No losing trade, therefore no drawdown.")
|
raise ValueError("No losing trade, therefore no drawdown.")
|
||||||
high_date = profit_results.loc[max_drawdown_df.iloc[:idxmin]['high_value'].idxmax(), date_col]
|
high_date = profit_results.loc[max_drawdown_df.iloc[:idxmin]['high_value'].idxmax(), date_col]
|
||||||
@ -129,12 +150,10 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date'
|
|||||||
high_val = max_drawdown_df.loc[max_drawdown_df.iloc[:idxmin]
|
high_val = max_drawdown_df.loc[max_drawdown_df.iloc[:idxmin]
|
||||||
['high_value'].idxmax(), 'cumulative']
|
['high_value'].idxmax(), 'cumulative']
|
||||||
low_val = max_drawdown_df.loc[idxmin, 'cumulative']
|
low_val = max_drawdown_df.loc[idxmin, 'cumulative']
|
||||||
max_drawdown_rel = 0.0
|
max_drawdown_rel = max_drawdown_df.loc[idxmin, 'drawdown_relative']
|
||||||
if high_val + starting_balance != 0:
|
|
||||||
max_drawdown_rel = (high_val - low_val) / (high_val + starting_balance)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
abs(min(max_drawdown_df['drawdown'])),
|
abs(max_drawdown_df.loc[idxmin, 'drawdown']),
|
||||||
high_date,
|
high_date,
|
||||||
low_date,
|
low_date,
|
||||||
high_val,
|
high_val,
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
"""
|
||||||
|
MaxDrawDownRelativeHyperOptLoss
|
||||||
|
|
||||||
|
This module defines the alternative HyperOptLoss class which can be used for
|
||||||
|
Hyperoptimization.
|
||||||
|
"""
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.data.metrics import calculate_underwater
|
||||||
|
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||||
|
|
||||||
|
|
||||||
|
class MaxDrawDownRelativeHyperOptLoss(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, config: Dict,
|
||||||
|
*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:
|
||||||
|
drawdown_df = calculate_underwater(
|
||||||
|
results,
|
||||||
|
value_col='profit_abs',
|
||||||
|
starting_balance=config['dry_run_wallet']
|
||||||
|
)
|
||||||
|
max_drawdown = abs(min(drawdown_df['drawdown']))
|
||||||
|
relative_drawdown = max(drawdown_df['drawdown_relative'])
|
||||||
|
if max_drawdown == 0:
|
||||||
|
return -total_profit
|
||||||
|
return -total_profit / max_drawdown / relative_drawdown
|
||||||
|
except (Exception, ValueError):
|
||||||
|
return -total_profit
|
@ -19,11 +19,11 @@ class IHyperOptLoss(ABC):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def hyperopt_loss_function(results: DataFrame, trade_count: int,
|
def hyperopt_loss_function(*, results: DataFrame, trade_count: int,
|
||||||
min_date: datetime, max_date: datetime,
|
min_date: datetime, max_date: datetime,
|
||||||
config: Dict, processed: Dict[str, DataFrame],
|
config: Dict, processed: Dict[str, DataFrame],
|
||||||
backtest_stats: Dict[str, Any],
|
backtest_stats: Dict[str, Any],
|
||||||
*args, **kwargs) -> float:
|
**kwargs) -> float:
|
||||||
"""
|
"""
|
||||||
Objective function, returns smaller number for better results
|
Objective function, returns smaller number for better results
|
||||||
"""
|
"""
|
||||||
|
@ -498,9 +498,12 @@ def generate_strategy_stats(pairlist: List[str],
|
|||||||
(drawdown_abs, drawdown_start, drawdown_end, high_val, low_val,
|
(drawdown_abs, drawdown_start, drawdown_end, high_val, low_val,
|
||||||
max_drawdown) = calculate_max_drawdown(
|
max_drawdown) = calculate_max_drawdown(
|
||||||
results, value_col='profit_abs', starting_balance=start_balance)
|
results, value_col='profit_abs', starting_balance=start_balance)
|
||||||
|
(_, _, _, _, _, max_relative_drawdown) = calculate_max_drawdown(
|
||||||
|
results, value_col='profit_abs', starting_balance=start_balance, relative=True)
|
||||||
strat_stats.update({
|
strat_stats.update({
|
||||||
'max_drawdown': max_drawdown_legacy, # Deprecated - do not use
|
'max_drawdown': max_drawdown_legacy, # Deprecated - do not use
|
||||||
'max_drawdown_account': max_drawdown,
|
'max_drawdown_account': max_drawdown,
|
||||||
|
'max_relative_drawdown': max_relative_drawdown,
|
||||||
'max_drawdown_abs': drawdown_abs,
|
'max_drawdown_abs': drawdown_abs,
|
||||||
'drawdown_start': drawdown_start.strftime(DATETIME_PRINT_FORMAT),
|
'drawdown_start': drawdown_start.strftime(DATETIME_PRINT_FORMAT),
|
||||||
'drawdown_start_ts': drawdown_start.timestamp() * 1000,
|
'drawdown_start_ts': drawdown_start.timestamp() * 1000,
|
||||||
@ -521,6 +524,7 @@ def generate_strategy_stats(pairlist: List[str],
|
|||||||
strat_stats.update({
|
strat_stats.update({
|
||||||
'max_drawdown': 0.0,
|
'max_drawdown': 0.0,
|
||||||
'max_drawdown_account': 0.0,
|
'max_drawdown_account': 0.0,
|
||||||
|
'max_relative_drawdown': 0.0,
|
||||||
'max_drawdown_abs': 0.0,
|
'max_drawdown_abs': 0.0,
|
||||||
'max_drawdown_low': 0.0,
|
'max_drawdown_low': 0.0,
|
||||||
'max_drawdown_high': 0.0,
|
'max_drawdown_high': 0.0,
|
||||||
@ -729,6 +733,26 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
|||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
] if strat_results.get('trade_count_short', 0) > 0 else []
|
] if strat_results.get('trade_count_short', 0) > 0 else []
|
||||||
|
|
||||||
|
drawdown_metrics = []
|
||||||
|
if 'max_relative_drawdown' in strat_results:
|
||||||
|
# Compatibility to show old hyperopt results
|
||||||
|
drawdown_metrics.append(
|
||||||
|
('Max % of account underwater', f"{strat_results['max_relative_drawdown']:.2%}")
|
||||||
|
)
|
||||||
|
drawdown_metrics.extend([
|
||||||
|
('Absolute Drawdown (Account)', f"{strat_results['max_drawdown_account']:.2%}")
|
||||||
|
if 'max_drawdown_account' in strat_results else (
|
||||||
|
'Drawdown', f"{strat_results['max_drawdown']:.2%}"),
|
||||||
|
('Absolute Drawdown', round_coin_value(strat_results['max_drawdown_abs'],
|
||||||
|
strat_results['stake_currency'])),
|
||||||
|
('Drawdown high', round_coin_value(strat_results['max_drawdown_high'],
|
||||||
|
strat_results['stake_currency'])),
|
||||||
|
('Drawdown low', round_coin_value(strat_results['max_drawdown_low'],
|
||||||
|
strat_results['stake_currency'])),
|
||||||
|
('Drawdown Start', strat_results['drawdown_start']),
|
||||||
|
('Drawdown End', strat_results['drawdown_end']),
|
||||||
|
])
|
||||||
|
|
||||||
# Newly added fields should be ignored if they are missing in strat_results. hyperopt-show
|
# 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
|
# command stores these results and newer version of freqtrade must be able to handle old
|
||||||
# results with missing new fields.
|
# results with missing new fields.
|
||||||
@ -784,18 +808,7 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
|||||||
('Max balance', round_coin_value(strat_results['csum_max'],
|
('Max balance', round_coin_value(strat_results['csum_max'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
|
|
||||||
# Compatibility to show old hyperopt results
|
*drawdown_metrics,
|
||||||
('Drawdown (Account)', f"{strat_results['max_drawdown_account']:.2%}")
|
|
||||||
if 'max_drawdown_account' in strat_results else (
|
|
||||||
'Drawdown', f"{strat_results['max_drawdown']:.2%}"),
|
|
||||||
('Drawdown', round_coin_value(strat_results['max_drawdown_abs'],
|
|
||||||
strat_results['stake_currency'])),
|
|
||||||
('Drawdown high', round_coin_value(strat_results['max_drawdown_high'],
|
|
||||||
strat_results['stake_currency'])),
|
|
||||||
('Drawdown low', round_coin_value(strat_results['max_drawdown_low'],
|
|
||||||
strat_results['stake_currency'])),
|
|
||||||
('Drawdown Start', strat_results['drawdown_start']),
|
|
||||||
('Drawdown End', strat_results['drawdown_end']),
|
|
||||||
('Market change', f"{strat_results['market_change']:.2%}"),
|
('Market change', f"{strat_results['market_change']:.2%}"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -159,12 +159,15 @@ def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> make_sub
|
|||||||
|
|
||||||
|
|
||||||
def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame,
|
def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame,
|
||||||
timeframe: str) -> make_subplots:
|
timeframe: str, starting_balance: float) -> make_subplots:
|
||||||
"""
|
"""
|
||||||
Add scatter points indicating max drawdown
|
Add scatter points indicating max drawdown
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
_, highdate, lowdate, _, _, max_drawdown = calculate_max_drawdown(trades)
|
_, highdate, lowdate, _, _, max_drawdown = calculate_max_drawdown(
|
||||||
|
trades,
|
||||||
|
starting_balance=starting_balance
|
||||||
|
)
|
||||||
|
|
||||||
drawdown = go.Scatter(
|
drawdown = go.Scatter(
|
||||||
x=[highdate, lowdate],
|
x=[highdate, lowdate],
|
||||||
@ -189,22 +192,37 @@ def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame,
|
|||||||
return fig
|
return fig
|
||||||
|
|
||||||
|
|
||||||
def add_underwater(fig, row, trades: pd.DataFrame) -> make_subplots:
|
def add_underwater(fig, row, trades: pd.DataFrame, starting_balance: float) -> make_subplots:
|
||||||
"""
|
"""
|
||||||
Add underwater plot
|
Add underwater plots
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
underwater = calculate_underwater(trades, value_col="profit_abs")
|
underwater = calculate_underwater(
|
||||||
|
trades,
|
||||||
|
value_col="profit_abs",
|
||||||
|
starting_balance=starting_balance
|
||||||
|
)
|
||||||
|
|
||||||
underwater = go.Scatter(
|
underwater_plot = go.Scatter(
|
||||||
x=underwater['date'],
|
x=underwater['date'],
|
||||||
y=underwater['drawdown'],
|
y=underwater['drawdown'],
|
||||||
name="Underwater Plot",
|
name="Underwater Plot",
|
||||||
fill='tozeroy',
|
fill='tozeroy',
|
||||||
fillcolor='#cc362b',
|
fillcolor='#cc362b',
|
||||||
line={'color': '#cc362b'},
|
line={'color': '#cc362b'}
|
||||||
)
|
)
|
||||||
fig.add_trace(underwater, row, 1)
|
|
||||||
|
underwater_plot_relative = go.Scatter(
|
||||||
|
x=underwater['date'],
|
||||||
|
y=(-underwater['drawdown_relative']),
|
||||||
|
name="Underwater Plot (%)",
|
||||||
|
fill='tozeroy',
|
||||||
|
fillcolor='green',
|
||||||
|
line={'color': 'green'}
|
||||||
|
)
|
||||||
|
|
||||||
|
fig.add_trace(underwater_plot, row, 1)
|
||||||
|
fig.add_trace(underwater_plot_relative, row + 1, 1)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.warning("No trades found - not plotting underwater plot")
|
logger.warning("No trades found - not plotting underwater plot")
|
||||||
return fig
|
return fig
|
||||||
@ -507,7 +525,8 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
|
|||||||
|
|
||||||
|
|
||||||
def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame],
|
def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame],
|
||||||
trades: pd.DataFrame, timeframe: str, stake_currency: str) -> go.Figure:
|
trades: pd.DataFrame, timeframe: str, stake_currency: str,
|
||||||
|
starting_balance: float) -> go.Figure:
|
||||||
# Combine close-values for all pairs, rename columns to "pair"
|
# Combine close-values for all pairs, rename columns to "pair"
|
||||||
try:
|
try:
|
||||||
df_comb = combine_dataframes_with_mean(data, "close")
|
df_comb = combine_dataframes_with_mean(data, "close")
|
||||||
@ -531,8 +550,8 @@ def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame],
|
|||||||
name='Avg close price',
|
name='Avg close price',
|
||||||
)
|
)
|
||||||
|
|
||||||
fig = make_subplots(rows=5, cols=1, shared_xaxes=True,
|
fig = make_subplots(rows=6, cols=1, shared_xaxes=True,
|
||||||
row_heights=[1, 1, 1, 0.5, 1],
|
row_heights=[1, 1, 1, 0.5, 0.75, 0.75],
|
||||||
vertical_spacing=0.05,
|
vertical_spacing=0.05,
|
||||||
subplot_titles=[
|
subplot_titles=[
|
||||||
"AVG Close Price",
|
"AVG Close Price",
|
||||||
@ -540,6 +559,7 @@ def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame],
|
|||||||
"Profit per pair",
|
"Profit per pair",
|
||||||
"Parallelism",
|
"Parallelism",
|
||||||
"Underwater",
|
"Underwater",
|
||||||
|
"Relative Drawdown",
|
||||||
])
|
])
|
||||||
fig['layout'].update(title="Freqtrade Profit plot")
|
fig['layout'].update(title="Freqtrade Profit plot")
|
||||||
fig['layout']['yaxis1'].update(title='Price')
|
fig['layout']['yaxis1'].update(title='Price')
|
||||||
@ -547,14 +567,16 @@ def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame],
|
|||||||
fig['layout']['yaxis3'].update(title=f'Profit {stake_currency}')
|
fig['layout']['yaxis3'].update(title=f'Profit {stake_currency}')
|
||||||
fig['layout']['yaxis4'].update(title='Trade count')
|
fig['layout']['yaxis4'].update(title='Trade count')
|
||||||
fig['layout']['yaxis5'].update(title='Underwater Plot')
|
fig['layout']['yaxis5'].update(title='Underwater Plot')
|
||||||
|
fig['layout']['yaxis6'].update(title='Underwater Plot Relative (%)', tickformat=',.2%')
|
||||||
fig['layout']['xaxis']['rangeslider'].update(visible=False)
|
fig['layout']['xaxis']['rangeslider'].update(visible=False)
|
||||||
fig.update_layout(modebar_add=["v1hovermode", "toggleSpikeLines"])
|
fig.update_layout(modebar_add=["v1hovermode", "toggleSpikeLines"])
|
||||||
|
|
||||||
fig.add_trace(avgclose, 1, 1)
|
fig.add_trace(avgclose, 1, 1)
|
||||||
fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit')
|
fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit')
|
||||||
fig = add_max_drawdown(fig, 2, trades, df_comb, timeframe)
|
fig = add_max_drawdown(fig, 2, trades, df_comb, timeframe, starting_balance)
|
||||||
fig = add_parallelism(fig, 4, trades, timeframe)
|
fig = add_parallelism(fig, 4, trades, timeframe)
|
||||||
fig = add_underwater(fig, 5, trades)
|
# Two rows consumed
|
||||||
|
fig = add_underwater(fig, 5, trades, starting_balance)
|
||||||
|
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
profit_col = f'cum_profit_{pair}'
|
profit_col = f'cum_profit_{pair}'
|
||||||
@ -670,7 +692,8 @@ def plot_profit(config: Dict[str, Any]) -> None:
|
|||||||
# this could be useful to gauge the overall market trend
|
# this could be useful to gauge the overall market trend
|
||||||
fig = generate_profit_graph(plot_elements['pairs'], plot_elements['ohlcv'],
|
fig = generate_profit_graph(plot_elements['pairs'], plot_elements['ohlcv'],
|
||||||
trades, config['timeframe'],
|
trades, config['timeframe'],
|
||||||
config.get('stake_currency', ''))
|
config.get('stake_currency', ''),
|
||||||
|
config.get('available_capital', config['dry_run_wallet']))
|
||||||
store_plot_file(fig, filename='freqtrade-profit-plot.html',
|
store_plot_file(fig, filename='freqtrade-profit-plot.html',
|
||||||
directory=config['user_data_dir'] / 'plot',
|
directory=config['user_data_dir'] / 'plot',
|
||||||
auto_open=config.get('plot_auto_open', False))
|
auto_open=config.get('plot_auto_open', False))
|
||||||
|
@ -376,3 +376,38 @@ def test_calculate_max_drawdown2():
|
|||||||
df = DataFrame(zip(values[:5], dates[:5]), columns=['profit', 'open_date'])
|
df = DataFrame(zip(values[:5], dates[:5]), columns=['profit', 'open_date'])
|
||||||
with pytest.raises(ValueError, match='No losing trade, therefore no drawdown.'):
|
with pytest.raises(ValueError, match='No losing trade, therefore no drawdown.'):
|
||||||
calculate_max_drawdown(df, date_col='open_date', value_col='profit')
|
calculate_max_drawdown(df, date_col='open_date', value_col='profit')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('profits,relative,highd,lowd,result,result_rel', [
|
||||||
|
([0.0, -500.0, 500.0, 10000.0, -1000.0], False, 3, 4, 1000.0, 0.090909),
|
||||||
|
([0.0, -500.0, 500.0, 10000.0, -1000.0], True, 0, 1, 500.0, 0.5),
|
||||||
|
|
||||||
|
])
|
||||||
|
def test_calculate_max_drawdown_abs(profits, relative, highd, lowd, result, result_rel):
|
||||||
|
"""
|
||||||
|
Test case from issue https://github.com/freqtrade/freqtrade/issues/6655
|
||||||
|
[1000, 500, 1000, 11000, 10000] # absolute results
|
||||||
|
[1000, 50%, 0%, 0%, ~9%] # Relative drawdowns
|
||||||
|
"""
|
||||||
|
init_date = Arrow(2020, 1, 1)
|
||||||
|
dates = [init_date.shift(days=i) for i in range(len(profits))]
|
||||||
|
df = DataFrame(zip(profits, dates), columns=['profit_abs', 'open_date'])
|
||||||
|
# sort by profit and reset index
|
||||||
|
df = df.sort_values('profit_abs').reset_index(drop=True)
|
||||||
|
df1 = df.copy()
|
||||||
|
drawdown, hdate, ldate, hval, lval, drawdown_rel = calculate_max_drawdown(
|
||||||
|
df, date_col='open_date', starting_balance=1000, relative=relative)
|
||||||
|
# Ensure df has not been altered.
|
||||||
|
assert df.equals(df1)
|
||||||
|
|
||||||
|
assert isinstance(drawdown, float)
|
||||||
|
assert isinstance(drawdown_rel, float)
|
||||||
|
assert hdate == init_date.shift(days=highd)
|
||||||
|
assert ldate == init_date.shift(days=lowd)
|
||||||
|
|
||||||
|
# High must be before low
|
||||||
|
assert hdate < ldate
|
||||||
|
# High value must be higher than low value
|
||||||
|
assert hval > lval
|
||||||
|
assert drawdown == result
|
||||||
|
assert pytest.approx(drawdown_rel) == result_rel
|
||||||
|
@ -85,6 +85,7 @@ def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) ->
|
|||||||
"SharpeHyperOptLoss",
|
"SharpeHyperOptLoss",
|
||||||
"SharpeHyperOptLossDaily",
|
"SharpeHyperOptLossDaily",
|
||||||
"MaxDrawDownHyperOptLoss",
|
"MaxDrawDownHyperOptLoss",
|
||||||
|
"MaxDrawDownRelativeHyperOptLoss",
|
||||||
"CalmarHyperOptLoss",
|
"CalmarHyperOptLoss",
|
||||||
"ProfitDrawDownHyperOptLoss",
|
"ProfitDrawDownHyperOptLoss",
|
||||||
|
|
||||||
|
@ -332,7 +332,13 @@ def test_generate_profit_graph(testdatadir):
|
|||||||
|
|
||||||
trades = trades[trades['pair'].isin(pairs)]
|
trades = trades[trades['pair'].isin(pairs)]
|
||||||
|
|
||||||
fig = generate_profit_graph(pairs, data, trades, timeframe="5m", stake_currency='BTC')
|
fig = generate_profit_graph(
|
||||||
|
pairs,
|
||||||
|
data,
|
||||||
|
trades,
|
||||||
|
timeframe="5m",
|
||||||
|
stake_currency='BTC',
|
||||||
|
starting_balance=0)
|
||||||
assert isinstance(fig, go.Figure)
|
assert isinstance(fig, go.Figure)
|
||||||
|
|
||||||
assert fig.layout.title.text == "Freqtrade Profit plot"
|
assert fig.layout.title.text == "Freqtrade Profit plot"
|
||||||
@ -341,7 +347,7 @@ def test_generate_profit_graph(testdatadir):
|
|||||||
assert fig.layout.yaxis3.title.text == "Profit BTC"
|
assert fig.layout.yaxis3.title.text == "Profit BTC"
|
||||||
|
|
||||||
figure = fig.layout.figure
|
figure = fig.layout.figure
|
||||||
assert len(figure.data) == 7
|
assert len(figure.data) == 8
|
||||||
|
|
||||||
avgclose = find_trace_in_fig_data(figure.data, "Avg close price")
|
avgclose = find_trace_in_fig_data(figure.data, "Avg close price")
|
||||||
assert isinstance(avgclose, go.Scatter)
|
assert isinstance(avgclose, go.Scatter)
|
||||||
@ -356,6 +362,9 @@ def test_generate_profit_graph(testdatadir):
|
|||||||
underwater = find_trace_in_fig_data(figure.data, "Underwater Plot")
|
underwater = find_trace_in_fig_data(figure.data, "Underwater Plot")
|
||||||
assert isinstance(underwater, go.Scatter)
|
assert isinstance(underwater, go.Scatter)
|
||||||
|
|
||||||
|
underwater_relative = find_trace_in_fig_data(figure.data, "Underwater Plot (%)")
|
||||||
|
assert isinstance(underwater_relative, go.Scatter)
|
||||||
|
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
profit_pair = find_trace_in_fig_data(figure.data, f"Profit {pair}")
|
profit_pair = find_trace_in_fig_data(figure.data, f"Profit {pair}")
|
||||||
assert isinstance(profit_pair, go.Scatter)
|
assert isinstance(profit_pair, go.Scatter)
|
||||||
@ -363,7 +372,7 @@ def test_generate_profit_graph(testdatadir):
|
|||||||
with pytest.raises(OperationalException, match=r"No trades found.*"):
|
with pytest.raises(OperationalException, match=r"No trades found.*"):
|
||||||
# Pair cannot be empty - so it's an empty dataframe.
|
# Pair cannot be empty - so it's an empty dataframe.
|
||||||
generate_profit_graph(pairs, data, trades.loc[trades['pair'].isnull()], timeframe="5m",
|
generate_profit_graph(pairs, data, trades.loc[trades['pair'].isnull()], timeframe="5m",
|
||||||
stake_currency='BTC')
|
stake_currency='BTC', starting_balance=0)
|
||||||
|
|
||||||
|
|
||||||
def test_start_plot_dataframe(mocker):
|
def test_start_plot_dataframe(mocker):
|
||||||
|
Loading…
Reference in New Issue
Block a user