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 |
|
||||
| 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 |
|
||||
================ SUMMARY METRICS ===============
|
||||
================== SUMMARY METRICS ==================
|
||||
| Metric | Value |
|
||||
|------------------------+---------------------|
|
||||
|-----------------------------+---------------------|
|
||||
| Backtesting from | 2019-01-01 00:00:00 |
|
||||
| Backtesting to | 2019-05-01 00:00:00 |
|
||||
| Max open trades | 3 |
|
||||
@ -300,10 +300,15 @@ A backtesting result will look like that:
|
||||
| Absolute profit | 0.00762792 BTC |
|
||||
| Total profit % | 76.2% |
|
||||
| CAGR % | 460.87% |
|
||||
| Trades per day | 3.575 |
|
||||
| Avg. stake amount | 0.001 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% |
|
||||
| Worst Pair | ZEC/BTC -10.18% |
|
||||
| Best Trade | LSK/BTC 4.25% |
|
||||
@ -318,14 +323,15 @@ A backtesting result will look like that:
|
||||
| | |
|
||||
| Min balance | 0.00945123 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 high | 0.0013 BTC |
|
||||
| Drawdown low | -0.0002 BTC |
|
||||
| Drawdown Start | 2019-02-15 14:10:00 |
|
||||
| Drawdown End | 2019-04-11 18:15:00 |
|
||||
| Market change | -5.88% |
|
||||
===============================================
|
||||
=====================================================
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
```
|
||||
================ SUMMARY METRICS ===============
|
||||
================== SUMMARY METRICS ==================
|
||||
| Metric | Value |
|
||||
|------------------------+---------------------|
|
||||
|-----------------------------+---------------------|
|
||||
| Backtesting from | 2019-01-01 00:00:00 |
|
||||
| Backtesting to | 2019-05-01 00:00:00 |
|
||||
| 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 |
|
||||
| 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 high | 0.0013 BTC |
|
||||
| Drawdown low | -0.0002 BTC |
|
||||
| Drawdown Start | 2019-02-15 14:10:00 |
|
||||
| Drawdown End | 2019-04-11 18:15:00 |
|
||||
| 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.
|
||||
- `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.
|
||||
- `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 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).
|
||||
|
@ -116,7 +116,9 @@ optional arguments:
|
||||
ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss,
|
||||
SharpeHyperOptLoss, SharpeHyperOptLossDaily,
|
||||
SortinoHyperOptLoss, SortinoHyperOptLossDaily,
|
||||
CalmarHyperOptLoss, MaxDrawDownHyperOptLoss, ProfitDrawDownHyperOptLoss
|
||||
CalmarHyperOptLoss, MaxDrawDownHyperOptLoss,
|
||||
MaxDrawDownRelativeHyperOptLoss,
|
||||
ProfitDrawDownHyperOptLoss
|
||||
--disable-param-export
|
||||
Disable automatic hyperopt parameter export.
|
||||
--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.
|
||||
* `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.
|
||||
* `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.
|
||||
* `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',
|
||||
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily',
|
||||
'CalmarHyperOptLoss',
|
||||
'MaxDrawDownHyperOptLoss', 'ProfitDrawDownHyperOptLoss']
|
||||
'MaxDrawDownHyperOptLoss', 'MaxDrawDownRelativeHyperOptLoss',
|
||||
'ProfitDrawDownHyperOptLoss']
|
||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
|
||||
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
|
||||
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
|
||||
|
@ -72,18 +72,28 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
|
||||
return df
|
||||
|
||||
|
||||
def _calc_drawdown_series(profit_results: pd.DataFrame, *, date_col: str, value_col: str
|
||||
) -> pd.DataFrame:
|
||||
def _calc_drawdown_series(profit_results: pd.DataFrame, *, date_col: str, value_col: str,
|
||||
starting_balance: float) -> pd.DataFrame:
|
||||
max_drawdown_df = pd.DataFrame()
|
||||
max_drawdown_df['cumulative'] = profit_results[value_col].cumsum()
|
||||
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['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
|
||||
|
||||
|
||||
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
|
||||
@ -97,13 +107,18 @@ def calculate_underwater(trades: pd.DataFrame, *, date_col: str = 'close_date',
|
||||
if len(trades) == 0:
|
||||
raise ValueError("Trade dataframe empty.")
|
||||
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
|
||||
|
||||
|
||||
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]:
|
||||
"""
|
||||
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:
|
||||
raise ValueError("Trade dataframe empty.")
|
||||
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:
|
||||
raise ValueError("No losing trade, therefore no drawdown.")
|
||||
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_value'].idxmax(), 'cumulative']
|
||||
low_val = max_drawdown_df.loc[idxmin, 'cumulative']
|
||||
max_drawdown_rel = 0.0
|
||||
if high_val + starting_balance != 0:
|
||||
max_drawdown_rel = (high_val - low_val) / (high_val + starting_balance)
|
||||
max_drawdown_rel = max_drawdown_df.loc[idxmin, 'drawdown_relative']
|
||||
|
||||
return (
|
||||
abs(min(max_drawdown_df['drawdown'])),
|
||||
abs(max_drawdown_df.loc[idxmin, 'drawdown']),
|
||||
high_date,
|
||||
low_date,
|
||||
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
|
||||
@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,
|
||||
config: Dict, processed: Dict[str, DataFrame],
|
||||
backtest_stats: Dict[str, Any],
|
||||
*args, **kwargs) -> float:
|
||||
**kwargs) -> float:
|
||||
"""
|
||||
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,
|
||||
max_drawdown) = calculate_max_drawdown(
|
||||
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({
|
||||
'max_drawdown': max_drawdown_legacy, # Deprecated - do not use
|
||||
'max_drawdown_account': max_drawdown,
|
||||
'max_relative_drawdown': max_relative_drawdown,
|
||||
'max_drawdown_abs': drawdown_abs,
|
||||
'drawdown_start': drawdown_start.strftime(DATETIME_PRINT_FORMAT),
|
||||
'drawdown_start_ts': drawdown_start.timestamp() * 1000,
|
||||
@ -521,6 +524,7 @@ def generate_strategy_stats(pairlist: List[str],
|
||||
strat_stats.update({
|
||||
'max_drawdown': 0.0,
|
||||
'max_drawdown_account': 0.0,
|
||||
'max_relative_drawdown': 0.0,
|
||||
'max_drawdown_abs': 0.0,
|
||||
'max_drawdown_low': 0.0,
|
||||
'max_drawdown_high': 0.0,
|
||||
@ -729,6 +733,26 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||
strat_results['stake_currency'])),
|
||||
] 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
|
||||
# command stores these results and newer version of freqtrade must be able to handle old
|
||||
# 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'],
|
||||
strat_results['stake_currency'])),
|
||||
|
||||
# Compatibility to show old hyperopt results
|
||||
('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']),
|
||||
*drawdown_metrics,
|
||||
('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,
|
||||
timeframe: str) -> make_subplots:
|
||||
timeframe: str, starting_balance: float) -> make_subplots:
|
||||
"""
|
||||
Add scatter points indicating max drawdown
|
||||
"""
|
||||
try:
|
||||
_, highdate, lowdate, _, _, max_drawdown = calculate_max_drawdown(trades)
|
||||
_, highdate, lowdate, _, _, max_drawdown = calculate_max_drawdown(
|
||||
trades,
|
||||
starting_balance=starting_balance
|
||||
)
|
||||
|
||||
drawdown = go.Scatter(
|
||||
x=[highdate, lowdate],
|
||||
@ -189,22 +192,37 @@ def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame,
|
||||
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:
|
||||
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'],
|
||||
y=underwater['drawdown'],
|
||||
name="Underwater Plot",
|
||||
fill='tozeroy',
|
||||
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:
|
||||
logger.warning("No trades found - not plotting underwater plot")
|
||||
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],
|
||||
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"
|
||||
try:
|
||||
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',
|
||||
)
|
||||
|
||||
fig = make_subplots(rows=5, cols=1, shared_xaxes=True,
|
||||
row_heights=[1, 1, 1, 0.5, 1],
|
||||
fig = make_subplots(rows=6, cols=1, shared_xaxes=True,
|
||||
row_heights=[1, 1, 1, 0.5, 0.75, 0.75],
|
||||
vertical_spacing=0.05,
|
||||
subplot_titles=[
|
||||
"AVG Close Price",
|
||||
@ -540,6 +559,7 @@ def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame],
|
||||
"Profit per pair",
|
||||
"Parallelism",
|
||||
"Underwater",
|
||||
"Relative Drawdown",
|
||||
])
|
||||
fig['layout'].update(title="Freqtrade Profit plot")
|
||||
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']['yaxis4'].update(title='Trade count')
|
||||
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.update_layout(modebar_add=["v1hovermode", "toggleSpikeLines"])
|
||||
|
||||
fig.add_trace(avgclose, 1, 1)
|
||||
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_underwater(fig, 5, trades)
|
||||
# Two rows consumed
|
||||
fig = add_underwater(fig, 5, trades, starting_balance)
|
||||
|
||||
for pair in pairs:
|
||||
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
|
||||
fig = generate_profit_graph(plot_elements['pairs'], plot_elements['ohlcv'],
|
||||
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',
|
||||
directory=config['user_data_dir'] / 'plot',
|
||||
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'])
|
||||
with pytest.raises(ValueError, match='No losing trade, therefore no drawdown.'):
|
||||
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",
|
||||
"SharpeHyperOptLossDaily",
|
||||
"MaxDrawDownHyperOptLoss",
|
||||
"MaxDrawDownRelativeHyperOptLoss",
|
||||
"CalmarHyperOptLoss",
|
||||
"ProfitDrawDownHyperOptLoss",
|
||||
|
||||
|
@ -332,7 +332,13 @@ def test_generate_profit_graph(testdatadir):
|
||||
|
||||
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 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"
|
||||
|
||||
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")
|
||||
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")
|
||||
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:
|
||||
profit_pair = find_trace_in_fig_data(figure.data, f"Profit {pair}")
|
||||
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.*"):
|
||||
# Pair cannot be empty - so it's an empty dataframe.
|
||||
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):
|
||||
|
Loading…
Reference in New Issue
Block a user