from datetime import datetime from typing import Dict, List, Tuple from freqtrade.strategy import IStrategy, merge_informative_pair from pandas import DataFrame from mods import exits, filters, indicators, logging as decision_log, protections as prot, regime, signals_trend class TrendV0(IStrategy): """ Strategia trendowa (long-only) na 30m z MTF (1h/4h) i detekcją reżimu. Wskaźniki bazowe: ema20/50/200, rsi14, atr14, bbands, volume_rel, roc, dist_ema50, ema50_slope. Reżim z 1h/4h: TREND_UP/TREND_DOWN/RANGE/HIGH_VOL_UNCERTAIN (patrz mods/regime). """ timeframe = "30m" informative_timeframes: List[str] = ["1h", "4h"] process_only_new_candles = True startup_candle_count: int = 210 use_exit_signal = True exit_profit_only = False ignore_buying_expired_candle_after = 20 minimal_roi = {"0": 0.05} stoploss = -0.1 entry_context: Dict[str, Dict] = {} protections = prot.DEFAULT_PROTECTIONS def informative_pairs(self) -> List[Tuple[str, str]]: pairs = self.dp.current_whitelist() if self.dp else [] return [(pair, tf) for pair in pairs for tf in self.informative_timeframes] @staticmethod def _merge(df: DataFrame, informative: DataFrame, tf: str) -> DataFrame: base = df.copy() if "date" not in base.columns: base["date"] = base.index inf = indicators.add_base_indicators(informative) if "date" not in inf.columns: inf["date"] = inf.index return merge_informative_pair(base, inf, "30m", tf, ffill=True, append_timeframe=True) def populate_indicators(self, dataframe: DataFrame, metadata: Dict) -> DataFrame: df = indicators.add_base_indicators(dataframe) if self.dp: for tf in self.informative_timeframes: inf = self.dp.get_pair_dataframe(pair=metadata["pair"], timeframe=tf) df = self._merge(df, inf, tf) df["regime"] = regime.detect_regime(df) return df def populate_entry_trend(self, dataframe: DataFrame, metadata: Dict) -> DataFrame: df = dataframe.copy() in_regime = df["regime"].isin([regime.TREND_UP, regime.TREND_DOWN]) vol_ok = filters.volume_ok(df) calm = filters.no_high_vol(df) signals = signals_trend.trend_long_signals(df) entry_mask = in_regime & vol_ok & calm & filters.daily_trade_cap(df) & signals df.loc[:, "enter_long"] = 0 df.loc[entry_mask, "enter_long"] = 1 df.loc[entry_mask, "enter_tag"] = decision_log.short_tag(df.loc[entry_mask, "regime"], "trend", "ok") # zapisz kontekst dla custom_data if metadata and metadata.get("pair"): pair = metadata["pair"] idx = entry_mask[entry_mask].index if not idx.empty: row = df.loc[idx[-1]] self.entry_context[pair] = { "regime": row.get("regime"), "engine": "trend", "filters": {"volume_ok": bool(vol_ok.iloc[idx[-1]]), "calm": bool(calm.iloc[idx[-1]])}, "indicators": { "rsi14": float(row.get("rsi14", 0)), "roc10": float(row.get("roc10", 0)), "roc20": float(row.get("roc20", 0)), "bb_pos": float(row.get("bb_position", 0)), "volume_rel": float(row.get("volume_rel", 0)), }, } decision_log.decision_log( event="entry", regime="trend", reason="aggregate", extra={ "candidates": int(signals.sum()), "accepted": int(entry_mask.sum()), "ts": datetime.utcnow().isoformat(), }, ) return df def populate_exit_trend(self, dataframe: DataFrame, metadata: Dict) -> DataFrame: df = dataframe.copy() exit_mask = exits.trend_exit(df) df.loc[:, "exit_long"] = 0 df.loc[exit_mask, "exit_long"] = 1 decision_log.decision_log( event="exit", regime="trend", reason="trend_exit", extra={"signals": int(exit_mask.sum())}, ) return df def custom_stoploss(self, pair: str, trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: # Uproszczone: korzystamy ze stałego stoplossu zdefiniowanego w klasie. return self.stoploss def custom_exit(self, pair: str, trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs): # zachowaj dane wyjścia i uzupełnij custom_data try: trade.set_custom_data("decision_exit", {"exit_at": current_time.isoformat(), "profit": current_profit, "regime": trade.enter_tag if hasattr(trade, "enter_tag") else None}) if trade.get_custom_data("decision_entry") is None and pair in self.entry_context: trade.set_custom_data("decision_entry", self.entry_context[pair]) except Exception: pass return None