# source: https://raw.githubusercontent.com/remiotore/freqtrade/44beaeb6a420cd8e9f2e4ea93e11d6cfa192ee03/strategies/HFT_Futures_Scalper.py
# Github_remiotore_freqtrade__HFT_Futures_Scalper__20260111_210550.py
# Self-contained Freqtrade IStrategy for conservative, conviction-based intraday scalping on futures.

from freqtrade.strategy.interface import IStrategy
from pandas import DataFrame
import talib.abstract as ta
import numpy as np
from datetime import datetime, timedelta

class Github_remiotore_freqtrade__HFT_Futures_Scalper__20260111_210550(IStrategy):
    """
    Conviction Adaptive Scalper:
      - timeframe: 1m
      - requires multiple confirmations: trend, VWAP slope, volume spike, orderbook imbalance, momentum
      - adaptive ATR-based stoploss, TP as factor * stoploss
      - trailing stop to protect wins
      - behavioral safety: cooldown after N consecutive losses, max trades per hour/day
    """

    # --- Meta ---
    timeframe = '1m'
    can_short = True

    # Money management (set conservative defaults; tune in hyperopt)
    # We use freqtrade ROI/stoploss plus custom trailing in custom_exit
    stoploss = -0.05  # fallback very large: we use dynamic stoploss via custom_stoploss
    minimal_roi = {
        "0": 0.01,   # fallback, rely mainly on custom exits/trailing
    }

    # startup candles
    startup_candle_count: int = 200

    # --- Tunable strategy parameters (hyperopt these ranges) ---
    ema_trend = 50                 # trend EMA
    ema_fast = 8
    ema_slow = 21

    atr_period = 14
    min_stop_atr = 0.8             # stoploss = max(atr*min_stop_atr, min_stop_pct)
    stop_min_pct = 0.0025          # min stop 0.25%

    tp_mult = 1.6                  # take-profit = tp_mult * stop_loss_size
    use_trailing = True
    trailing_pullback = 0.5        # trailing stop triggers when profit >= trailing_start_pct, pulls back trailing by this fraction
    trailing_start_pct = 0.0035    # start trailing after 0.35%

    vol_sma = 60
    vol_spike_factor = 1.25        # volume must exceed this * vol_sma

    vwap_slope_lookback = 6        # measure VWAP slope over these bars
    min_vwap_slope = 1e-7          # tiny positive slope threshold (adjust after scale)

    obi_threshold = 1.4            # orderbook imbalance bids/asks (long > threshold). For shorts use < 1/threshold

    max_trades_per_hour = 4
    max_trades_per_day = 20

    cooldown_after_losses = 3      # number of consecutive losing trades to trigger cooldown
    cooldown_minutes = 60          # cooldown period length

    # For tracking (not persisted across restarts)
    _recent_trades = []
    _last_cooldown_until = None

    def informative_pairs(self):
        return []

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # Trend EMAs
        dataframe['ema_trend'] = ta.EMA(dataframe['close'], timeperiod=self.ema_trend)
        dataframe['ema_fast'] = ta.EMA(dataframe['close'], timeperiod=self.ema_fast)
        dataframe['ema_slow'] = ta.EMA(dataframe['close'], timeperiod=self.ema_slow)

        # ATR for dynamic stops
        dataframe['atr'] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=self.atr_period)

        # VWAP (cumulative)
        pv = (dataframe['close'] * dataframe['volume']).fillna(0)
        dataframe['pv_cum'] = pv.cumsum()
        dataframe['vol_cum'] = dataframe['volume'].cumsum().replace(0, np.nan)
        dataframe['vwap'] = dataframe['pv_cum'] / dataframe['vol_cum']

        # VWAP slope (simple linear approx: difference over lookback)
        dataframe['vwap_slope'] = dataframe['vwap'].diff(self.vwap_slope_lookback) / self.vwap_slope_lookback

        # Volume environment
        dataframe['vol_sma'] = dataframe['volume'].rolling(self.vol_sma, min_periods=1).mean()

        # Momentum
        dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14)
        dataframe['mom_3'] = dataframe['close'].pct_change(3)

        # Price relative to ema_trend and vwap
        dataframe['pct_above_trend'] = (dataframe['close'] - dataframe['ema_trend']) / dataframe['ema_trend']
        dataframe['pct_vs_vwap'] = (dataframe['close'] - dataframe['vwap']) / dataframe['vwap']

        return dataframe

    def order_book_imbalance(self, metadata: dict) -> float:
        """Return bids/asks volume ratio for top 5 levels; neutral 1.0 on failure."""
        try:
            ob = self.dp.orderbook(metadata['pair'], 5)
            bids = sum([b[1] for b in ob['bids']]) if ob.get('bids') else 0.0
            asks = sum([a[1] for a in ob['asks']]) if ob.get('asks') else 0.0
            if asks == 0:
                return 1.0
            return float(bids) / (float(asks) + 1e-9)
        except Exception:
            return 1.0

    def _within_cooldown(self):
        """Check if currently in cooldown region due to recent losses."""
        if self._last_cooldown_until is None:
            return False
        return datetime.utcnow() < self._last_cooldown_until

    def _register_trade_result(self, trade_outcome: dict):
        """
        Call externally after trades close (freqtrade does not provide a trivial hook to persist across runs).
        This function is here as a guidance placeholder — in live you can implement event hooks.
        """
        # trade_outcome: {'profit_pct': float, 'close_time': datetime}
        self._recent_trades.append(trade_outcome)
        # keep only last 50
        if len(self._recent_trades) > 50:
            self._recent_trades = self._recent_trades[-50:]

        # check consecutive losses
        consec_losses = 0
        for t in reversed(self._recent_trades):
            if t['profit_pct'] < 0:
                consec_losses += 1
            else:
                break
        if consec_losses >= self.cooldown_after_losses:
            self._last_cooldown_until = datetime.utcnow() + timedelta(minutes=self.cooldown_minutes)

    def _count_trades_in_period(self, pair: str, minutes: int, trades: list) -> int:
        """Count trades in last `minutes` from trades list; placeholder for manager integration."""
        # This is a placeholder. Use trade history from trade manager in real deployment.
        return 0

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # default flags
        dataframe['enter_long'] = 0
        dataframe['enter_short'] = 0

        # Do not generate entries when in cooldown
        if self._within_cooldown():
            return dataframe

        # fetch OBI once per call
        obi = self.order_book_imbalance(metadata)

        # Common filters
        vol_ok = dataframe['volume'] > dataframe['vol_sma'] * self.vol_spike_factor
        vwap_slope_ok = dataframe['vwap_slope'] > self.min_vwap_slope
        trend_up = dataframe['pct_above_trend'] > 0.0  # price above long-term trend EMA
        trend_down = dataframe['pct_above_trend'] < 0.0

        # Long conviction:
        # 1) Price above trend (trend_up)
        # 2) VWAP slope positive
        # 3) Volume spike
        # 4) Recent momentum positive (mom_3)
        # 5) Orderbook imbalance favors bids (obi > obi_threshold)
        cond_long = (
            trend_up &
            vwap_slope_ok &
            vol_ok &
            (dataframe['mom_3'] > 0) &
            (obi > self.obi_threshold) &
            (dataframe['rsi'] < 75)  # avoid overbought extremes
        )

        # Short conviction (mirrored):
        cond_short = (
            trend_down &
            (dataframe['vwap_slope'] < -self.min_vwap_slope) &
            vol_ok &
            (dataframe['mom_3'] < 0) &
            (obi < 1.0 / self.obi_threshold) &
            (dataframe['rsi'] > 25)
        )

        # Additionally require price not too far from VWAP (avoid trading outliers)
        within_vwap_long = dataframe['pct_vs_vwap'] > -0.01  # not more than 1% below vwap
        within_vwap_short = dataframe['pct_vs_vwap'] < 0.01  # not more than 1% above vwap

        dataframe.loc[cond_long & within_vwap_long, 'enter_long'] = 1
        dataframe.loc[cond_short & within_vwap_short, 'enter_short'] = 1

        # Tag signals
        dataframe.loc[cond_long, 'entry_tag'] = 'CONV_LONG'
        dataframe.loc[cond_short, 'entry_tag'] = 'CONV_SHORT'

        return dataframe
        
        
    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        No fixed exit signals — exits handled by minimal_roi, stoploss, and custom_exit.
        This method must still return 'exit_long' and 'exit_short' columns.
        """
        dataframe['exit_long'] = 0
        dataframe['exit_short'] = 0
        return dataframe    

    def custom_stoploss(self, pair: str, trade, current_time, current_rate, current_profit, **kwargs):
        """
        Dynamic stoploss based on ATR at trade open:
         - stop_size = max(atr_at_entry * min_stop_atr, stop_min_pct)
         - returns negative fraction (e.g., -0.01)
        Freqtrade passes this called repeatedly; we compute once and stash into trade.meta for reuse.
        """
        try:
            # If we already saved stop in trade.meta, return it
            if trade is not None and hasattr(trade, 'meta') and trade.meta.get('dynamic_stop'):
                return trade.meta['dynamic_stop']

            # Acquire current ATR from pair candle series via dp (best-effort)
            candles = self.dp.get_pair_dataframe(pair=pair, timeframe=self.timeframe, limit=5 * self.atr_period)
            if candles is None or len(candles) < self.atr_period:
                return self.stoploss  # fallback

            recent_atr = ta.ATR(candles['high'], candles['low'], candles['close'], timeperiod=self.atr_period).iloc[-1]
            if np.isnan(recent_atr) or recent_atr == 0:
                return self.stoploss

            # stop as percentage ~ recent_atr / current_price * factor
            stop_pct = max((recent_atr / current_rate) * self.min_stop_atr, self.stop_min_pct)
            dyn_stop = -abs(stop_pct)

            # stash in trade.meta if available
            if trade is not None and hasattr(trade, 'meta'):
                meta = trade.meta if trade.meta is not None else {}
                meta['dynamic_stop'] = dyn_stop
                trade.meta = meta

            return dyn_stop
        except Exception:
            return self.stoploss

    def custom_exit(self, pair: str, trade, current_time, current_rate, current_profit, **kwargs):
        """
        Implements:
         - take profit at tp_mult * stop_size (we compute using dynamic_stop from trade.meta if available),
         - trailing stop: once profit > trailing_start_pct, apply trailing logic.
        """
        # ensure trade.meta has dynamic_stop if possible
        dynamic_stop = None
        try:
            if trade is not None and hasattr(trade, 'meta'):
                dynamic_stop = trade.meta.get('dynamic_stop')
        except Exception:
            dynamic_stop = None

        # fallback: estimate dynamic stop if missing
        if dynamic_stop is None:
            dynamic_stop = self.custom_stoploss(pair, trade, current_time, current_rate, current_profit, **kwargs)

        stop_abs = abs(dynamic_stop)
        if stop_abs == 0:
            return None

        tp_target = stop_abs * self.tp_mult

        # If profit reached TP target, exit
        if current_profit is not None and current_profit >= tp_target:
            return 'tp_dynamic'

        # Trailing stop logic
        if self.use_trailing and current_profit is not None:
            if current_profit >= self.trailing_start_pct:
                # set trailing threshold: if current profit retracts by trailing_pullback*current_profit, exit
                # we compute last_peak from trade.meta (placeholder) — for simplicity use current_profit as proxy
                # A better implementation should track trade peak profit in an external persistent store.
                peak = trade.meta.get('peak_profit', current_profit) if (trade and hasattr(trade, 'meta')) else current_profit
                # update peak
                if trade is not None and hasattr(trade, 'meta'):
                    trade.meta['peak_profit'] = max(peak, current_profit)
                    peak = trade.meta['peak_profit']

                # if profit pulled back beyond allowed fraction, exit
                if current_profit < peak * (1.0 - self.trailing_pullback):
                    return 'trailing_pullback'

        # Else hold
        return None

    # Optional hook: selection of pairs / stop trading based on session — not implemented here
