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:
1
.claude/scheduled_tasks.lock
Normal file
1
.claude/scheduled_tasks.lock
Normal 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
23
.env.example
Normal 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
36
.gitignore
vendored
Normal 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
10
.mcp.json
Normal 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
128
README.md
Normal 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
23
ai_analyzer/Dockerfile
Normal 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
92
ai_analyzer/analyzer.py
Normal 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
111
ai_analyzer/backfill.py
Normal 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()
|
||||||
154
ai_analyzer/claude_client.py
Normal file
154
ai_analyzer/claude_client.py
Normal 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
|
||||||
92
ai_analyzer/market_data.py
Normal file
92
ai_analyzer/market_data.py
Normal 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
58
ai_analyzer/models.py
Normal 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"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
3
ai_analyzer/requirements.txt
Normal file
3
ai_analyzer/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ccxt>=4.4
|
||||||
|
redis>=5.0
|
||||||
|
pydantic>=2.6
|
||||||
89
ai_analyzer/signal_store.py
Normal file
89
ai_analyzer/signal_store.py
Normal 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
|
||||||
13
ai_analyzer/system_prompt.md
Normal file
13
ai_analyzer/system_prompt.md
Normal 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
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
|
||||||
13
dashboard/Dockerfile
Normal file
13
dashboard/Dockerfile
Normal 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
113
dashboard/app.py
Normal 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&L, trades) · <a href="/api/biases">API JSON</a></div>
|
||||||
|
</body></html>"""
|
||||||
3
dashboard/requirements.txt
Normal file
3
dashboard/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fastapi>=0.110
|
||||||
|
uvicorn[standard]>=0.27
|
||||||
|
redis>=5.0
|
||||||
92
docker-compose.yml
Normal file
92
docker-compose.yml
Normal 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
5
freqtrade/Dockerfile
Normal 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
|
||||||
80
freqtrade/user_data/config.json
Normal file
80
freqtrade/user_data/config.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
80
freqtrade/user_data/config_futures.json
Normal file
80
freqtrade/user_data/config_futures.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
88
freqtrade/user_data/config_futures_multi.json
Normal file
88
freqtrade/user_data/config_futures_multi.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
80
freqtrade/user_data/config_ich.json
Normal file
80
freqtrade/user_data/config_ich.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
80
freqtrade/user_data/config_ich15.json
Normal file
80
freqtrade/user_data/config_ich15.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
80
freqtrade/user_data/config_live.json
Normal file
80
freqtrade/user_data/config_live.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
110
freqtrade/user_data/config_nfi.json
Normal file
110
freqtrade/user_data/config_nfi.json
Normal 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
|
||||||
|
}
|
||||||
@@ -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"}}
|
||||||
171
freqtrade/user_data/strategies/AiBiasStrategy.py
Normal file
171
freqtrade/user_data/strategies/AiBiasStrategy.py
Normal 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
|
||||||
82
freqtrade/user_data/strategies/BBMeanRev.py
Normal file
82
freqtrade/user_data/strategies/BBMeanRev.py
Normal 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
|
||||||
34
freqtrade/user_data/strategies/HyperStrategy.json
Normal file
34
freqtrade/user_data/strategies/HyperStrategy.json
Normal 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"
|
||||||
|
}
|
||||||
82
freqtrade/user_data/strategies/HyperStrategy.py
Normal file
82
freqtrade/user_data/strategies/HyperStrategy.py
Normal 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
|
||||||
31
freqtrade/user_data/strategies/IchimokuChop.json
Normal file
31
freqtrade/user_data/strategies/IchimokuChop.json
Normal 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"
|
||||||
|
}
|
||||||
115
freqtrade/user_data/strategies/IchimokuChop.py
Normal file
115
freqtrade/user_data/strategies/IchimokuChop.py
Normal 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
|
||||||
30
freqtrade/user_data/strategies/IchimokuHyper.best.json
Normal file
30
freqtrade/user_data/strategies/IchimokuHyper.best.json
Normal 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"
|
||||||
|
}
|
||||||
30
freqtrade/user_data/strategies/IchimokuHyper.json
Normal file
30
freqtrade/user_data/strategies/IchimokuHyper.json
Normal 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"
|
||||||
|
}
|
||||||
118
freqtrade/user_data/strategies/IchimokuHyper.py
Normal file
118
freqtrade/user_data/strategies/IchimokuHyper.py
Normal 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
|
||||||
31
freqtrade/user_data/strategies/IchimokuHyper2.json
Normal file
31
freqtrade/user_data/strategies/IchimokuHyper2.json
Normal 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"
|
||||||
|
}
|
||||||
158
freqtrade/user_data/strategies/IchimokuHyper2.py
Normal file
158
freqtrade/user_data/strategies/IchimokuHyper2.py
Normal 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
|
||||||
36
freqtrade/user_data/strategies/IchimokuHyper3.json
Normal file
36
freqtrade/user_data/strategies/IchimokuHyper3.json
Normal 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"
|
||||||
|
}
|
||||||
117
freqtrade/user_data/strategies/IchimokuHyper3.py
Normal file
117
freqtrade/user_data/strategies/IchimokuHyper3.py
Normal 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
|
||||||
30
freqtrade/user_data/strategies/IchimokuHyperVol.json
Normal file
30
freqtrade/user_data/strategies/IchimokuHyperVol.json
Normal 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"
|
||||||
|
}
|
||||||
126
freqtrade/user_data/strategies/IchimokuHyperVol.py
Normal file
126
freqtrade/user_data/strategies/IchimokuHyperVol.py
Normal 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
|
||||||
108
freqtrade/user_data/strategies/IchimokuLS.py
Normal file
108
freqtrade/user_data/strategies/IchimokuLS.py
Normal 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
|
||||||
117
freqtrade/user_data/strategies/IchimokuStrategy.py
Normal file
117
freqtrade/user_data/strategies/IchimokuStrategy.py
Normal 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
|
||||||
64
freqtrade/user_data/strategies/LeveragedStrategy.py
Normal file
64
freqtrade/user_data/strategies/LeveragedStrategy.py
Normal 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
|
||||||
94
freqtrade/user_data/strategies/LongShortStrategy.py
Normal file
94
freqtrade/user_data/strategies/LongShortStrategy.py
Normal 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
|
||||||
33
freqtrade/user_data/strategies/MTFIchimoku.json
Normal file
33
freqtrade/user_data/strategies/MTFIchimoku.json
Normal 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"
|
||||||
|
}
|
||||||
135
freqtrade/user_data/strategies/MTFIchimoku.py
Normal file
135
freqtrade/user_data/strategies/MTFIchimoku.py
Normal 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
|
||||||
75
freqtrade/user_data/strategies/SampleStrategy.py
Normal file
75
freqtrade/user_data/strategies/SampleStrategy.py
Normal 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
|
||||||
68
freqtrade/user_data/strategies/TrendMomentumStrategy.py
Normal file
68
freqtrade/user_data/strategies/TrendMomentumStrategy.py
Normal 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
|
||||||
31
scripts/com.midasbot.analyzer.plist
Normal file
31
scripts/com.midasbot.analyzer.plist
Normal 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
21
scripts/run_analyzer.sh
Executable 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
32
scripts/walkforward.sh
Normal 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"
|
||||||
31
scripts/walkforward_chop.sh
Normal file
31
scripts/walkforward_chop.sh
Normal 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
0
scripts/walkforward_gen.sh
Executable file
31
scripts/walkforward_vol.sh
Normal file
31
scripts/walkforward_vol.sh
Normal 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"
|
||||||
Reference in New Issue
Block a user