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:
9
arbitrage/Dockerfile
Normal file
9
arbitrage/Dockerfile
Normal 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
169
arbitrage/cex_arb.py
Normal 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()
|
||||
1
arbitrage/requirements.txt
Normal file
1
arbitrage/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
ccxt>=4.4
|
||||
Reference in New Issue
Block a user