commit 633b033f4d3b7e328b9b0eaa5aae8850ea9bb600 Author: jerem Date: Tue Jun 23 19:25:49 2026 +0200 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 diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock new file mode 100644 index 0000000..466d7e1 --- /dev/null +++ b/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"8ae84397-5702-4993-8957-3e59bfe5c1b6","pid":88193,"procStart":"Tue Jun 23 12:10:23 2026","acquiredAt":1782221447804} \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6c13dc1 --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +# ── MidasBot — variables d'environnement ── +# Copier ce fichier en `.env` et remplir les valeurs. NE JAMAIS committer `.env`. + +# --- IA : abonnement Claude (PAS l'API facturée au token) --- +# Générer une fois avec : claude setup-token +# IMPORTANT : ne PAS définir ANTHROPIC_API_KEY, sinon Claude bascule sur la facturation au token. +CLAUDE_CODE_OAUTH_TOKEN= + +# --- FreqUI / API server Freqtrade --- +# Freqtrade lit nativement les variables FREQTRADE__SECTION__CLE et surcharge config.json. +# Identifiants de login FreqUI (changer le mot de passe avant tout passage en live) : +FREQTRADE__API_SERVER__USERNAME=midas +FREQTRADE__API_SERVER__PASSWORD=midas-dev-pass +# Secrets — JWT doit faire >= 32 caractères. Régénérer avec : openssl rand -hex 32 +FREQTRADE__API_SERVER__JWT_SECRET_KEY=CHANGE_ME_openssl_rand_hex_32_minimum_length_required +FREQTRADE__API_SERVER__WS_TOKEN=CHANGE_ME_openssl_rand_hex_16 + +# --- Exchange (laisser vide en dry-run ; rempli seulement au passage en live) --- +# FREQTRADE__EXCHANGE__KEY= +# FREQTRADE__EXCHANGE__SECRET= + +# --- Redis (utilisé par ai_analyzer / dashboard) --- +REDIS_URL=redis://redis:6379/0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f376cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Secrets / credentials — NE JAMAIS committer +.env +*.key +*.pem + +# Freqtrade +freqtrade/user_data/data/ +freqtrade/user_data/logs/ +freqtrade/user_data/backtest_results/ +freqtrade/user_data/hyperopt_results/ +freqtrade/user_data/models/ +freqtrade/user_data/notebooks/ +freqtrade/user_data/plot/ +freqtrade/user_data/tradesv3*.sqlite +freqtrade/user_data/*.sqlite* + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +.pytest_cache/ +*.egg-info/ + +# OS / IDE +.DS_Store +.idea/ +.vscode/ + +# Redis dump +dump.rdb +logs/ +freqtrade/user_data/ai_bias_history/ + +# Code tiers (re-téléchargeable, non versionné ici) +freqtrade/user_data/strategies/NostalgiaForInfinity*.py diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..a582dba --- /dev/null +++ b/.mcp.json @@ -0,0 +1,10 @@ +{ + "_comment": "Serveur MCP TradingView (optionnel). Activer pour que Claude confirme via RSI/MACD/Bollinger. Install : `uv tool install` ou clonage du repo atilaahmettaner/tradingview-mcp. Adapter command/args à ton installation locale.", + "mcpServers": { + "tradingview": { + "command": "uvx", + "args": ["tradingview-mcp"], + "env": {} + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..6fe6b78 --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ +# MidasBot 🪙 + +Bot de trading/arbitrage crypto **assisté par IA**, conçu pour tourner en **dry-run** (simulation, aucun risque financier) avant tout passage en réel. + +- **Moteur** : [Freqtrade](https://www.freqtrade.io/) (backtesting, dry-run, dashboard FreqUI, connecteurs CEX via ccxt). +- **IA** : Claude (analyse de marché) via l'**abonnement** Claude Code en mode headless — **pas** l'API facturée au token. +- **Arbitrage** : module séparé (CEX d'abord, DEX ensuite). + +> ⚠️ **Statut : dry-run.** Aucune clé d'échange n'est requise. Le passage en trading réel est une décision explicite et séparée. + +## Architecture + +``` +ai_analyzer ──(biais de marché)──▶ Redis ──(lecture)──▶ AiBiasStrategy (Freqtrade) + │ │ + └── claude -p (abonnement) + MCP TradingView dry-run + FreqUI (localhost:8080) + +arbitrage/ ──(scan d'écarts inter-CEX via ccxt, dry-run)──▶ logs +dashboard/ ──(insights IA + lien FreqUI) +``` + +## Prérequis + +- **Docker** + Docker Compose (le moteur tourne en conteneurs ; Python local n'est pas requis). +- **Claude Code** installé et connecté à ton abonnement (`claude login`). + +## Démarrage rapide + +```bash +# 1. Configurer les secrets +cp .env.example .env +# (puis générer des secrets FreqUI : openssl rand -hex 32 pour JWT_SECRET_KEY) + +# 2. Lancer le socle (Freqtrade dry-run AiBiasStrategy + Redis + dashboard) +docker compose up -d # freqtrade + redis + dashboard + +# 3. Ouvrir les interfaces +open http://127.0.0.1:8080 # FreqUI (positions, P&L, trades) +open http://127.0.0.1:8500 # Dashboard « Insights IA » +``` + +## Services & ports + +| Service | Port (hôte) | Rôle | Profil compose | +|---------|-------------|------|----------------| +| `freqtrade` | 8080 | Moteur dry-run + FreqUI | défaut | +| `dashboard` | 8500 | Panneau Insights IA | défaut | +| `redis` | 6380 | Bus de signaux | défaut | +| `ai-analyzer` | — | Analyse Claude → biais Redis | `ai` | +| `arbitrage` | — | Scan d'écarts inter-CEX (dry-run) | `arb` | + +## Lancer la couche IA (analyzer) + +L'analyzer interroge Claude via ton **abonnement** (pas l'API). Deux options : + +**A. En conteneur** (orchestration complète) : +```bash +claude setup-token # → coller dans .env (CLAUDE_CODE_OAUTH_TOKEN) +docker compose --profile ai up -d --build ai-analyzer +``` + +**B. En local, à la main** (rapide pour tester, utilise ton `claude` déjà connecté) : +```bash +python3 -m venv .venv && .venv/bin/pip install -r ai_analyzer/requirements.txt +cd ai_analyzer +REDIS_URL=redis://localhost:6380/0 ../.venv/bin/python analyzer.py --once +``` + +**C. En local, automatique toutes les heures (recommandé)** — via `launchd`, sans +manipuler de token (utilise ton `claude` hôte qui gère le rafraîchissement) : +```bash +cp scripts/com.midasbot.analyzer.plist ~/Library/LaunchAgents/ +launchctl load -w ~/Library/LaunchAgents/com.midasbot.analyzer.plist # 1er run immédiat +tail -f logs/analyzer.log # suivre +``` +Gestion : `launchctl list | grep midasbot` (état) · `launchctl unload ~/Library/LaunchAgents/com.midasbot.analyzer.plist` (arrêter). +Le script lancé est `scripts/run_analyzer.sh` (paires, modèle, intervalle modifiables dedans). + +> Cadence basse par défaut (1 cycle/h, 1 seul appel Claude couvrant toutes les paires) +> pour ménager les limites d'usage de l'abonnement. + +Chaque cycle **historise** les biais dans `freqtrade/user_data/ai_bias_history/.csv` +(horodaté). En **live/dry-run** la stratégie lit le biais courant (Redis) ; en **backtest** +elle lit l'historique et associe à chaque bougie le biais valide à cette date (`merge_asof`). + +## Backtester l'IA (vrai backtest) + +L'historique s'accumule au fil des cycles. Pour amorcer un backtest immédiatement, génère +des biais passés avec le **backfill** (⚠️ 1 appel Claude par pas de temps — consomme le quota) : + +```bash +cd ai_analyzer +# ~180 appels pour 6 mois en cadence 24 h. Ajuste --step-hours selon ton quota. +REDIS_URL=redis://localhost:6380/0 ../.venv/bin/python backfill.py \ + --start 20260101 --step-hours 24 +``` +Puis lance le backtest qui lira cet historique : +```bash +docker compose run --rm freqtrade backtesting \ + --strategy AiBiasStrategy --timeframe 1h --timerange 20260101- +``` + +## Lancer le scanner d'arbitrage (dry-run) + +```bash +docker compose --profile arb up -d --build arbitrage +docker compose logs -f arbitrage +``` + +## Phases + +| Phase | Contenu | Statut | +|-------|---------|--------| +| 1 | Freqtrade dry-run + FreqUI | ✅ | +| 2 | Backtesting | ✅ | +| 3 | Couche IA (Claude + MCP TradingView) | ✅ | +| 4 | Arbitrage CEX (dry-run) | ✅ | +| 5 | Dashboard insights IA | ✅ | + +À venir : MCP TradingView branché par défaut, biais historiques pour backtester l'IA, connecteur DEX, passage en réel (clés exchange + garde-fous). + +Plan détaillé : voir `~/.claude/plans/le-but-du-jeu-streamed-journal.md`. + +## Sécurité + +- `.env` n'est jamais commité (cf. `.gitignore`). +- `CLAUDE_CODE_OAUTH_TOKEN` (abonnement) — **ne pas** définir `ANTHROPIC_API_KEY` (basculerait sur la facturation au token). +- Ports exposés sur `127.0.0.1` uniquement. diff --git a/ai_analyzer/Dockerfile b/ai_analyzer/Dockerfile new file mode 100644 index 0000000..a2b8392 --- /dev/null +++ b/ai_analyzer/Dockerfile @@ -0,0 +1,23 @@ +# Service ai-analyzer : Python + CLI Claude (Node), authentifié par l'abonnement. +FROM node:20-slim + +RUN apt-get update \ + && apt-get install -y --no-install-recommends python3 python3-pip \ + && rm -rf /var/lib/apt/lists/* + +# CLI Claude Code (mode headless via `claude -p`). +RUN npm install -g @anthropic-ai/claude-code + +WORKDIR /app +COPY ai_analyzer/requirements.txt . +RUN pip3 install --no-cache-dir --break-system-packages -r requirements.txt + +COPY ai_analyzer/ /app/ +COPY .mcp.json /app/.mcp.json + +ENV REDIS_URL=redis://redis:6379/0 \ + ANALYZER_MODEL=claude-sonnet-4-6 + +# Auth : CLAUDE_CODE_OAUTH_TOKEN fourni par l'environnement (cf. .env). +# NE PAS définir ANTHROPIC_API_KEY (basculerait sur la facturation au token). +CMD ["python3", "analyzer.py"] diff --git a/ai_analyzer/analyzer.py b/ai_analyzer/analyzer.py new file mode 100644 index 0000000..a9605ec --- /dev/null +++ b/ai_analyzer/analyzer.py @@ -0,0 +1,92 @@ +"""Boucle d'analyse : marché -> Claude -> biais dans Redis. + +Cadence VOLONTAIREMENT basse (1 cycle par bougie sur timeframe >= 15 min) : +l'usage automatisé d'un abonnement a des limites ; un cycle = UN seul appel Claude +couvrant toutes les paires (batch), pour économiser le quota. + +Usage : + python analyzer.py --once # un seul cycle (test) + python analyzer.py # boucle continue +""" +from __future__ import annotations + +import argparse +import os +import time +from datetime import datetime, timezone +from pathlib import Path + +from claude_client import ClaudeClient, ClaudeError +from market_data import build_snapshots +from signal_store import append_history, write_bias + +_PROJECT_ROOT = Path(__file__).resolve().parent.parent + +# --- Configuration via variables d'environnement --- +EXCHANGE = os.environ.get("ANALYZER_EXCHANGE", "binance") +PAIRS = os.environ.get("ANALYZER_PAIRS", "BTC/USDT,ETH/USDT,SOL/USDT,BNB/USDT").split(",") +TIMEFRAME = os.environ.get("ANALYZER_TIMEFRAME", "1h") +INTERVAL_S = int(os.environ.get("ANALYZER_INTERVAL_S", "3600")) # 1 h par défaut +MODEL = os.environ.get("ANALYZER_MODEL", "claude-sonnet-4-6") +MCP_CONFIG = os.environ.get("ANALYZER_MCP_CONFIG") # ex. "/app/.mcp.json" +ALLOWED_TOOLS = os.environ.get("ANALYZER_ALLOWED_TOOLS") # ex. "mcp__tradingview" +# Historique horodaté (pour backtester l'IA). Lu par AiBiasStrategy en backtest. +HISTORY_DIR = os.environ.get( + "ANALYZER_HISTORY_DIR", + str(_PROJECT_ROOT / "freqtrade" / "user_data" / "ai_bias_history"), +) + + +def _log(msg: str) -> None: + ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") + print(f"[analyzer {ts}] {msg}", flush=True) + + +def run_cycle(client: ClaudeClient) -> int: + pairs = [p.strip() for p in PAIRS if p.strip()] + _log(f"Instantané {EXCHANGE} {TIMEFRAME} pour {pairs}…") + snapshots = build_snapshots(EXCHANGE, pairs, timeframe=TIMEFRAME) + if not snapshots: + _log("Aucun instantané récupéré — cycle ignoré.") + return 0 + + _log(f"Appel Claude ({MODEL}) pour {len(snapshots)} paire(s)…") + batch = client.get_biases(snapshots) + + written = 0 + ts = datetime.now(timezone.utc) + for bias in batch.biases: + write_bias(bias) # état courant (live/dry-run) + append_history(bias, HISTORY_DIR, ts) # trace horodatée (backtest IA) + written += 1 + _log( + f" → {bias.pair}: {bias.direction} (conf={bias.confidence:.2f}) — {bias.rationale}" + ) + _log(f"{written} biais écrit(s) dans Redis + historique ({HISTORY_DIR}).") + return written + + +def main() -> None: + parser = argparse.ArgumentParser(description="MidasBot — analyzer IA") + parser.add_argument("--once", action="store_true", help="un seul cycle puis sortie") + args = parser.parse_args() + + client = ClaudeClient(model=MODEL, mcp_config=MCP_CONFIG, allowed_tools=ALLOWED_TOOLS) + + if args.once: + run_cycle(client) + return + + _log(f"Démarrage boucle (intervalle {INTERVAL_S}s).") + while True: + try: + run_cycle(client) + except ClaudeError as exc: + _log(f"Erreur Claude : {exc}") + except Exception as exc: # noqa: BLE001 + _log(f"Erreur inattendue : {exc}") + time.sleep(INTERVAL_S) + + +if __name__ == "__main__": + main() diff --git a/ai_analyzer/backfill.py b/ai_analyzer/backfill.py new file mode 100644 index 0000000..b8ab0e3 --- /dev/null +++ b/ai_analyzer/backfill.py @@ -0,0 +1,111 @@ +"""Backfill — génère un historique de biais IA pour backtester la stratégie. + +Parcourt l'historique des bougies à une cadence donnée et, à chaque pas de temps, +demande à Claude un biais en n'utilisant QUE les données disponibles jusqu'à ce +moment-là (pas de fuite du futur). Écrit chaque biais dans l'historique CSV avec +l'horodatage de la bougie correspondante. + +⚠️ COÛTEUX EN QUOTA D'ABONNEMENT : 1 appel Claude par pas de temps. + 6 mois × cadence 24 h ≈ 180 appels. Choisis une cadence raisonnable. + +Usage : + python backfill.py --start 20260101 --step-hours 24 + python backfill.py --start 20260101 --end 20260301 --step-hours 12 --pairs BTC/USDT,ETH/USDT +""" +from __future__ import annotations + +import argparse +import os +from datetime import datetime, timedelta, timezone + +import ccxt + +from claude_client import ClaudeClient +from market_data import snapshot_from_candles +from signal_store import append_history + +EXCHANGE = os.environ.get("ANALYZER_EXCHANGE", "binance") +MODEL = os.environ.get("ANALYZER_MODEL", "claude-sonnet-4-6") +HISTORY_DIR = os.environ.get( + "ANALYZER_HISTORY_DIR", + os.path.join(os.path.dirname(os.path.dirname(__file__)), + "freqtrade", "user_data", "ai_bias_history"), +) +WINDOW = 100 # nb de bougies fournies à Claude à chaque pas + + +def _parse_day(s: str) -> datetime: + return datetime.strptime(s, "%Y%m%d").replace(tzinfo=timezone.utc) + + +def _fetch_full(exchange, pair: str, timeframe: str, since_ms: int) -> list: + """Récupère tout l'OHLCV depuis `since_ms` (pagination ccxt).""" + out: list = [] + cursor = since_ms + while True: + batch = exchange.fetch_ohlcv(pair, timeframe=timeframe, since=cursor, limit=1000) + if not batch: + break + out += batch + cursor = batch[-1][0] + 1 + if len(batch) < 1000: + break + return out + + +def main() -> None: + p = argparse.ArgumentParser(description="MidasBot — backfill historique des biais IA") + p.add_argument("--pairs", default="BTC/USDT,ETH/USDT,SOL/USDT,BNB/USDT") + p.add_argument("--timeframe", default="1h") + p.add_argument("--start", required=True, help="AAAAMMJJ") + p.add_argument("--end", default=None, help="AAAAMMJJ (défaut: maintenant)") + p.add_argument("--step-hours", type=int, default=24, help="cadence d'analyse") + p.add_argument("--yes", action="store_true", help="ne pas demander confirmation") + args = p.parse_args() + + pairs = [x.strip() for x in args.pairs.split(",") if x.strip()] + tf = args.timeframe + start = _parse_day(args.start) + end = _parse_day(args.end) if args.end else datetime.now(timezone.utc) + step = timedelta(hours=args.step_hours) + n_steps = int((end - start) / step) + + print(f"Backfill {pairs} {tf} | {start.date()} → {end.date()} | " + f"pas {args.step_hours} h | ~{n_steps} appels Claude ({MODEL})") + if not args.yes: + if input("Continuer ? Ça consomme ton quota d'abonnement [y/N] ").strip().lower() != "y": + print("Annulé.") + return + + exchange = getattr(ccxt, EXCHANGE)({"enableRateLimit": True}) + since_ms = int(start.timestamp() * 1000) + # Récupère tout l'historique une fois par paire (puis on tranche par pas de temps). + full = {pair: _fetch_full(exchange, pair, tf, since_ms) for pair in pairs} + + client = ClaudeClient(model=MODEL) + t = start + done = 0 + while t <= end: + t_ms = int(t.timestamp() * 1000) + snaps = [] + for pair in pairs: + candles = [c for c in full[pair] if c[0] <= t_ms][-WINDOW:] + snap = snapshot_from_candles(pair, tf, candles) + if snap: + snaps.append(snap) + if snaps: + try: + batch = client.get_biases(snaps) + for bias in batch.biases: + append_history(bias, HISTORY_DIR, ts=t) + done += 1 + print(f" {t.isoformat()} → {len(batch.biases)} biais") + except Exception as exc: # noqa: BLE001 + print(f" {t.isoformat()} → erreur: {exc}") + t += step + + print(f"Terminé : {done} pas écrits dans {HISTORY_DIR}") + + +if __name__ == "__main__": + main() diff --git a/ai_analyzer/claude_client.py b/ai_analyzer/claude_client.py new file mode 100644 index 0000000..e7531cd --- /dev/null +++ b/ai_analyzer/claude_client.py @@ -0,0 +1,154 @@ +"""Client Claude headless via le CLI `claude` (authentifié par l'abonnement). + +On n'utilise PAS le SDK `anthropic` ni de clé API. On invoque `claude -p` en +sous-processus avec sortie structurée (`--json-schema`), ce qui consomme l'abonnement +Claude Code de l'utilisateur (pas de facturation au token). +""" +from __future__ import annotations + +import json +import os +import subprocess +from pathlib import Path +from typing import Optional + +from market_data import PairSnapshot +from models import MarketBias, MarketBiasBatch + +_HERE = Path(__file__).resolve().parent +_SYSTEM_PROMPT_FILE = _HERE / "system_prompt.md" + + +class ClaudeError(RuntimeError): + pass + + +class ClaudeClient: + def __init__( + self, + model: str = "claude-sonnet-4-6", + max_turns: int = 6, + mcp_config: Optional[str] = None, + allowed_tools: Optional[str] = None, + timeout_s: int = 180, + ) -> None: + self.model = model + self.max_turns = max_turns + # .mcp.json à la racine projet, chargé seulement si présent. + self.mcp_config = mcp_config + # Ex. "mcp__tradingview" pour autoriser uniquement les outils du serveur MCP. + self.allowed_tools = allowed_tools + self.timeout_s = timeout_s + self._guard_no_api_key() + + @staticmethod + def _guard_no_api_key() -> None: + # ANTHROPIC_API_KEY ferait basculer Claude sur la facturation au token. + if os.environ.get("ANTHROPIC_API_KEY"): + raise ClaudeError( + "ANTHROPIC_API_KEY est défini : on veut l'abonnement, pas l'API. " + "Retire cette variable (utilise CLAUDE_CODE_OAUTH_TOKEN)." + ) + + def _build_prompt(self, snapshots: list[PairSnapshot]) -> str: + payload = { + "instruction": "Produis un biais directionnel pour chaque paire ci-dessous.", + "market_snapshot": [s.to_dict() for s in snapshots], + "pairs": [s.pair for s in snapshots], + } + return ( + "Voici l'instantané de marché (JSON). Si le MCP TradingView est disponible, " + "tu peux l'utiliser pour confirmer (RSI/MACD/Bollinger).\n\n" + + json.dumps(payload, ensure_ascii=False) + ) + + def get_biases(self, snapshots: list[PairSnapshot]) -> MarketBiasBatch: + if not snapshots: + return MarketBiasBatch(biases=[]) + + system_prompt = ( + _SYSTEM_PROMPT_FILE.read_text(encoding="utf-8") + if _SYSTEM_PROMPT_FILE.exists() + else "" + ) + + cmd = [ + "claude", + "-p", + self._build_prompt(snapshots), + "--model", + self.model, + "--output-format", + "json", + "--json-schema", + json.dumps(MarketBiasBatch.json_schema()), + "--max-turns", + str(self.max_turns), + ] + if system_prompt: + cmd += ["--append-system-prompt", system_prompt] + if self.mcp_config: + cmd += ["--mcp-config", self.mcp_config, "--strict-mcp-config"] + if self.allowed_tools: + cmd += ["--allowedTools", self.allowed_tools] + else: + # Pas de MCP : on coupe les outils intégrés (analyse pure sur l'instantané). + cmd += [ + "--disallowedTools", + "Bash,Read,Write,Edit,WebSearch,WebFetch,Glob,Grep,Task", + ] + + try: + proc = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=self.timeout_s, + env=os.environ.copy(), + ) + except subprocess.TimeoutExpired as exc: + raise ClaudeError(f"claude -p a dépassé {self.timeout_s}s") from exc + + if proc.returncode != 0: + raise ClaudeError( + f"claude -p code {proc.returncode}: {proc.stderr.strip()[:500]}" + ) + + return self._parse(proc.stdout) + + @staticmethod + def _parse(stdout: str) -> MarketBiasBatch: + try: + envelope = json.loads(stdout) + except json.JSONDecodeError as exc: + raise ClaudeError(f"Sortie claude non-JSON: {stdout[:300]}") from exc + + if envelope.get("is_error"): + raise ClaudeError( + f"claude erreur ({envelope.get('subtype')}): " + f"{envelope.get('errors') or envelope.get('result')}" + ) + + # Préférence : sortie structurée validée par le schéma. + structured = envelope.get("structured_output") + if isinstance(structured, dict): + return MarketBiasBatch.model_validate(structured) + + # Repli : extraire un objet JSON du champ `result`. + result = envelope.get("result", "") + obj = _extract_json_object(result) + if obj is not None: + return MarketBiasBatch.model_validate(obj) + + raise ClaudeError(f"Pas de sortie structurée exploitable: {result[:300]}") + + +def _extract_json_object(text: str) -> Optional[dict]: + start = text.find("{") + end = text.rfind("}") + if start == -1 or end == -1 or end <= start: + return None + try: + return json.loads(text[start : end + 1]) + except json.JSONDecodeError: + return None diff --git a/ai_analyzer/market_data.py b/ai_analyzer/market_data.py new file mode 100644 index 0000000..ed4c732 --- /dev/null +++ b/ai_analyzer/market_data.py @@ -0,0 +1,92 @@ +"""Construction d'un instantané de marché compact à fournir à Claude. + +On reste volontairement léger : prix courant, variation, et quelques statistiques +dérivées des dernières bougies (pas de dépendance lourde type talib). Claude raisonne +sur ce résumé ; le MCP TradingView (optionnel) peut enrichir l'analyse côté Claude. +""" +from __future__ import annotations + +from dataclasses import dataclass, asdict +from typing import Optional + +import ccxt + + +@dataclass +class PairSnapshot: + pair: str + timeframe: str + last_price: float + change_pct_window: float # variation % sur la fenêtre observée + high_window: float + low_window: float + sma_fast: float + sma_slow: float + n_candles: int + + def to_dict(self) -> dict: + return asdict(self) + + +def _sma(values: list[float], period: int) -> Optional[float]: + if len(values) < period: + return None + return sum(values[-period:]) / period + + +def snapshot_from_candles( + pair: str, + timeframe: str, + ohlcv: list, + fast: int = 9, + slow: int = 21, +) -> Optional[PairSnapshot]: + """Calcule un résumé à partir d'une liste de bougies OHLCV (live OU historique).""" + if not ohlcv: + return None + closes = [c[4] for c in ohlcv] + highs = [c[2] for c in ohlcv] + lows = [c[3] for c in ohlcv] + last = closes[-1] + first = closes[0] + return PairSnapshot( + pair=pair, + timeframe=timeframe, + last_price=round(last, 6), + change_pct_window=round((last - first) / first * 100, 2) if first else 0.0, + high_window=round(max(highs), 6), + low_window=round(min(lows), 6), + sma_fast=round(_sma(closes, fast) or last, 6), + sma_slow=round(_sma(closes, slow) or last, 6), + n_candles=len(closes), + ) + + +def build_snapshot( + exchange_name: str, + pair: str, + timeframe: str = "1h", + limit: int = 100, + fast: int = 9, + slow: int = 21, +) -> Optional[PairSnapshot]: + """Récupère les dernières bougies via ccxt et calcule un résumé.""" + exchange_cls = getattr(ccxt, exchange_name) + exchange = exchange_cls({"enableRateLimit": True}) + try: + ohlcv = exchange.fetch_ohlcv(pair, timeframe=timeframe, limit=limit) + except Exception as exc: # noqa: BLE001 — on journalise et on ignore la paire + print(f"[market_data] échec fetch {pair}: {exc}") + return None + return snapshot_from_candles(pair, timeframe, ohlcv, fast=fast, slow=slow) + + +def build_snapshots( + exchange_name: str, pairs: list[str], timeframe: str = "1h", limit: int = 100 +) -> list[PairSnapshot]: + out = [] + for pair in pairs: + snap = build_snapshot(exchange_name, pair, timeframe=timeframe, limit=limit) + if snap: + out.append(snap) + return out diff --git a/ai_analyzer/models.py b/ai_analyzer/models.py new file mode 100644 index 0000000..dec4f91 --- /dev/null +++ b/ai_analyzer/models.py @@ -0,0 +1,58 @@ +"""Schémas de données partagés (Pydantic) pour la couche IA.""" +from __future__ import annotations + +from typing import Literal, Optional + +from pydantic import BaseModel, Field + +Direction = Literal["bullish", "bearish", "neutral"] + + +class MarketBias(BaseModel): + """Biais de marché produit par Claude pour une paire.""" + + pair: str = Field(..., description="Paire, ex. 'BTC/USDT'") + direction: Direction = Field(..., description="Sens du biais") + confidence: float = Field(..., ge=0.0, le=1.0, description="Confiance [0..1]") + rationale: str = Field(..., description="Justification courte et factuelle") + key_support: Optional[float] = Field(None, description="Support clé (prix)") + key_resistance: Optional[float] = Field(None, description="Résistance clé (prix)") + + +class MarketBiasBatch(BaseModel): + """Lot de biais (un appel Claude couvre toutes les paires).""" + + biases: list[MarketBias] + + def by_pair(self) -> dict[str, MarketBias]: + return {b.pair: b for b in self.biases} + + @staticmethod + def json_schema() -> dict: + """Schéma JSON passé à `claude -p --json-schema` (sous-ensemble supporté).""" + return { + "type": "object", + "additionalProperties": False, + "required": ["biases"], + "properties": { + "biases": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": False, + "required": ["pair", "direction", "confidence", "rationale"], + "properties": { + "pair": {"type": "string"}, + "direction": { + "type": "string", + "enum": ["bullish", "bearish", "neutral"], + }, + "confidence": {"type": "number"}, + "rationale": {"type": "string"}, + "key_support": {"type": "number"}, + "key_resistance": {"type": "number"}, + }, + }, + } + }, + } diff --git a/ai_analyzer/requirements.txt b/ai_analyzer/requirements.txt new file mode 100644 index 0000000..b314a78 --- /dev/null +++ b/ai_analyzer/requirements.txt @@ -0,0 +1,3 @@ +ccxt>=4.4 +redis>=5.0 +pydantic>=2.6 diff --git a/ai_analyzer/signal_store.py b/ai_analyzer/signal_store.py new file mode 100644 index 0000000..c4ac38a --- /dev/null +++ b/ai_analyzer/signal_store.py @@ -0,0 +1,89 @@ +"""Stockage des biais de marché. + +- Redis : état COURANT (lu par la stratégie en live/dry-run). TTL court. +- Historique CSV horodaté par paire : trace durable pour backtester l'IA + (la stratégie en mode backtest lit le biais valide à chaque bougie). +""" +from __future__ import annotations + +import csv +import json +import os +from datetime import datetime, timezone +from pathlib import Path +from typing import Optional + +import redis + +from models import MarketBias + +_KEY_PREFIX = "bias:" +_DEFAULT_TTL = 3 * 3600 # 3 h : un biais expire s'il n'est pas rafraîchi +_HISTORY_HEADER = ["timestamp", "direction", "confidence", "key_support", "key_resistance"] + + +def _client() -> redis.Redis: + url = os.environ.get("REDIS_URL", "redis://localhost:6379/0") + return redis.Redis.from_url(url, decode_responses=True) + + +def _key(pair: str) -> str: + return f"{_KEY_PREFIX}{pair}" + + +def write_bias(bias: MarketBias, ttl: int = _DEFAULT_TTL, r: Optional[redis.Redis] = None) -> None: + r = r or _client() + r.set(_key(bias.pair), bias.model_dump_json(), ex=ttl) + + +def read_bias(pair: str, r: Optional[redis.Redis] = None) -> Optional[MarketBias]: + r = r or _client() + raw = r.get(_key(pair)) + if not raw: + return None + try: + return MarketBias.model_validate_json(raw) + except Exception: # noqa: BLE001 — donnée corrompue : on l'ignore + return None + + +def _history_path(history_dir: str, pair: str) -> Path: + return Path(history_dir) / f"{pair.replace('/', '_')}.csv" + + +def append_history( + bias: MarketBias, history_dir: str, ts: Optional[datetime] = None +) -> None: + """Ajoute une ligne horodatée à l'historique CSV de la paire (créé si absent).""" + ts = ts or datetime.now(timezone.utc) + path = _history_path(history_dir, bias.pair) + path.parent.mkdir(parents=True, exist_ok=True) + write_header = not path.exists() + with path.open("a", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + if write_header: + writer.writerow(_HISTORY_HEADER) + writer.writerow( + [ + ts.isoformat(), + bias.direction, + bias.confidence, + bias.key_support if bias.key_support is not None else "", + bias.key_resistance if bias.key_resistance is not None else "", + ] + ) + + +def read_all(r: Optional[redis.Redis] = None) -> dict[str, MarketBias]: + r = r or _client() + out: dict[str, MarketBias] = {} + for key in r.scan_iter(f"{_KEY_PREFIX}*"): + raw = r.get(key) + if not raw: + continue + try: + bias = MarketBias.model_validate_json(raw) + out[bias.pair] = bias + except Exception: # noqa: BLE001 + continue + return out diff --git a/ai_analyzer/system_prompt.md b/ai_analyzer/system_prompt.md new file mode 100644 index 0000000..7594dd5 --- /dev/null +++ b/ai_analyzer/system_prompt.md @@ -0,0 +1,13 @@ +Tu es un analyste de marché crypto pour un bot de trading qui opère en simulation (dry-run). + +Ton rôle : à partir d'un instantané de marché compact (et, si disponibles, des outils TradingView via MCP : RSI, MACD, Bollinger, screening), produire un **biais directionnel** par paire. + +Règles : +- Sois factuel et prudent. Si le signal est ambigu, réponds `neutral` avec une confiance basse. +- `confidence` reflète la force/clarté du signal technique (0 = aucune conviction, 1 = signal très net). +- `rationale` : une phrase, concrète, citant les éléments observés (tendance, momentum, niveaux). Pas de blabla. +- Si des niveaux clairs existent, renseigne `key_support` / `key_resistance` (prix). +- N'invente pas de données. Tu n'exécutes aucun trade ; tu ne fais que qualifier le contexte. +- Couvre TOUTES les paires demandées, dans le même ordre. + +Réponds UNIQUEMENT via la sortie structurée demandée (schéma JSON), sans texte additionnel. diff --git a/arbitrage/Dockerfile b/arbitrage/Dockerfile new file mode 100644 index 0000000..b945e2b --- /dev/null +++ b/arbitrage/Dockerfile @@ -0,0 +1,9 @@ +# Service arbitrage CEX (dry-run) — autonome, sans Claude. +FROM python:3.11-slim + +WORKDIR /app +COPY arbitrage/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY arbitrage/ /app/ + +CMD ["python", "cex_arb.py"] diff --git a/arbitrage/cex_arb.py b/arbitrage/cex_arb.py new file mode 100644 index 0000000..4e4dfcd --- /dev/null +++ b/arbitrage/cex_arb.py @@ -0,0 +1,169 @@ +"""Module d'arbitrage inter-CEX (Phase 4) — DRY-RUN. + +Scanne en continu les écarts de prix d'une même paire entre plusieurs exchanges +centralisés (via ccxt) et journalise les opportunités nettes (après frais). +AUCUNE exécution réelle : on se contente de détecter et journaliser. + +L'interface `ArbStrategy` est abstraite pour brancher plus tard l'arbitrage +triangulaire ou DEX sans réécrire le scanner. + +Usage : + python cex_arb.py --once + python cex_arb.py # boucle continue +""" +from __future__ import annotations + +import argparse +import os +import time +from abc import ABC, abstractmethod +from dataclasses import dataclass +from datetime import datetime, timezone +from typing import Optional + +import ccxt + +# Frais taker par défaut (fraction). Ajuster selon ton palier réel sur chaque exchange. +DEFAULT_TAKER_FEE = { + "binance": 0.001, + "kraken": 0.0026, + "coinbase": 0.006, + "bybit": 0.001, +} + +EXCHANGES = os.environ.get("ARB_EXCHANGES", "binance,kraken").split(",") +PAIRS = os.environ.get("ARB_PAIRS", "BTC/USDT,ETH/USDT").split(",") +MIN_NET_SPREAD_PCT = float(os.environ.get("ARB_MIN_SPREAD_PCT", "0.2")) # seuil net % +INTERVAL_S = int(os.environ.get("ARB_INTERVAL_S", "30")) + + +def _log(msg: str) -> None: + ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") + print(f"[arb {ts}] {msg}", flush=True) + + +@dataclass +class ArbOpportunity: + pair: str + buy_exchange: str + sell_exchange: str + buy_price: float + sell_price: float + gross_spread_pct: float + net_spread_pct: float # après frais taker des deux côtés + + +class ArbStrategy(ABC): + """Interface d'une stratégie d'arbitrage (extensible : triangulaire, DEX…).""" + + @abstractmethod + def scan(self) -> list[ArbOpportunity]: # pragma: no cover - interface + ... + + +class CrossExchangeArb(ArbStrategy): + """Arbitrage 'spatial' : même paire, deux exchanges, acheter bas / vendre haut.""" + + def __init__(self, exchange_names: list[str], pairs: list[str]) -> None: + self.pairs = pairs + self.clients: dict[str, ccxt.Exchange] = {} + for name in exchange_names: + name = name.strip() + try: + self.clients[name] = getattr(ccxt, name)({"enableRateLimit": True}) + except Exception as exc: # noqa: BLE001 + _log(f"exchange '{name}' indisponible: {exc}") + + def _fee(self, exchange: str) -> float: + return DEFAULT_TAKER_FEE.get(exchange, 0.001) + + def _ticker(self, exchange: str, pair: str) -> Optional[dict]: + client = self.clients.get(exchange) + if client is None: + return None + try: + return client.fetch_ticker(pair) + except Exception: # noqa: BLE001 — paire absente / erreur réseau : on ignore + return None + + def scan(self) -> list[ArbOpportunity]: + opportunities: list[ArbOpportunity] = [] + names = list(self.clients.keys()) + + for pair in self.pairs: + # Récupère le prix (last) sur chaque exchange. + prices: dict[str, float] = {} + for ex in names: + t = self._ticker(ex, pair) + if t and t.get("last"): + prices[ex] = float(t["last"]) + + if len(prices) < 2: + continue + + # Compare toutes les paires d'exchanges dans les deux sens. + for buy_ex, buy_price in prices.items(): + for sell_ex, sell_price in prices.items(): + if buy_ex == sell_ex or sell_price <= buy_price: + continue + gross = (sell_price - buy_price) / buy_price * 100 + # Coût aller-retour : frais à l'achat + frais à la vente. + net = ( + sell_price * (1 - self._fee(sell_ex)) + - buy_price * (1 + self._fee(buy_ex)) + ) / buy_price * 100 + if net >= MIN_NET_SPREAD_PCT: + opportunities.append( + ArbOpportunity( + pair=pair, + buy_exchange=buy_ex, + sell_exchange=sell_ex, + buy_price=round(buy_price, 6), + sell_price=round(sell_price, 6), + gross_spread_pct=round(gross, 3), + net_spread_pct=round(net, 3), + ) + ) + return opportunities + + +def run_cycle(strategy: ArbStrategy) -> int: + opportunities = strategy.scan() + if not opportunities: + _log("aucune opportunité au-dessus du seuil.") + return 0 + for opp in opportunities: + _log( + f"💡 {opp.pair} : acheter {opp.buy_exchange} @ {opp.buy_price} → " + f"vendre {opp.sell_exchange} @ {opp.sell_price} | " + f"net {opp.net_spread_pct:.3f}% (brut {opp.gross_spread_pct:.3f}%) [DRY-RUN]" + ) + return len(opportunities) + + +def main() -> None: + parser = argparse.ArgumentParser(description="MidasBot — arbitrage CEX (dry-run)") + parser.add_argument("--once", action="store_true", help="un seul scan puis sortie") + args = parser.parse_args() + + pairs = [p.strip() for p in PAIRS if p.strip()] + strategy = CrossExchangeArb(EXCHANGES, pairs) + _log( + f"Scanner inter-CEX : {list(strategy.clients.keys())} | paires {pairs} | " + f"seuil net {MIN_NET_SPREAD_PCT}% | DRY-RUN" + ) + + if args.once: + run_cycle(strategy) + return + + while True: + try: + run_cycle(strategy) + except Exception as exc: # noqa: BLE001 + _log(f"erreur: {exc}") + time.sleep(INTERVAL_S) + + +if __name__ == "__main__": + main() diff --git a/arbitrage/requirements.txt b/arbitrage/requirements.txt new file mode 100644 index 0000000..92f49b8 --- /dev/null +++ b/arbitrage/requirements.txt @@ -0,0 +1 @@ +ccxt>=4.4 diff --git a/dashboard/Dockerfile b/dashboard/Dockerfile new file mode 100644 index 0000000..4679475 --- /dev/null +++ b/dashboard/Dockerfile @@ -0,0 +1,13 @@ +# Dashboard « Insights IA » (FastAPI). +FROM python:3.11-slim + +WORKDIR /app +COPY dashboard/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY dashboard/ /app/ + +ENV REDIS_URL=redis://redis:6379/0 \ + FREQUI_URL=http://127.0.0.1:8080 + +EXPOSE 8500 +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8500"] diff --git a/dashboard/app.py b/dashboard/app.py new file mode 100644 index 0000000..9a87de4 --- /dev/null +++ b/dashboard/app.py @@ -0,0 +1,113 @@ +"""Dashboard MidasBot — panneau « Insights IA ». + +Affiche les derniers biais de marché produits par Claude (lus depuis Redis) et +un lien vers FreqUI (positions / P&L / trades). Volontairement minimal et autonome +(pas de dépendance au package ai_analyzer). +""" +from __future__ import annotations + +import json +import os +from datetime import datetime, timezone + +import redis +from fastapi import FastAPI +from fastapi.responses import HTMLResponse, JSONResponse + +REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379/0") +FREQUI_URL = os.environ.get("FREQUI_URL", "http://127.0.0.1:8080") + +app = FastAPI(title="MidasBot Dashboard") + + +def _redis() -> redis.Redis: + return redis.Redis.from_url(REDIS_URL, decode_responses=True) + + +def _read_biases() -> list[dict]: + r = _redis() + out: list[dict] = [] + try: + for key in r.scan_iter("bias:*"): + raw = r.get(key) + if not raw: + continue + try: + out.append(json.loads(raw)) + except json.JSONDecodeError: + continue + except redis.RedisError: + return [] + out.sort(key=lambda b: b.get("pair", "")) + return out + + +@app.get("/api/biases") +def api_biases() -> JSONResponse: + return JSONResponse({"biases": _read_biases(), "generated_at": _now()}) + + +@app.get("/api/health") +def health() -> dict: + try: + _redis().ping() + redis_ok = True + except redis.RedisError: + redis_ok = False + return {"status": "ok", "redis": redis_ok} + + +def _now() -> str: + return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") + + +_COLORS = {"bullish": "#16a34a", "bearish": "#dc2626", "neutral": "#6b7280"} + + +@app.get("/", response_class=HTMLResponse) +def index() -> str: + biases = _read_biases() + if biases: + rows = "\n".join( + f""" + {b.get('pair','?')} + {b.get('direction','?')} + {float(b.get('confidence',0)):.0%} + {b.get('rationale','')} + {b.get('key_support','—')} + {b.get('key_resistance','—')} + """ + for b in biases + ) + else: + rows = 'Aucun biais en cache. Lance l\'analyzer (ai-analyzer).' + + return f""" + + + +MidasBot — Insights IA + + +

🪙 MidasBot — Insights IA

+
Biais de marché produits par Claude · {_now()} · rafraîchissement auto 30 s · DRY-RUN
+ + + {rows} +
PaireBiaisConfianceJustificationSupportRésistance
+ +""" diff --git a/dashboard/requirements.txt b/dashboard/requirements.txt new file mode 100644 index 0000000..d6c612b --- /dev/null +++ b/dashboard/requirements.txt @@ -0,0 +1,3 @@ +fastapi>=0.110 +uvicorn[standard]>=0.27 +redis>=5.0 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6ed0e4f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,92 @@ +# MidasBot — orchestration des services. +# Phase 1-2 : freqtrade + redis. ai-analyzer / dashboard ajoutés aux phases 3 et 5. +services: + freqtrade: + build: ./freqtrade # image officielle + client redis (cf. freqtrade/Dockerfile) + image: midas-freqtrade:latest + container_name: midas-freqtrade + restart: unless-stopped + depends_on: + - redis + volumes: + - "./freqtrade/user_data:/freqtrade/user_data" + ports: + - "127.0.0.1:8080:8080" # FreqUI / API server — exposé en local uniquement + env_file: + - .env + # Dry-run par défaut. La stratégie est passée en argument ; on démarre sur SampleStrategy + # (Phase 1) puis AiBiasStrategy (Phase 3). + command: > + trade + --logfile /freqtrade/user_data/logs/freqtrade.log + --db-url sqlite:////freqtrade/user_data/tradesv3.ichimoku.sqlite + --config /freqtrade/user_data/config_live.json + --strategy IchimokuLS + + # Service IA — nécessite CLAUDE_CODE_OAUTH_TOKEN dans .env (claude setup-token). + # Démarrage explicite : docker compose --profile ai up -d ai-analyzer + ai-analyzer: + build: + context: . + dockerfile: ai_analyzer/Dockerfile + image: midas-ai-analyzer:latest + container_name: midas-ai-analyzer + restart: unless-stopped + profiles: ["ai"] + depends_on: + - redis + env_file: + - .env + environment: + REDIS_URL: redis://redis:6379/0 + ANALYZER_PAIRS: "BTC/USDT,ETH/USDT,SOL/USDT,BNB/USDT" + ANALYZER_TIMEFRAME: "1h" + ANALYZER_INTERVAL_S: "3600" + ANALYZER_MODEL: "claude-sonnet-4-6" + # Décommenter pour activer le MCP TradingView (voir .mcp.json) : + # ANALYZER_MCP_CONFIG: "/app/.mcp.json" + # ANALYZER_ALLOWED_TOOLS: "mcp__tradingview" + + # Scanner d'arbitrage inter-CEX (dry-run). Démarrage : docker compose --profile arb up -d arbitrage + arbitrage: + build: + context: . + dockerfile: arbitrage/Dockerfile + image: midas-arbitrage:latest + container_name: midas-arbitrage + restart: unless-stopped + profiles: ["arb"] + environment: + ARB_EXCHANGES: "binance,kraken" + ARB_PAIRS: "BTC/USDT,ETH/USDT" + ARB_MIN_SPREAD_PCT: "0.2" + ARB_INTERVAL_S: "30" + + # Dashboard « Insights IA » (FastAPI) — lit les biais dans Redis. + dashboard: + build: + context: . + dockerfile: dashboard/Dockerfile + image: midas-dashboard:latest + container_name: midas-dashboard + restart: unless-stopped + depends_on: + - redis + ports: + - "127.0.0.1:8500:8500" + environment: + REDIS_URL: redis://redis:6379/0 + FREQUI_URL: http://127.0.0.1:8080 # ouvert dans le navigateur de l'hôte + + redis: + image: redis:7-alpine + container_name: midas-redis + restart: unless-stopped + command: ["redis-server", "--save", "60", "1", "--appendonly", "no"] + ports: + - "127.0.0.1:6380:6379" # hôte:6380 (6379 déjà pris) ; les conteneurs utilisent redis:6379 + volumes: + - redis-data:/data + +volumes: + redis-data: diff --git a/freqtrade/Dockerfile b/freqtrade/Dockerfile new file mode 100644 index 0000000..6fb9b44 --- /dev/null +++ b/freqtrade/Dockerfile @@ -0,0 +1,5 @@ +# Image Freqtrade + client Redis (pour qu'AiBiasStrategy lise les biais IA en live/dry-run). +FROM freqtradeorg/freqtrade:stable + +# Installé en tant qu'utilisateur ftuser (pattern recommandé par Freqtrade). +RUN pip install --user --no-cache-dir redis>=5.0 diff --git a/freqtrade/user_data/config.json b/freqtrade/user_data/config.json new file mode 100644 index 0000000..f8874fb --- /dev/null +++ b/freqtrade/user_data/config.json @@ -0,0 +1,80 @@ +{ + "$schema": "https://schema.freqtrade.io/schema.json", + "bot_name": "MidasBot", + "max_open_trades": 3, + "stake_currency": "USDT", + "stake_amount": 100, + "tradable_balance_ratio": 0.99, + "fiat_display_currency": "EUR", + "dry_run": true, + "dry_run_wallet": 1000, + "cancel_open_orders_on_exit": false, + "trading_mode": "spot", + "margin_mode": "", + "timeframe": "1h", + "unfilledtimeout": { + "entry": 10, + "exit": 10, + "exit_timeout_count": 0, + "unit": "minutes" + }, + "entry_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1, + "price_last_balance": 0.0, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "exit_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1 + }, + "exchange": { + "name": "binance", + "key": "", + "secret": "", + "ccxt_config": {}, + "ccxt_async_config": {}, + "pair_whitelist": [ + "BTC/USDT", + "ETH/USDT", + "SOL/USDT", + "BNB/USDT" + ], + "pair_blacklist": [ + ".*(BULL|BEAR|UP|DOWN)/.*", + ".*(USDC|TUSD|BUSD|DAI|FDUSD)/.*" + ] + }, + "pairlists": [ + { + "method": "StaticPairList" + } + ], + "telegram": { + "enabled": false, + "token": "", + "chat_id": "" + }, + "api_server": { + "enabled": true, + "listen_ip_address": "0.0.0.0", + "listen_port": 8080, + "verbosity": "error", + "enable_openapi": true, + "jwt_secret_key": "set-via-env-FREQTRADE__API_SERVER__JWT_SECRET_KEY", + "ws_token": "set-via-env-FREQTRADE__API_SERVER__WS_TOKEN", + "CORS_origins": [], + "username": "set-via-env", + "password": "set-via-env" + }, + "initial_state": "running", + "force_entry_enable": false, + "internals": { + "process_throttle_secs": 5 + } +} diff --git a/freqtrade/user_data/config_futures.json b/freqtrade/user_data/config_futures.json new file mode 100644 index 0000000..62b558e --- /dev/null +++ b/freqtrade/user_data/config_futures.json @@ -0,0 +1,80 @@ +{ + "$schema": "https://schema.freqtrade.io/schema.json", + "bot_name": "MidasBot", + "max_open_trades": 3, + "stake_currency": "USDT", + "stake_amount": 100, + "tradable_balance_ratio": 0.99, + "fiat_display_currency": "EUR", + "dry_run": true, + "dry_run_wallet": 1000, + "cancel_open_orders_on_exit": false, + "trading_mode": "futures", + "margin_mode": "isolated", + "timeframe": "1h", + "unfilledtimeout": { + "entry": 10, + "exit": 10, + "exit_timeout_count": 0, + "unit": "minutes" + }, + "entry_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1, + "price_last_balance": 0.0, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "exit_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1 + }, + "exchange": { + "name": "binance", + "key": "", + "secret": "", + "ccxt_config": {}, + "ccxt_async_config": {}, + "pair_whitelist": [ + "BTC/USDT:USDT", + "ETH/USDT:USDT", + "SOL/USDT:USDT", + "BNB/USDT:USDT" + ], + "pair_blacklist": [ + ".*(BULL|BEAR|UP|DOWN)/.*", + ".*(USDC|TUSD|BUSD|DAI|FDUSD)/.*" + ] + }, + "pairlists": [ + { + "method": "StaticPairList" + } + ], + "telegram": { + "enabled": false, + "token": "", + "chat_id": "" + }, + "api_server": { + "enabled": false, + "listen_ip_address": "0.0.0.0", + "listen_port": 8080, + "verbosity": "error", + "enable_openapi": true, + "jwt_secret_key": "set-via-env-FREQTRADE__API_SERVER__JWT_SECRET_KEY", + "ws_token": "set-via-env-FREQTRADE__API_SERVER__WS_TOKEN", + "CORS_origins": [], + "username": "set-via-env", + "password": "set-via-env" + }, + "initial_state": "running", + "force_entry_enable": false, + "internals": { + "process_throttle_secs": 5 + } +} \ No newline at end of file diff --git a/freqtrade/user_data/config_futures_multi.json b/freqtrade/user_data/config_futures_multi.json new file mode 100644 index 0000000..b07387a --- /dev/null +++ b/freqtrade/user_data/config_futures_multi.json @@ -0,0 +1,88 @@ +{ + "$schema": "https://schema.freqtrade.io/schema.json", + "bot_name": "MidasBot", + "max_open_trades": 9, + "stake_currency": "USDT", + "stake_amount": 100, + "tradable_balance_ratio": 0.99, + "fiat_display_currency": "EUR", + "dry_run": true, + "dry_run_wallet": 1000, + "cancel_open_orders_on_exit": false, + "trading_mode": "futures", + "margin_mode": "isolated", + "timeframe": "1h", + "unfilledtimeout": { + "entry": 10, + "exit": 10, + "exit_timeout_count": 0, + "unit": "minutes" + }, + "entry_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1, + "price_last_balance": 0.0, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "exit_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1 + }, + "exchange": { + "name": "binance", + "key": "", + "secret": "", + "ccxt_config": {}, + "ccxt_async_config": {}, + "pair_whitelist": [ + "BTC/USDT:USDT", + "ETH/USDT:USDT", + "SOL/USDT:USDT", + "BNB/USDT:USDT", + "XRP/USDT:USDT", + "ADA/USDT:USDT", + "AVAX/USDT:USDT", + "DOGE/USDT:USDT", + "LINK/USDT:USDT", + "DOT/USDT:USDT", + "LTC/USDT:USDT", + "TRX/USDT:USDT" + ], + "pair_blacklist": [ + ".*(BULL|BEAR|UP|DOWN)/.*", + ".*(USDC|TUSD|BUSD|DAI|FDUSD)/.*" + ] + }, + "pairlists": [ + { + "method": "StaticPairList" + } + ], + "telegram": { + "enabled": false, + "token": "", + "chat_id": "" + }, + "api_server": { + "enabled": false, + "listen_ip_address": "0.0.0.0", + "listen_port": 8080, + "verbosity": "error", + "enable_openapi": true, + "jwt_secret_key": "set-via-env-FREQTRADE__API_SERVER__JWT_SECRET_KEY", + "ws_token": "set-via-env-FREQTRADE__API_SERVER__WS_TOKEN", + "CORS_origins": [], + "username": "set-via-env", + "password": "set-via-env" + }, + "initial_state": "running", + "force_entry_enable": false, + "internals": { + "process_throttle_secs": 5 + } +} \ No newline at end of file diff --git a/freqtrade/user_data/config_ich.json b/freqtrade/user_data/config_ich.json new file mode 100644 index 0000000..9b96e58 --- /dev/null +++ b/freqtrade/user_data/config_ich.json @@ -0,0 +1,80 @@ +{ + "$schema": "https://schema.freqtrade.io/schema.json", + "bot_name": "MidasBot", + "max_open_trades": 3, + "stake_currency": "USDT", + "stake_amount": "unlimited", + "tradable_balance_ratio": 0.99, + "fiat_display_currency": "EUR", + "dry_run": true, + "dry_run_wallet": 100, + "cancel_open_orders_on_exit": false, + "trading_mode": "futures", + "margin_mode": "isolated", + "timeframe": "1h", + "unfilledtimeout": { + "entry": 10, + "exit": 10, + "exit_timeout_count": 0, + "unit": "minutes" + }, + "entry_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1, + "price_last_balance": 0.0, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "exit_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1 + }, + "exchange": { + "name": "binance", + "key": "", + "secret": "", + "ccxt_config": {}, + "ccxt_async_config": {}, + "pair_whitelist": [ + "BTC/USDT:USDT", + "ETH/USDT:USDT", + "SOL/USDT:USDT", + "BNB/USDT:USDT" + ], + "pair_blacklist": [ + ".*(BULL|BEAR|UP|DOWN)/.*", + ".*(USDC|TUSD|BUSD|DAI|FDUSD)/.*" + ] + }, + "pairlists": [ + { + "method": "StaticPairList" + } + ], + "telegram": { + "enabled": false, + "token": "", + "chat_id": "" + }, + "api_server": { + "enabled": false, + "listen_ip_address": "0.0.0.0", + "listen_port": 8080, + "verbosity": "error", + "enable_openapi": true, + "jwt_secret_key": "set-via-env-FREQTRADE__API_SERVER__JWT_SECRET_KEY", + "ws_token": "set-via-env-FREQTRADE__API_SERVER__WS_TOKEN", + "CORS_origins": [], + "username": "set-via-env", + "password": "set-via-env" + }, + "initial_state": "running", + "force_entry_enable": false, + "internals": { + "process_throttle_secs": 5 + } +} \ No newline at end of file diff --git a/freqtrade/user_data/config_ich15.json b/freqtrade/user_data/config_ich15.json new file mode 100644 index 0000000..05a31d3 --- /dev/null +++ b/freqtrade/user_data/config_ich15.json @@ -0,0 +1,80 @@ +{ + "$schema": "https://schema.freqtrade.io/schema.json", + "bot_name": "MidasBot", + "max_open_trades": 3, + "stake_currency": "USDT", + "stake_amount": "unlimited", + "tradable_balance_ratio": 0.99, + "fiat_display_currency": "EUR", + "dry_run": true, + "dry_run_wallet": 100, + "cancel_open_orders_on_exit": false, + "trading_mode": "futures", + "margin_mode": "isolated", + "timeframe": "15m", + "unfilledtimeout": { + "entry": 10, + "exit": 10, + "exit_timeout_count": 0, + "unit": "minutes" + }, + "entry_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1, + "price_last_balance": 0.0, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "exit_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1 + }, + "exchange": { + "name": "binance", + "key": "", + "secret": "", + "ccxt_config": {}, + "ccxt_async_config": {}, + "pair_whitelist": [ + "BTC/USDT:USDT", + "ETH/USDT:USDT", + "SOL/USDT:USDT", + "BNB/USDT:USDT" + ], + "pair_blacklist": [ + ".*(BULL|BEAR|UP|DOWN)/.*", + ".*(USDC|TUSD|BUSD|DAI|FDUSD)/.*" + ] + }, + "pairlists": [ + { + "method": "StaticPairList" + } + ], + "telegram": { + "enabled": false, + "token": "", + "chat_id": "" + }, + "api_server": { + "enabled": false, + "listen_ip_address": "0.0.0.0", + "listen_port": 8080, + "verbosity": "error", + "enable_openapi": true, + "jwt_secret_key": "set-via-env-FREQTRADE__API_SERVER__JWT_SECRET_KEY", + "ws_token": "set-via-env-FREQTRADE__API_SERVER__WS_TOKEN", + "CORS_origins": [], + "username": "set-via-env", + "password": "set-via-env" + }, + "initial_state": "running", + "force_entry_enable": false, + "internals": { + "process_throttle_secs": 5 + } +} \ No newline at end of file diff --git a/freqtrade/user_data/config_live.json b/freqtrade/user_data/config_live.json new file mode 100644 index 0000000..de9aa39 --- /dev/null +++ b/freqtrade/user_data/config_live.json @@ -0,0 +1,80 @@ +{ + "$schema": "https://schema.freqtrade.io/schema.json", + "bot_name": "MidasBot-Ichimoku", + "max_open_trades": 3, + "stake_currency": "USDT", + "stake_amount": 100, + "tradable_balance_ratio": 0.99, + "fiat_display_currency": "EUR", + "dry_run": true, + "dry_run_wallet": 1000, + "cancel_open_orders_on_exit": false, + "trading_mode": "futures", + "margin_mode": "isolated", + "timeframe": "1h", + "unfilledtimeout": { + "entry": 10, + "exit": 10, + "exit_timeout_count": 0, + "unit": "minutes" + }, + "entry_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1, + "price_last_balance": 0.0, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "exit_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1 + }, + "exchange": { + "name": "binance", + "key": "", + "secret": "", + "ccxt_config": {}, + "ccxt_async_config": {}, + "pair_whitelist": [ + "BTC/USDT:USDT", + "ETH/USDT:USDT", + "SOL/USDT:USDT", + "BNB/USDT:USDT" + ], + "pair_blacklist": [ + ".*(BULL|BEAR|UP|DOWN)/.*", + ".*(USDC|TUSD|BUSD|DAI|FDUSD)/.*" + ] + }, + "pairlists": [ + { + "method": "StaticPairList" + } + ], + "telegram": { + "enabled": false, + "token": "", + "chat_id": "" + }, + "api_server": { + "enabled": true, + "listen_ip_address": "0.0.0.0", + "listen_port": 8080, + "verbosity": "error", + "enable_openapi": true, + "jwt_secret_key": "set-via-env-FREQTRADE__API_SERVER__JWT_SECRET_KEY", + "ws_token": "set-via-env-FREQTRADE__API_SERVER__WS_TOKEN", + "CORS_origins": [], + "username": "set-via-env", + "password": "set-via-env" + }, + "initial_state": "running", + "force_entry_enable": false, + "internals": { + "process_throttle_secs": 5 + } +} \ No newline at end of file diff --git a/freqtrade/user_data/config_nfi.json b/freqtrade/user_data/config_nfi.json new file mode 100644 index 0000000..453e96a --- /dev/null +++ b/freqtrade/user_data/config_nfi.json @@ -0,0 +1,110 @@ +{ + "$schema": "https://schema.freqtrade.io/schema.json", + "bot_name": "MidasBot", + "max_open_trades": 6, + "stake_currency": "USDT", + "stake_amount": "unlimited", + "tradable_balance_ratio": 0.99, + "fiat_display_currency": "EUR", + "dry_run": true, + "dry_run_wallet": 1000, + "cancel_open_orders_on_exit": false, + "trading_mode": "spot", + "margin_mode": "", + "timeframe": "5m", + "unfilledtimeout": { + "entry": 10, + "exit": 10, + "exit_timeout_count": 0, + "unit": "minutes" + }, + "entry_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1, + "price_last_balance": 0.0, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "exit_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1 + }, + "exchange": { + "name": "binance", + "key": "", + "secret": "", + "ccxt_config": {}, + "ccxt_async_config": {}, + "pair_whitelist": [ + "BTC/USDT", + "ETH/USDT", + "SOL/USDT", + "BNB/USDT", + "XRP/USDT", + "ADA/USDT", + "AVAX/USDT", + "DOGE/USDT", + "LINK/USDT", + "DOT/USDT", + "LTC/USDT", + "TRX/USDT", + "ATOM/USDT", + "NEAR/USDT", + "APT/USDT", + "ARB/USDT", + "OP/USDT", + "FIL/USDT", + "INJ/USDT", + "SUI/USDT", + "UNI/USDT", + "AAVE/USDT", + "ETC/USDT", + "XLM/USDT", + "ALGO/USDT", + "VET/USDT", + "HBAR/USDT", + "RUNE/USDT", + "SAND/USDT", + "GALA/USDT" + ], + "pair_blacklist": [ + ".*(BULL|BEAR|UP|DOWN)/.*", + ".*(USDC|TUSD|BUSD|DAI|FDUSD)/.*" + ] + }, + "pairlists": [ + { + "method": "StaticPairList" + } + ], + "telegram": { + "enabled": false, + "token": "", + "chat_id": "" + }, + "api_server": { + "enabled": false, + "listen_ip_address": "0.0.0.0", + "listen_port": 8080, + "verbosity": "error", + "enable_openapi": true, + "jwt_secret_key": "set-via-env-FREQTRADE__API_SERVER__JWT_SECRET_KEY", + "ws_token": "set-via-env-FREQTRADE__API_SERVER__WS_TOKEN", + "CORS_origins": [], + "username": "set-via-env", + "password": "set-via-env" + }, + "initial_state": "running", + "force_entry_enable": false, + "internals": { + "process_throttle_secs": 5 + }, + "use_exit_signal": true, + "exit_profit_only": false, + "ignore_roi_if_entry_signal": true, + "position_adjustment_enable": true +} \ No newline at end of file diff --git a/freqtrade/user_data/nfix6-profit_max-MidasBot-binance-USDT-(backtest).json b/freqtrade/user_data/nfix6-profit_max-MidasBot-binance-USDT-(backtest).json new file mode 100644 index 0000000..ba256a3 --- /dev/null +++ b/freqtrade/user_data/nfix6-profit_max-MidasBot-binance-USDT-(backtest).json @@ -0,0 +1 @@ +{"AAVE/USDT":{"rate":73.39,"profit":-0.06942172132549185,"sell_reason":"exit_long_tc_stoploss_doom","time_profit_reached":"2026-06-23T14:45:00+00:00"},"NEAR/USDT":{"rate":1.996,"profit":-0.04442309198882848,"sell_reason":"exit_long_tc_stoploss_doom","time_profit_reached":"2026-06-23T14:45:00+00:00"}} \ No newline at end of file diff --git a/freqtrade/user_data/strategies/AiBiasStrategy.py b/freqtrade/user_data/strategies/AiBiasStrategy.py new file mode 100644 index 0000000..ae96b55 --- /dev/null +++ b/freqtrade/user_data/strategies/AiBiasStrategy.py @@ -0,0 +1,171 @@ +# 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 diff --git a/freqtrade/user_data/strategies/BBMeanRev.py b/freqtrade/user_data/strategies/BBMeanRev.py new file mode 100644 index 0000000..85c06ef --- /dev/null +++ b/freqtrade/user_data/strategies/BBMeanRev.py @@ -0,0 +1,82 @@ +# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods +""" +BBMeanRev — 2e moteur : mean-reversion (Bollinger + RSI), long/short futures. + +Logique CONTRAIRE au trend-follower (Ichimoku) → décorrélée par construction : + LONG quand le prix casse SOUS la bande basse + RSI survendu (rebond attendu). + SHORT quand le prix casse AU-DESSUS de la bande haute + RSI suracheté (repli attendu). + Sortie : retour à la moyenne (bande médiane) + ROI/stop. + +But : gagner dans les marchés en range (là où l'Ichimoku perd), pour qu'en +combinaison le ratio rendement/risque monte. Paramétrable pour hyperopt. +""" +from __future__ import annotations + +import talib.abstract as ta +from pandas import DataFrame + +from freqtrade.strategy import IStrategy, IntParameter + + +class BBMeanRev(IStrategy): + INTERFACE_VERSION = 3 + timeframe = "1h" + can_short = True + + # Mean-reversion = sorties rapides ; valeurs par défaut, surchargées par hyperopt. + minimal_roi = {"0": 0.03, "60": 0.02, "180": 0.01, "360": 0} + stoploss = -0.05 + trailing_stop = False + + startup_candle_count: int = 50 + process_only_new_candles = True + use_exit_signal = True + + buy_rsi = IntParameter(10, 40, default=30, space="buy", optimize=True) + sell_rsi = IntParameter(60, 90, default=70, space="sell", optimize=True) + + def leverage(self, pair, current_time, current_rate, proposed_leverage, + max_leverage, entry_tag, side, **kwargs) -> float: + return 1.0 + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + bb = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0) + dataframe["bb_lower"] = bb["lowerband"] + dataframe["bb_mid"] = bb["middleband"] + dataframe["bb_upper"] = bb["upperband"] + dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # LONG : survente (RSI bas) dans la zone basse des bandes + dataframe.loc[ + ( + (dataframe["rsi"] < self.buy_rsi.value) + & (dataframe["close"] < dataframe["bb_mid"]) + & (dataframe["volume"] > 0) + ), + "enter_long", + ] = 1 + # SHORT : surchauffe (RSI haut) dans la zone haute des bandes + dataframe.loc[ + ( + (dataframe["rsi"] > self.sell_rsi.value) + & (dataframe["close"] > dataframe["bb_mid"]) + & (dataframe["volume"] > 0) + ), + "enter_short", + ] = 1 + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # Sortie LONG : RSI revenu vers la moyenne (réversion réalisée) + dataframe.loc[ + ((dataframe["rsi"] > 50) & (dataframe["volume"] > 0)), + "exit_long", + ] = 1 + # Sortie SHORT : RSI revenu vers la moyenne + dataframe.loc[ + ((dataframe["rsi"] < 50) & (dataframe["volume"] > 0)), + "exit_short", + ] = 1 + return dataframe diff --git a/freqtrade/user_data/strategies/HyperStrategy.json b/freqtrade/user_data/strategies/HyperStrategy.json new file mode 100644 index 0000000..1b81190 --- /dev/null +++ b/freqtrade/user_data/strategies/HyperStrategy.json @@ -0,0 +1,34 @@ +{ + "strategy_name": "HyperStrategy", + "params": { + "max_open_trades": { + "max_open_trades": 3 + }, + "buy": { + "buy_adx_min": 25, + "buy_require_macd": true, + "buy_require_trend": false, + "buy_rsi_max": 78 + }, + "sell": { + "sell_rsi_min": 47 + }, + "roi": { + "0": 0.459, + "187": 0.06, + "693": 0.032, + "1425": 0 + }, + "stoploss": { + "stoploss": -0.137 + }, + "trailing": { + "trailing_stop": true, + "trailing_stop_positive": 0.016, + "trailing_stop_positive_offset": 0.053, + "trailing_only_offset_is_reached": true + } + }, + "ft_stratparam_v": 1, + "export_time": "2026-06-23 14:08:47.078605+00:00" +} \ No newline at end of file diff --git a/freqtrade/user_data/strategies/HyperStrategy.py b/freqtrade/user_data/strategies/HyperStrategy.py new file mode 100644 index 0000000..0cec41a --- /dev/null +++ b/freqtrade/user_data/strategies/HyperStrategy.py @@ -0,0 +1,82 @@ +# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods +""" +HyperStrategy — stratégie paramétrable pour optimisation (hyperopt). + +Indicateurs fixes (EMA/RSI/ADX/MACD), conditions d'entrée et gestion de sortie +PARAMÉTRÉES → Freqtrade peut optimiser les seuils, le ROI, le stoploss et le trailing. +Méthode honnête : on optimise sur une période d'entraînement puis on VALIDE sur une +période hors-échantillon (test) pour détecter le sur-apprentissage. +""" +from __future__ import annotations + +import talib.abstract as ta +from pandas import DataFrame + +from freqtrade.strategy import ( + IStrategy, + IntParameter, + BooleanParameter, +) + + +class HyperStrategy(IStrategy): + INTERFACE_VERSION = 3 + timeframe = "1h" + + # Valeurs par défaut — surchargées par les résultats d'hyperopt. + minimal_roi = {"0": 0.10, "240": 0.05, "720": 0.02, "1440": 0} + stoploss = -0.08 + trailing_stop = True + trailing_stop_positive = 0.02 + trailing_stop_positive_offset = 0.04 + trailing_only_offset_is_reached = True + + startup_candle_count: int = 60 + process_only_new_candles = True + use_exit_signal = True + + # --- Paramètres optimisables (espace "buy") --- + buy_rsi_max = IntParameter(60, 80, default=70, space="buy", optimize=True) + buy_adx_min = IntParameter(15, 40, default=25, space="buy", optimize=True) + buy_require_trend = BooleanParameter(default=True, space="buy", optimize=True) + buy_require_macd = BooleanParameter(default=True, space="buy", optimize=True) + + # --- Paramètres optimisables (espace "sell") --- + sell_rsi_min = IntParameter(20, 50, default=35, space="sell", optimize=True) + + 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["ema_trend"] = ta.EMA(dataframe, timeperiod=50) + dataframe["adx"] = ta.ADX(dataframe, timeperiod=14) + dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14) + macd = ta.MACD(dataframe) + dataframe["macd"] = macd["macd"] + dataframe["macdsignal"] = macd["macdsignal"] + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + cond = ( + (dataframe["ema_fast"] > dataframe["ema_slow"]) + & (dataframe["ema_fast"].shift(1) <= dataframe["ema_slow"].shift(1)) + & (dataframe["rsi"] < self.buy_rsi_max.value) + & (dataframe["adx"] > self.buy_adx_min.value) + & (dataframe["volume"] > 0) + ) + if self.buy_require_trend.value: + cond &= dataframe["close"] > dataframe["ema_trend"] + if self.buy_require_macd.value: + cond &= dataframe["macd"] > dataframe["macdsignal"] + dataframe.loc[cond, "enter_long"] = 1 + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + (dataframe["ema_fast"] < dataframe["ema_slow"]) + & (dataframe["rsi"] < self.sell_rsi_min.value) + & (dataframe["volume"] > 0) + ), + "exit_long", + ] = 1 + return dataframe diff --git a/freqtrade/user_data/strategies/IchimokuChop.json b/freqtrade/user_data/strategies/IchimokuChop.json new file mode 100644 index 0000000..c3ba504 --- /dev/null +++ b/freqtrade/user_data/strategies/IchimokuChop.json @@ -0,0 +1,31 @@ +{ + "strategy_name": "IchimokuChop", + "params": { + "max_open_trades": { + "max_open_trades": 3 + }, + "buy": { + "buy_adx_min": 30, + "buy_chop_max": 43, + "buy_cloud_min_pct": 0.57, + "require_tk_cross": false + }, + "roi": { + "0": 0.207, + "345": 0.144, + "1052": 0.048, + "2483": 0 + }, + "stoploss": { + "stoploss": -0.236 + }, + "trailing": { + "trailing_stop": true, + "trailing_stop_positive": 0.099, + "trailing_stop_positive_offset": 0.162, + "trailing_only_offset_is_reached": false + } + }, + "ft_stratparam_v": 1, + "export_time": "2026-06-23 16:58:02.232138+00:00" +} \ No newline at end of file diff --git a/freqtrade/user_data/strategies/IchimokuChop.py b/freqtrade/user_data/strategies/IchimokuChop.py new file mode 100644 index 0000000..5325ed7 --- /dev/null +++ b/freqtrade/user_data/strategies/IchimokuChop.py @@ -0,0 +1,115 @@ +# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods +""" +IchimokuChop — IchimokuHyper + FILTRE DE RÉGIME (Choppiness Index). + +Hypothèse : les folds perdants du walk-forward sont des marchés en RANGE (chop), +où un trend-follower se fait whipsaw. On ajoute un filtre Choppiness Index : + - CI élevé (>~61) = consolidation/range → on NE trade PAS. + - CI bas (<~38) = tendance forte → on trade. +Le seuil est optimisable. On valide ensuite en WALK-FORWARD (pas en hindsight). +""" +from __future__ import annotations + +import numpy as np +import talib.abstract as ta +from pandas import DataFrame + +from freqtrade.strategy import ( + IStrategy, + IntParameter, + DecimalParameter, + BooleanParameter, +) + + +class IchimokuChop(IStrategy): + INTERFACE_VERSION = 3 + timeframe = "1h" + can_short = True + + minimal_roi = {"0": 0.08, "240": 0.04, "720": 0.02, "1440": 0} + stoploss = -0.08 + trailing_stop = True + trailing_stop_positive = 0.02 + trailing_stop_positive_offset = 0.03 + trailing_only_offset_is_reached = True + + startup_candle_count: int = 220 + process_only_new_candles = True + use_exit_signal = True + + buy_adx_min = IntParameter(15, 40, default=25, space="buy", optimize=True) + buy_cloud_min_pct = DecimalParameter(0.0, 2.0, default=0.3, decimals=2, space="buy", optimize=True) + require_tk_cross = BooleanParameter(default=False, space="buy", optimize=True) + # Filtre de régime : ne trade que si Choppiness Index < seuil (= marché qui tend) + buy_chop_max = IntParameter(35, 70, default=61, space="buy", optimize=True) + + def leverage(self, pair, current_time, current_rate, proposed_leverage, + max_leverage, entry_tag, side, **kwargs) -> float: + return 1.0 + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + high, low, close = dataframe["high"], dataframe["low"], dataframe["close"] + tenkan = (high.rolling(9).max() + low.rolling(9).min()) / 2 + kijun = (high.rolling(26).max() + low.rolling(26).min()) / 2 + dataframe["tenkan"] = tenkan + dataframe["kijun"] = kijun + dataframe["senkou_a"] = ((tenkan + kijun) / 2).shift(26) + dataframe["senkou_b"] = ((high.rolling(52).max() + low.rolling(52).min()) / 2).shift(26) + dataframe["cloud_top"] = dataframe[["senkou_a", "senkou_b"]].max(axis=1) + dataframe["cloud_bot"] = dataframe[["senkou_a", "senkou_b"]].min(axis=1) + dataframe["cloud_width_pct"] = (dataframe["cloud_top"] - dataframe["cloud_bot"]) / close * 100 + dataframe["close_prev26"] = close.shift(26) + dataframe["adx"] = ta.ADX(dataframe, timeperiod=14) + + # --- Choppiness Index (14) --- + n = 14 + atr1 = ta.ATR(dataframe, timeperiod=1) + tr_sum = atr1.rolling(n).sum() + rng = high.rolling(n).max() - low.rolling(n).min() + dataframe["chop"] = 100 * np.log10(tr_sum / rng) / np.log10(n) + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + adx_min = self.buy_adx_min.value + cloud_min = self.buy_cloud_min_pct.value + not_choppy = dataframe["chop"] < self.buy_chop_max.value # ← filtre régime + + long_cond = ( + (dataframe["close"] > dataframe["cloud_top"]) + & (dataframe["tenkan"] > dataframe["kijun"]) + & (dataframe["close"] > dataframe["close_prev26"]) + & (dataframe["adx"] > adx_min) + & (dataframe["cloud_width_pct"] > cloud_min) + & not_choppy + & (dataframe["volume"] > 0) + ) + short_cond = ( + (dataframe["close"] < dataframe["cloud_bot"]) + & (dataframe["tenkan"] < dataframe["kijun"]) + & (dataframe["close"] < dataframe["close_prev26"]) + & (dataframe["adx"] > adx_min) + & (dataframe["cloud_width_pct"] > cloud_min) + & not_choppy + & (dataframe["volume"] > 0) + ) + if self.require_tk_cross.value: + long_cond &= dataframe["tenkan"].shift(1) <= dataframe["kijun"].shift(1) + short_cond &= dataframe["tenkan"].shift(1) >= dataframe["kijun"].shift(1) + + dataframe.loc[long_cond, "enter_long"] = 1 + dataframe.loc[short_cond, "enter_short"] = 1 + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + (((dataframe["tenkan"] < dataframe["kijun"]) | (dataframe["close"] < dataframe["cloud_bot"])) + & (dataframe["volume"] > 0)), + "exit_long", + ] = 1 + dataframe.loc[ + (((dataframe["tenkan"] > dataframe["kijun"]) | (dataframe["close"] > dataframe["cloud_top"])) + & (dataframe["volume"] > 0)), + "exit_short", + ] = 1 + return dataframe diff --git a/freqtrade/user_data/strategies/IchimokuHyper.best.json b/freqtrade/user_data/strategies/IchimokuHyper.best.json new file mode 100644 index 0000000..5549d6c --- /dev/null +++ b/freqtrade/user_data/strategies/IchimokuHyper.best.json @@ -0,0 +1,30 @@ +{ + "strategy_name": "IchimokuHyper", + "params": { + "max_open_trades": { + "max_open_trades": 3 + }, + "buy": { + "buy_adx_min": 29, + "buy_cloud_min_pct": 0.4, + "require_tk_cross": false + }, + "roi": { + "0": 0.139, + "206": 0.071, + "326": 0.025, + "1563": 0 + }, + "stoploss": { + "stoploss": -0.299 + }, + "trailing": { + "trailing_stop": true, + "trailing_stop_positive": 0.014, + "trailing_stop_positive_offset": 0.042, + "trailing_only_offset_is_reached": true + } + }, + "ft_stratparam_v": 1, + "export_time": "2026-06-23 15:52:37.295448+00:00" +} \ No newline at end of file diff --git a/freqtrade/user_data/strategies/IchimokuHyper.json b/freqtrade/user_data/strategies/IchimokuHyper.json new file mode 100644 index 0000000..2f4d79b --- /dev/null +++ b/freqtrade/user_data/strategies/IchimokuHyper.json @@ -0,0 +1,30 @@ +{ + "strategy_name": "IchimokuHyper", + "params": { + "max_open_trades": { + "max_open_trades": 3 + }, + "buy": { + "buy_adx_min": 30, + "buy_cloud_min_pct": 1.37, + "require_tk_cross": false + }, + "roi": { + "0": 0.465, + "302": 0.171, + "728": 0.08, + "2097": 0 + }, + "stoploss": { + "stoploss": -0.174 + }, + "trailing": { + "trailing_stop": true, + "trailing_stop_positive": 0.043, + "trailing_stop_positive_offset": 0.115, + "trailing_only_offset_is_reached": true + } + }, + "ft_stratparam_v": 1, + "export_time": "2026-06-23 16:43:16.595085+00:00" +} \ No newline at end of file diff --git a/freqtrade/user_data/strategies/IchimokuHyper.py b/freqtrade/user_data/strategies/IchimokuHyper.py new file mode 100644 index 0000000..bd16c12 --- /dev/null +++ b/freqtrade/user_data/strategies/IchimokuHyper.py @@ -0,0 +1,118 @@ +# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods +""" +IchimokuHyper — version paramétrable d'IchimokuStrategy pour hyperopt (futures, long/short). + +Paramètres optimisables : + - buy_adx_min : force de tendance minimale + - buy_cloud_min_pct : largeur minimale du nuage (anti-chop) en % du prix + - require_tk_cross : exiger le croisement Tenkan/Kijun (sinon simple position vs nuage) ++ espaces ROI / stoploss / trailing. + +Anti-lookahead : Senkou décalés de +26 ; confirmation via close vs close[-26]. +""" +from __future__ import annotations + +import talib.abstract as ta +from pandas import DataFrame + +from freqtrade.strategy import ( + IStrategy, + IntParameter, + DecimalParameter, + BooleanParameter, +) + + +class IchimokuHyper(IStrategy): + INTERFACE_VERSION = 3 + timeframe = "1h" + can_short = True + + minimal_roi = {"0": 0.06, "240": 0.03, "720": 0.01, "1440": 0} + stoploss = -0.08 + trailing_stop = True + trailing_stop_positive = 0.025 + trailing_stop_positive_offset = 0.04 + trailing_only_offset_is_reached = True + + startup_candle_count: int = 120 + process_only_new_candles = True + use_exit_signal = True + + # --- Paramètres optimisables (espace "buy", appliqués long ET short) --- + buy_adx_min = IntParameter(15, 40, default=20, space="buy", optimize=True) + buy_cloud_min_pct = DecimalParameter( + 0.0, 2.0, default=0.0, decimals=2, space="buy", optimize=True + ) + require_tk_cross = BooleanParameter(default=True, space="buy", optimize=True) + + def leverage(self, pair, current_time, current_rate, proposed_leverage, + max_leverage, entry_tag, side, **kwargs) -> float: + return 1.0 + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + high, low, close = dataframe["high"], dataframe["low"], dataframe["close"] + tenkan = (high.rolling(9).max() + low.rolling(9).min()) / 2 + kijun = (high.rolling(26).max() + low.rolling(26).min()) / 2 + dataframe["tenkan"] = tenkan + dataframe["kijun"] = kijun + dataframe["senkou_a"] = ((tenkan + kijun) / 2).shift(26) + dataframe["senkou_b"] = ( + (high.rolling(52).max() + low.rolling(52).min()) / 2 + ).shift(26) + dataframe["cloud_top"] = dataframe[["senkou_a", "senkou_b"]].max(axis=1) + dataframe["cloud_bot"] = dataframe[["senkou_a", "senkou_b"]].min(axis=1) + # Largeur du nuage en % du prix (filtre anti-chop) + dataframe["cloud_width_pct"] = ( + (dataframe["cloud_top"] - dataframe["cloud_bot"]) / close * 100 + ) + dataframe["close_prev26"] = close.shift(26) + dataframe["adx"] = ta.ADX(dataframe, timeperiod=14) + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + adx_min = self.buy_adx_min.value + cloud_min = self.buy_cloud_min_pct.value + + long_cond = ( + (dataframe["close"] > dataframe["cloud_top"]) + & (dataframe["tenkan"] > dataframe["kijun"]) + & (dataframe["close"] > dataframe["close_prev26"]) + & (dataframe["adx"] > adx_min) + & (dataframe["cloud_width_pct"] > cloud_min) + & (dataframe["volume"] > 0) + ) + short_cond = ( + (dataframe["close"] < dataframe["cloud_bot"]) + & (dataframe["tenkan"] < dataframe["kijun"]) + & (dataframe["close"] < dataframe["close_prev26"]) + & (dataframe["adx"] > adx_min) + & (dataframe["cloud_width_pct"] > cloud_min) + & (dataframe["volume"] > 0) + ) + if self.require_tk_cross.value: + long_cond &= dataframe["tenkan"].shift(1) <= dataframe["kijun"].shift(1) + short_cond &= dataframe["tenkan"].shift(1) >= dataframe["kijun"].shift(1) + + dataframe.loc[long_cond, "enter_long"] = 1 + dataframe.loc[short_cond, "enter_short"] = 1 + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + ((dataframe["tenkan"] < dataframe["kijun"]) + | (dataframe["close"] < dataframe["cloud_bot"])) + & (dataframe["volume"] > 0) + ), + "exit_long", + ] = 1 + dataframe.loc[ + ( + ((dataframe["tenkan"] > dataframe["kijun"]) + | (dataframe["close"] > dataframe["cloud_top"])) + & (dataframe["volume"] > 0) + ), + "exit_short", + ] = 1 + return dataframe diff --git a/freqtrade/user_data/strategies/IchimokuHyper2.json b/freqtrade/user_data/strategies/IchimokuHyper2.json new file mode 100644 index 0000000..58b49cd --- /dev/null +++ b/freqtrade/user_data/strategies/IchimokuHyper2.json @@ -0,0 +1,31 @@ +{ + "strategy_name": "IchimokuHyper2", + "params": { + "max_open_trades": { + "max_open_trades": 3 + }, + "buy": { + "buy_adx_min": 31, + "buy_cloud_min_pct": 0.48, + "require_tk_cross": true, + "use_macro": false + }, + "roi": { + "0": 0.088, + "20": 0.066, + "63": 0.053, + "180": 0 + }, + "stoploss": { + "stoploss": -0.095 + }, + "trailing": { + "trailing_stop": true, + "trailing_stop_positive": 0.046, + "trailing_stop_positive_offset": 0.08, + "trailing_only_offset_is_reached": true + } + }, + "ft_stratparam_v": 1, + "export_time": "2026-06-23 15:17:56.904527+00:00" +} \ No newline at end of file diff --git a/freqtrade/user_data/strategies/IchimokuHyper2.py b/freqtrade/user_data/strategies/IchimokuHyper2.py new file mode 100644 index 0000000..162c8f5 --- /dev/null +++ b/freqtrade/user_data/strategies/IchimokuHyper2.py @@ -0,0 +1,158 @@ +# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods +""" +IchimokuHyper2 — Ichimoku long/short hyperoptable, AVEC contraintes de risque. + +Améliorations vs IchimokuHyper : + - Filtre macro EMA200 optimisable (use_macro). + - STOP-LOSS CONTRAINT à une plage réaliste [-10% .. -2%] (le -23% précédent était le + vrai point faible : trop large pour envisager le levier). + - ROI plafonné à des valeurs réalistes. +Optimisation orientée robustesse (Sharpe) + train/test strict pour éviter l'overfit. +""" +from __future__ import annotations + +import talib.abstract as ta +from pandas import DataFrame + +from freqtrade.strategy import ( + IStrategy, + IntParameter, + DecimalParameter, + BooleanParameter, +) +from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal + + +class IchimokuHyper2(IStrategy): + INTERFACE_VERSION = 3 + timeframe = "1h" + can_short = True + + # Défauts (surchargés par hyperopt) + minimal_roi = {"0": 0.08, "240": 0.04, "720": 0.02, "1440": 0} + stoploss = -0.06 + trailing_stop = True + trailing_stop_positive = 0.02 + trailing_stop_positive_offset = 0.03 + trailing_only_offset_is_reached = True + + startup_candle_count: int = 220 + process_only_new_candles = True + use_exit_signal = True + + buy_adx_min = IntParameter(15, 40, default=25, space="buy", optimize=True) + buy_cloud_min_pct = DecimalParameter( + 0.0, 2.0, default=0.3, decimals=2, space="buy", optimize=True + ) + require_tk_cross = BooleanParameter(default=False, space="buy", optimize=True) + use_macro = BooleanParameter(default=True, space="buy", optimize=True) + + # --- Espaces hyperopt CONTRAINTS (le coeur de cette optimisation) --- + class HyperOpt: + @staticmethod + def stoploss_space() -> list[Dimension]: + # Stop réaliste : entre -2% et -10% (fini le -23%). + return [SKDecimal(-0.10, -0.02, decimals=3, name="stoploss")] + + @staticmethod + def roi_space() -> list[Dimension]: + return [ + Integer(0, 120, name="roi_t1"), + Integer(0, 60, name="roi_t2"), + Integer(0, 30, name="roi_t3"), + SKDecimal(0.02, 0.12, decimals=3, name="roi_p1"), + SKDecimal(0.01, 0.06, decimals=3, name="roi_p2"), + SKDecimal(0.005, 0.03, decimals=3, name="roi_p3"), + ] + + @staticmethod + def generate_roi_table(params: dict) -> dict[int, float]: + roi = {} + roi[0] = params["roi_p1"] + params["roi_p2"] + params["roi_p3"] + roi[params["roi_t3"]] = params["roi_p1"] + params["roi_p2"] + roi[params["roi_t3"] + params["roi_t2"]] = params["roi_p1"] + roi[params["roi_t3"] + params["roi_t2"] + params["roi_t1"]] = 0 + return roi + + @staticmethod + def trailing_space() -> list[Dimension]: + return [ + Categorical([True], name="trailing_stop"), + SKDecimal(0.01, 0.06, decimals=3, name="trailing_stop_positive"), + SKDecimal(0.005, 0.05, decimals=3, name="trailing_stop_positive_offset_p1"), + Categorical([True, False], name="trailing_only_offset_is_reached"), + ] + + def leverage(self, pair, current_time, current_rate, proposed_leverage, + max_leverage, entry_tag, side, **kwargs) -> float: + return 1.0 + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + high, low, close = dataframe["high"], dataframe["low"], dataframe["close"] + tenkan = (high.rolling(9).max() + low.rolling(9).min()) / 2 + kijun = (high.rolling(26).max() + low.rolling(26).min()) / 2 + dataframe["tenkan"] = tenkan + dataframe["kijun"] = kijun + dataframe["senkou_a"] = ((tenkan + kijun) / 2).shift(26) + dataframe["senkou_b"] = ( + (high.rolling(52).max() + low.rolling(52).min()) / 2 + ).shift(26) + dataframe["cloud_top"] = dataframe[["senkou_a", "senkou_b"]].max(axis=1) + dataframe["cloud_bot"] = dataframe[["senkou_a", "senkou_b"]].min(axis=1) + dataframe["cloud_width_pct"] = ( + (dataframe["cloud_top"] - dataframe["cloud_bot"]) / close * 100 + ) + dataframe["close_prev26"] = close.shift(26) + dataframe["adx"] = ta.ADX(dataframe, timeperiod=14) + dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200) + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + adx_min = self.buy_adx_min.value + cloud_min = self.buy_cloud_min_pct.value + + long_cond = ( + (dataframe["close"] > dataframe["cloud_top"]) + & (dataframe["tenkan"] > dataframe["kijun"]) + & (dataframe["close"] > dataframe["close_prev26"]) + & (dataframe["adx"] > adx_min) + & (dataframe["cloud_width_pct"] > cloud_min) + & (dataframe["volume"] > 0) + ) + short_cond = ( + (dataframe["close"] < dataframe["cloud_bot"]) + & (dataframe["tenkan"] < dataframe["kijun"]) + & (dataframe["close"] < dataframe["close_prev26"]) + & (dataframe["adx"] > adx_min) + & (dataframe["cloud_width_pct"] > cloud_min) + & (dataframe["volume"] > 0) + ) + if self.require_tk_cross.value: + long_cond &= dataframe["tenkan"].shift(1) <= dataframe["kijun"].shift(1) + short_cond &= dataframe["tenkan"].shift(1) >= dataframe["kijun"].shift(1) + if self.use_macro.value: + long_cond &= dataframe["close"] > dataframe["ema200"] + short_cond &= dataframe["close"] < dataframe["ema200"] + + dataframe.loc[long_cond, "enter_long"] = 1 + dataframe.loc[short_cond, "enter_short"] = 1 + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + ((dataframe["tenkan"] < dataframe["kijun"]) + | (dataframe["close"] < dataframe["cloud_bot"])) + & (dataframe["volume"] > 0) + ), + "exit_long", + ] = 1 + dataframe.loc[ + ( + ((dataframe["tenkan"] > dataframe["kijun"]) + | (dataframe["close"] > dataframe["cloud_top"])) + & (dataframe["volume"] > 0) + ), + "exit_short", + ] = 1 + return dataframe diff --git a/freqtrade/user_data/strategies/IchimokuHyper3.json b/freqtrade/user_data/strategies/IchimokuHyper3.json new file mode 100644 index 0000000..60aa31b --- /dev/null +++ b/freqtrade/user_data/strategies/IchimokuHyper3.json @@ -0,0 +1,36 @@ +{ + "strategy_name": "IchimokuHyper3", + "params": { + "max_open_trades": { + "max_open_trades": 3 + }, + "buy": { + "buy_adx_min": 29, + "buy_cloud_min_pct": 0.56, + "p_kijun": 29, + "p_mom": 27, + "p_senkou": 72, + "p_shift": 23, + "p_tenkan": 14, + "require_tk_cross": false, + "use_macro": false + }, + "roi": { + "0": 0.353, + "401": 0.176, + "925": 0.079, + "2340": 0 + }, + "stoploss": { + "stoploss": -0.209 + }, + "trailing": { + "trailing_stop": true, + "trailing_stop_positive": 0.287, + "trailing_stop_positive_offset": 0.326, + "trailing_only_offset_is_reached": true + } + }, + "ft_stratparam_v": 1, + "export_time": "2026-06-23 15:41:09.034742+00:00" +} \ No newline at end of file diff --git a/freqtrade/user_data/strategies/IchimokuHyper3.py b/freqtrade/user_data/strategies/IchimokuHyper3.py new file mode 100644 index 0000000..a4778eb --- /dev/null +++ b/freqtrade/user_data/strategies/IchimokuHyper3.py @@ -0,0 +1,117 @@ +# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods +""" +IchimokuHyper3 — espace de paramètres ÉLARGI (périodes Ichimoku optimisables). + +Round 3 de la boucle d'optimisation : on donne plus de liberté à l'optimiseur +(Tenkan/Kijun/Senkou B + lookback momentum + filtres) pour pousser le gain. +⚠️ Plus de paramètres = plus de capacité d'overfitting (le gain in-sample monte, +mais l'OOS sera à surveiller). +""" +from __future__ import annotations + +import talib.abstract as ta +from pandas import DataFrame + +from freqtrade.strategy import ( + IStrategy, + IntParameter, + DecimalParameter, + BooleanParameter, +) + + +class IchimokuHyper3(IStrategy): + INTERFACE_VERSION = 3 + timeframe = "1h" + can_short = True + + minimal_roi = {"0": 0.08, "240": 0.04, "720": 0.02, "1440": 0} + stoploss = -0.08 + trailing_stop = True + trailing_stop_positive = 0.02 + trailing_stop_positive_offset = 0.03 + trailing_only_offset_is_reached = True + + startup_candle_count: int = 260 + process_only_new_candles = True + use_exit_signal = True + + # Périodes Ichimoku optimisables + p_tenkan = IntParameter(5, 20, default=9, space="buy", optimize=True) + p_kijun = IntParameter(15, 45, default=26, space="buy", optimize=True) + p_senkou = IntParameter(40, 90, default=52, space="buy", optimize=True) + p_shift = IntParameter(15, 40, default=26, space="buy", optimize=True) + p_mom = IntParameter(10, 40, default=26, space="buy", optimize=True) + # Filtres + buy_adx_min = IntParameter(10, 40, default=20, space="buy", optimize=True) + buy_cloud_min_pct = DecimalParameter(0.0, 2.0, default=0.3, decimals=2, space="buy", optimize=True) + require_tk_cross = BooleanParameter(default=False, space="buy", optimize=True) + use_macro = BooleanParameter(default=False, space="buy", optimize=True) + + def leverage(self, pair, current_time, current_rate, proposed_leverage, + max_leverage, entry_tag, side, **kwargs) -> float: + return 1.0 + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + high, low, close = dataframe["high"], dataframe["low"], dataframe["close"] + t, k, s, sh = ( + self.p_tenkan.value, self.p_kijun.value, + self.p_senkou.value, self.p_shift.value, + ) + tenkan = (high.rolling(t).max() + low.rolling(t).min()) / 2 + kijun = (high.rolling(k).max() + low.rolling(k).min()) / 2 + dataframe["tenkan"] = tenkan + dataframe["kijun"] = kijun + dataframe["senkou_a"] = ((tenkan + kijun) / 2).shift(sh) + dataframe["senkou_b"] = ((high.rolling(s).max() + low.rolling(s).min()) / 2).shift(sh) + dataframe["cloud_top"] = dataframe[["senkou_a", "senkou_b"]].max(axis=1) + dataframe["cloud_bot"] = dataframe[["senkou_a", "senkou_b"]].min(axis=1) + dataframe["cloud_width_pct"] = ( + (dataframe["cloud_top"] - dataframe["cloud_bot"]) / close * 100 + ) + dataframe["close_prev"] = close.shift(self.p_mom.value) + dataframe["adx"] = ta.ADX(dataframe, timeperiod=14) + dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200) + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + adx_min = self.buy_adx_min.value + cloud_min = self.buy_cloud_min_pct.value + long_cond = ( + (dataframe["close"] > dataframe["cloud_top"]) + & (dataframe["tenkan"] > dataframe["kijun"]) + & (dataframe["close"] > dataframe["close_prev"]) + & (dataframe["adx"] > adx_min) + & (dataframe["cloud_width_pct"] > cloud_min) + & (dataframe["volume"] > 0) + ) + short_cond = ( + (dataframe["close"] < dataframe["cloud_bot"]) + & (dataframe["tenkan"] < dataframe["kijun"]) + & (dataframe["close"] < dataframe["close_prev"]) + & (dataframe["adx"] > adx_min) + & (dataframe["cloud_width_pct"] > cloud_min) + & (dataframe["volume"] > 0) + ) + if self.require_tk_cross.value: + long_cond &= dataframe["tenkan"].shift(1) <= dataframe["kijun"].shift(1) + short_cond &= dataframe["tenkan"].shift(1) >= dataframe["kijun"].shift(1) + if self.use_macro.value: + long_cond &= dataframe["close"] > dataframe["ema200"] + short_cond &= dataframe["close"] < dataframe["ema200"] + dataframe.loc[long_cond, "enter_long"] = 1 + dataframe.loc[short_cond, "enter_short"] = 1 + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + (((dataframe["tenkan"] < dataframe["kijun"]) | (dataframe["close"] < dataframe["cloud_bot"])) + & (dataframe["volume"] > 0)), + "exit_long", + ] = 1 + dataframe.loc[ + (((dataframe["tenkan"] > dataframe["kijun"]) | (dataframe["close"] > dataframe["cloud_top"])) + & (dataframe["volume"] > 0)), + "exit_short", + ] = 1 + return dataframe diff --git a/freqtrade/user_data/strategies/IchimokuHyperVol.json b/freqtrade/user_data/strategies/IchimokuHyperVol.json new file mode 100644 index 0000000..eca4e86 --- /dev/null +++ b/freqtrade/user_data/strategies/IchimokuHyperVol.json @@ -0,0 +1,30 @@ +{ + "strategy_name": "IchimokuHyperVol", + "params": { + "max_open_trades": { + "max_open_trades": 3 + }, + "buy": { + "buy_adx_min": 34, + "buy_cloud_min_pct": 0.41, + "require_tk_cross": false + }, + "roi": { + "0": 0.435, + "342": 0.209, + "509": 0.055, + "1579": 0 + }, + "stoploss": { + "stoploss": -0.192 + }, + "trailing": { + "trailing_stop": true, + "trailing_stop_positive": 0.251, + "trailing_stop_positive_offset": 0.255, + "trailing_only_offset_is_reached": false + } + }, + "ft_stratparam_v": 1, + "export_time": "2026-06-23 17:08:14.826029+00:00" +} \ No newline at end of file diff --git a/freqtrade/user_data/strategies/IchimokuHyperVol.py b/freqtrade/user_data/strategies/IchimokuHyperVol.py new file mode 100644 index 0000000..89ff471 --- /dev/null +++ b/freqtrade/user_data/strategies/IchimokuHyperVol.py @@ -0,0 +1,126 @@ +# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods +""" +IchimokuHyperVol — IchimokuHyper + VOLATILITY TARGETING (sizing dynamique). + +Même logique d'entrée/sortie qu'IchimokuHyper. Seule différence : la TAILLE de +position s'ajuste à l'inverse de la volatilité récente (ATR%) : + vol haute → position réduite → pertes bornées (notamment dans le chop volatil) + vol normale→ pleine taille → gains préservés en tendance + +Contrôle de risque DYNAMIQUE et non-prédictif (pas de filtre de régime overfit). +Référence de vol = médiane glissante de l'ATR% (trailing → pas de fuite du futur). +""" +from __future__ import annotations + +import numpy as np +import talib.abstract as ta +from pandas import DataFrame + +from freqtrade.strategy import ( + IStrategy, + IntParameter, + DecimalParameter, + BooleanParameter, +) + + +class IchimokuHyperVol(IStrategy): + INTERFACE_VERSION = 3 + timeframe = "1h" + can_short = True + + minimal_roi = {"0": 0.08, "240": 0.04, "720": 0.02, "1440": 0} + stoploss = -0.08 + trailing_stop = True + trailing_stop_positive = 0.02 + trailing_stop_positive_offset = 0.03 + trailing_only_offset_is_reached = True + + startup_candle_count: int = 520 # médiane ATR% sur 500 + Ichimoku + process_only_new_candles = True + use_exit_signal = True + + buy_adx_min = IntParameter(15, 40, default=25, space="buy", optimize=True) + buy_cloud_min_pct = DecimalParameter(0.0, 2.0, default=0.3, decimals=2, space="buy", optimize=True) + require_tk_cross = BooleanParameter(default=False, space="buy", optimize=True) + + # Bornes du facteur de sizing (réduit jusqu'à 0.4x, augmente jusqu'à 1.4x) + VOL_FACTOR_MIN = 0.4 + VOL_FACTOR_MAX = 1.4 + + def leverage(self, pair, current_time, current_rate, proposed_leverage, + max_leverage, entry_tag, side, **kwargs) -> float: + return 1.0 + + def custom_stake_amount(self, pair, current_time, current_rate, proposed_stake, + min_stake, max_stake, leverage, entry_tag, side, **kwargs): + df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + if df is None or len(df) == 0: + return proposed_stake + factor = df["vol_factor"].iat[-1] + if factor is None or not np.isfinite(factor): + return proposed_stake + stake = proposed_stake * float(factor) + if min_stake is not None: + stake = max(stake, min_stake) + return min(stake, max_stake) + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + high, low, close = dataframe["high"], dataframe["low"], dataframe["close"] + tenkan = (high.rolling(9).max() + low.rolling(9).min()) / 2 + kijun = (high.rolling(26).max() + low.rolling(26).min()) / 2 + dataframe["tenkan"] = tenkan + dataframe["kijun"] = kijun + dataframe["senkou_a"] = ((tenkan + kijun) / 2).shift(26) + dataframe["senkou_b"] = ((high.rolling(52).max() + low.rolling(52).min()) / 2).shift(26) + dataframe["cloud_top"] = dataframe[["senkou_a", "senkou_b"]].max(axis=1) + dataframe["cloud_bot"] = dataframe[["senkou_a", "senkou_b"]].min(axis=1) + dataframe["cloud_width_pct"] = (dataframe["cloud_top"] - dataframe["cloud_bot"]) / close * 100 + dataframe["close_prev26"] = close.shift(26) + dataframe["adx"] = ta.ADX(dataframe, timeperiod=14) + + # --- Volatility targeting --- + atr_pct = ta.ATR(dataframe, timeperiod=14) / close * 100 + atr_ref = atr_pct.rolling(500).median() # vol "normale" (trailing) + factor = (atr_ref / atr_pct).clip(self.VOL_FACTOR_MIN, self.VOL_FACTOR_MAX) + dataframe["vol_factor"] = factor.fillna(1.0) + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + adx_min = self.buy_adx_min.value + cloud_min = self.buy_cloud_min_pct.value + long_cond = ( + (dataframe["close"] > dataframe["cloud_top"]) + & (dataframe["tenkan"] > dataframe["kijun"]) + & (dataframe["close"] > dataframe["close_prev26"]) + & (dataframe["adx"] > adx_min) + & (dataframe["cloud_width_pct"] > cloud_min) + & (dataframe["volume"] > 0) + ) + short_cond = ( + (dataframe["close"] < dataframe["cloud_bot"]) + & (dataframe["tenkan"] < dataframe["kijun"]) + & (dataframe["close"] < dataframe["close_prev26"]) + & (dataframe["adx"] > adx_min) + & (dataframe["cloud_width_pct"] > cloud_min) + & (dataframe["volume"] > 0) + ) + if self.require_tk_cross.value: + long_cond &= dataframe["tenkan"].shift(1) <= dataframe["kijun"].shift(1) + short_cond &= dataframe["tenkan"].shift(1) >= dataframe["kijun"].shift(1) + dataframe.loc[long_cond, "enter_long"] = 1 + dataframe.loc[short_cond, "enter_short"] = 1 + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + (((dataframe["tenkan"] < dataframe["kijun"]) | (dataframe["close"] < dataframe["cloud_bot"])) + & (dataframe["volume"] > 0)), + "exit_long", + ] = 1 + dataframe.loc[ + (((dataframe["tenkan"] > dataframe["kijun"]) | (dataframe["close"] > dataframe["cloud_top"])) + & (dataframe["volume"] > 0)), + "exit_short", + ] = 1 + return dataframe diff --git a/freqtrade/user_data/strategies/IchimokuLS.py b/freqtrade/user_data/strategies/IchimokuLS.py new file mode 100644 index 0000000..7b9162e --- /dev/null +++ b/freqtrade/user_data/strategies/IchimokuLS.py @@ -0,0 +1,108 @@ +# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods +""" +IchimokuLS — Ichimoku long/short avec FILTRE DE TENDANCE MACRO (EMA200). + +Reprend les paramètres optimisés d'IchimokuHyper (figés), et ajoute : + - LONG uniquement si close > EMA200 (tendance de fond haussière) + - SHORT uniquement si close < EMA200 (tendance de fond baissière) + +But : réparer le côté long, qui perdait en entrant à contre-tendance macro. +On compare A/B contre IchimokuHyper (mêmes params, sans le filtre). +""" +from __future__ import annotations + +import talib.abstract as ta +from pandas import DataFrame + +from freqtrade.strategy import IStrategy + + +class IchimokuLS(IStrategy): + INTERFACE_VERSION = 3 + timeframe = "1h" + can_short = True + + # --- Paramètres figés (issus de l'hyperopt d'IchimokuHyper) --- + minimal_roi = {"0": 0.488, "213": 0.136, "639": 0.05, "2021": 0} + stoploss = -0.232 + trailing_stop = True + trailing_stop_positive = 0.341 + trailing_stop_positive_offset = 0.441 + trailing_only_offset_is_reached = False + + buy_adx_min = 36 + buy_cloud_min_pct = 0.56 + + startup_candle_count: int = 220 # EMA200 + marge + process_only_new_candles = True + use_exit_signal = True + + def leverage(self, pair, current_time, current_rate, proposed_leverage, + max_leverage, entry_tag, side, **kwargs) -> float: + return 1.0 + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + high, low, close = dataframe["high"], dataframe["low"], dataframe["close"] + tenkan = (high.rolling(9).max() + low.rolling(9).min()) / 2 + kijun = (high.rolling(26).max() + low.rolling(26).min()) / 2 + dataframe["tenkan"] = tenkan + dataframe["kijun"] = kijun + dataframe["senkou_a"] = ((tenkan + kijun) / 2).shift(26) + dataframe["senkou_b"] = ( + (high.rolling(52).max() + low.rolling(52).min()) / 2 + ).shift(26) + dataframe["cloud_top"] = dataframe[["senkou_a", "senkou_b"]].max(axis=1) + dataframe["cloud_bot"] = dataframe[["senkou_a", "senkou_b"]].min(axis=1) + dataframe["cloud_width_pct"] = ( + (dataframe["cloud_top"] - dataframe["cloud_bot"]) / close * 100 + ) + dataframe["close_prev26"] = close.shift(26) + dataframe["adx"] = ta.ADX(dataframe, timeperiod=14) + dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200) # filtre macro + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + (dataframe["close"] > dataframe["cloud_top"]) + & (dataframe["tenkan"] > dataframe["kijun"]) + & (dataframe["close"] > dataframe["close_prev26"]) + & (dataframe["adx"] > self.buy_adx_min) + & (dataframe["cloud_width_pct"] > self.buy_cloud_min_pct) + & (dataframe["close"] > dataframe["ema200"]) # ← filtre macro LONG + & (dataframe["volume"] > 0) + ), + "enter_long", + ] = 1 + dataframe.loc[ + ( + (dataframe["close"] < dataframe["cloud_bot"]) + & (dataframe["tenkan"] < dataframe["kijun"]) + & (dataframe["close"] < dataframe["close_prev26"]) + & (dataframe["adx"] > self.buy_adx_min) + & (dataframe["cloud_width_pct"] > self.buy_cloud_min_pct) + & (dataframe["close"] < dataframe["ema200"]) # ← filtre macro SHORT + & (dataframe["volume"] > 0) + ), + "enter_short", + ] = 1 + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + ((dataframe["tenkan"] < dataframe["kijun"]) + | (dataframe["close"] < dataframe["cloud_bot"])) + & (dataframe["volume"] > 0) + ), + "exit_long", + ] = 1 + dataframe.loc[ + ( + ((dataframe["tenkan"] > dataframe["kijun"]) + | (dataframe["close"] > dataframe["cloud_top"])) + & (dataframe["volume"] > 0) + ), + "exit_short", + ] = 1 + return dataframe diff --git a/freqtrade/user_data/strategies/IchimokuStrategy.py b/freqtrade/user_data/strategies/IchimokuStrategy.py new file mode 100644 index 0000000..abb647d --- /dev/null +++ b/freqtrade/user_data/strategies/IchimokuStrategy.py @@ -0,0 +1,117 @@ +# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods +""" +IchimokuStrategy — long/short basé sur Ichimoku Kinko Hyo (futures). + +Composants : + Tenkan-sen (9), Kijun-sen (26), Senkou A/B (nuage/Kumo), confirmation type Chikou. + +Signaux : + LONG : prix au-dessus du nuage + croisement Tenkan>Kijun + momentum (close > close[-26]). + SHORT : miroir exact (prix sous le nuage + Tenkan float: + return 1.0 # edge directionnel pur + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + high, low, close = dataframe["high"], dataframe["low"], dataframe["close"] + + tenkan = (high.rolling(9).max() + low.rolling(9).min()) / 2 + kijun = (high.rolling(26).max() + low.rolling(26).min()) / 2 + dataframe["tenkan"] = tenkan + dataframe["kijun"] = kijun + + # Senkou décalés de +26 : valeur du nuage au présent issue de données passées. + dataframe["senkou_a"] = ((tenkan + kijun) / 2).shift(26) + dataframe["senkou_b"] = ( + (high.rolling(52).max() + low.rolling(52).min()) / 2 + ).shift(26) + + # Bornes du nuage + dataframe["cloud_top"] = dataframe[["senkou_a", "senkou_b"]].max(axis=1) + dataframe["cloud_bot"] = dataframe[["senkou_a", "senkou_b"]].min(axis=1) + + # Confirmation momentum type Chikou (close vs close d'il y a 26 bougies) + dataframe["close_prev26"] = close.shift(26) + + dataframe["adx"] = ta.ADX(dataframe, timeperiod=14) + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # LONG : au-dessus du nuage + croisement TK haussier + momentum + dataframe.loc[ + ( + (dataframe["close"] > dataframe["cloud_top"]) + & (dataframe["tenkan"] > dataframe["kijun"]) + & (dataframe["tenkan"].shift(1) <= dataframe["kijun"].shift(1)) + & (dataframe["close"] > dataframe["close_prev26"]) + & (dataframe["adx"] > 20) + & (dataframe["volume"] > 0) + ), + "enter_long", + ] = 1 + # SHORT : sous le nuage + croisement TK baissier + momentum baissier + dataframe.loc[ + ( + (dataframe["close"] < dataframe["cloud_bot"]) + & (dataframe["tenkan"] < dataframe["kijun"]) + & (dataframe["tenkan"].shift(1) >= dataframe["kijun"].shift(1)) + & (dataframe["close"] < dataframe["close_prev26"]) + & (dataframe["adx"] > 20) + & (dataframe["volume"] > 0) + ), + "enter_short", + ] = 1 + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # Sortie LONG : Tenkan repasse sous Kijun, ou prix replonge dans le nuage + dataframe.loc[ + ( + ( + (dataframe["tenkan"] < dataframe["kijun"]) + | (dataframe["close"] < dataframe["cloud_bot"]) + ) + & (dataframe["volume"] > 0) + ), + "exit_long", + ] = 1 + # Sortie SHORT : Tenkan repasse au-dessus de Kijun, ou prix remonte dans le nuage + dataframe.loc[ + ( + ( + (dataframe["tenkan"] > dataframe["kijun"]) + | (dataframe["close"] > dataframe["cloud_top"]) + ) + & (dataframe["volume"] > 0) + ), + "exit_short", + ] = 1 + return dataframe diff --git a/freqtrade/user_data/strategies/LeveragedStrategy.py b/freqtrade/user_data/strategies/LeveragedStrategy.py new file mode 100644 index 0000000..3148216 --- /dev/null +++ b/freqtrade/user_data/strategies/LeveragedStrategy.py @@ -0,0 +1,64 @@ +# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods +""" +LeveragedStrategy — DÉMONSTRATION du risque du levier (NE PAS utiliser en réel). + +Même logique technique que SampleStrategy, mais en futures avec levier 10x. +But : montrer empiriquement que le levier crée des semaines à +10 % ET des semaines +catastrophiques — donc « +10 % chaque semaine » reste impossible, et le levier ruine. +""" +from __future__ import annotations + +import talib.abstract as ta +from pandas import DataFrame + +from freqtrade.strategy import IStrategy + + +class LeveragedStrategy(IStrategy): + INTERFACE_VERSION = 3 + timeframe = "1h" + can_short = False + + 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 + + def leverage(self, pair, current_time, current_rate, proposed_leverage, + max_leverage, entry_tag, side, **kwargs) -> float: + return min(5.0, max_leverage) # 5x + + 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) + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + (dataframe["ema_fast"] > dataframe["ema_slow"]) + & (dataframe["ema_fast"].shift(1) <= dataframe["ema_slow"].shift(1)) + & (dataframe["rsi"] < 70) + & (dataframe["volume"] > 0) + ), + "enter_long", + ] = 1 + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + (dataframe["ema_fast"] < dataframe["ema_slow"]) + & (dataframe["ema_fast"].shift(1) >= dataframe["ema_slow"].shift(1)) + & (dataframe["volume"] > 0) + ), + "exit_long", + ] = 1 + return dataframe diff --git a/freqtrade/user_data/strategies/LongShortStrategy.py b/freqtrade/user_data/strategies/LongShortStrategy.py new file mode 100644 index 0000000..3c446f8 --- /dev/null +++ b/freqtrade/user_data/strategies/LongShortStrategy.py @@ -0,0 +1,94 @@ +# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods +""" +LongShortStrategy — stratégie symétrique (futures) qui gagne dans les deux sens. + +- LONG quand la tendance est haussière (EMA fast>slow, prix>EMA50, momentum +). +- SHORT quand la tendance est baissière (miroir exact). +But : ne plus subir les marchés baissiers — profiter de la baisse comme de la hausse. +Levier 1x par défaut (on isole l'edge directionnel ; le levier viendra après si robuste). +""" +from __future__ import annotations + +import talib.abstract as ta +from pandas import DataFrame + +from freqtrade.strategy import IStrategy + + +class LongShortStrategy(IStrategy): + INTERFACE_VERSION = 3 + timeframe = "1h" + can_short = True # futures requis + + minimal_roi = {"0": 0.05, "120": 0.03, "360": 0.01, "720": 0} + stoploss = -0.08 + trailing_stop = True + trailing_stop_positive = 0.02 + trailing_stop_positive_offset = 0.03 + trailing_only_offset_is_reached = True + + startup_candle_count: int = 60 + process_only_new_candles = True + use_exit_signal = True + + def leverage(self, pair, current_time, current_rate, proposed_leverage, + max_leverage, entry_tag, side, **kwargs) -> float: + return 1.0 # 1x — edge directionnel pur, sans amplification + + 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["ema_trend"] = ta.EMA(dataframe, timeperiod=50) + dataframe["adx"] = ta.ADX(dataframe, timeperiod=14) + dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # LONG : croisement haussier + tendance MT haussière + tendance forte + dataframe.loc[ + ( + (dataframe["ema_fast"] > dataframe["ema_slow"]) + & (dataframe["ema_fast"].shift(1) <= dataframe["ema_slow"].shift(1)) + & (dataframe["close"] > dataframe["ema_trend"]) + & (dataframe["adx"] > 20) + & (dataframe["rsi"] > 45) + & (dataframe["rsi"] < 75) + & (dataframe["volume"] > 0) + ), + "enter_long", + ] = 1 + # SHORT : miroir exact + dataframe.loc[ + ( + (dataframe["ema_fast"] < dataframe["ema_slow"]) + & (dataframe["ema_fast"].shift(1) >= dataframe["ema_slow"].shift(1)) + & (dataframe["close"] < dataframe["ema_trend"]) + & (dataframe["adx"] > 20) + & (dataframe["rsi"] < 55) + & (dataframe["rsi"] > 25) + & (dataframe["volume"] > 0) + ), + "enter_short", + ] = 1 + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # Sortie LONG : la tendance courte se retourne à la baisse + dataframe.loc[ + ( + (dataframe["ema_fast"] < dataframe["ema_slow"]) + & (dataframe["ema_fast"].shift(1) >= dataframe["ema_slow"].shift(1)) + & (dataframe["volume"] > 0) + ), + "exit_long", + ] = 1 + # Sortie SHORT : la tendance courte se retourne à la hausse + dataframe.loc[ + ( + (dataframe["ema_fast"] > dataframe["ema_slow"]) + & (dataframe["ema_fast"].shift(1) <= dataframe["ema_slow"].shift(1)) + & (dataframe["volume"] > 0) + ), + "exit_short", + ] = 1 + return dataframe diff --git a/freqtrade/user_data/strategies/MTFIchimoku.json b/freqtrade/user_data/strategies/MTFIchimoku.json new file mode 100644 index 0000000..6673412 --- /dev/null +++ b/freqtrade/user_data/strategies/MTFIchimoku.json @@ -0,0 +1,33 @@ +{ + "strategy_name": "MTFIchimoku", + "params": { + "max_open_trades": { + "max_open_trades": 3 + }, + "buy": { + "buy_adx_min": 32, + "buy_pullback_pct": 0.008, + "buy_rsi_max": 52 + }, + "sell": { + "sell_rsi_min": 39 + }, + "roi": { + "0": 0.264, + "49": 0.057, + "170": 0.035, + "468": 0 + }, + "stoploss": { + "stoploss": -0.154 + }, + "trailing": { + "trailing_stop": true, + "trailing_stop_positive": 0.232, + "trailing_stop_positive_offset": 0.309, + "trailing_only_offset_is_reached": false + } + }, + "ft_stratparam_v": 1, + "export_time": "2026-06-23 16:09:25.675598+00:00" +} \ No newline at end of file diff --git a/freqtrade/user_data/strategies/MTFIchimoku.py b/freqtrade/user_data/strategies/MTFIchimoku.py new file mode 100644 index 0000000..0d1614d --- /dev/null +++ b/freqtrade/user_data/strategies/MTFIchimoku.py @@ -0,0 +1,135 @@ +# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods +""" +MTFIchimoku — Ichimoku MULTI-TIMEFRAME (technique S/R inter-unités). + +Idée (méthode de l'utilisateur) : trader sur une unité basse (15m) en utilisant +les Tenkan/Kijun de l'unité SUPÉRIEURE (1h) comme supports/résistances. + +Règles : + - Tendance 1h donnée par tenkan_1h vs kijun_1h. + - LONG : en tendance 1h haussière, le prix 15m RECLAIME le support (croise au-dessus + de la Kijun 1h) → rebond sur support. + - SHORT : en tendance 1h baissière, le prix 15m CASSE le support (croise sous la + Kijun 1h) → rejet sur résistance. + - Sortie : perte du niveau (close repasse de l'autre côté de la Kijun 1h) + ROI/stop. + +La Kijun 1h sert de S/R principal (niveau lent/fort), la Tenkan 1h de filtre de tendance. +""" +from __future__ import annotations + +import talib.abstract as ta +from pandas import DataFrame + +from freqtrade.strategy import ( + IStrategy, + informative, + IntParameter, + DecimalParameter, +) + + +class MTFIchimoku(IStrategy): + INTERFACE_VERSION = 3 + timeframe = "15m" # unité de trading + can_short = True + + # Paramètres optimisables + buy_adx_min = IntParameter(15, 40, default=25, space="buy", optimize=True) + buy_pullback_pct = DecimalParameter(0.004, 0.03, default=0.012, decimals=3, space="buy", optimize=True) + buy_rsi_max = IntParameter(50, 72, default=60, space="buy", optimize=True) + sell_rsi_min = IntParameter(28, 50, default=40, space="sell", optimize=True) + + minimal_roi = {"0": 0.02, "60": 0.012, "180": 0.006, "360": 0} + stoploss = -0.025 + trailing_stop = True + trailing_stop_positive = 0.008 + trailing_stop_positive_offset = 0.014 + trailing_only_offset_is_reached = True + + startup_candle_count: int = 240 + process_only_new_candles = True + use_exit_signal = True + + def leverage(self, pair, current_time, current_rate, proposed_leverage, + max_leverage, entry_tag, side, **kwargs) -> float: + return 1.0 + + @informative("1h") + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # Tenkan / Kijun sur l'unité supérieure (1h) → deviendront tenkan_1h / kijun_1h. + high, low = dataframe["high"], dataframe["low"] + dataframe["tenkan"] = (high.rolling(9).max() + low.rolling(9).min()) / 2 + dataframe["kijun"] = (high.rolling(26).max() + low.rolling(26).min()) / 2 + return dataframe + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # tenkan_1h / kijun_1h sont injectés automatiquement (forward-fill sur le 15m). + dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14) + dataframe["adx"] = ta.ADX(dataframe, timeperiod=14) + # Plus haut/bas récents (pour exiger un VRAI pullback vers le niveau) + dataframe["hh8"] = dataframe["high"].rolling(8).max() + dataframe["ll8"] = dataframe["low"].rolling(8).min() + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + kijun = dataframe["kijun_1h"] + tenkan = dataframe["tenkan_1h"] + close, op, low, high = ( + dataframe["close"], dataframe["open"], dataframe["low"], dataframe["high"] + ) + + rsi = dataframe["rsi"] + adx = dataframe["adx"] + adx_min = self.buy_adx_min.value + pb = self.buy_pullback_pct.value + uptrend_1h = (tenkan > kijun) & (close > kijun) # biais haussier 1h + downtrend_1h = (tenkan < kijun) & (close < kijun) # biais baissier 1h + + # LONG : en tendance forte, VRAI pullback vers le support Kijun 1h puis rebond + momentum. + dataframe.loc[ + ( + uptrend_1h + & (adx > adx_min) # tendance forte + & (dataframe["hh8"] > kijun * (1 + pb)) # le prix venait nettement au-dessus + & (low <= kijun * 1.001) # mèche teste le support + & (close > kijun) # clôture au-dessus (support tient) + & (close > op) # bougie de rebond + & (rsi > rsi.shift(1)) # momentum qui se retourne à la hausse + & (rsi < self.buy_rsi_max.value) # pas déjà suracheté + & (dataframe["volume"] > 0) + ), + "enter_long", + ] = 1 + + # SHORT : miroir exact (rejet sur résistance en tendance baissière forte). + dataframe.loc[ + ( + downtrend_1h + & (adx > adx_min) + & (dataframe["ll8"] < kijun * (1 - pb)) + & (high >= kijun * 0.999) + & (close < kijun) + & (close < op) + & (rsi < rsi.shift(1)) + & (rsi > self.sell_rsi_min.value) + & (dataframe["volume"] > 0) + ), + "enter_short", + ] = 1 + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + kijun = dataframe["kijun_1h"] + tenkan = dataframe["tenkan_1h"] + close = dataframe["close"] + # Sortie LONG : cassure NETTE du support (clôture sous la Kijun ET sous la Tenkan 1h) + dataframe.loc[ + ((close < kijun) & (close < tenkan) & (dataframe["volume"] > 0)), + "exit_long", + ] = 1 + # Sortie SHORT : reprise NETTE au-dessus de la résistance + dataframe.loc[ + ((close > kijun) & (close > tenkan) & (dataframe["volume"] > 0)), + "exit_short", + ] = 1 + return dataframe diff --git a/freqtrade/user_data/strategies/SampleStrategy.py b/freqtrade/user_data/strategies/SampleStrategy.py new file mode 100644 index 0000000..9b663a2 --- /dev/null +++ b/freqtrade/user_data/strategies/SampleStrategy.py @@ -0,0 +1,75 @@ +# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods +""" +SampleStrategy — stratégie de base MidasBot (Phase 1). + +Croisement de moyennes mobiles exponentielles (EMA) avec filtre RSI. +Sert à valider le pipeline Freqtrade (dry-run, backtesting, FreqUI) avant +d'ajouter la couche IA (cf. AiBiasStrategy, Phase 3). +""" +from __future__ import annotations + +import talib.abstract as ta +from pandas import DataFrame + +from freqtrade.strategy import IStrategy + + +class SampleStrategy(IStrategy): + INTERFACE_VERSION = 3 + + # Timeframe d'analyse + timeframe = "1h" + + # Take-profit échelonné (ROI minimal par durée, en minutes) + minimal_roi = { + "0": 0.05, + "120": 0.03, + "360": 0.01, + "720": 0, + } + + # Stop-loss dur + stoploss = -0.10 + + # Trailing stop + trailing_stop = True + trailing_stop_positive = 0.02 + trailing_stop_positive_offset = 0.03 + trailing_only_offset_is_reached = True + + # Nombre de bougies nécessaires avant de produire un signal + startup_candle_count: int = 50 + + # Ordres + process_only_new_candles = True + use_exit_signal = True + exit_profit_only = False + + 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) + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + (dataframe["ema_fast"] > dataframe["ema_slow"]) + & (dataframe["ema_fast"].shift(1) <= dataframe["ema_slow"].shift(1)) + & (dataframe["rsi"] < 70) + & (dataframe["volume"] > 0) + ), + "enter_long", + ] = 1 + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + (dataframe["ema_fast"] < dataframe["ema_slow"]) + & (dataframe["ema_fast"].shift(1) >= dataframe["ema_slow"].shift(1)) + & (dataframe["volume"] > 0) + ), + "exit_long", + ] = 1 + return dataframe diff --git a/freqtrade/user_data/strategies/TrendMomentumStrategy.py b/freqtrade/user_data/strategies/TrendMomentumStrategy.py new file mode 100644 index 0000000..dd3fe79 --- /dev/null +++ b/freqtrade/user_data/strategies/TrendMomentumStrategy.py @@ -0,0 +1,68 @@ +# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods +""" +TrendMomentumStrategy — tentative d'amélioration du rendement (vs SampleStrategy). + +Idée : n'entrer que sur des tendances confirmées et fortes (filtre EMA50 + ADX), +laisser courir les gains (ROI plus haut + trailing), couper vite les perdants. +Objectif : meilleur rendement ajusté du risque — PAS de promesse de +10 %/semaine. +""" +from __future__ import annotations + +import talib.abstract as ta +from pandas import DataFrame + +from freqtrade.strategy import IStrategy + + +class TrendMomentumStrategy(IStrategy): + INTERFACE_VERSION = 3 + timeframe = "1h" + + # Laisser courir : on vise des gains plus gros, on attend plus longtemps. + minimal_roi = {"0": 0.12, "240": 0.06, "720": 0.03, "1440": 0} + stoploss = -0.06 # on coupe vite les perdants + trailing_stop = True + trailing_stop_positive = 0.025 + trailing_stop_positive_offset = 0.04 + trailing_only_offset_is_reached = True + + startup_candle_count: int = 60 + process_only_new_candles = True + use_exit_signal = True + + 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["ema_trend"] = ta.EMA(dataframe, timeperiod=50) + dataframe["adx"] = ta.ADX(dataframe, timeperiod=14) + dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14) + macd = ta.MACD(dataframe) + dataframe["macd"] = macd["macd"] + dataframe["macdsignal"] = macd["macdsignal"] + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + (dataframe["ema_fast"] > dataframe["ema_slow"]) # tendance courte haussière + & (dataframe["close"] > dataframe["ema_trend"]) # au-dessus tendance MT + & (dataframe["adx"] > 25) # tendance forte + & (dataframe["macd"] > dataframe["macdsignal"]) # momentum haussier + & (dataframe["rsi"] > 50) + & (dataframe["rsi"] < 75) # pas en surachat extrême + & (dataframe["volume"] > 0) + ), + "enter_long", + ] = 1 + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + (dataframe["macd"] < dataframe["macdsignal"]) # momentum se retourne + & (dataframe["close"] < dataframe["ema_slow"]) + & (dataframe["volume"] > 0) + ), + "exit_long", + ] = 1 + return dataframe diff --git a/scripts/com.midasbot.analyzer.plist b/scripts/com.midasbot.analyzer.plist new file mode 100644 index 0000000..785235b --- /dev/null +++ b/scripts/com.midasbot.analyzer.plist @@ -0,0 +1,31 @@ + + + + + Label + com.midasbot.analyzer + + ProgramArguments + + /bin/bash + /Users/jerem/Documents/projects/perso/MidasBot/scripts/run_analyzer.sh + + + + StartInterval + 3600 + RunAtLoad + + + ThrottleInterval + 60 + + StandardOutPath + /Users/jerem/Documents/projects/perso/MidasBot/logs/analyzer.log + StandardErrorPath + /Users/jerem/Documents/projects/perso/MidasBot/logs/analyzer.log + + ProcessType + Background + + diff --git a/scripts/run_analyzer.sh b/scripts/run_analyzer.sh new file mode 100755 index 0000000..513a4f4 --- /dev/null +++ b/scripts/run_analyzer.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# MidasBot — exécute UN cycle de l'analyzer IA (sur l'hôte, auth abonnement Claude). +# Appelé par launchd (horaire). Parle au Redis dockerisé (port hôte 6380). +set -eo pipefail + +PROJECT="/Users/jerem/Documents/projects/perso/MidasBot" + +# PATH explicite : claude + node + outils système (launchd a un PATH minimal). +export PATH="/Users/jerem/.local/bin:/Users/jerem/.nvm/versions/node/v24.15.0/bin:/usr/bin:/bin:/usr/sbin:/sbin" + +# IMPORTANT : pas de clé API (on veut l'abonnement, pas la facturation au token). +unset ANTHROPIC_API_KEY || true + +export REDIS_URL="redis://localhost:6380/0" +export ANALYZER_PAIRS="BTC/USDT,ETH/USDT,SOL/USDT,BNB/USDT" +export ANALYZER_TIMEFRAME="1h" +export ANALYZER_MODEL="claude-sonnet-4-6" +export ANALYZER_HISTORY_DIR="$PROJECT/freqtrade/user_data/ai_bias_history" + +cd "$PROJECT/ai_analyzer" +exec "$PROJECT/.venv/bin/python" analyzer.py --once diff --git a/scripts/walkforward.sh b/scripts/walkforward.sh new file mode 100644 index 0000000..22467e6 --- /dev/null +++ b/scripts/walkforward.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Walk-forward IchimokuHyper : pour chaque fold, hyperopt sur train -> backtest OOS sur test. +set -eo pipefail +cd /Users/jerem/Documents/projects/perso/MidasBot +CFG=/freqtrade/user_data/config_ich.json +DC="docker compose run --rm --no-deps freqtrade" + +# Paires train|test (train 6 mois glissant, test 2 mois OOS), 2024-06 -> 2026-06 +FOLDS=( + "20240601-20241201|20241201-20250201" + "20240801-20250201|20250201-20250401" + "20241001-20250401|20250401-20250601" + "20241201-20250601|20250601-20250801" + "20250201-20250801|20250801-20251001" + "20250401-20251001|20251001-20251201" + "20250601-20251201|20251201-20260201" + "20250801-20260201|20260201-20260401" + "20251001-20260401|20260401-20260623" +) + +echo "FOLD | TEST_PERIOD | OOS_GAIN%" +for f in "${FOLDS[@]}"; do + tr="${f%%|*}"; te="${f##*|}" + $DC hyperopt --config $CFG --strategy IchimokuHyper \ + --hyperopt-loss ProfitDrawDownHyperOptLoss --spaces buy roi stoploss trailing \ + --timeframe 1h --timerange "$tr" --epochs 80 -j 4 > /tmp/wf_ho.log 2>&1 + g=$($DC backtesting --config $CFG --strategy IchimokuHyper \ + --timeframe 1h --timerange "$te" 2>&1 \ + | grep -iE 'Total profit %' | head -1 | grep -oE '[-0-9.]+%' | head -1) + echo "$tr | $te | $g" +done +echo "WALKFORWARD_DONE" diff --git a/scripts/walkforward_chop.sh b/scripts/walkforward_chop.sh new file mode 100644 index 0000000..d55c4e7 --- /dev/null +++ b/scripts/walkforward_chop.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Walk-forward IchimokuChop (avec filtre anti-chop) — mêmes folds que le baseline. +set -eo pipefail +cd /Users/jerem/Documents/projects/perso/MidasBot +CFG=/freqtrade/user_data/config_ich.json +DC="docker compose run --rm --no-deps freqtrade" + +FOLDS=( + "20240601-20241201|20241201-20250201" + "20240801-20250201|20250201-20250401" + "20241001-20250401|20250401-20250601" + "20241201-20250601|20250601-20250801" + "20250201-20250801|20250801-20251001" + "20250401-20251001|20251001-20251201" + "20250601-20251201|20251201-20260201" + "20250801-20260201|20260201-20260401" + "20251001-20260401|20260401-20260623" +) + +echo "FOLD | TEST_PERIOD | OOS_GAIN%" +for f in "${FOLDS[@]}"; do + tr="${f%%|*}"; te="${f##*|}" + $DC hyperopt --config $CFG --strategy IchimokuChop \ + --hyperopt-loss ProfitDrawDownHyperOptLoss --spaces buy roi stoploss trailing \ + --timeframe 1h --timerange "$tr" --epochs 80 -j 4 > /tmp/wf_chop_ho.log 2>&1 + g=$($DC backtesting --config $CFG --strategy IchimokuChop \ + --timeframe 1h --timerange "$te" 2>&1 \ + | grep -iE 'Total profit %' | head -1 | grep -oE '[-0-9.]+%' | head -1) + echo "$tr | $te | $g" +done +echo "WALKFORWARD_CHOP_DONE" diff --git a/scripts/walkforward_gen.sh b/scripts/walkforward_gen.sh new file mode 100755 index 0000000..e69de29 diff --git a/scripts/walkforward_vol.sh b/scripts/walkforward_vol.sh new file mode 100644 index 0000000..f55f7e4 --- /dev/null +++ b/scripts/walkforward_vol.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Walk-forward IchimokuHyperVol (volatility targeting) — mêmes folds que le baseline. +set -eo pipefail +cd /Users/jerem/Documents/projects/perso/MidasBot +CFG=/freqtrade/user_data/config_ich.json +DC="docker compose run --rm --no-deps freqtrade" + +FOLDS=( + "20240601-20241201|20241201-20250201" + "20240801-20250201|20250201-20250401" + "20241001-20250401|20250401-20250601" + "20241201-20250601|20250601-20250801" + "20250201-20250801|20250801-20251001" + "20250401-20251001|20251001-20251201" + "20250601-20251201|20251201-20260201" + "20250801-20260201|20260201-20260401" + "20251001-20260401|20260401-20260623" +) + +echo "FOLD | TEST_PERIOD | OOS_GAIN%" +for f in "${FOLDS[@]}"; do + tr="${f%%|*}"; te="${f##*|}" + $DC hyperopt --config $CFG --strategy IchimokuHyperVol \ + --hyperopt-loss ProfitDrawDownHyperOptLoss --spaces buy roi stoploss trailing \ + --timeframe 1h --timerange "$tr" --epochs 80 -j 4 > /tmp/wf_vol_ho.log 2>&1 + g=$($DC backtesting --config $CFG --strategy IchimokuHyperVol \ + --timeframe 1h --timerange "$te" 2>&1 \ + | grep -iE 'Total profit %' | head -1 | grep -oE '[-0-9.]+%' | head -1) + echo "$tr | $te | $g" +done +echo "WALKFORWARD_VOL_DONE"