"""
MultiMetricHyperOptLoss

This module defines the alternative HyperOptLoss class based on:
  - Profit
  - Drawdown
  - Profit Factor
  - Expectancy Ratio
  - Winrate
  - Amount of trades

Possible to change:
  - `DRAWDOWN_MULT` to penalize drawdown objective for individual needs;
  - `TARGET_TRADE_AMOUNT` to adjust amount of trades impact.
  - `EXPECTANCY_CONST` to adjust expectancy ratio impact.
  - `PF_CONST` to adjust profit factor impact.
  - `WINRATE_CONST` to adjust winrate impact.


DRAWDOWN_MULT variable within the hyperoptloss file can be adjusted to be stricter or more
              flexible on drawdown purposes. Smaller numbers penalize drawdowns more severely.
PF_CONST variable adjusts the impact of the Profit Factor on the optimization.
EXPECTANCY_CONST variable controls the influence of the Expectancy Ratio.
WINRATE_CONST variable can be adjusted to increase or decrease impact of winrate.

PF_CONST, EXPECTANCY_CONST, WINRATE_CONST all operate in a similar manner:
        a higher value means that the metric has a lesser impact on the objective,
        while a lower value means that it has a greater impact.
TARGET_TRADE_AMOUNT variable sets the minimum number of trades required to avoid penalties.
            If the trade amount falls below this threshold, the penalty is applied.
"""

import numpy as np
from pandas import DataFrame

from freqtrade.data.metrics import calculate_expectancy, calculate_max_drawdown
from freqtrade.optimize.hyperopt import IHyperOptLoss


# smaller numbers penalize drawdowns more severely
DRAWDOWN_MULT = 0.055
# A very large number to use as a replacement for infinity
LARGE_NUMBER = 1e6
# Target trade amount, if higher that TARGET_TRADE_AMOUNT - no penalty
TARGET_TRADE_AMOUNT = 50
# Coefficient to adjust impact of expectancy
EXPECTANCY_CONST = 2.0
# Coefficient to adjust profit factor impact
PF_CONST = 1.0
# Coefficient to adjust winrate impact
WINRATE_CONST = 1.2


class MultiMetricHyperOptLoss(IHyperOptLoss):
    @staticmethod
    def hyperopt_loss_function(
        results: DataFrame,
        trade_count: int,
        starting_balance: float,
        **kwargs,
    ) -> float:
        total_profit = results["profit_abs"].sum()

        # Calculate profit factor
        winning_profit = results.loc[results["profit_abs"] > 0, "profit_abs"].sum()
        losing_profit = results.loc[results["profit_abs"] < 0, "profit_abs"].sum()
        profit_factor = winning_profit / (abs(losing_profit) + 1e-6)
        log_profit_factor = np.log(profit_factor + PF_CONST)

        # Calculate expectancy
        _, expectancy_ratio = calculate_expectancy(results)
        log_expectancy_ratio = np.log(min(10, expectancy_ratio) + EXPECTANCY_CONST)

        # Calculate winrate
        winning_trades = results.loc[results["profit_abs"] > 0]
        winrate = len(winning_trades) / len(results)
        log_winrate_coef = np.log(WINRATE_CONST + winrate)

        # Calculate drawdown
        try:
            drawdown = calculate_max_drawdown(
                results, starting_balance=starting_balance, value_col="profit_abs"
            )
            relative_account_drawdown = drawdown.relative_account_drawdown
        except ValueError:
            relative_account_drawdown = 0

        # Trade Count Penalty
        trade_count_penalty = 1.0  # Default: no penalty
        if trade_count < TARGET_TRADE_AMOUNT:
            trade_count_penalty = 1 - (abs(trade_count - TARGET_TRADE_AMOUNT) / TARGET_TRADE_AMOUNT)
            trade_count_penalty = max(trade_count_penalty, 0.1)

        profit_draw_function = total_profit - (relative_account_drawdown * total_profit) * (
            1 - DRAWDOWN_MULT
        )

        return -1 * (
            profit_draw_function
            * log_profit_factor
            * log_expectancy_ratio
            * log_winrate_coef
            * trade_count_penalty
        )
