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
This commit is contained in:
jerem
2026-06-23 19:25:49 +02:00
commit 633b033f4d
59 changed files with 3868 additions and 0 deletions

View File

@@ -0,0 +1 @@
{"sessionId":"8ae84397-5702-4993-8957-3e59bfe5c1b6","pid":88193,"procStart":"Tue Jun 23 12:10:23 2026","acquiredAt":1782221447804}

23
.env.example Normal file
View File

@@ -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

36
.gitignore vendored Normal file
View File

@@ -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

10
.mcp.json Normal file
View File

@@ -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": {}
}
}
}

128
README.md Normal file
View File

@@ -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/<PAIR>.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.

23
ai_analyzer/Dockerfile Normal file
View File

@@ -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"]

92
ai_analyzer/analyzer.py Normal file
View File

@@ -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()

111
ai_analyzer/backfill.py Normal file
View File

@@ -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()

View File

@@ -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

View File

@@ -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

58
ai_analyzer/models.py Normal file
View File

@@ -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"},
},
},
}
},
}

View File

@@ -0,0 +1,3 @@
ccxt>=4.4
redis>=5.0
pydantic>=2.6

View File

@@ -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

View File

@@ -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.

9
arbitrage/Dockerfile Normal file
View File

@@ -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"]

169
arbitrage/cex_arb.py Normal file
View File

@@ -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()

View File

@@ -0,0 +1 @@
ccxt>=4.4

13
dashboard/Dockerfile Normal file
View File

@@ -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"]

113
dashboard/app.py Normal file
View File

@@ -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"""<tr>
<td class="pair">{b.get('pair','?')}</td>
<td><span class="badge" style="background:{_COLORS.get(b.get('direction'),'#6b7280')}">{b.get('direction','?')}</span></td>
<td>{float(b.get('confidence',0)):.0%}</td>
<td class="rat">{b.get('rationale','')}</td>
<td>{b.get('key_support','')}</td>
<td>{b.get('key_resistance','')}</td>
</tr>"""
for b in biases
)
else:
rows = '<tr><td colspan="6" class="empty">Aucun biais en cache. Lance l\'analyzer (ai-analyzer).</td></tr>'
return f"""<!doctype html>
<html lang="fr"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="refresh" content="30">
<title>MidasBot — Insights IA</title>
<style>
:root {{ color-scheme: dark; }}
body {{ font-family: system-ui, sans-serif; background:#0b0e14; color:#e6e6e6; margin:0; padding:2rem; }}
h1 {{ margin:0 0 .25rem; font-size:1.5rem; }}
.sub {{ color:#8b95a7; margin-bottom:1.5rem; font-size:.9rem; }}
a {{ color:#60a5fa; }}
table {{ width:100%; border-collapse:collapse; background:#111722; border-radius:10px; overflow:hidden; }}
th, td {{ padding:.7rem .9rem; text-align:left; border-bottom:1px solid #1f2937; vertical-align:top; }}
th {{ background:#0f1521; color:#94a3b8; font-weight:600; font-size:.8rem; text-transform:uppercase; letter-spacing:.04em; }}
.pair {{ font-weight:700; }}
.rat {{ color:#cbd5e1; font-size:.9rem; max-width:520px; }}
.badge {{ color:#fff; padding:.15rem .55rem; border-radius:999px; font-size:.8rem; text-transform:capitalize; }}
.empty {{ color:#8b95a7; text-align:center; padding:2rem; }}
.links {{ margin-top:1.5rem; }}
</style></head>
<body>
<h1>🪙 MidasBot — Insights IA</h1>
<div class="sub">Biais de marché produits par Claude · {_now()} · rafraîchissement auto 30 s · <strong>DRY-RUN</strong></div>
<table>
<thead><tr><th>Paire</th><th>Biais</th><th>Confiance</th><th>Justification</th><th>Support</th><th>Résistance</th></tr></thead>
<tbody>{rows}</tbody>
</table>
<div class="links">📊 <a href="{FREQUI_URL}" target="_blank">Ouvrir FreqUI</a> (positions, P&amp;L, trades) &nbsp;·&nbsp; <a href="/api/biases">API JSON</a></div>
</body></html>"""

View File

@@ -0,0 +1,3 @@
fastapi>=0.110
uvicorn[standard]>=0.27
redis>=5.0

92
docker-compose.yml Normal file
View File

@@ -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:

5
freqtrade/Dockerfile Normal file
View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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"}}

View File

@@ -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

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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

View File

@@ -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

View File

@@ -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<Kijun + close < close[-26]).
⚠️ Anti-lookahead : les Senkou sont décalés de +26 (donnée passée projetée au présent),
la confirmation "Chikou" utilise close vs close d'il y a 26 bougies (pas de futur).
"""
from __future__ import annotations
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.strategy import IStrategy
class IchimokuStrategy(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 # 52 (Senkou B) + 26 (décalage) + 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 # 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

View File

@@ -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

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.midasbot.analyzer</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Users/jerem/Documents/projects/perso/MidasBot/scripts/run_analyzer.sh</string>
</array>
<!-- Toutes les heures (3600 s). Premier lancement immédiat au chargement. -->
<key>StartInterval</key>
<integer>3600</integer>
<key>RunAtLoad</key>
<true/>
<key>ThrottleInterval</key>
<integer>60</integer>
<key>StandardOutPath</key>
<string>/Users/jerem/Documents/projects/perso/MidasBot/logs/analyzer.log</string>
<key>StandardErrorPath</key>
<string>/Users/jerem/Documents/projects/perso/MidasBot/logs/analyzer.log</string>
<key>ProcessType</key>
<string>Background</string>
</dict>
</plist>

21
scripts/run_analyzer.sh Executable file
View File

@@ -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

32
scripts/walkforward.sh Normal file
View File

@@ -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"

View File

@@ -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"

0
scripts/walkforward_gen.sh Executable file
View File

View File

@@ -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"