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

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