# source: https://raw.githubusercontent.com/Filimonov-Eremeyka/freqtrade/edf4ff8f6dc4d6617f318dd5ebaa0c3427654b1f/user_data/strategies/PolusFinalVisualization_touchstop_v2.py

# user_data/strategies/Github_Filimonov_Eremeyka_freqtrade__PolusFinalVisualization_touchstop_v2__20250905_154014.py
from freqtrade.strategy import IStrategy
from pandas import DataFrame
import pandas as pd
import numpy as np

# Try to import user's PolusLevels, but don't rely on its internals.
try:
    from polus_levels import PolusLevels as _ExternalPolusLevels
except Exception:
    _ExternalPolusLevels = None

class Github_Filimonov_Eremeyka_freqtrade__PolusFinalVisualization_touchstop_v2__20250905_154014(IStrategy):
    INTERFACE_VERSION = 3
    timeframe = '5m'
    minimal_roi = {'0': 10}
    stoploss = -0.99
    use_custom_stoploss = False
    process_only_new_candles = False
    startup_candle_count = 200

    # Neutral gray viz
    plot_config = {
        'main_plot': {
            'polus_high_signal': {'type': 'scatter', 'plotly': {'mode': 'markers', 'marker': {'symbol': 'x', 'size': 8, 'color': '#888888'}}},
            'polus_low_signal':  {'type': 'scatter', 'plotly': {'mode': 'markers', 'marker': {'symbol': 'x', 'size': 8, 'color': '#888888'}}},
            # Dynamically add 6 level lines for high/low
            **{f'level_high_{i}': {'type': 'scatter', 'plotly': {'mode': 'lines', 'line': {'color': '#888888', 'width': 1}}} for i in range(6)},
            **{f'level_low_{i}':  {'type': 'scatter', 'plotly': {'mode': 'lines', 'line': {'color': '#888888', 'width': 1}}} for i in range(6)},
        }
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # If external class exists, instantiate it. We'll still compute signals internally.
        self._pl = _ExternalPolusLevels(use_tick_volume=False) if _ExternalPolusLevels else None

    @staticmethod
    def _compute_polus_signals(df: DataFrame, use_tick_volume: bool = False):
        """Self-contained signal detection compatible with the user's logic.
        Returns 3 numpy arrays: high_levels, low_levels, close_ref (all length len(df))."""
        high = df['high'].to_numpy(copy=False) if 'high' in df.columns else df['close'].to_numpy(copy=False)
        low  = df['low'].to_numpy(copy=False)  if 'low' in df.columns  else df['close'].to_numpy(copy=False)
        close = df['close'].to_numpy(copy=False)
        volcol = 'tick_volume' if use_tick_volume and 'tick_volume' in df.columns else ('volume' if 'volume' in df.columns else None)
        vol = df[volcol].to_numpy(copy=False) if volcol else np.ones_like(close)

        hi_out = np.full(len(df), np.nan, dtype='float64')
        lo_out = np.full(len(df), np.nan, dtype='float64')
        cl_out = np.full(len(df), np.nan, dtype='float64')

        for i in range(1, len(df) - 1):
            if vol[i - 1] > vol[i] < vol[i + 1]:
                hi_out[i] = high[i]
                lo_out[i] = low[i]
                cl_out[i] = close[i - 1]
        return hi_out, lo_out, cl_out

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        df = dataframe.copy()

        # Use internal computation (robust). If external is present and has compute(), we could cross-check.
        hi, lo, cl = self._compute_polus_signals(df, use_tick_volume=False)
        df['polus_high_signal'] = pd.Series(hi, index=df.index)
        df['polus_low_signal']  = pd.Series(lo, index=df.index)
        df['polus_close_ref']   = pd.Series(cl, index=df.index)

        df = self._manage_levels(df, max_levels=6)
        return df

    def _manage_levels(self, dataframe: DataFrame, max_levels: int) -> DataFrame:
        # Pre-create columns
        for slot in range(max_levels):
            dataframe[f'level_high_{slot}'] = np.nan
            dataframe[f'level_low_{slot}'] = np.nan

        active_high = {}  # price -> slot
        active_low = {}

        eps = 1e-6
        for i in range(len(dataframe)):
            hi = float(dataframe.at[i, 'high']) if 'high' in dataframe.columns else float(dataframe.at[i, 'close'])
            lo = float(dataframe.at[i, 'low'])  if 'low'  in dataframe.columns else float(dataframe.at[i, 'close'])

            # draw-until-touch (exclusive): don't write value on the touching bar
            to_remove = []
            for price, slot in active_high.items():
                if hi >= price * (1 - eps):
                    to_remove.append(price)
                else:
                    dataframe.loc[i, f'level_high_{slot}'] = price
            for price in to_remove:
                active_high.pop(price, None)

            to_remove = []
            for price, slot in active_low.items():
                if lo <= price * (1 + eps):
                    to_remove.append(price)
                else:
                    dataframe.loc[i, f'level_low_{slot}'] = price
            for price in to_remove:
                active_low.pop(price, None)

            # add new levels from signals
            new_high = dataframe.at[i, 'polus_high_signal']
            if pd.notna(new_high) and float(new_high) not in active_high:
                used = set(active_high.values())
                for slot in range(max_levels):
                    if slot not in used:
                        active_high[float(new_high)] = slot
                        break

            new_low = dataframe.at[i, 'polus_low_signal']
            if pd.notna(new_low) and float(new_low) not in active_low:
                used = set(active_low.values())
                for slot in range(max_levels):
                    if slot not in used:
                        active_low[float(new_low)] = slot
                        break

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe['enter_long'] = 0
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe['exit_long'] = 0
        return dataframe
