# source: https://raw.githubusercontent.com/jaredrsommer/freqtradestrategies/63cd46ea087ed8ed01f7d54d8cfcf1dbed166b04/Astro.py
import logging
from functools import reduce
import datetime
import ephem
import talib.abstract as ta
import pandas_ta as pta
import numpy as np
import pandas as pd
import freqtrade.vendor.qtpylib.indicators as qtpylib
from datetime import timedelta, datetime, timezone
from pandas import DataFrame, Series
from freqtrade.persistence import Trade
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter, 
                                IStrategy, IntParameter, RealParameter)
from typing import Optional

logger = logging.getLogger(__name__)

class Github_jaredrsommer_freqtradestrategies__Astro__20260509_225308(IStrategy):
    exit_profit_only = True
    use_custom_stoploss = True
    trailing_stop = True
    position_adjustment_enable = True
    ignore_roi_if_entry_signal = True
    max_entry_position_adjustment = 2
    max_dca_multiplier = 2.5
    process_only_new_candles = True
    can_short = False
    use_exit_signal = True
    startup_candle_count: int = 50
    stoploss = -0.99
    timeframe = '1d'

    minimal_roi = {
        "48000": 0.01,
        "24000": 0.025,
        "12000": 0.05,
        "4800": 0.10,
        "300": 0.15,
        "180": 0.30,
        "120": 0.40,
        "60": 0.45,
        "0": 0.50
    }

    # Protections
    cooldown_lookback = IntParameter(24, 48, default=12, space="protection", optimize=True)
    stop_duration = IntParameter(12, 200, default=5, space="protection", optimize=True)
    use_stop_protection = BooleanParameter(default=True, space="protection", optimize=True)

    # Trailing stop loss optimization
    tsl_enable = IntParameter(low=70, high=90, default=80, space='sell', optimize=True, load=True)
    tsl_target5 = DecimalParameter(low=0.3, high=0.4, default=0.3, decimals=2, space='sell', optimize=True, load=True)
    ts5 = DecimalParameter(low=0.04, high=0.06, default=0.05, decimals=2, space='sell', optimize=True, load=True)
    tsl_target4 = DecimalParameter(low=0.18, high=0.3, default=0.2, decimals=2, space='sell', optimize=True, load=True)
    ts4 = DecimalParameter(low=0.03, high=0.05, default=0.045, decimals=3, space='sell', optimize=True, load=True)
    tsl_target3 = DecimalParameter(low=0.12, high=0.18, default=0.15, decimals=2, space='sell', optimize=True, load=True)
    ts3 = DecimalParameter(low=0.025, high=0.04, default=0.035, decimals=3, space='sell', optimize=True, load=True)
    tsl_target2 = DecimalParameter(low=0.07, high=0.12, default=0.1, decimals=2, space='sell', optimize=True, load=True)
    ts2 = DecimalParameter(low=0.015, high=0.03, default=0.02, decimals=2, space='sell', optimize=True, load=True)
    tsl_target1 = DecimalParameter(low=0.04, high=0.08, default=0.075, decimals=3, space='sell', optimize=True, load=True)
    ts1 = DecimalParameter(low=0.01, high=0.016, default=0.013, decimals=3, space='sell', optimize=True, load=True)
    tsl_target0 = DecimalParameter(low=0.02, high=0.05, default=0.05, decimals=3, space='sell', optimize=True, load=True)
    ts0 = DecimalParameter(low=0.008, high=0.015, default=0.01, decimals=3, space='sell', optimize=True, load=True)

    @property
    def protections(self):
        prot = []
        prot.append({
            "method": "CooldownPeriod",
            "stop_duration_candles": self.cooldown_lookback.value
        })
        if self.use_stop_protection.value:
            prot.append({
                "method": "StoplossGuard",
                "lookback_period_candles": 24 * 3,
                "trade_limit": 2,
                "stop_duration_candles": self.stop_duration.value,
                "only_per_pair": False
            })
        return prot

    def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
                            proposed_stake: float, min_stake: Optional[float], max_stake: float,
                            leverage: float, entry_tag: Optional[str], side: str, **kwargs) -> float:
        return proposed_stake / self.max_dca_multiplier

    def adjust_trade_position(self, trade: Trade, current_time: datetime,
                              current_rate: float, current_profit: float,
                              min_stake: Optional[float], max_stake: float,
                              current_entry_rate: float, current_exit_rate: float,
                              current_entry_profit: float, current_exit_profit: float, **kwargs) -> Optional[float]:
        dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
        filled_entries = trade.select_filled_orders(trade.entry_side)
        count_of_entries = trade.nr_of_successful_entries

        if current_profit > 0.10 and trade.nr_of_successful_exits == 0:
            return -(trade.stake_amount / 2)

        if current_profit > -0.015 and trade.nr_of_successful_entries == 1:
            return None
        if current_profit > -0.065 and trade.nr_of_successful_entries == 2:
            return None
        if current_profit > -0.105 and trade.nr_of_successful_entries == 3:
            return None
        if current_profit > -0.13 and trade.nr_of_successful_entries == 4:
            return None
        if current_profit > -0.18 and trade.nr_of_successful_entries == 5:
            return None

        try:
            stake_amount = filled_entries[0].cost
            if count_of_entries == 1:
                stake_amount = stake_amount * 1
            elif count_of_entries == 2:
                stake_amount = stake_amount * 1
            elif count_of_entries == 3:
                stake_amount = stake_amount * 1
            elif count_of_entries == 4:
                stake_amount = stake_amount * 1.5
            elif count_of_entries == 5:
                stake_amount = stake_amount * 1.625
            else:
                stake_amount = stake_amount
            return stake_amount
        except Exception:
            return None

    def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
                        current_rate: float, current_profit: float, **kwargs) -> float:
        dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe)
        current_candle = dataframe.iloc[-1].squeeze()

        # Tighten stoploss during challenging aspects
        challenging_aspects = [col for col in dataframe.columns if any(
            aspect in col for aspect in ['square', 'opposition', 'quincunx']
        )]
        if any(current_candle[col] == 1 for col in challenging_aspects) and current_profit > 0:
            self.dp.send_msg(f'*** {pair} *** Challenging aspect detected - Tightening stoploss')
            return 0.02  # Tighten to 2%

        if current_candle['rsi'] < self.tsl_enable.value:
            for stop5 in self.tsl_target5.range:
                if current_profit > stop5:
                    for stop5a in self.ts5.range:
                        self.dp.send_msg(f'*** {pair} *** Profit: {current_profit} - lvl5 {stop5}/{stop5a} activated')
                        return stop5a
            for stop4 in self.tsl_target4.range:
                if current_profit > stop4:
                    for stop4a in self.ts4.range:
                        self.dp.send_msg(f'*** {pair} *** Profit {current_profit} - lvl4 {stop4}/{stop4a} activated')
                        return stop4a
            for stop3 in self.tsl_target3.range:
                if current_profit > stop3:
                    for stop3a in self.ts3.range:
                        self.dp.send_msg(f'*** {pair} *** Profit {current_profit} - lvl3 {stop3}/{stop3a} activated')
                        return stop3a
            for stop2 in self.tsl_target2.range:
                if current_profit > stop2:
                    for stop2a in self.ts2.range:
                        self.dp.send_msg(f'*** {pair} *** Profit {current_profit} - lvl2 {stop2}/{stop2a} activated')
                        return stop2a
            for stop1 in self.tsl_target1.range:
                if current_profit > stop1:
                    for stop1a in self.ts1.range:
                        self.dp.send_msg(f'*** {pair} *** Profit {current_profit} - lvl1 {stop1}/{stop1a} activated')
                        return stop1a
            for stop0 in self.tsl_target0.range:
                if current_profit > stop0:
                    for stop0a in self.ts0.range:
                        self.dp.send_msg(f'*** {pair} *** Profit {current_profit} - lvl0 {stop0}/{stop0a} activated')
                        return stop0a
        else:
            for stop0 in self.tsl_target0.range:
                if current_profit > stop0:
                    self.dp.send_msg(f'*** {pair} *** Profit {current_profit} SWINGING FOR THE MOON!!!')
                    return 0.99
        return self.stoploss

    def compute_planet_position(self, planet_name, date):
        observer = ephem.Observer()
        observer.lat = '33.4484'  # Phoenix, Arizona
        observer.lon = '-112.0740'
        planet = getattr(ephem, planet_name)(observer)
        planet.compute(date)
        return planet.ra, planet.dec

    def calculate_angular_separation(self, ra1, ra2):
        """Calculate angular separation between two right ascensions."""
        separation = abs(ra1 - ra2)
        if separation > np.pi:
            separation = 2 * np.pi - separation
        return separation * 180 / np.pi  # Convert to degrees

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # Core Technical Indicators
        dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
        dataframe['ema_8'] = ta.EMA(dataframe, timeperiod=8)
        dataframe['ema_34'] = ta.EMA(dataframe, timeperiod=34)

        # VWAP
        vwap_low, vwap, vwap_high = VWAPB(dataframe, 20, 1)
        dataframe['%-vwap_middleband'] = vwap
        dataframe['%-dist_to_vwap'] = get_distance(dataframe['close'], dataframe['%-vwap_middleband'])

        # TTM Squeeze
        ttm_Squeeze = pta.squeeze(high=dataframe['high'], low=dataframe['low'], close=dataframe["close"], lazybear=True)
        dataframe['%-ttm_Squeeze'] = ttm_Squeeze['SQZ_20_2.0_20_1.5_LB']
        dataframe['%-squeeze_ON'] = ttm_Squeeze['SQZ_ON']

        # Github_jaredrsommer_freqtradestrategies__Astro__20260509_225308logical Features
        dataframe['e_date'] = dataframe['date'].apply(lambda x: ephem.Date(x.strftime("%Y/%m/%d %H:%M:%S")))
        moon_phase = dataframe['e_date'].apply(lambda x: ephem.Moon(x).phase)
        dataframe['%-Moon_Phase'] = moon_phase

        planets = [
            'Sun', 'Moon', 'Mercury', 'Venus', 'Mars', 
            'Jupiter', 'Saturn', 'Uranus', 'Neptune', 'Pluto'
        ]

        # Compute planet positions
        for planet_name in planets:
            dataframe[f'{planet_name}_ra'], dataframe[f'{planet_name}_dec'] = zip(*dataframe['e_date'].apply(
                lambda x: self.compute_planet_position(planet_name, x)
            ))
            dataframe[planet_name] = dataframe['e_date'].apply(
                lambda x: ephem.constellation(getattr(ephem, planet_name)(x))[1]
            )
            dataframe[f'%-{planet_name}_rax'] = dataframe[f'{planet_name}_ra'] * 180 / np.pi
            dataframe[f'%-{planet_name}_dec'] = (dataframe[f'{planet_name}_dec'] + 0.5) * 100

        # Define aspects with angles and tolerances
        aspects = {
            'conjunction': (0, 8),
            'semi_sextile': (30, 4),
            'semi_square': (45, 4),
            'sextile': (60, 6),
            'square': (90, 8),
            'trine': (120, 8),
            'sesquiquadrate': (135, 4),
            'quincunx': (150, 5),
            'opposition': (180, 8)
        }

        # Calculate aspects for all planet pairs
        for i, planet1 in enumerate(planets):
            for planet2 in planets[i+1:]:
                angle_col = f'{planet1.lower()}_{planet2.lower()}_angle'
                dataframe[angle_col] = dataframe.apply(
                    lambda row: self.calculate_angular_separation(
                        row[f'%-{planet1}_rax'], row[f'%-{planet2}_rax']
                    ), axis=1
                )
                for aspect, (angle, orb) in aspects.items():
                    dataframe[f'{planet1.lower()}_{planet2.lower()}_{aspect}'] = np.where(
                        (dataframe[angle_col] >= angle - orb) & 
                        (dataframe[angle_col] <= angle + orb), 1, 0
                    )

        # Balsamic Moon
        dataframe['balsamic_moon'] = np.where(
            (dataframe['%-Moon_Phase'] >= 315) & (dataframe['%-Moon_Phase'] < 360), 1, 0
        )

        dataframe['aspects'] = [col for col in dataframe.columns if any(
            aspect in col for aspect in ['trine', 'sextile', 'conjunction']
        )]


        return dataframe

    def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
        # Harmonious aspects
        harmonious_aspects = [col for col in df.columns if any(
            aspect in col for aspect in ['trine', 'sextile', 'conjunction']
        )]

        # Entry: RSI oversold, close below VWAP, harmonious aspect
        df.loc[
            (
                (df['rsi'] < 40) &
                (df['close'] < df['%-vwap_middleband']) &
                (df['ema_8'] > df['ema_34']) &
                (df[harmonious_aspects].sum(axis=1) > 0) &
                (df['balsamic_moon'] == 0) &
                (df['volume'] > 0)
            ),
            ['enter_long', 'enter_tag']] = (1, 'Oversold with Harmonious Aspect')

        return df

    def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
        # Challenging aspects
        challenging_aspects = [col for col in df.columns if any(
            aspect in col for aspect in ['square', 'opposition', 'quincunx', 'semi_square', 'sesquiquadrate']
        )]

        # Exit: RSI overbought or challenging aspect
        df.loc[
            (
                ((df['rsi'] > 70) | (df['%-squeeze_ON'] == 1)) &
                (df[challenging_aspects].sum(axis=1) > 0) &
                (df['volume'] > 0)
            ),
            ['exit_long', 'exit_tag']] = (1, 'Overbought or Challenging Aspect')

        return df

# Helper Functions
def VWAPB(dataframe, window_size=20, num_of_std=1):
    df = dataframe.copy()
    df['vwap'] = qtpylib.rolling_vwap(df, window=window_size)
    rolling_std = df['vwap'].rolling(window=window_size).std()
    df['vwap_low'] = df['vwap'] - (rolling_std * num_of_std)
    df['vwap_high'] = df['vwap'] + (rolling_std * num_of_std)
    return df['vwap_low'], df['vwap'], df['vwap_high']

def get_distance(p1, p2):
    return abs((p1) - (p2))

def celestial_house(planet_name):
    house = 0
    if planet_name == "Aries":
        house = 1
    elif planet_name == "Taurus":
        house = 2
    elif planet_name == "Gemini":
        house = 3
    elif planet_name == "Cancer":
        house = 4
    elif planet_name == "Leo":
        house = 5
    elif planet_name == "Virgo":
        house = 6
    elif planet_name == "Libra":
        house = 7
    elif planet_name == "Scorpio":
        house = 8
    elif planet_name == "Sagittarius":
        house = 9
    elif planet_name == "Capricorn":
        house = 10
    elif planet_name == "Aquarius":
        house = 11
    elif planet_name == "Pisces":
        house = 12
    return house