Files
MidasBot/freqtrade/user_data/strategies/AiBiasStrategy.py
jerem 633b033f4d MidasBot: bot trading crypto IA + stratégies Ichimoku validées
- Infra: Freqtrade (futures dry-run) + Redis + dashboard + Docker Compose
- Couche IA: ai_analyzer (Claude via abonnement, MCP TradingView, backfill biais)
- Stratégies: SampleStrategy, AiBiasStrategy, IchimokuLS (long/short, validée
  train/test + données vierges + walk-forward), MTFIchimoku, variantes hyperopt
- Arbitrage CEX (dry-run), backtesting, walk-forward, volatility targeting
- IchimokuLS en dry-run live (config_live.json)

Claude-Session: https://claude.ai/code/session_01VHETcFacdnDhQzthLpdYFR
2026-06-23 19:25:49 +02:00

172 lines
6.6 KiB
Python

# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods
"""
AiBiasStrategy — stratégie directionnelle pilotée par le biais IA (Phase 3).
Base technique : croisement EMA + RSI (comme SampleStrategy).
Surcouche IA : un biais de marché produit par Claude (service ai_analyzer) est lu
depuis Redis et filtre/renforce les entrées.
- Live / dry-run : lit le biais COURANT de la paire à chaque nouvelle bougie.
- Backtest : Redis ne contient pas d'historique de biais -> repli `neutral` (la
surcouche IA est neutralisée, seule la base technique joue). Backtester finement
l'IA nécessiterait d'enregistrer les biais historiques (amélioration future).
"""
from __future__ import annotations
import json
import os
from functools import lru_cache
from pathlib import Path
import pandas as pd
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.enums import RunMode
from freqtrade.strategy import IStrategy
# Répertoire de l'historique des biais (monté dans le conteneur freqtrade).
_HISTORY_DIR = os.environ.get(
"AI_HISTORY_DIR", "/freqtrade/user_data/ai_bias_history"
)
@lru_cache(maxsize=1)
def _redis_client():
"""Client Redis paresseux et tolérant aux pannes (None si indisponible)."""
try:
import redis # importé ici pour ne pas casser si le paquet manque
url = os.environ.get("REDIS_URL", "redis://redis:6379/0")
client = redis.Redis.from_url(
url, decode_responses=True, socket_connect_timeout=1, socket_timeout=1
)
client.ping()
return client
except Exception: # noqa: BLE001
return None
def _read_bias(pair: str) -> dict:
"""Renvoie {'direction', 'confidence'} COURANT pour la paire (live/dry-run)."""
client = _redis_client()
if client is None:
return {"direction": "neutral", "confidence": 0.0}
try:
raw = client.get(f"bias:{pair}")
if not raw:
return {"direction": "neutral", "confidence": 0.0}
data = json.loads(raw)
return {
"direction": data.get("direction", "neutral"),
"confidence": float(data.get("confidence", 0.0)),
}
except Exception: # noqa: BLE001
return {"direction": "neutral", "confidence": 0.0}
def _load_history(pair: str) -> pd.DataFrame:
"""Charge l'historique horodaté des biais d'une paire (vide si absent)."""
path = Path(_HISTORY_DIR) / f"{pair.replace('/', '_')}.csv"
if not path.exists():
return pd.DataFrame(columns=["timestamp", "direction", "confidence"])
try:
hist = pd.read_csv(path)
hist["timestamp"] = pd.to_datetime(hist["timestamp"], utc=True)
hist["confidence"] = pd.to_numeric(hist["confidence"], errors="coerce").fillna(0.0)
return hist.sort_values("timestamp").reset_index(drop=True)
except Exception: # noqa: BLE001 — historique corrompu : on l'ignore
return pd.DataFrame(columns=["timestamp", "direction", "confidence"])
def _merge_history(dataframe: DataFrame, pair: str) -> DataFrame:
"""Associe à chaque bougie le dernier biais connu À CETTE DATE (merge_asof)."""
hist = _load_history(pair)
if hist.empty:
dataframe["ai_direction"] = "neutral"
dataframe["ai_confidence"] = 0.0
return dataframe
# Aligner la résolution temporelle (Freqtrade=ms, pandas parse=us) sinon merge_asof échoue.
hist = hist.copy()
hist["timestamp"] = hist["timestamp"].astype(dataframe["date"].dtype)
# Le dataframe Freqtrade est déjà trié par date croissante (prérequis merge_asof).
merged = pd.merge_asof(
dataframe[["date"]],
hist[["timestamp", "direction", "confidence"]],
left_on="date",
right_on="timestamp",
direction="backward",
)
dataframe["ai_direction"] = merged["direction"].fillna("neutral").values
dataframe["ai_confidence"] = merged["confidence"].fillna(0.0).values
return dataframe
class AiBiasStrategy(IStrategy):
INTERFACE_VERSION = 3
timeframe = "1h"
minimal_roi = {"0": 0.05, "120": 0.03, "360": 0.01, "720": 0}
stoploss = -0.10
trailing_stop = True
trailing_stop_positive = 0.02
trailing_stop_positive_offset = 0.03
trailing_only_offset_is_reached = True
startup_candle_count: int = 50
process_only_new_candles = True
use_exit_signal = True
# Confiance minimale du biais IA pour qu'il influence les décisions.
ai_min_confidence: float = float(os.environ.get("AI_MIN_CONFIDENCE", "0.6"))
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["ema_fast"] = ta.EMA(dataframe, timeperiod=9)
dataframe["ema_slow"] = ta.EMA(dataframe, timeperiod=21)
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
# Source du biais IA selon le mode :
# - backtest/hyperopt/plot : historique horodaté (biais valide à chaque bougie)
# - dry-run/live : biais COURANT depuis Redis (dernière bougie)
runmode = self.dp.runmode if self.dp is not None else RunMode.OTHER
if runmode in (RunMode.BACKTEST, RunMode.HYPEROPT, RunMode.PLOT):
dataframe = _merge_history(dataframe, metadata["pair"])
else:
bias = _read_bias(metadata["pair"])
dataframe["ai_direction"] = bias["direction"]
dataframe["ai_confidence"] = bias["confidence"]
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
base_long = (
(dataframe["ema_fast"] > dataframe["ema_slow"])
& (dataframe["ema_fast"].shift(1) <= dataframe["ema_slow"].shift(1))
& (dataframe["rsi"] < 70)
& (dataframe["volume"] > 0)
)
# Filtre IA : si le biais est suffisamment confiant, on bloque les longs
# quand il est baissier ; on les laisse passer sinon (neutre/haussier).
ai_blocks_long = (dataframe["ai_direction"] == "bearish") & (
dataframe["ai_confidence"] >= self.ai_min_confidence
)
dataframe.loc[base_long & (~ai_blocks_long), "enter_long"] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
base_exit = (
(dataframe["ema_fast"] < dataframe["ema_slow"])
& (dataframe["ema_fast"].shift(1) >= dataframe["ema_slow"].shift(1))
& (dataframe["volume"] > 0)
)
# Sortie anticipée si l'IA devient nettement baissière.
ai_exit = (dataframe["ai_direction"] == "bearish") & (
dataframe["ai_confidence"] >= self.ai_min_confidence
)
dataframe.loc[base_exit | ai_exit, "exit_long"] = 1
return dataframe