Exemplo n.º 1
0
def test_calculate_max_drawdown2():
    values = [
        0.011580, 0.010048, 0.011340, 0.012161, 0.010416, 0.010009, 0.020024,
        -0.024662, -0.022350, 0.020496, -0.029859, -0.030511, 0.010041,
        0.010872, -0.025782, 0.010400, 0.012374, 0.012467, 0.114741, 0.010303,
        0.010088, -0.033961, 0.010680, 0.010886, -0.029274, 0.011178, 0.010693,
        0.010711
    ]

    dates = [Arrow(2020, 1, 1).shift(days=i) for i in range(len(values))]
    df = DataFrame(zip(values, dates), columns=['profit', 'open_date'])
    # sort by profit and reset index
    df = df.sort_values('profit').reset_index(drop=True)
    df1 = df.copy()
    drawdown, hdate, ldate, hval, lval = calculate_max_drawdown(
        df, date_col='open_date', value_col='profit')
    # Ensure df has not been altered.
    assert df.equals(df1)

    assert isinstance(drawdown, float)
    # High must be before low
    assert hdate < ldate
    # High value must be higher than low value
    assert hval > lval
    assert drawdown == 0.091755

    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')
Exemplo n.º 2
0
def generate_strategy_comparison(all_results: Dict) -> List[Dict]:
    """
    Generate summary per strategy
    :param all_results: Dict of <Strategyname: DataFrame> containing results for all strategies
    :return: List of Dicts containing the metrics per Strategy
    """

    tabular_data = []
    for strategy, results in all_results.items():
        tabular_data.append(
            _generate_result_line(results['results'],
                                  results['config']['dry_run_wallet'],
                                  strategy))
        try:
            max_drawdown_per, _, _, _, _ = calculate_max_drawdown(
                results['results'], value_col='profit_ratio')
            max_drawdown_abs, _, _, _, _ = calculate_max_drawdown(
                results['results'], value_col='profit_abs')
        except ValueError:
            max_drawdown_per = 0
            max_drawdown_abs = 0
        tabular_data[-1]['max_drawdown_per'] = round(max_drawdown_per * 100, 2)
        tabular_data[-1]['max_drawdown_abs'] = \
            round_coin_value(max_drawdown_abs, results['config']['stake_currency'], False)
    return tabular_data
Exemplo n.º 3
0
def test_calculate_max_drawdown(testdatadir):
    filename = testdatadir / "backtest-result_test.json"
    bt_data = load_backtest_data(filename)
    drawdown, h, low = calculate_max_drawdown(bt_data)
    assert isinstance(drawdown, float)
    assert pytest.approx(drawdown) == 0.21142322
    assert isinstance(h, Timestamp)
    assert isinstance(low, Timestamp)
    assert h == Timestamp('2018-01-24 14:25:00', tz='UTC')
    assert low == Timestamp('2018-01-30 04:45:00', tz='UTC')
    with pytest.raises(ValueError, match='Trade dataframe empty.'):
        drawdown, h, low = calculate_max_drawdown(DataFrame())
Exemplo n.º 4
0
def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame,
                     timeframe: str) -> make_subplots:
    """
    Add scatter points indicating max drawdown
    """
    try:
        max_drawdown, highdate, lowdate, _, _ = calculate_max_drawdown(trades)

        drawdown = go.Scatter(
            x=[highdate, lowdate],
            y=[
                df_comb.loc[timeframe_to_prev_date(timeframe, highdate),
                            'cum_profit'],
                df_comb.loc[timeframe_to_prev_date(timeframe, lowdate),
                            'cum_profit'],
            ],
            mode='markers',
            name=f"Max drawdown {max_drawdown:.2%}",
            text=f"Max drawdown {max_drawdown:.2%}",
            marker=dict(symbol='square-open',
                        size=9,
                        line=dict(width=2),
                        color='green'))
        fig.add_trace(drawdown, row, 1)
    except ValueError:
        logger.warning("No trades found - not plotting max drawdown.")
    return fig
Exemplo n.º 5
0
    def _max_drawdown(self, date_now: datetime) -> ProtectionReturn:
        """
        Evaluate recent trades for drawdown ...
        """
        look_back_until = date_now - timedelta(minutes=self._lookback_period)

        trades = Trade.get_trades_proxy(is_open=False, close_date=look_back_until)

        trades_df = pd.DataFrame([trade.to_json() for trade in trades])

        if len(trades) < self._trade_limit:
            # Not enough trades in the relevant period
            return False, None, None

        # Drawdown is always positive
        try:
            drawdown, _, _, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit')
        except ValueError:
            return False, None, None

        if drawdown > self._max_allowed_drawdown:
            self.log_once(
                f"Trading stopped due to Max Drawdown {drawdown:.2f} > {self._max_allowed_drawdown}"
                f" within {self.lookback_period_str}.", logger.info)
            until = self.calculate_lock_end(trades, self._stop_duration)

            return True, until, self._reason(drawdown)

        return False, None, None
    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:
        """
        Objective function, returns smaller number for more optimal results.

        Uses Calmar Ratio calculation.
        """
        total_profit = backtest_stats["profit_total"]
        days_period = (max_date - min_date).days

        # adding slippage of 0.1% per trade
        total_profit = total_profit - 0.0005
        expected_returns_mean = total_profit.sum() / days_period * 100

        # calculate max drawdown
        try:
            _, _, _, high_val, low_val = calculate_max_drawdown(
                results, value_col="profit_abs")
            max_drawdown = (high_val - low_val) / high_val
        except ValueError:
            max_drawdown = 0

        if max_drawdown != 0:
            calmar_ratio = expected_returns_mean / max_drawdown * msqrt(365)
        else:
            # Define high (negative) calmar ratio to be clear that this is NOT optimal.
            calmar_ratio = -20.0

        # print(expected_returns_mean, max_drawdown, calmar_ratio)
        return -calmar_ratio
    def hyperopt_loss_function(results: DataFrame, trade_count: int, *args,
                               **kwargs) -> float:
        total_profit = results["profit_abs"].sum()

        try:
            max_drawdown_abs = calculate_max_drawdown(
                results, value_col="profit_abs")[5]
        except ValueError:
            max_drawdown_abs = 0

        return -1 * (total_profit * (1 - max_drawdown_abs * DRAWDOWN_MULT))
Exemplo n.º 8
0
def test_calculate_max_drawdown(testdatadir):
    filename = testdatadir / "backtest-result_new.json"
    bt_data = load_backtest_data(filename)
    _, hdate, lowdate, hval, lval, drawdown = calculate_max_drawdown(
        bt_data, value_col="profit_abs")
    assert isinstance(drawdown, float)
    assert pytest.approx(drawdown) == 0.12071099
    assert isinstance(hdate, Timestamp)
    assert isinstance(lowdate, Timestamp)
    assert isinstance(hval, float)
    assert isinstance(lval, float)
    assert hdate == Timestamp('2018-01-25 01:30:00', tz='UTC')
    assert lowdate == Timestamp('2018-01-25 03:50:00', tz='UTC')

    underwater = calculate_underwater(bt_data)
    assert isinstance(underwater, DataFrame)

    with pytest.raises(ValueError, match='Trade dataframe empty.'):
        calculate_max_drawdown(DataFrame())

    with pytest.raises(ValueError, match='Trade dataframe empty.'):
        calculate_underwater(DataFrame())
Exemplo n.º 9
0
    def hyperopt_loss_function(results: DataFrame, trade_count: int,
                               min_date: datetime, max_date: datetime,
                               *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:
            max_drawdown = calculate_max_drawdown(results, value_col='profit_abs')
        except ValueError:
            # No losing trade, therefore no drawdown.
            return -total_profit
        return -total_profit / max_drawdown[0]
Exemplo n.º 10
0
    def hyperopt_loss_function(results: DataFrame, trade_count: int,
                               min_date: datetime, max_date: datetime,
                               *args, **kwargs) -> float:
        """
        Objective function, returns smaller number for better results.
        """
        profit_threshold = 0

        if IGNORE_SMALL_PROFITS:
            profit_threshold = SMALL_PROFITS_THRESHOLD

        total_profit = results['profit_ratio'].sum()
        total_win = len(results[(results['profit_ratio'] > profit_threshold)])
        total_lose = len(results[(results['profit_ratio'] <= 0)])
        average_profit = results['profit_ratio'].mean() * 100
        sortino_ratio = sortino_daily(results, trade_count, min_date, max_date)
        trade_duration = results['trade_duration'].mean()

        max_drawdown = 100
        try:
            max_drawdown, _, _, _, _ = calculate_max_drawdown(
                results, value_col='profit_ratio')
        except:
            pass

        if total_lose == 0:
            total_lose = 1

        profit_loss = (1 - total_profit / EXPECTED_MAX_PROFIT) * TOTAL_PROFIT_WEIGHT
        win_lose_loss = (1 - (total_win / total_lose)) * WIN_LOSS_WEIGHT
        average_profit_loss = 1 - (min(average_profit, AVERAGE_PROFIT_THRESHOLD) * AVERAGE_PROFIT_WEIGHT)
        sortino_ratio_loss = SORTINO_WEIGHT * sortino_ratio
        drawdown_loss = max_drawdown * DRAWDOWN_WEIGHT
        duration_loss = DURATION_WEIGHT * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1)

        result = profit_loss + win_lose_loss + average_profit_loss + sortino_ratio_loss + drawdown_loss + duration_loss

        return result
Exemplo n.º 11
0
def generate_backtest_stats(btdata: Dict[str, DataFrame],
                            all_results: Dict[str, Dict[str, Union[DataFrame, Dict]]],
                            min_date: Arrow, max_date: Arrow
                            ) -> Dict[str, Any]:
    """
    :param btdata: Backtest data
    :param all_results: backtest result - dictionary in the form:
                     { Strategy: {'results: results, 'config: config}}.
    :param min_date: Backtest start date
    :param max_date: Backtest end date
    :return:
    Dictionary containing results per strategy and a stratgy summary.
    """
    result: Dict[str, Any] = {'strategy': {}}
    market_change = calculate_market_change(btdata, 'close')

    for strategy, content in all_results.items():
        results: Dict[str, DataFrame] = content['results']
        if not isinstance(results, DataFrame):
            continue
        config = content['config']
        max_open_trades = config['max_open_trades']
        stake_currency = config['stake_currency']

        pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
                                             max_open_trades=max_open_trades,
                                             results=results, skip_nan=False)
        sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades,
                                                       results=results)
        left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
                                                  max_open_trades=max_open_trades,
                                                  results=results.loc[results['open_at_end']],
                                                  skip_nan=True)
        daily_stats = generate_daily_stats(results)

        results['open_timestamp'] = results['open_date'].astype(int64) // 1e6
        results['close_timestamp'] = results['close_date'].astype(int64) // 1e6

        backtest_days = (max_date - min_date).days
        strat_stats = {
            'trades': results.to_dict(orient='records'),
            'results_per_pair': pair_results,
            'sell_reason_summary': sell_reason_stats,
            'left_open_trades': left_open_results,
            'total_trades': len(results),
            'profit_mean': results['profit_percent'].mean() if len(results) > 0 else 0,
            'profit_total': results['profit_percent'].sum(),
            'profit_total_abs': results['profit_abs'].sum(),
            'backtest_start': min_date.datetime,
            'backtest_start_ts': min_date.int_timestamp * 1000,
            'backtest_end': max_date.datetime,
            'backtest_end_ts': max_date.int_timestamp * 1000,
            'backtest_days': backtest_days,

            'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0,
            'market_change': market_change,
            'pairlist': list(btdata.keys()),
            'stake_amount': config['stake_amount'],
            'stake_currency': config['stake_currency'],
            'max_open_trades': (config['max_open_trades']
                                if config['max_open_trades'] != float('inf') else -1),
            'timeframe': config['timeframe'],
            # Parameters relevant for backtesting
            'stoploss': config['stoploss'],
            'trailing_stop': config.get('trailing_stop', False),
            'trailing_stop_positive': config.get('trailing_stop_positive'),
            'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset', 0.0),
            'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False),
            'minimal_roi': config['minimal_roi'],
            'use_sell_signal': config['ask_strategy']['use_sell_signal'],
            'sell_profit_only': config['ask_strategy']['sell_profit_only'],
            'ignore_roi_if_buy_signal': config['ask_strategy']['ignore_roi_if_buy_signal'],
            **daily_stats,
        }
        result['strategy'][strategy] = strat_stats

        try:
            max_drawdown, drawdown_start, drawdown_end = calculate_max_drawdown(
                results, value_col='profit_percent')
            strat_stats.update({
                'max_drawdown': max_drawdown,
                'drawdown_start': drawdown_start,
                'drawdown_start_ts': drawdown_start.timestamp() * 1000,
                'drawdown_end': drawdown_end,
                'drawdown_end_ts': drawdown_end.timestamp() * 1000,
            })
        except ValueError:
            strat_stats.update({
                'max_drawdown': 0.0,
                'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc),
                'drawdown_start_ts': 0,
                'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc),
                'drawdown_end_ts': 0,
            })

    strategy_results = generate_strategy_metrics(all_results=all_results)

    result['strategy_comparison'] = strategy_results

    return result
Exemplo n.º 12
0
def generate_strategy_stats(pairlist: List[str], strategy: str,
                            content: Dict[str, Any], min_date: datetime,
                            max_date: datetime,
                            market_change: float) -> Dict[str, Any]:
    """
    :param pairlist: List of pairs to backtest
    :param strategy: Strategy name
    :param content: Backtest result data in the format:
                    {'results: results, 'config: config}}.
    :param min_date: Backtest start date
    :param max_date: Backtest end date
    :param market_change: float indicating the market change
    :return: Dictionary containing results per strategy and a strategy summary.
    """
    results: Dict[str, DataFrame] = content['results']
    if not isinstance(results, DataFrame):
        return {}
    config = content['config']
    max_open_trades = min(config['max_open_trades'], len(pairlist))
    start_balance = config['dry_run_wallet']
    stake_currency = config['stake_currency']

    pair_results = generate_pair_metrics(pairlist,
                                         stake_currency=stake_currency,
                                         starting_balance=start_balance,
                                         results=results,
                                         skip_nan=False)

    enter_tag_results = generate_tag_metrics("enter_tag",
                                             starting_balance=start_balance,
                                             results=results,
                                             skip_nan=False)

    exit_reason_stats = generate_exit_reason_stats(
        max_open_trades=max_open_trades, results=results)
    left_open_results = generate_pair_metrics(
        pairlist,
        stake_currency=stake_currency,
        starting_balance=start_balance,
        results=results.loc[results['is_open']],
        skip_nan=True)
    daily_stats = generate_daily_stats(results)
    trade_stats = generate_trading_stats(results)
    best_pair = max(
        [pair for pair in pair_results if pair['key'] != 'TOTAL'],
        key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
    worst_pair = min(
        [pair for pair in pair_results if pair['key'] != 'TOTAL'],
        key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
    if not results.empty:
        results['open_timestamp'] = results['open_date'].view(int64) // 1e6
        results['close_timestamp'] = results['close_date'].view(int64) // 1e6

    backtest_days = (max_date - min_date).days or 1
    strat_stats = {
        'trades':
        results.to_dict(orient='records'),
        'locks': [lock.to_json() for lock in content['locks']],
        'best_pair':
        best_pair,
        'worst_pair':
        worst_pair,
        'results_per_pair':
        pair_results,
        'results_per_enter_tag':
        enter_tag_results,
        'exit_reason_summary':
        exit_reason_stats,
        'left_open_trades':
        left_open_results,
        # 'days_breakdown_stats': days_breakdown_stats,
        'total_trades':
        len(results),
        'trade_count_long':
        len(results.loc[~results['is_short']]),
        'trade_count_short':
        len(results.loc[results['is_short']]),
        'total_volume':
        float(results['stake_amount'].sum()),
        'avg_stake_amount':
        results['stake_amount'].mean() if len(results) > 0 else 0,
        'profit_mean':
        results['profit_ratio'].mean() if len(results) > 0 else 0,
        'profit_median':
        results['profit_ratio'].median() if len(results) > 0 else 0,
        'profit_total':
        results['profit_abs'].sum() / start_balance,
        'profit_total_long':
        results.loc[~results['is_short'], 'profit_abs'].sum() / start_balance,
        'profit_total_short':
        results.loc[results['is_short'], 'profit_abs'].sum() / start_balance,
        'profit_total_abs':
        results['profit_abs'].sum(),
        'profit_total_long_abs':
        results.loc[~results['is_short'], 'profit_abs'].sum(),
        'profit_total_short_abs':
        results.loc[results['is_short'], 'profit_abs'].sum(),
        'backtest_start':
        min_date.strftime(DATETIME_PRINT_FORMAT),
        'backtest_start_ts':
        int(min_date.timestamp() * 1000),
        'backtest_end':
        max_date.strftime(DATETIME_PRINT_FORMAT),
        'backtest_end_ts':
        int(max_date.timestamp() * 1000),
        'backtest_days':
        backtest_days,
        'backtest_run_start_ts':
        content['backtest_start_time'],
        'backtest_run_end_ts':
        content['backtest_end_time'],
        'trades_per_day':
        round(len(results) / backtest_days, 2),
        'market_change':
        market_change,
        'pairlist':
        pairlist,
        'stake_amount':
        config['stake_amount'],
        'stake_currency':
        config['stake_currency'],
        'stake_currency_decimals':
        decimals_per_coin(config['stake_currency']),
        'starting_balance':
        start_balance,
        'dry_run_wallet':
        start_balance,
        'final_balance':
        content['final_balance'],
        'rejected_signals':
        content['rejected_signals'],
        'timedout_entry_orders':
        content['timedout_entry_orders'],
        'timedout_exit_orders':
        content['timedout_exit_orders'],
        'max_open_trades':
        max_open_trades,
        'max_open_trades_setting':
        (config['max_open_trades']
         if config['max_open_trades'] != float('inf') else -1),
        'timeframe':
        config['timeframe'],
        'timeframe_detail':
        config.get('timeframe_detail', ''),
        'timerange':
        config.get('timerange', ''),
        'enable_protections':
        config.get('enable_protections', False),
        'strategy_name':
        strategy,
        # Parameters relevant for backtesting
        'stoploss':
        config['stoploss'],
        'trailing_stop':
        config.get('trailing_stop', False),
        'trailing_stop_positive':
        config.get('trailing_stop_positive'),
        'trailing_stop_positive_offset':
        config.get('trailing_stop_positive_offset', 0.0),
        'trailing_only_offset_is_reached':
        config.get('trailing_only_offset_is_reached', False),
        'use_custom_stoploss':
        config.get('use_custom_stoploss', False),
        'minimal_roi':
        config['minimal_roi'],
        'use_exit_signal':
        config['use_exit_signal'],
        'exit_profit_only':
        config['exit_profit_only'],
        'exit_profit_offset':
        config['exit_profit_offset'],
        'ignore_roi_if_entry_signal':
        config['ignore_roi_if_entry_signal'],
        **daily_stats,
        **trade_stats
    }

    try:
        max_drawdown_legacy, _, _, _, _, _ = calculate_max_drawdown(
            results, value_col='profit_ratio')
        (drawdown_abs, drawdown_start, drawdown_end, high_val, low_val,
         max_drawdown) = calculate_max_drawdown(results,
                                                value_col='profit_abs',
                                                starting_balance=start_balance)
        strat_stats.update({
            'max_drawdown':
            max_drawdown_legacy,  # Deprecated - do not use
            'max_drawdown_account':
            max_drawdown,
            'max_drawdown_abs':
            drawdown_abs,
            'drawdown_start':
            drawdown_start.strftime(DATETIME_PRINT_FORMAT),
            'drawdown_start_ts':
            drawdown_start.timestamp() * 1000,
            'drawdown_end':
            drawdown_end.strftime(DATETIME_PRINT_FORMAT),
            'drawdown_end_ts':
            drawdown_end.timestamp() * 1000,
            'max_drawdown_low':
            low_val,
            'max_drawdown_high':
            high_val,
        })

        csum_min, csum_max = calculate_csum(results, start_balance)
        strat_stats.update({'csum_min': csum_min, 'csum_max': csum_max})

    except ValueError:
        strat_stats.update({
            'max_drawdown':
            0.0,
            'max_drawdown_account':
            0.0,
            'max_drawdown_abs':
            0.0,
            'max_drawdown_low':
            0.0,
            'max_drawdown_high':
            0.0,
            'drawdown_start':
            datetime(1970, 1, 1, tzinfo=timezone.utc),
            'drawdown_start_ts':
            0,
            'drawdown_end':
            datetime(1970, 1, 1, tzinfo=timezone.utc),
            'drawdown_end_ts':
            0,
            'csum_min':
            0,
            'csum_max':
            0
        })

    return strat_stats
Exemplo n.º 13
0
def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame],
                            all_results: Dict[str, DataFrame], min_date: Arrow,
                            max_date: Arrow) -> Dict[str, Any]:
    """
    :param config: Configuration object used for backtest
    :param btdata: Backtest data
    :param all_results: backtest result - dictionary with { Strategy: results}.
    :param min_date: Backtest start date
    :param max_date: Backtest end date
    :return:
    Dictionary containing results per strategy and a stratgy summary.
    """
    stake_currency = config['stake_currency']
    max_open_trades = config['max_open_trades']
    result: Dict[str, Any] = {'strategy': {}}
    market_change = calculate_market_change(btdata, 'close')

    for strategy, results in all_results.items():

        pair_results = generate_pair_metrics(btdata,
                                             stake_currency=stake_currency,
                                             max_open_trades=max_open_trades,
                                             results=results,
                                             skip_nan=False)
        sell_reason_stats = generate_sell_reason_stats(
            max_open_trades=max_open_trades, results=results)
        left_open_results = generate_pair_metrics(
            btdata,
            stake_currency=stake_currency,
            max_open_trades=max_open_trades,
            results=results.loc[results['open_at_end']],
            skip_nan=True)
        daily_stats = generate_daily_stats(results)

        results['open_timestamp'] = results['open_date'].astype(int64) // 1e6
        results['close_timestamp'] = results['close_date'].astype(int64) // 1e6

        backtest_days = (max_date - min_date).days
        strat_stats = {
            'trades':
            results.to_dict(orient='records'),
            'results_per_pair':
            pair_results,
            'sell_reason_summary':
            sell_reason_stats,
            'left_open_trades':
            left_open_results,
            'total_trades':
            len(results),
            'profit_mean':
            results['profit_percent'].mean() if len(results) > 0 else 0,
            'profit_total':
            results['profit_percent'].sum(),
            'profit_total_abs':
            results['profit_abs'].sum(),
            'backtest_start':
            min_date.datetime,
            'backtest_start_ts':
            min_date.timestamp * 1000,
            'backtest_end':
            max_date.datetime,
            'backtest_end_ts':
            max_date.timestamp * 1000,
            'backtest_days':
            backtest_days,
            'trades_per_day':
            round(len(results) / backtest_days, 2) if backtest_days > 0 else 0,
            'market_change':
            market_change,
            'pairlist':
            list(btdata.keys()),
            'stake_amount':
            config['stake_amount'],
            'stake_currency':
            config['stake_currency'],
            'max_open_trades':
            (config['max_open_trades']
             if config['max_open_trades'] != float('inf') else -1),
            'timeframe':
            config['timeframe'],
            **daily_stats,
        }
        result['strategy'][strategy] = strat_stats

        try:
            max_drawdown, drawdown_start, drawdown_end = calculate_max_drawdown(
                results, value_col='profit_percent')
            strat_stats.update({
                'max_drawdown':
                max_drawdown,
                'drawdown_start':
                drawdown_start,
                'drawdown_start_ts':
                drawdown_start.timestamp() * 1000,
                'drawdown_end':
                drawdown_end,
                'drawdown_end_ts':
                drawdown_end.timestamp() * 1000,
            })
        except ValueError:
            strat_stats.update({
                'max_drawdown':
                0.0,
                'drawdown_start':
                datetime(1970, 1, 1, tzinfo=timezone.utc),
                'drawdown_start_ts':
                0,
                'drawdown_end':
                datetime(1970, 1, 1, tzinfo=timezone.utc),
                'drawdown_end_ts':
                0,
            })

    strategy_results = generate_strategy_metrics(
        stake_currency=stake_currency,
        max_open_trades=max_open_trades,
        all_results=all_results)

    result['strategy_comparison'] = strategy_results

    return result
Exemplo n.º 14
0
def generate_backtest_stats(btdata: Dict[str, DataFrame],
                            all_results: Dict[str, Dict[str, Union[DataFrame,
                                                                   Dict]]],
                            min_date: Arrow,
                            max_date: Arrow) -> Dict[str, Any]:
    """
    :param btdata: Backtest data
    :param all_results: backtest result - dictionary in the form:
                     { Strategy: {'results: results, 'config: config}}.
    :param min_date: Backtest start date
    :param max_date: Backtest end date
    :return:
    Dictionary containing results per strategy and a stratgy summary.
    """
    result: Dict[str, Any] = {'strategy': {}}
    market_change = calculate_market_change(btdata, 'close')

    for strategy, content in all_results.items():
        results: Dict[str, DataFrame] = content['results']
        if not isinstance(results, DataFrame):
            continue
        config = content['config']
        max_open_trades = min(config['max_open_trades'], len(btdata.keys()))
        starting_balance = config['dry_run_wallet']
        stake_currency = config['stake_currency']

        pair_results = generate_pair_metrics(btdata,
                                             stake_currency=stake_currency,
                                             starting_balance=starting_balance,
                                             results=results,
                                             skip_nan=False)
        sell_reason_stats = generate_sell_reason_stats(
            max_open_trades=max_open_trades, results=results)
        left_open_results = generate_pair_metrics(
            btdata,
            stake_currency=stake_currency,
            starting_balance=starting_balance,
            results=results.loc[results['is_open']],
            skip_nan=True)
        daily_stats = generate_daily_stats(results)
        best_pair = max(
            [pair for pair in pair_results if pair['key'] != 'TOTAL'],
            key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
        worst_pair = min(
            [pair for pair in pair_results if pair['key'] != 'TOTAL'],
            key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
        results['open_timestamp'] = results['open_date'].astype(int64) // 1e6
        results['close_timestamp'] = results['close_date'].astype(int64) // 1e6

        backtest_days = (max_date - min_date).days
        strat_stats = {
            'trades':
            results.to_dict(orient='records'),
            'locks': [lock.to_json() for lock in content['locks']],
            'best_pair':
            best_pair,
            'worst_pair':
            worst_pair,
            'results_per_pair':
            pair_results,
            'sell_reason_summary':
            sell_reason_stats,
            'left_open_trades':
            left_open_results,
            'total_trades':
            len(results),
            'total_volume':
            float(results['stake_amount'].sum()),
            'avg_stake_amount':
            results['stake_amount'].mean() if len(results) > 0 else 0,
            'profit_mean':
            results['profit_ratio'].mean() if len(results) > 0 else 0,
            'profit_total':
            results['profit_abs'].sum() / starting_balance,
            'profit_total_abs':
            results['profit_abs'].sum(),
            'backtest_start':
            min_date.datetime,
            'backtest_start_ts':
            min_date.int_timestamp * 1000,
            'backtest_end':
            max_date.datetime,
            'backtest_end_ts':
            max_date.int_timestamp * 1000,
            'backtest_days':
            backtest_days,
            'backtest_run_start_ts':
            content['backtest_start_time'],
            'backtest_run_end_ts':
            content['backtest_end_time'],
            'trades_per_day':
            round(len(results) / backtest_days, 2) if backtest_days > 0 else 0,
            'market_change':
            market_change,
            'pairlist':
            list(btdata.keys()),
            'stake_amount':
            config['stake_amount'],
            'stake_currency':
            config['stake_currency'],
            'stake_currency_decimals':
            decimals_per_coin(config['stake_currency']),
            'starting_balance':
            starting_balance,
            'dry_run_wallet':
            starting_balance,
            'final_balance':
            content['final_balance'],
            'max_open_trades':
            max_open_trades,
            'max_open_trades_setting':
            (config['max_open_trades']
             if config['max_open_trades'] != float('inf') else -1),
            'timeframe':
            config['timeframe'],
            'timerange':
            config.get('timerange', ''),
            'enable_protections':
            config.get('enable_protections', False),
            'strategy_name':
            strategy,
            # Parameters relevant for backtesting
            'stoploss':
            config['stoploss'],
            'trailing_stop':
            config.get('trailing_stop', False),
            'trailing_stop_positive':
            config.get('trailing_stop_positive'),
            'trailing_stop_positive_offset':
            config.get('trailing_stop_positive_offset', 0.0),
            'trailing_only_offset_is_reached':
            config.get('trailing_only_offset_is_reached', False),
            'use_custom_stoploss':
            config.get('use_custom_stoploss', False),
            'minimal_roi':
            config['minimal_roi'],
            'use_sell_signal':
            config['ask_strategy']['use_sell_signal'],
            'sell_profit_only':
            config['ask_strategy']['sell_profit_only'],
            'sell_profit_offset':
            config['ask_strategy']['sell_profit_offset'],
            'ignore_roi_if_buy_signal':
            config['ask_strategy']['ignore_roi_if_buy_signal'],
            **daily_stats,
        }
        result['strategy'][strategy] = strat_stats

        try:
            max_drawdown, _, _, _, _ = calculate_max_drawdown(
                results, value_col='profit_ratio')
            drawdown_abs, drawdown_start, drawdown_end, high_val, low_val = calculate_max_drawdown(
                results, value_col='profit_abs')
            strat_stats.update({
                'max_drawdown':
                max_drawdown,
                'max_drawdown_abs':
                drawdown_abs,
                'drawdown_start':
                drawdown_start,
                'drawdown_start_ts':
                drawdown_start.timestamp() * 1000,
                'drawdown_end':
                drawdown_end,
                'drawdown_end_ts':
                drawdown_end.timestamp() * 1000,
                'max_drawdown_low':
                low_val,
                'max_drawdown_high':
                high_val,
            })

            csum_min, csum_max = calculate_csum(results, starting_balance)
            strat_stats.update({'csum_min': csum_min, 'csum_max': csum_max})

        except ValueError:
            strat_stats.update({
                'max_drawdown':
                0.0,
                'max_drawdown_abs':
                0.0,
                'max_drawdown_low':
                0.0,
                'max_drawdown_high':
                0.0,
                'drawdown_start':
                datetime(1970, 1, 1, tzinfo=timezone.utc),
                'drawdown_start_ts':
                0,
                'drawdown_end':
                datetime(1970, 1, 1, tzinfo=timezone.utc),
                'drawdown_end_ts':
                0,
                'csum_min':
                0,
                'csum_max':
                0
            })

    strategy_results = generate_strategy_metrics(all_results=all_results)

    result['strategy_comparison'] = strategy_results

    return result