Merge branch 'develop' into feat_readjust_entry

This commit is contained in:
eSeR1805 2022-05-04 21:43:41 +03:00
commit 496bf84e3a
No known key found for this signature in database
GPG Key ID: BA53686259B46936
15 changed files with 314 additions and 139 deletions

View File

@ -287,45 +287,51 @@ 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 |
| | | | | |
| Total/Daily Avg Trades | 429 / 3.575 | | Total/Daily Avg Trades | 429 / 3.575 |
| Starting balance | 0.01000000 BTC | | Starting balance | 0.01000000 BTC |
| Final balance | 0.01762792 BTC | | Final balance | 0.01762792 BTC |
| 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 |
| Best Pair | LSK/BTC 26.26% | | Total profit Long % | 1250.58% |
| Worst Pair | ZEC/BTC -10.18% | | Total profit Short % | -15.02% |
| Best Trade | LSK/BTC 4.25% | | Absolute profit Long | 0.00838792 BTC |
| Worst Trade | ZEC/BTC -10.25% | | Absolute profit Short | -0.00076 BTC |
| Best day | 0.00076 BTC | | | |
| Worst day | -0.00036 BTC | | Best Pair | LSK/BTC 26.26% |
| Days win/draw/lose | 12 / 82 / 25 | | Worst Pair | ZEC/BTC -10.18% |
| Avg. Duration Winners | 4:23:00 | | Best Trade | LSK/BTC 4.25% |
| Avg. Duration Loser | 6:55:00 | | Worst Trade | ZEC/BTC -10.25% |
| Rejected Entry signals | 3089 | | Best day | 0.00076 BTC |
| Entry/Exit Timeouts | 0 / 0 | | Worst day | -0.00036 BTC |
| | | | Days win/draw/lose | 12 / 82 / 25 |
| Min balance | 0.00945123 BTC | | Avg. Duration Winners | 4:23:00 |
| Max balance | 0.01846651 BTC | | Avg. Duration Loser | 6:55:00 |
| Drawdown (Account) | 13.33% | | Rejected Entry signals | 3089 |
| Drawdown | 0.0015 BTC | | Entry/Exit Timeouts | 0 / 0 |
| Drawdown high | 0.0013 BTC | | | |
| Drawdown low | -0.0002 BTC | | Min balance | 0.00945123 BTC |
| Drawdown Start | 2019-02-15 14:10:00 | | Max balance | 0.01846651 BTC |
| Drawdown End | 2019-04-11 18:15:00 | | Max % of account underwater | 25.19% |
| Market change | -5.88% | | 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 ### Backtesting report table
@ -377,50 +383,51 @@ 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 |
| | | | | |
| Total/Daily Avg Trades | 429 / 3.575 | | Total/Daily Avg Trades | 429 / 3.575 |
| Starting balance | 0.01000000 BTC | | Starting balance | 0.01000000 BTC |
| Final balance | 0.01762792 BTC | | Final balance | 0.01762792 BTC |
| Absolute profit | 0.00762792 BTC | | Absolute profit | 0.00762792 BTC |
| Total profit % | 76.2% | | Total profit % | 76.2% |
| CAGR % | 460.87% | | CAGR % | 460.87% |
| 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 | | Long / Short | 352 / 77 |
| Total profit Long % | 1250.58% | | Total profit Long % | 1250.58% |
| Total profit Short % | -15.02% | | Total profit Short % | -15.02% |
| Absolute profit Long | 0.00838792 BTC | | Absolute profit Long | 0.00838792 BTC |
| Absolute profit Short | -0.00076 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% |
| Worst Trade | ZEC/BTC -10.25% | | Worst Trade | ZEC/BTC -10.25% |
| Best day | 0.00076 BTC | | Best day | 0.00076 BTC |
| Worst day | -0.00036 BTC | | Worst day | -0.00036 BTC |
| Days win/draw/lose | 12 / 82 / 25 | | Days win/draw/lose | 12 / 82 / 25 |
| Avg. Duration Winners | 4:23:00 | | Avg. Duration Winners | 4:23:00 |
| Avg. Duration Loser | 6:55:00 | | Avg. Duration Loser | 6:55:00 |
| Rejected Entry signals | 3089 | | Rejected Entry signals | 3089 |
| Entry/Exit Timeouts | 0 / 0 | | Entry/Exit Timeouts | 0 / 0 |
| | | | | |
| 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% |
| Drawdown | 0.0015 BTC | | Absolute Drawdown (Account) | 13.33% |
| Drawdown high | 0.0013 BTC | | Drawdown | 0.0015 BTC |
| Drawdown low | -0.0002 BTC | | Drawdown high | 0.0013 BTC |
| Drawdown Start | 2019-02-15 14:10:00 | | Drawdown low | -0.0002 BTC |
| Drawdown End | 2019-04-11 18:15:00 | | Drawdown Start | 2019-02-15 14:10:00 |
| Market change | -5.88% | | 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. - `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).

View File

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

View File

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

View File

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

View File

@ -1613,7 +1613,9 @@ class Exchange:
order['fee']['cost'] / safe_value_fallback2(order, order, 'filled', 'amount'), 8) order['fee']['cost'] / safe_value_fallback2(order, order, 'filled', 'amount'), 8)
elif fee_curr in self.get_pair_quote_currency(order['symbol']): elif fee_curr in self.get_pair_quote_currency(order['symbol']):
# Quote currency - divide by cost # Quote currency - divide by cost
return round(order['fee']['cost'] / order['cost'], 8) if order['cost'] else None return round(self._contracts_to_amount(
order['symbol'], order['fee']['cost']) / order['cost'],
8) if order['cost'] else None
else: else:
# If Fee currency is a different currency # If Fee currency is a different currency
if not order['cost']: if not order['cost']:
@ -1628,7 +1630,8 @@ class Exchange:
fee_to_quote_rate = self._config['exchange'].get('unknown_fee_rate', None) fee_to_quote_rate = self._config['exchange'].get('unknown_fee_rate', None)
if not fee_to_quote_rate: if not fee_to_quote_rate:
return None return None
return round((order['fee']['cost'] * fee_to_quote_rate) / order['cost'], 8) return round((self._contracts_to_amount(
order['symbol'], order['fee']['cost']) * fee_to_quote_rate) / order['cost'], 8)
def extract_cost_curr_rate(self, order: Dict) -> Tuple[float, str, Optional[float]]: def extract_cost_curr_rate(self, order: Dict) -> Tuple[float, str, Optional[float]]:
""" """

View File

@ -603,7 +603,6 @@ class FreqtradeBot(LoggingMixin):
pair, price, stake_amount, trade_side, enter_tag, trade) pair, price, stake_amount, trade_side, enter_tag, trade)
if not stake_amount: if not stake_amount:
logger.info(f"No stake amount to enter a trade for {pair}.")
return False return False
if pos_adjust: if pos_adjust:

View File

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

View File

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

View File

@ -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%}"),
] ]

View File

@ -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}'
@ -612,6 +634,7 @@ def load_and_plot_trades(config: Dict[str, Any]):
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config) exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
IStrategy.dp = DataProvider(config, exchange) IStrategy.dp = DataProvider(config, exchange)
strategy.bot_start() strategy.bot_start()
strategy.bot_loop_start()
plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count) plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count)
timerange = plot_elements['timerange'] timerange = plot_elements['timerange']
trades = plot_elements['trades'] trades = plot_elements['trades']
@ -670,7 +693,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))

View File

@ -155,7 +155,7 @@ function install_macos() {
# Install bot Debian_ubuntu # Install bot Debian_ubuntu
function install_debian() { function install_debian() {
sudo apt-get update sudo apt-get update
sudo apt-get install -y gcc build-essential autoconf libtool pkg-config make wget git $(echo lib${PYTHON}-dev ${PYTHON}-venv) sudo apt-get install -y gcc build-essential autoconf libtool pkg-config make wget git curl $(echo lib${PYTHON}-dev ${PYTHON}-venv)
install_talib install_talib
} }

View File

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

View File

@ -4165,7 +4165,10 @@ def test__order_contracts_to_amount(
'cost': 60.0, 'cost': 60.0,
'filled': None, 'filled': None,
'remaining': 30.0, 'remaining': 30.0,
'fee': 0.06, 'fee': {
'currency': 'USDT',
'cost': 0.06,
},
'fees': [{ 'fees': [{
'currency': 'USDT', 'currency': 'USDT',
'cost': 0.06, 'cost': 0.06,
@ -4192,7 +4195,10 @@ def test__order_contracts_to_amount(
'cost': 80.0, 'cost': 80.0,
'filled': None, 'filled': None,
'remaining': 40.0, 'remaining': 40.0,
'fee': 0.08, 'fee': {
'currency': 'USDT',
'cost': 0.08,
},
'fees': [{ 'fees': [{
'currency': 'USDT', 'currency': 'USDT',
'cost': 0.08, 'cost': 0.08,
@ -4226,12 +4232,18 @@ def test__order_contracts_to_amount(
'info': {}, 'info': {},
}, },
] ]
order1_bef = orders[0]
order2_bef = orders[1]
order1 = exchange._order_contracts_to_amount(deepcopy(order1_bef))
order2 = exchange._order_contracts_to_amount(deepcopy(order2_bef))
assert order1['amount'] == order1_bef['amount'] * contract_size
assert order1['cost'] == order1_bef['cost'] * contract_size
order1 = exchange._order_contracts_to_amount(orders[0]) assert order2['amount'] == order2_bef['amount'] * contract_size
order2 = exchange._order_contracts_to_amount(orders[1]) assert order2['cost'] == order2_bef['cost'] * contract_size
# Don't fail
exchange._order_contracts_to_amount(orders[2]) exchange._order_contracts_to_amount(orders[2])
assert order1['amount'] == 30.0 * contract_size
assert order2['amount'] == 40.0 * contract_size
@pytest.mark.parametrize('pair,contract_size,trading_mode', [ @pytest.mark.parametrize('pair,contract_size,trading_mode', [

View File

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

View File

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