# source: https://raw.githubusercontent.com/jaredrsommer/freqtradestrategies/9d66f71ce21f2f65feaad6bb967215be22a3cfd6/MSO.py
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, 
                               DecimalParameter, IStrategy, IntParameter, 
                               RealParameter, merge_informative_pair, informative)
import pandas as pd
import numpy as np
import talib.abstract as ta
from pandas import DataFrame
import logging

class Github_jaredrsommer_freqtradestrategies__MSO__20260518_195114(IStrategy):
    # Strategy interface version
    INTERFACE_VERSION = 3

    # Minimal ROI
    minimal_roi = {
        "0": 0.05,
        "30": 0.03,
        "60": 0.02,
        "120": 0
    }

    # Stop loss
    stoploss = -0.1

    # Trailing stop
    trailing_stop = False

    # Timeframe
    timeframe = '1h'

    # Hyperopt parameters
    ms_weight_k1 = DecimalParameter(0.0, 5.0, default=1.0, space='buy')
    ms_weight_k2 = DecimalParameter(0.0, 5.0, default=3.0, space='buy')
    ms_weight_k3 = DecimalParameter(0.0, 5.0, default=5.0, space='buy')
    norm_smooth = IntParameter(1, 10, default=3, space='buy')
    cyc_smooth = IntParameter(1, 14, default=9, space='buy')
    buy_threshold = DecimalParameter(50.0, 85.0, default=60.0, space='buy')
    sell_threshold = DecimalParameter(15.0, 50.0, default=40.0, space='sell')

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # Initialize swing structures
        st_high = {'last_price': 0, 'mid_price': 0, 'prev_price': 0, 'is_crossed': False}
        st_low = {'last_price': 0, 'mid_price': 0, 'prev_price': 0, 'is_crossed': False}
        it_high = {'last_price': 0, 'mid_price': 0, 'prev_price': 0, 'is_crossed': False}
        it_low = {'last_price': 0, 'mid_price': 0, 'prev_price': 0, 'is_crossed': False}
        lt_high = {'last_price': 0, 'mid_price': 0, 'prev_price': 0, 'is_crossed': False}
        lt_low = {'last_price': 0, 'mid_price': 0, 'prev_price': 0, 'is_crossed': False}

        # Short-term market structure
        def short_market_structure(df, high, low, close):
            bull = np.zeros(len(df))
            bear = np.zeros(len(df))
            osc = np.zeros(len(df))
            max_vals = np.zeros(len(df))
            min_vals = np.zeros(len(df))

            for i in range(2, len(df)):
                # High swing detection
                if high[i-2] < high[i-1] and high[i-1] >= high[i]:
                    st_high['prev_price'] = st_high['mid_price']
                    st_high['mid_price'] = st_high['last_price']
                    st_high['last_price'] = high[i-1]
                    st_high['is_crossed'] = False

                if close[i] > st_high['last_price'] and not st_high['is_crossed']:
                    st_high['is_crossed'] = True
                    bull[i] = 1

                # Low swing detection
                if low[i-2] > low[i-1] and low[i-1] <= low[i]:
                    st_low['prev_price'] = st_low['mid_price']
                    st_low['mid_price'] = st_low['last_price']
                    st_low['last_price'] = low[i-1]
                    st_low['is_crossed'] = False

                if close[i] < st_low['last_price'] and not st_low['is_crossed']:
                    st_low['is_crossed'] = True
                    bear[i] = 1

                # Normalize oscillator
                os = 1 if bull[i] else -1 if bear[i] else osc[i-1] if i > 0 else 0
                max_vals[i] = close[i] if os > (osc[i-1] if i > 0 else 0) else max(close[i], max_vals[i-1] if i > 0 else close[i])
                min_vals[i] = close[i] if os < (osc[i-1] if i > 0 else 0) else min(close[i], min_vals[i-1] if i > 0 else close[i])
                
                if max_vals[i] != min_vals[i] and i >= self.norm_smooth.value:
                    series = np.array([(close[j] - min_vals[j]) / (max_vals[j] - min_vals[j]) * 100 for j in range(i - self.norm_smooth.value + 1, i + 1)])
                    sma_result = ta.SMA(series, timeperiod=self.norm_smooth.value)
                    osc[i] = sma_result[-1] if sma_result is not None and not np.isnan(sma_result[-1]) else osc[i-1] if i > 0 else 50.0
                else:
                    osc[i] = osc[i-1] if i > 0 else 50.0

            return osc

        # General market structure
        def market_structure(df, h_swing_high, h_swing_low, l_swing_high, l_swing_low, close):
            bull = np.zeros(len(df))
            bear = np.zeros(len(df))
            osc = np.zeros(len(df))
            max_vals = np.zeros(len(df))
            min_vals = np.zeros(len(df))

            for i in range(2, len(df)):
                # High swing detection
                c_swing_high = l_swing_high['prev_price'] < l_swing_high['mid_price'] and l_swing_high['mid_price'] >= l_swing_high['last_price']
                if c_swing_high:
                    h_swing_high['prev_price'] = h_swing_high['mid_price']
                    h_swing_high['mid_price'] = h_swing_high['last_price']
                    h_swing_high['last_price'] = l_swing_high['mid_price']
                    h_swing_high['is_crossed'] = False

                if close[i] > h_swing_high['last_price'] and not h_swing_high['is_crossed']:
                    h_swing_high['is_crossed'] = True
                    bull[i] = 1

                # Low swing detection
                c_swing_low = l_swing_low['prev_price'] > l_swing_low['mid_price'] and l_swing_low['mid_price'] <= l_swing_low['last_price']
                if c_swing_low:
                    h_swing_low['prev_price'] = h_swing_low['mid_price']
                    h_swing_low['mid_price'] = h_swing_low['last_price']
                    h_swing_low['last_price'] = l_swing_low['mid_price']
                    h_swing_low['is_crossed'] = False

                if close[i] < h_swing_low['last_price'] and not h_swing_low['is_crossed']:
                    h_swing_low['is_crossed'] = True
                    bear[i] = 1

                # Normalize oscillator
                os = 1 if bull[i] else -1 if bear[i] else osc[i-1] if i > 0 else 0
                max_vals[i] = close[i] if os > (osc[i-1] if i > 0 else 0) else max(close[i], max_vals[i-1] if i > 0 else close[i])
                min_vals[i] = close[i] if os < (osc[i-1] if i > 0 else 0) else min(close[i], min_vals[i-1] if i > 0 else close[i])
                
                if max_vals[i] != min_vals[i] and i >= self.norm_smooth.value:
                    series = np.array([(close[j] - min_vals[j]) / (max_vals[j] - min_vals[j]) * 100 for j in range(i - self.norm_smooth.value + 1, i + 1)])
                    sma_result = ta.SMA(series, timeperiod=self.norm_smooth.value)
                    osc[i] = sma_result[-1] if sma_result is not None and not np.isnan(sma_result[-1]) else osc[i-1] if i > 0 else 50.0
                else:
                    osc[i] = osc[i-1] if i > 0 else 50.0

            return osc

        # Calculate oscillators
        dataframe['st_osc'] = short_market_structure(dataframe, dataframe['high'], dataframe['low'], dataframe['close'])
        dataframe['it_osc'] = market_structure(dataframe, it_high, it_low, st_high, st_low, dataframe['close'])
        dataframe['lt_osc'] = market_structure(dataframe, lt_high, lt_low, it_high, it_low, dataframe['close'])

        # Market Structure Oscillator
        weights_sum = self.ms_weight_k1.value + self.ms_weight_k2.value + self.ms_weight_k3.value
        weights_count = (1 if not np.isnan(dataframe['st_osc']).any() else 0) + \
                        (1 if not np.isnan(dataframe['it_osc']).any() else 0) + \
                        (1 if not np.isnan(dataframe['lt_osc']).any() else 0)
        dataframe['ms_osc'] = (self.ms_weight_k1.value * dataframe['st_osc'].fillna(0) +
                              self.ms_weight_k2.value * dataframe['it_osc'].fillna(0) +
                              self.ms_weight_k3.value * dataframe['lt_osc'].fillna(0)) / (weights_sum if weights_sum > 0 else 1)

        dataframe['ms_osc_smo'] = dataframe['ms_osc'].rolling(self.cyc_smooth.value).mean()
        # Cycle Oscillator
        dataframe['cycle_osc'] = (dataframe['ms_osc'] - dataframe['ms_osc_smo']) + 50

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[
            (
                (dataframe['ms_osc'] > self.buy_threshold.value) &
                (dataframe['cycle_osc'] > 50)
            ),
            'enter_long'] = 1
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[
            (
                (dataframe['ms_osc'] < self.sell_threshold.value) &
                (dataframe['cycle_osc'] < 50)
            ),
            'exit_long'] = 1
        return dataframe