from __future__ import annotations import json from datetime import datetime, timezone from pathlib import Path from typing import Optional import pandas as pd from freqtrade.persistence import Trade from freqtrade.strategy import IStrategy class AIStrategy(IStrategy): timeframe = "1m" process_only_new_candles = True startup_candle_count = 50 use_exit_signal = True exit_profit_only = False ignore_roi_if_entry_signal = False # ROI is managed mostly via custom_exit + AI targets. minimal_roi = {"0": 0.10} stoploss = -0.99 can_short = False base_dir = Path(__file__).resolve().parents[1] ai_strategy_file = base_dir / "ai_strategy.json" signals_file = base_dir / "signals.json" def _load_json(self, path: Path, default): if not path.exists(): return default try: with path.open("r", encoding="utf-8") as f: return json.load(f) except Exception: return default def _load_ai_cfg(self) -> dict: default = { "pairs": ["DOGE/USD"], "timeframe": "1m", "target_profit": 0.01, "stop_loss": 0.01, "confidence_threshold": 0.7, "max_daily_loss": 0.01, "max_daily_profit": 0.05, } return self._load_json(self.ai_strategy_file, default) def _load_signals(self) -> dict: return self._load_json(self.signals_file, {"signals": []}) def informative_pairs(self): cfg = self._load_ai_cfg() pairs = [p for p in cfg.get("pairs", []) if isinstance(p, str)] return [(p, self.timeframe) for p in pairs] def _signal_for_pair(self, pair: str) -> Optional[dict]: pair_dash = pair.replace("/", "-") data = self._load_signals() for sig in data.get("signals", []): if sig.get("coin") == pair_dash: return sig return None def _ai_pair_allowed(self, pair: str) -> bool: cfg = self._load_ai_cfg() return pair in cfg.get("pairs", []) def _daily_pnl_ratio(self) -> float: """ Sum realized close_profit (ratio) for current UTC day. Example: two closed trades at +1% and -0.5% => +0.5% day ratio (0.005). """ today = datetime.now(timezone.utc).date() closed = Trade.get_trades_proxy(is_open=False) profits: list[float] = [] for trade in closed: if trade.close_date_utc and trade.close_date_utc.date() == today: profits.append(float(trade.close_profit or 0.0)) return float(sum(profits)) def _risk_limits_reached(self) -> bool: cfg = self._load_ai_cfg() pnl = self._daily_pnl_ratio() max_loss = -abs(float(cfg.get("max_daily_loss", 0.01))) max_profit = abs(float(cfg.get("max_daily_profit", 0.05))) return pnl <= max_loss or pnl >= max_profit def _threshold(self) -> float: cfg = self._load_ai_cfg() return float(cfg.get("confidence_threshold", 0.7)) def _target_profit_ratio(self, signal: Optional[dict]) -> float: cfg = self._load_ai_cfg() if signal and signal.get("take_profit_ratio") is not None: try: return abs(float(signal["take_profit_ratio"])) except Exception: pass return abs(float(cfg.get("target_profit", 0.01))) def _stop_loss_ratio(self, signal: Optional[dict]) -> float: cfg = self._load_ai_cfg() if signal and signal.get("stop_loss_ratio") is not None: try: return abs(float(signal["stop_loss_ratio"])) except Exception: pass return abs(float(cfg.get("stop_loss", 0.01))) def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame: return dataframe def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame: dataframe["enter_long"] = 0 pair = metadata["pair"] if not self._ai_pair_allowed(pair): return dataframe if self._risk_limits_reached(): return dataframe sig = self._signal_for_pair(pair) threshold = self._threshold() if sig and sig.get("action") == "BUY" and float(sig.get("confidence", 0.0)) >= threshold: dataframe.loc[dataframe.index[-1], "enter_long"] = 1 return dataframe def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame: dataframe["exit_long"] = 0 pair = metadata["pair"] sig = self._signal_for_pair(pair) if sig and sig.get("action") == "SELL": dataframe.loc[dataframe.index[-1], "exit_long"] = 1 return dataframe def custom_stoploss( self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, after_fill: bool = False, **kwargs, ) -> float: sig = self._signal_for_pair(pair) stop_ratio = self._stop_loss_ratio(sig) # Freqtrade expects negative ratio. return -abs(stop_ratio) def custom_exit( self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs, ): sig = self._signal_for_pair(pair) if self._risk_limits_reached(): return "daily_limit_reached" if sig and sig.get("action") == "SELL": return "ai_sell_signal" target = self._target_profit_ratio(sig) if current_profit >= target: return "ai_take_profit" return None 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: if not self._ai_pair_allowed(pair): return 0.0 if self._risk_limits_reached(): return 0.0 sig = self._signal_for_pair(pair) if not sig: return 0.0 if sig.get("action") != "BUY": return 0.0 if float(sig.get("confidence", 0.0)) < self._threshold(): return 0.0 stake = min(proposed_stake, max_stake) if min_stake is not None: stake = max(stake, min_stake) return stake