# source: https://raw.githubusercontent.com/haydarsahin0/tradedair/41cf415d8bac3f69db9fa1c0f998ce364d4e2fae/user_data/strategies/CandleExpansion.py
# directory_url: https://github.com/haydarsahin0/tradedair/blob/claude/vibe-trading-bot-strategy-ex4d14/user_data/strategies/
# User: haydarsahin0
# Repository: tradedair
# --------------------# pragma pylint: disable=missing-docstring, invalid-name, too-many-locals
"""
Github_haydarsahin0_tradedair__CandleExpansion__20260803_123908
===============

4 saatlik mum genişlemesi (momentum devamı) stratejisi.

KURAL
-----
1. Bir önceki 4 saatlik mum kapanır. Yönü ve gövde boyu ölçülür.
       onceki_hareket = (onceki_kapanis - onceki_acilis) / onceki_acilis

2. Yeni 4 saatlik mum açılır. Yeni mum, önceki mumla AYNI yönde ve onun
   gövdesinin 1.4 KATI kadar hareket ederse, o anda o yönde işleme girilir.

       tetik_fiyat = yeni_acilis * (1 + 1.4 * onceki_hareket)

   Örnek: önceki 4s mum %1 düştü. Yeni mum kendi açılışından %1.4
   düştüğü anda SHORT açılır. Önceki mum %1 yükseldiyse ve yeni mum
   %1.4 yükselirse LONG açılır.

3. STOP: önceki mumun AÇILIŞ fiyatının %0.5 ötesine konur.
       SHORT -> onceki_acilis * 1.005
       LONG  -> onceki_acilis * 0.995

4. KAR AL: %9  (fiyat hareketi; 8x kaldiracla hesapta ~%72)

5. Kaldıraç: 8x

Her 4 saatlik mumda en fazla BİR giriş yapılır.

NEDEN 15 DAKİKALIK GRAFİK ÜZERİNDE ÇALIŞIR
-------------------------------------------
Analiz 4 saatliktir ama giriş mum İÇİNDE, tetik fiyatına değildiği anda
olmalıdır. Freqtrade 4 saatlik zaman diliminde çalışsaydı yalnızca mum
kapanışlarında karar verir ve girişleri kaçırırdı.

Bu yüzden yürütme 15 dakikalıktır: 4 saatlik mumlar 15 dakikalık veriden
türetilir, tetik her 15 dakikada bir kontrol edilir. Geleceğe bakma yoktur —
her barda yalnızca TAMAMLANMIŞ önceki 4 saatlik mum ve İÇİNDE bulunulan
mumun açılışı kullanılır.
"""

import logging
from datetime import datetime

import numpy as np
import pandas as pd
from pandas import DataFrame

from freqtrade.persistence import Trade
from freqtrade.strategy import (
    BooleanParameter,
    DecimalParameter,
    IntParameter,
    IStrategy,
    stoploss_from_absolute,
)

logger = logging.getLogger(__name__)


class Github_haydarsahin0_tradedair__CandleExpansion__20260803_123908(IStrategy):

    INTERFACE_VERSION = 3

    can_short: bool = True

    # Yürütme zaman dilimi (analiz 4 saatlik — yukarıdaki açıklamaya bak)
    timeframe = "15m"
    htf = "4h"

    # Çıkışlar custom_exit (kâr hedefi) ve custom_stoploss (yapısal stop) ile.
    # minimal_roi devre dışı — kâr hedefini biz yönetiyoruz.
    minimal_roi = {"0": 10.0}

    # Son çare emniyet ağı. Gerçek stop custom_stoploss'tan gelir ve
    # her zaman bundan çok daha yakındır.
    stoploss = -0.85

    use_custom_stoploss = True
    use_exit_signal = True
    exit_profit_only = False
    process_only_new_candles = True

    # 4 saatlik mumları türetmek için birkaç günlük 15dk verisi yeter
    startup_candle_count: int = 200

    # Stop emirleri borsada durur — sunucu çökse bile koruma devam eder
    order_types = {
        "entry": "limit",
        "exit": "limit",
        "stoploss": "market",
        "stoploss_on_exchange": True,
        "stoploss_on_exchange_interval": 60,
        "stoploss_on_exchange_market_ratio": 0.99,
    }

    order_time_in_force = {"entry": "GTC", "exit": "GTC"}

    # ---------------------- Parametreler ---------------------- #

    # Tetik: önceki mum gövdesinin kaç katı
    expansion_mult = DecimalParameter(1.1, 2.5, default=1.4, decimals=2, space="buy")

    # Stop: önceki mumun açılışından ne kadar öte
    stop_beyond_open_pct = DecimalParameter(0.1, 2.0, default=0.5, decimals=2, space="sell")

    # Kâr hedefi — FİYAT hareketi olarak (8x kaldıraçla hesapta 8 katı eder).
    #
    # OLCULEN:
    #   %8 -> kazanma orani %23.3 | basabas gereken %21.3 | beklenen +1.71  KARLI
    #   %4 -> kazanma orani %32.0 | basabas gereken %35.1 | beklenen -1.58  ZARARLI
    #
    # Hedefi yariya indirmek kazanma oranini %23.3'ten sadece %32'ye tasidi;
    # gereken %35.1 idi. Rastgele yuruyus modeli stop/(stop+hedef) ile
    # kiyaslandiginda TP4 modelin ALTINDA, TP8 modelin USTUNDE kaliyor.
    #
    # Anlami: kazananlar hedefe zar zor degip donmuyor, cok daha uzaga
    # gidiyor. Bu stratejinin kenari KALIN SAG KUYRUKTA. Kucuk hedef o
    # kuyrugu kesip kenari yok ediyor — bu yuzden hedefi buyuk tutmak
    # dogru yon.
    take_profit_pct = DecimalParameter(2.0, 20.0, default=9.0, decimals=1, space="sell")

    # Kâr hedefi FİYAT hareketi mi, yoksa kaldıraçlı hesap kârı mı?
    #   False -> %8 FİYAT hareketi  (8x kaldıraçla hesapta ~%64)
    #   True  -> %8 HESAP kârı      (8x kaldıraçla ~%1 fiyat hareketi)
    tp_on_leveraged = BooleanParameter(default=False, space="sell", optimize=False)

    # Çok küçük (doji) önceki mumları ele — tetik gürültü seviyesinde kalmasın
    min_prev_body_pct = DecimalParameter(0.0, 1.0, default=0.15, decimals=2, space="buy")

    # GÜVENLİK SINIRI — stop mesafesi bundan uzaksa işleme girilmez.
    # Stop, önceki mumun açılışına sabit olduğu için önceki mum ne kadar
    # büyükse stop o kadar uzaklaşır. 8x kaldıraçta likidasyon ~%12.5
    # fiyat hareketindedir; bu sınır stop'un likidasyonun ötesine
    # geçmesini engeller.
    max_stop_pct = DecimalParameter(2.0, 12.0, default=10.0, decimals=1, space="buy")

    # --- TAKIP EDEN STOP ---
    # Fiyat lehimize gittikce stop da pesinden gelir.
    #
    # Esikler "R" cinsinden: 1R = giristen yapisal stop'a olan mesafe.
    # Olculen: 1R ~ %2.3 fiyat, kar hedefi %9 = ~4R, kazananlar ort. 4.1R.
    #
    # DIKKAT: bu stratejinin kenari KALIN SAG KUYRUKTA. Kar hedefini
    # %8'den %4'e indirdigimizde strateji ZARARA gecmisti. Cok siki bir
    # takip eden stop ayni hatayi yapar — kazananlari erken keser.
    # Bu yuzden varsayilanlar bilerek GEVSEK secildi.
    # OLCULEN (4 cift, TP %9):
    #   takipsiz : 621 islem | kazanma %21.6 | +%216.08 | dusus %54.0 | Sharpe 1.14
    #   takipli  : 815 islem | kazanma %26.4 | +%138.04 | dusus %46.0 | Sharpe 0.73
    #
    # Takip kazanma oranini yukseltti ve dususu azaltti AMA getiriyi %36
    # dusurdu; getiri/dusus orani 4.00 -> 3.00. Yine ayni ders: kenar
    # kalin sag kuyrukta, takip o kuyrugu kesiyor. Bu yuzden VARSAYILAN
    # KAPALI.
    #
    # use_breakeven tek basina acilabilir: stop girise cekilir ama
    # kazananin ustune sinir konmaz. Kuyrugu kesmeden dususu azaltmayi
    # hedefleyen ara yol.
    use_breakeven = BooleanParameter(default=False, space="sell", optimize=True)
    use_trailing = BooleanParameter(default=False, space="sell", optimize=True)
    be_trigger_r = DecimalParameter(0.5, 3.0, default=2.0, decimals=1, space="sell")
    trail_trigger_r = DecimalParameter(1.0, 4.0, default=3.0, decimals=1, space="sell")
    trail_dist_r = DecimalParameter(0.5, 2.5, default=1.5, decimals=1, space="sell")

    leverage_num = IntParameter(1, 20, default=8, space="buy", optimize=False)

    # Borsa istenen kaldiraci veremiyorsa isleme GIRME.
    # False yaparsan dusuk kaldiracla acar (pozisyon kucuk olur).
    require_full_leverage = BooleanParameter(default=True, space="buy", optimize=False)

    # leverage() ile confirm_trade_entry arasinda bilgi tasir
    _lev_seen: dict = {}

    # --- Yon acma/kapama ---
    # Ilk gercek backtest'te karin %92.7'si SHORT tarafindan geldi ve o donemde
    # piyasa %40.73 dustu. Long tarafi 360 islemde islem basina sadece
    # +0.29 USDT uretti (short: +3.87). Bu, edge'in gercek mi yoksa ayi
    # piyasasina denk gelmis olmaktan mi ibaret oldugunu test etmek icin.
    allow_long = BooleanParameter(default=True, space="buy", optimize=True)
    allow_short = BooleanParameter(default=True, space="buy", optimize=True)

    # --- Trend filtresi (varsayilan KAPALI) ---
    # Acikken: fiyat EMA ustundeyse sadece long, altindaysa sadece short.
    # Yonu piyasa rejimine baglar; sabit yon tercihinden daha durustur.
    use_trend_filter = BooleanParameter(default=False, space="buy", optimize=True)
    trend_ema = IntParameter(50, 400, default=200, space="buy", optimize=True)

    # ---------------------------------------------------------------- #

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

        # --- 4 saatlik mumları 15 dakikalık veriden türet --- #
        # floor("4h") UTC'de 00:00/04:00/08:00... sınırlarına oturur,
        # yani borsanın gerçek 4 saatlik mumlarıyla aynı hizada.
        period = df["date"].dt.floor(self.htf)
        g = df.groupby(period, sort=False)

        # İÇİNDE bulunulan 4s mumunun açılışı — periyodun ilk barında bilinir
        cur_open = g["open"].transform("first")

        # ÖNCEKİ (tamamlanmış) 4s mumunun açılış ve kapanışı.
        # shift(1) ile bir önceki periyoda kayar; o periyot kapandığı için
        # bu bilgi geleceğe bakma değildir.
        per_open = g["open"].first()
        per_close = g["close"].last()
        prev_open = period.map(per_open.shift(1))
        prev_close = period.map(per_close.shift(1))

        df["cur_open"] = cur_open
        df["prev_open"] = prev_open
        df["prev_close"] = prev_close

        # Önceki mumun yönü ve gövde boyu (işaretli oran)
        prev_move = (prev_close - prev_open) / prev_open
        df["prev_move"] = prev_move

        # Tetik fiyatı: yeni mum açılışından, önceki gövdenin katı kadar öte
        mult = float(self.expansion_mult.value)
        df["trigger"] = cur_open * (1.0 + mult * prev_move)

        # Stop: önceki mumun açılışının %0.5 ötesi
        sb = float(self.stop_beyond_open_pct.value) / 100.0
        df["stop_short"] = prev_open * (1.0 + sb)
        df["stop_long"] = prev_open * (1.0 - sb)

        # Trend filtresi icin EMA (yalnizca use_trend_filter acikken kullanilir)
        df["trend_ema_val"] = df["close"].ewm(
            span=int(self.trend_ema.value), adjust=False
        ).mean()

        df["period"] = period
        return df

    # ---------------------------------------------------------------- #

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        df = dataframe

        min_body = float(self.min_prev_body_pct.value) / 100.0
        max_stop = float(self.max_stop_pct.value) / 100.0

        body_ok = df["prev_move"].abs() >= min_body
        valid = df["prev_open"].notna() & df["trigger"].notna() & (df["volume"] > 0)

        use_trend = bool(self.use_trend_filter.value)
        trend_short_ok = (df["close"] < df["trend_ema_val"]) if use_trend else True
        trend_long_ok = (df["close"] > df["trend_ema_val"]) if use_trend else True

        # --- SHORT: önceki mum düşüş, fiyat tetiğe indi --- #
        short_setup = (valid & body_ok & (df["prev_move"] < 0)
                       & bool(self.allow_short.value) & trend_short_ok)
        short_hit = short_setup & (df["low"] <= df["trigger"])
        # Stop girişin ÜSTÜNDE olmalı (yukarı boşlukta bozulabilir)
        short_hit &= df["stop_short"] > df["trigger"]
        # Stop mesafesi likidasyonu geçmesin
        short_hit &= (df["stop_short"] - df["trigger"]) / df["trigger"] <= max_stop

        # --- LONG: önceki mum yükseliş, fiyat tetiğe çıktı --- #
        long_setup = (valid & body_ok & (df["prev_move"] > 0)
                      & bool(self.allow_long.value) & trend_long_ok)
        long_hit = long_setup & (df["high"] >= df["trigger"])
        long_hit &= df["stop_long"] < df["trigger"]
        long_hit &= (df["trigger"] - df["stop_long"]) / df["trigger"] <= max_stop

        # --- Her 4 saatlik mumda yalnızca İLK tetiklenme --- #
        first_short = short_hit & (
            short_hit.groupby(df["period"]).cumsum().eq(1)
        )
        first_long = long_hit & (
            long_hit.groupby(df["period"]).cumsum().eq(1)
        )

        df.loc[first_long, ["enter_long", "enter_tag"]] = (1, "genisleme_long")
        df.loc[first_short, ["enter_short", "enter_tag"]] = (1, "genisleme_short")

        self._notify_signal(df, metadata)
        return df

    # ---------------------------------------------------------------- #

    def _notify_signal(self, df: DataFrame, metadata: dict) -> None:
        """
        Sinyal olustugu anda Telegram'a bildirim gonderir.

        Freqtrade varsayilan olarak yalnizca ISLEM acilinca/kapaninca haber
        verir. Bu, sinyalin kendisini de duyurur — slot dolu oldugu icin
        isleme donusmeyen sinyalleri de gorursun.

        Yalnizca canli/dry-run modda ve yalnizca SON mum icin calisir;
        backtest'te hic devreye girmez.
        """
        dp = getattr(self, "dp", None)
        if dp is None or getattr(dp, "runmode", None) is None:
            return
        if dp.runmode.value not in ("live", "dry_run"):
            return
        if df.empty:
            return

        last = df.iloc[-1]
        is_long = bool(last.get("enter_long") == 1)
        is_short = bool(last.get("enter_short") == 1)
        if not (is_long or is_short):
            return

        pair = metadata.get("pair", "?")
        trigger = float(last["trigger"])
        stop = float(last["stop_long"] if is_long else last["stop_short"])
        prev_move = float(last["prev_move"]) * 100
        tp = float(self.take_profit_pct.value)
        lev = float(self.leverage_num.value)

        target = trigger * (1 + tp / 100) if is_long else trigger * (1 - tp / 100)
        risk = abs(stop - trigger) / trigger * 100

        yon = "LONG" if is_long else "SHORT"
        msg = (
            f"SINYAL - {yon}\n"
            f"{pair}\n\n"
            f"Onceki 4s mum : %{prev_move:+.2f}\n"
            f"Giris (tetik) : {trigger:.6g}\n"
            f"Stop          : {stop:.6g}  (%{risk:.2f} uzakta)\n"
            f"Hedef         : {target:.6g}  (%{tp:.1f})\n"
            f"Risk/Odul     : {tp / risk:.2f}"
        )
        dp.send_msg(msg)

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # Çıkışlar custom_exit ve custom_stoploss ile yönetiliyor
        return dataframe

    # ---------------------------------------------------------------- #

    def confirm_trade_entry(
        self,
        pair: str,
        order_type: str,
        amount: float,
        rate: float,
        time_in_force: str,
        current_time: datetime,
        entry_tag: str | None,
        side: str,
        **kwargs,
    ) -> bool:
        """
        Emir gonderilmeden hemen once son kontrol ve Telegram bildirimi.

        1) KALDIRAC KONTROLU — borsa istenen kaldiraci veremiyorsa islemi
           engeller. Bybit bazi ciftlerde kaldirac kademesi vermiyor; o
           durumda pozisyon sessizce 1x acilir, yani hedeflenenin sekizde
           biri buyuklugunde. Bu, test ettigimiz strateji degildir.
        2) Giris ozetini Telegram'a atar (stop ve hedef dahil).
        """
        want = float(self.leverage_num.value)
        actual, max_lev = self._lev_seen.get(pair, (want, want))

        dp = getattr(self, "dp", None)
        live = (dp is not None and getattr(dp, "runmode", None) is not None
                and dp.runmode.value in ("live", "dry_run"))

        if actual < want - 1e-9 and self.require_full_leverage.value:
            msg = (
                f"KALDIRAC YETERSIZ - ISLEM IPTAL\n"
                f"{pair}\n\n"
                f"Istenen : {want:.0f}x\n"
                f"Borsanin verdigi: {max_lev:.2f}x\n\n"
                f"Bybit bu cift icin kaldirac kademesi vermiyor.\n"
                f"Islem acilsaydi hedeflenenin {want / max(actual, 0.01):.0f} kati "
                f"kucuk olurdu. Bu yuzden girilmedi."
            )
            logger.warning("%s: kaldirac %.2fx < %.0fx — giris iptal", pair, actual, want)
            if live:
                try:
                    dp.send_msg(msg, always_send=True)
                except Exception:
                    pass
            return False

        if not live:
            return True

        try:
            df, _ = dp.get_analyzed_dataframe(pair, self.timeframe)
            if df is None or df.empty:
                return True

            last = df.iloc[-1]
            is_short = side == "short"
            stop = float(last["stop_short"] if is_short else last["stop_long"])
            tp = float(self.take_profit_pct.value)

            target = rate * (1 - tp / 100) if is_short else rate * (1 + tp / 100)
            risk = abs(stop - rate) / rate * 100

            yon = "SHORT" if is_short else "LONG"
            msg = (
                f"ISLEM ACILIYOR - {yon}\n"
                f"{pair}\n\n"
                f"Giris    : {rate:.6g}\n"
                f"Miktar   : {amount:.6g}\n"
                f"Kaldirac : {actual:.0f}x\n"
                f"Nominal  : {amount * rate:.2f} USDT\n\n"
                f"STOP  : {stop:.6g}\n"
                f"        %{risk:.2f} uzakta = hesapta -%{risk * actual:.1f}\n"
                f"HEDEF : {target:.6g}\n"
                f"        %{tp:.1f} = hesapta +%{tp * actual:.0f}\n\n"
                f"Risk/Odul: {tp / risk:.2f}"
            )
            dp.send_msg(msg, always_send=True)
        except Exception as e:
            logger.warning("Giris bildirimi gonderilemedi: %s", e)

        return True

    # ---------------------------------------------------------------- #

    def leverage(
        self,
        pair: str,
        current_time: datetime,
        current_rate: float,
        proposed_leverage: float,
        max_leverage: float,
        entry_tag: str | None,
        side: str,
        **kwargs,
    ) -> float:
        """
        Istenen kaldirac, borsanin o cift icin izin verdiginin ustune cikamaz.

        DIKKAT: Bybit bazi ciftler icin kaldirac kademesi (leverage tier)
        verisi dondurmuyor. O durumda freqtrade max_leverage'i 1.0 kabul
        eder ve pozisyon SESSIZCE 8x yerine 1x acilir — yani hedeflenenin
        sekizde biri buyuklugunde. Bunu yakalayip kaydediyoruz;
        confirm_trade_entry bu bilgiye bakip islemi engelliyor.
        """
        want = float(self.leverage_num.value)
        actual = float(min(want, max_leverage))
        self._lev_seen[pair] = (actual, float(max_leverage))
        if actual < want:
            logger.warning(
                "%s: istenen kaldirac %.0fx ama borsa en fazla %.2fx veriyor",
                pair, want, max_leverage,
            )
        return actual

    # ---------------------------------------------------------------- #

    def _stop_for(self, pair: str, trade: Trade) -> float | None:
        """İşlemin açıldığı andaki yapısal stop fiyatı (önceki mum açılışı ± %0.5)."""
        cached = trade.get_custom_data("stop_price")
        if cached is not None:
            return float(cached)

        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if dataframe is None or dataframe.empty:
            return None

        rows = dataframe.loc[dataframe["date"] <= trade.open_date_utc]
        if rows.empty:
            return None

        candle = rows.iloc[-1]
        val = candle["stop_short"] if trade.is_short else candle["stop_long"]
        if val is None or np.isnan(val):
            return None

        trade.set_custom_data("stop_price", float(val))
        return float(val)

    def custom_stoploss(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        after_fill: bool,
        **kwargs,
    ) -> float | None:
        """
        Uc kademeli stop:

          1. Yapisal — onceki 4s mumun acilisinin %0.5 otesi (degismez taban)
          2. Basabas — lehimize be_trigger_r kadar gidince stop girise cekilir
          3. Takip   — trail_trigger_r'den sonra stop, gorulen en iyi fiyatin
                       trail_dist_r kadar arkasindan surüklenir

        Stop asla gevsemez; sadece sikilasir.
        """
        struct = self._stop_for(pair, trade)
        if struct is None:
            return None

        entry = trade.open_rate
        if not entry or entry <= 0:
            return None

        if not (self.use_breakeven.value or self.use_trailing.value):
            return stoploss_from_absolute(
                struct, current_rate, is_short=trade.is_short,
                leverage=trade.leverage,
            )

        risk = abs(entry - struct) / entry     # 1R, oran olarak
        if risk <= 0:
            return None

        is_short = trade.is_short
        best = (trade.min_rate if is_short else trade.max_rate) or current_rate
        move = ((entry - best) if is_short else (best - entry)) / entry
        move_r = move / risk

        new_stop = struct

        # Kademe 2 — basabas
        if self.use_breakeven.value and move_r >= float(self.be_trigger_r.value):
            new_stop = min(new_stop, entry) if is_short else max(new_stop, entry)

        # Kademe 3 — takip
        if self.use_trailing.value and move_r >= float(self.trail_trigger_r.value):
            gap = float(self.trail_dist_r.value) * risk
            trail = best * (1 + gap) if is_short else best * (1 - gap)
            new_stop = min(new_stop, trail) if is_short else max(new_stop, trail)

        return stoploss_from_absolute(
            new_stop, current_rate, is_short=is_short, leverage=trade.leverage,
        )

    def custom_exit(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        **kwargs,
    ) -> str | None:
        tp = float(self.take_profit_pct.value) / 100.0

        if self.tp_on_leveraged.value:
            # current_profit futures'ta zaten kaldıraçlıdır
            if current_profit >= tp:
                return "kar_hedefi"
            return None

        # Fiyat bazında %8
        if trade.is_short:
            if current_rate <= trade.open_rate * (1.0 - tp):
                return "kar_hedefi"
        else:
            if current_rate >= trade.open_rate * (1.0 + tp):
                return "kar_hedefi"
        return None
