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