MidasBot: bot trading crypto IA + stratégies Ichimoku validées

- Infra: Freqtrade (futures dry-run) + Redis + dashboard + Docker Compose
- Couche IA: ai_analyzer (Claude via abonnement, MCP TradingView, backfill biais)
- Stratégies: SampleStrategy, AiBiasStrategy, IchimokuLS (long/short, validée
  train/test + données vierges + walk-forward), MTFIchimoku, variantes hyperopt
- Arbitrage CEX (dry-run), backtesting, walk-forward, volatility targeting
- IchimokuLS en dry-run live (config_live.json)

Claude-Session: https://claude.ai/code/session_01VHETcFacdnDhQzthLpdYFR
This commit is contained in:
jerem
2026-06-23 19:25:49 +02:00
commit 633b033f4d
59 changed files with 3868 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
{
"$schema": "https://schema.freqtrade.io/schema.json",
"bot_name": "MidasBot",
"max_open_trades": 3,
"stake_currency": "USDT",
"stake_amount": 100,
"tradable_balance_ratio": 0.99,
"fiat_display_currency": "EUR",
"dry_run": true,
"dry_run_wallet": 1000,
"cancel_open_orders_on_exit": false,
"trading_mode": "spot",
"margin_mode": "",
"timeframe": "1h",
"unfilledtimeout": {
"entry": 10,
"exit": 10,
"exit_timeout_count": 0,
"unit": "minutes"
},
"entry_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"exit_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1
},
"exchange": {
"name": "binance",
"key": "",
"secret": "",
"ccxt_config": {},
"ccxt_async_config": {},
"pair_whitelist": [
"BTC/USDT",
"ETH/USDT",
"SOL/USDT",
"BNB/USDT"
],
"pair_blacklist": [
".*(BULL|BEAR|UP|DOWN)/.*",
".*(USDC|TUSD|BUSD|DAI|FDUSD)/.*"
]
},
"pairlists": [
{
"method": "StaticPairList"
}
],
"telegram": {
"enabled": false,
"token": "",
"chat_id": ""
},
"api_server": {
"enabled": true,
"listen_ip_address": "0.0.0.0",
"listen_port": 8080,
"verbosity": "error",
"enable_openapi": true,
"jwt_secret_key": "set-via-env-FREQTRADE__API_SERVER__JWT_SECRET_KEY",
"ws_token": "set-via-env-FREQTRADE__API_SERVER__WS_TOKEN",
"CORS_origins": [],
"username": "set-via-env",
"password": "set-via-env"
},
"initial_state": "running",
"force_entry_enable": false,
"internals": {
"process_throttle_secs": 5
}
}

View File

@@ -0,0 +1,80 @@
{
"$schema": "https://schema.freqtrade.io/schema.json",
"bot_name": "MidasBot",
"max_open_trades": 3,
"stake_currency": "USDT",
"stake_amount": 100,
"tradable_balance_ratio": 0.99,
"fiat_display_currency": "EUR",
"dry_run": true,
"dry_run_wallet": 1000,
"cancel_open_orders_on_exit": false,
"trading_mode": "futures",
"margin_mode": "isolated",
"timeframe": "1h",
"unfilledtimeout": {
"entry": 10,
"exit": 10,
"exit_timeout_count": 0,
"unit": "minutes"
},
"entry_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"exit_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1
},
"exchange": {
"name": "binance",
"key": "",
"secret": "",
"ccxt_config": {},
"ccxt_async_config": {},
"pair_whitelist": [
"BTC/USDT:USDT",
"ETH/USDT:USDT",
"SOL/USDT:USDT",
"BNB/USDT:USDT"
],
"pair_blacklist": [
".*(BULL|BEAR|UP|DOWN)/.*",
".*(USDC|TUSD|BUSD|DAI|FDUSD)/.*"
]
},
"pairlists": [
{
"method": "StaticPairList"
}
],
"telegram": {
"enabled": false,
"token": "",
"chat_id": ""
},
"api_server": {
"enabled": false,
"listen_ip_address": "0.0.0.0",
"listen_port": 8080,
"verbosity": "error",
"enable_openapi": true,
"jwt_secret_key": "set-via-env-FREQTRADE__API_SERVER__JWT_SECRET_KEY",
"ws_token": "set-via-env-FREQTRADE__API_SERVER__WS_TOKEN",
"CORS_origins": [],
"username": "set-via-env",
"password": "set-via-env"
},
"initial_state": "running",
"force_entry_enable": false,
"internals": {
"process_throttle_secs": 5
}
}

View File

@@ -0,0 +1,88 @@
{
"$schema": "https://schema.freqtrade.io/schema.json",
"bot_name": "MidasBot",
"max_open_trades": 9,
"stake_currency": "USDT",
"stake_amount": 100,
"tradable_balance_ratio": 0.99,
"fiat_display_currency": "EUR",
"dry_run": true,
"dry_run_wallet": 1000,
"cancel_open_orders_on_exit": false,
"trading_mode": "futures",
"margin_mode": "isolated",
"timeframe": "1h",
"unfilledtimeout": {
"entry": 10,
"exit": 10,
"exit_timeout_count": 0,
"unit": "minutes"
},
"entry_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"exit_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1
},
"exchange": {
"name": "binance",
"key": "",
"secret": "",
"ccxt_config": {},
"ccxt_async_config": {},
"pair_whitelist": [
"BTC/USDT:USDT",
"ETH/USDT:USDT",
"SOL/USDT:USDT",
"BNB/USDT:USDT",
"XRP/USDT:USDT",
"ADA/USDT:USDT",
"AVAX/USDT:USDT",
"DOGE/USDT:USDT",
"LINK/USDT:USDT",
"DOT/USDT:USDT",
"LTC/USDT:USDT",
"TRX/USDT:USDT"
],
"pair_blacklist": [
".*(BULL|BEAR|UP|DOWN)/.*",
".*(USDC|TUSD|BUSD|DAI|FDUSD)/.*"
]
},
"pairlists": [
{
"method": "StaticPairList"
}
],
"telegram": {
"enabled": false,
"token": "",
"chat_id": ""
},
"api_server": {
"enabled": false,
"listen_ip_address": "0.0.0.0",
"listen_port": 8080,
"verbosity": "error",
"enable_openapi": true,
"jwt_secret_key": "set-via-env-FREQTRADE__API_SERVER__JWT_SECRET_KEY",
"ws_token": "set-via-env-FREQTRADE__API_SERVER__WS_TOKEN",
"CORS_origins": [],
"username": "set-via-env",
"password": "set-via-env"
},
"initial_state": "running",
"force_entry_enable": false,
"internals": {
"process_throttle_secs": 5
}
}

View File

@@ -0,0 +1,80 @@
{
"$schema": "https://schema.freqtrade.io/schema.json",
"bot_name": "MidasBot",
"max_open_trades": 3,
"stake_currency": "USDT",
"stake_amount": "unlimited",
"tradable_balance_ratio": 0.99,
"fiat_display_currency": "EUR",
"dry_run": true,
"dry_run_wallet": 100,
"cancel_open_orders_on_exit": false,
"trading_mode": "futures",
"margin_mode": "isolated",
"timeframe": "1h",
"unfilledtimeout": {
"entry": 10,
"exit": 10,
"exit_timeout_count": 0,
"unit": "minutes"
},
"entry_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"exit_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1
},
"exchange": {
"name": "binance",
"key": "",
"secret": "",
"ccxt_config": {},
"ccxt_async_config": {},
"pair_whitelist": [
"BTC/USDT:USDT",
"ETH/USDT:USDT",
"SOL/USDT:USDT",
"BNB/USDT:USDT"
],
"pair_blacklist": [
".*(BULL|BEAR|UP|DOWN)/.*",
".*(USDC|TUSD|BUSD|DAI|FDUSD)/.*"
]
},
"pairlists": [
{
"method": "StaticPairList"
}
],
"telegram": {
"enabled": false,
"token": "",
"chat_id": ""
},
"api_server": {
"enabled": false,
"listen_ip_address": "0.0.0.0",
"listen_port": 8080,
"verbosity": "error",
"enable_openapi": true,
"jwt_secret_key": "set-via-env-FREQTRADE__API_SERVER__JWT_SECRET_KEY",
"ws_token": "set-via-env-FREQTRADE__API_SERVER__WS_TOKEN",
"CORS_origins": [],
"username": "set-via-env",
"password": "set-via-env"
},
"initial_state": "running",
"force_entry_enable": false,
"internals": {
"process_throttle_secs": 5
}
}

View File

@@ -0,0 +1,80 @@
{
"$schema": "https://schema.freqtrade.io/schema.json",
"bot_name": "MidasBot",
"max_open_trades": 3,
"stake_currency": "USDT",
"stake_amount": "unlimited",
"tradable_balance_ratio": 0.99,
"fiat_display_currency": "EUR",
"dry_run": true,
"dry_run_wallet": 100,
"cancel_open_orders_on_exit": false,
"trading_mode": "futures",
"margin_mode": "isolated",
"timeframe": "15m",
"unfilledtimeout": {
"entry": 10,
"exit": 10,
"exit_timeout_count": 0,
"unit": "minutes"
},
"entry_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"exit_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1
},
"exchange": {
"name": "binance",
"key": "",
"secret": "",
"ccxt_config": {},
"ccxt_async_config": {},
"pair_whitelist": [
"BTC/USDT:USDT",
"ETH/USDT:USDT",
"SOL/USDT:USDT",
"BNB/USDT:USDT"
],
"pair_blacklist": [
".*(BULL|BEAR|UP|DOWN)/.*",
".*(USDC|TUSD|BUSD|DAI|FDUSD)/.*"
]
},
"pairlists": [
{
"method": "StaticPairList"
}
],
"telegram": {
"enabled": false,
"token": "",
"chat_id": ""
},
"api_server": {
"enabled": false,
"listen_ip_address": "0.0.0.0",
"listen_port": 8080,
"verbosity": "error",
"enable_openapi": true,
"jwt_secret_key": "set-via-env-FREQTRADE__API_SERVER__JWT_SECRET_KEY",
"ws_token": "set-via-env-FREQTRADE__API_SERVER__WS_TOKEN",
"CORS_origins": [],
"username": "set-via-env",
"password": "set-via-env"
},
"initial_state": "running",
"force_entry_enable": false,
"internals": {
"process_throttle_secs": 5
}
}

View File

@@ -0,0 +1,80 @@
{
"$schema": "https://schema.freqtrade.io/schema.json",
"bot_name": "MidasBot-Ichimoku",
"max_open_trades": 3,
"stake_currency": "USDT",
"stake_amount": 100,
"tradable_balance_ratio": 0.99,
"fiat_display_currency": "EUR",
"dry_run": true,
"dry_run_wallet": 1000,
"cancel_open_orders_on_exit": false,
"trading_mode": "futures",
"margin_mode": "isolated",
"timeframe": "1h",
"unfilledtimeout": {
"entry": 10,
"exit": 10,
"exit_timeout_count": 0,
"unit": "minutes"
},
"entry_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"exit_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1
},
"exchange": {
"name": "binance",
"key": "",
"secret": "",
"ccxt_config": {},
"ccxt_async_config": {},
"pair_whitelist": [
"BTC/USDT:USDT",
"ETH/USDT:USDT",
"SOL/USDT:USDT",
"BNB/USDT:USDT"
],
"pair_blacklist": [
".*(BULL|BEAR|UP|DOWN)/.*",
".*(USDC|TUSD|BUSD|DAI|FDUSD)/.*"
]
},
"pairlists": [
{
"method": "StaticPairList"
}
],
"telegram": {
"enabled": false,
"token": "",
"chat_id": ""
},
"api_server": {
"enabled": true,
"listen_ip_address": "0.0.0.0",
"listen_port": 8080,
"verbosity": "error",
"enable_openapi": true,
"jwt_secret_key": "set-via-env-FREQTRADE__API_SERVER__JWT_SECRET_KEY",
"ws_token": "set-via-env-FREQTRADE__API_SERVER__WS_TOKEN",
"CORS_origins": [],
"username": "set-via-env",
"password": "set-via-env"
},
"initial_state": "running",
"force_entry_enable": false,
"internals": {
"process_throttle_secs": 5
}
}

View File

@@ -0,0 +1,110 @@
{
"$schema": "https://schema.freqtrade.io/schema.json",
"bot_name": "MidasBot",
"max_open_trades": 6,
"stake_currency": "USDT",
"stake_amount": "unlimited",
"tradable_balance_ratio": 0.99,
"fiat_display_currency": "EUR",
"dry_run": true,
"dry_run_wallet": 1000,
"cancel_open_orders_on_exit": false,
"trading_mode": "spot",
"margin_mode": "",
"timeframe": "5m",
"unfilledtimeout": {
"entry": 10,
"exit": 10,
"exit_timeout_count": 0,
"unit": "minutes"
},
"entry_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"exit_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1
},
"exchange": {
"name": "binance",
"key": "",
"secret": "",
"ccxt_config": {},
"ccxt_async_config": {},
"pair_whitelist": [
"BTC/USDT",
"ETH/USDT",
"SOL/USDT",
"BNB/USDT",
"XRP/USDT",
"ADA/USDT",
"AVAX/USDT",
"DOGE/USDT",
"LINK/USDT",
"DOT/USDT",
"LTC/USDT",
"TRX/USDT",
"ATOM/USDT",
"NEAR/USDT",
"APT/USDT",
"ARB/USDT",
"OP/USDT",
"FIL/USDT",
"INJ/USDT",
"SUI/USDT",
"UNI/USDT",
"AAVE/USDT",
"ETC/USDT",
"XLM/USDT",
"ALGO/USDT",
"VET/USDT",
"HBAR/USDT",
"RUNE/USDT",
"SAND/USDT",
"GALA/USDT"
],
"pair_blacklist": [
".*(BULL|BEAR|UP|DOWN)/.*",
".*(USDC|TUSD|BUSD|DAI|FDUSD)/.*"
]
},
"pairlists": [
{
"method": "StaticPairList"
}
],
"telegram": {
"enabled": false,
"token": "",
"chat_id": ""
},
"api_server": {
"enabled": false,
"listen_ip_address": "0.0.0.0",
"listen_port": 8080,
"verbosity": "error",
"enable_openapi": true,
"jwt_secret_key": "set-via-env-FREQTRADE__API_SERVER__JWT_SECRET_KEY",
"ws_token": "set-via-env-FREQTRADE__API_SERVER__WS_TOKEN",
"CORS_origins": [],
"username": "set-via-env",
"password": "set-via-env"
},
"initial_state": "running",
"force_entry_enable": false,
"internals": {
"process_throttle_secs": 5
},
"use_exit_signal": true,
"exit_profit_only": false,
"ignore_roi_if_entry_signal": true,
"position_adjustment_enable": true
}

View File

@@ -0,0 +1 @@
{"AAVE/USDT":{"rate":73.39,"profit":-0.06942172132549185,"sell_reason":"exit_long_tc_stoploss_doom","time_profit_reached":"2026-06-23T14:45:00+00:00"},"NEAR/USDT":{"rate":1.996,"profit":-0.04442309198882848,"sell_reason":"exit_long_tc_stoploss_doom","time_profit_reached":"2026-06-23T14:45:00+00:00"}}

View File

@@ -0,0 +1,171 @@
# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods
"""
AiBiasStrategy — stratégie directionnelle pilotée par le biais IA (Phase 3).
Base technique : croisement EMA + RSI (comme SampleStrategy).
Surcouche IA : un biais de marché produit par Claude (service ai_analyzer) est lu
depuis Redis et filtre/renforce les entrées.
- Live / dry-run : lit le biais COURANT de la paire à chaque nouvelle bougie.
- Backtest : Redis ne contient pas d'historique de biais -> repli `neutral` (la
surcouche IA est neutralisée, seule la base technique joue). Backtester finement
l'IA nécessiterait d'enregistrer les biais historiques (amélioration future).
"""
from __future__ import annotations
import json
import os
from functools import lru_cache
from pathlib import Path
import pandas as pd
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.enums import RunMode
from freqtrade.strategy import IStrategy
# Répertoire de l'historique des biais (monté dans le conteneur freqtrade).
_HISTORY_DIR = os.environ.get(
"AI_HISTORY_DIR", "/freqtrade/user_data/ai_bias_history"
)
@lru_cache(maxsize=1)
def _redis_client():
"""Client Redis paresseux et tolérant aux pannes (None si indisponible)."""
try:
import redis # importé ici pour ne pas casser si le paquet manque
url = os.environ.get("REDIS_URL", "redis://redis:6379/0")
client = redis.Redis.from_url(
url, decode_responses=True, socket_connect_timeout=1, socket_timeout=1
)
client.ping()
return client
except Exception: # noqa: BLE001
return None
def _read_bias(pair: str) -> dict:
"""Renvoie {'direction', 'confidence'} COURANT pour la paire (live/dry-run)."""
client = _redis_client()
if client is None:
return {"direction": "neutral", "confidence": 0.0}
try:
raw = client.get(f"bias:{pair}")
if not raw:
return {"direction": "neutral", "confidence": 0.0}
data = json.loads(raw)
return {
"direction": data.get("direction", "neutral"),
"confidence": float(data.get("confidence", 0.0)),
}
except Exception: # noqa: BLE001
return {"direction": "neutral", "confidence": 0.0}
def _load_history(pair: str) -> pd.DataFrame:
"""Charge l'historique horodaté des biais d'une paire (vide si absent)."""
path = Path(_HISTORY_DIR) / f"{pair.replace('/', '_')}.csv"
if not path.exists():
return pd.DataFrame(columns=["timestamp", "direction", "confidence"])
try:
hist = pd.read_csv(path)
hist["timestamp"] = pd.to_datetime(hist["timestamp"], utc=True)
hist["confidence"] = pd.to_numeric(hist["confidence"], errors="coerce").fillna(0.0)
return hist.sort_values("timestamp").reset_index(drop=True)
except Exception: # noqa: BLE001 — historique corrompu : on l'ignore
return pd.DataFrame(columns=["timestamp", "direction", "confidence"])
def _merge_history(dataframe: DataFrame, pair: str) -> DataFrame:
"""Associe à chaque bougie le dernier biais connu À CETTE DATE (merge_asof)."""
hist = _load_history(pair)
if hist.empty:
dataframe["ai_direction"] = "neutral"
dataframe["ai_confidence"] = 0.0
return dataframe
# Aligner la résolution temporelle (Freqtrade=ms, pandas parse=us) sinon merge_asof échoue.
hist = hist.copy()
hist["timestamp"] = hist["timestamp"].astype(dataframe["date"].dtype)
# Le dataframe Freqtrade est déjà trié par date croissante (prérequis merge_asof).
merged = pd.merge_asof(
dataframe[["date"]],
hist[["timestamp", "direction", "confidence"]],
left_on="date",
right_on="timestamp",
direction="backward",
)
dataframe["ai_direction"] = merged["direction"].fillna("neutral").values
dataframe["ai_confidence"] = merged["confidence"].fillna(0.0).values
return dataframe
class AiBiasStrategy(IStrategy):
INTERFACE_VERSION = 3
timeframe = "1h"
minimal_roi = {"0": 0.05, "120": 0.03, "360": 0.01, "720": 0}
stoploss = -0.10
trailing_stop = True
trailing_stop_positive = 0.02
trailing_stop_positive_offset = 0.03
trailing_only_offset_is_reached = True
startup_candle_count: int = 50
process_only_new_candles = True
use_exit_signal = True
# Confiance minimale du biais IA pour qu'il influence les décisions.
ai_min_confidence: float = float(os.environ.get("AI_MIN_CONFIDENCE", "0.6"))
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["ema_fast"] = ta.EMA(dataframe, timeperiod=9)
dataframe["ema_slow"] = ta.EMA(dataframe, timeperiod=21)
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
# Source du biais IA selon le mode :
# - backtest/hyperopt/plot : historique horodaté (biais valide à chaque bougie)
# - dry-run/live : biais COURANT depuis Redis (dernière bougie)
runmode = self.dp.runmode if self.dp is not None else RunMode.OTHER
if runmode in (RunMode.BACKTEST, RunMode.HYPEROPT, RunMode.PLOT):
dataframe = _merge_history(dataframe, metadata["pair"])
else:
bias = _read_bias(metadata["pair"])
dataframe["ai_direction"] = bias["direction"]
dataframe["ai_confidence"] = bias["confidence"]
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
base_long = (
(dataframe["ema_fast"] > dataframe["ema_slow"])
& (dataframe["ema_fast"].shift(1) <= dataframe["ema_slow"].shift(1))
& (dataframe["rsi"] < 70)
& (dataframe["volume"] > 0)
)
# Filtre IA : si le biais est suffisamment confiant, on bloque les longs
# quand il est baissier ; on les laisse passer sinon (neutre/haussier).
ai_blocks_long = (dataframe["ai_direction"] == "bearish") & (
dataframe["ai_confidence"] >= self.ai_min_confidence
)
dataframe.loc[base_long & (~ai_blocks_long), "enter_long"] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
base_exit = (
(dataframe["ema_fast"] < dataframe["ema_slow"])
& (dataframe["ema_fast"].shift(1) >= dataframe["ema_slow"].shift(1))
& (dataframe["volume"] > 0)
)
# Sortie anticipée si l'IA devient nettement baissière.
ai_exit = (dataframe["ai_direction"] == "bearish") & (
dataframe["ai_confidence"] >= self.ai_min_confidence
)
dataframe.loc[base_exit | ai_exit, "exit_long"] = 1
return dataframe

View File

@@ -0,0 +1,82 @@
# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods
"""
BBMeanRev — 2e moteur : mean-reversion (Bollinger + RSI), long/short futures.
Logique CONTRAIRE au trend-follower (Ichimoku) → décorrélée par construction :
LONG quand le prix casse SOUS la bande basse + RSI survendu (rebond attendu).
SHORT quand le prix casse AU-DESSUS de la bande haute + RSI suracheté (repli attendu).
Sortie : retour à la moyenne (bande médiane) + ROI/stop.
But : gagner dans les marchés en range (là où l'Ichimoku perd), pour qu'en
combinaison le ratio rendement/risque monte. Paramétrable pour hyperopt.
"""
from __future__ import annotations
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.strategy import IStrategy, IntParameter
class BBMeanRev(IStrategy):
INTERFACE_VERSION = 3
timeframe = "1h"
can_short = True
# Mean-reversion = sorties rapides ; valeurs par défaut, surchargées par hyperopt.
minimal_roi = {"0": 0.03, "60": 0.02, "180": 0.01, "360": 0}
stoploss = -0.05
trailing_stop = False
startup_candle_count: int = 50
process_only_new_candles = True
use_exit_signal = True
buy_rsi = IntParameter(10, 40, default=30, space="buy", optimize=True)
sell_rsi = IntParameter(60, 90, default=70, space="sell", optimize=True)
def leverage(self, pair, current_time, current_rate, proposed_leverage,
max_leverage, entry_tag, side, **kwargs) -> float:
return 1.0
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
bb = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
dataframe["bb_lower"] = bb["lowerband"]
dataframe["bb_mid"] = bb["middleband"]
dataframe["bb_upper"] = bb["upperband"]
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# LONG : survente (RSI bas) dans la zone basse des bandes
dataframe.loc[
(
(dataframe["rsi"] < self.buy_rsi.value)
& (dataframe["close"] < dataframe["bb_mid"])
& (dataframe["volume"] > 0)
),
"enter_long",
] = 1
# SHORT : surchauffe (RSI haut) dans la zone haute des bandes
dataframe.loc[
(
(dataframe["rsi"] > self.sell_rsi.value)
& (dataframe["close"] > dataframe["bb_mid"])
& (dataframe["volume"] > 0)
),
"enter_short",
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Sortie LONG : RSI revenu vers la moyenne (réversion réalisée)
dataframe.loc[
((dataframe["rsi"] > 50) & (dataframe["volume"] > 0)),
"exit_long",
] = 1
# Sortie SHORT : RSI revenu vers la moyenne
dataframe.loc[
((dataframe["rsi"] < 50) & (dataframe["volume"] > 0)),
"exit_short",
] = 1
return dataframe

View File

@@ -0,0 +1,34 @@
{
"strategy_name": "HyperStrategy",
"params": {
"max_open_trades": {
"max_open_trades": 3
},
"buy": {
"buy_adx_min": 25,
"buy_require_macd": true,
"buy_require_trend": false,
"buy_rsi_max": 78
},
"sell": {
"sell_rsi_min": 47
},
"roi": {
"0": 0.459,
"187": 0.06,
"693": 0.032,
"1425": 0
},
"stoploss": {
"stoploss": -0.137
},
"trailing": {
"trailing_stop": true,
"trailing_stop_positive": 0.016,
"trailing_stop_positive_offset": 0.053,
"trailing_only_offset_is_reached": true
}
},
"ft_stratparam_v": 1,
"export_time": "2026-06-23 14:08:47.078605+00:00"
}

View File

@@ -0,0 +1,82 @@
# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods
"""
HyperStrategy — stratégie paramétrable pour optimisation (hyperopt).
Indicateurs fixes (EMA/RSI/ADX/MACD), conditions d'entrée et gestion de sortie
PARAMÉTRÉES → Freqtrade peut optimiser les seuils, le ROI, le stoploss et le trailing.
Méthode honnête : on optimise sur une période d'entraînement puis on VALIDE sur une
période hors-échantillon (test) pour détecter le sur-apprentissage.
"""
from __future__ import annotations
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.strategy import (
IStrategy,
IntParameter,
BooleanParameter,
)
class HyperStrategy(IStrategy):
INTERFACE_VERSION = 3
timeframe = "1h"
# Valeurs par défaut — surchargées par les résultats d'hyperopt.
minimal_roi = {"0": 0.10, "240": 0.05, "720": 0.02, "1440": 0}
stoploss = -0.08
trailing_stop = True
trailing_stop_positive = 0.02
trailing_stop_positive_offset = 0.04
trailing_only_offset_is_reached = True
startup_candle_count: int = 60
process_only_new_candles = True
use_exit_signal = True
# --- Paramètres optimisables (espace "buy") ---
buy_rsi_max = IntParameter(60, 80, default=70, space="buy", optimize=True)
buy_adx_min = IntParameter(15, 40, default=25, space="buy", optimize=True)
buy_require_trend = BooleanParameter(default=True, space="buy", optimize=True)
buy_require_macd = BooleanParameter(default=True, space="buy", optimize=True)
# --- Paramètres optimisables (espace "sell") ---
sell_rsi_min = IntParameter(20, 50, default=35, space="sell", optimize=True)
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["ema_fast"] = ta.EMA(dataframe, timeperiod=9)
dataframe["ema_slow"] = ta.EMA(dataframe, timeperiod=21)
dataframe["ema_trend"] = ta.EMA(dataframe, timeperiod=50)
dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
macd = ta.MACD(dataframe)
dataframe["macd"] = macd["macd"]
dataframe["macdsignal"] = macd["macdsignal"]
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
cond = (
(dataframe["ema_fast"] > dataframe["ema_slow"])
& (dataframe["ema_fast"].shift(1) <= dataframe["ema_slow"].shift(1))
& (dataframe["rsi"] < self.buy_rsi_max.value)
& (dataframe["adx"] > self.buy_adx_min.value)
& (dataframe["volume"] > 0)
)
if self.buy_require_trend.value:
cond &= dataframe["close"] > dataframe["ema_trend"]
if self.buy_require_macd.value:
cond &= dataframe["macd"] > dataframe["macdsignal"]
dataframe.loc[cond, "enter_long"] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(dataframe["ema_fast"] < dataframe["ema_slow"])
& (dataframe["rsi"] < self.sell_rsi_min.value)
& (dataframe["volume"] > 0)
),
"exit_long",
] = 1
return dataframe

View File

@@ -0,0 +1,31 @@
{
"strategy_name": "IchimokuChop",
"params": {
"max_open_trades": {
"max_open_trades": 3
},
"buy": {
"buy_adx_min": 30,
"buy_chop_max": 43,
"buy_cloud_min_pct": 0.57,
"require_tk_cross": false
},
"roi": {
"0": 0.207,
"345": 0.144,
"1052": 0.048,
"2483": 0
},
"stoploss": {
"stoploss": -0.236
},
"trailing": {
"trailing_stop": true,
"trailing_stop_positive": 0.099,
"trailing_stop_positive_offset": 0.162,
"trailing_only_offset_is_reached": false
}
},
"ft_stratparam_v": 1,
"export_time": "2026-06-23 16:58:02.232138+00:00"
}

View File

@@ -0,0 +1,115 @@
# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods
"""
IchimokuChop — IchimokuHyper + FILTRE DE RÉGIME (Choppiness Index).
Hypothèse : les folds perdants du walk-forward sont des marchés en RANGE (chop),
où un trend-follower se fait whipsaw. On ajoute un filtre Choppiness Index :
- CI élevé (>~61) = consolidation/range → on NE trade PAS.
- CI bas (<~38) = tendance forte → on trade.
Le seuil est optimisable. On valide ensuite en WALK-FORWARD (pas en hindsight).
"""
from __future__ import annotations
import numpy as np
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.strategy import (
IStrategy,
IntParameter,
DecimalParameter,
BooleanParameter,
)
class IchimokuChop(IStrategy):
INTERFACE_VERSION = 3
timeframe = "1h"
can_short = True
minimal_roi = {"0": 0.08, "240": 0.04, "720": 0.02, "1440": 0}
stoploss = -0.08
trailing_stop = True
trailing_stop_positive = 0.02
trailing_stop_positive_offset = 0.03
trailing_only_offset_is_reached = True
startup_candle_count: int = 220
process_only_new_candles = True
use_exit_signal = True
buy_adx_min = IntParameter(15, 40, default=25, space="buy", optimize=True)
buy_cloud_min_pct = DecimalParameter(0.0, 2.0, default=0.3, decimals=2, space="buy", optimize=True)
require_tk_cross = BooleanParameter(default=False, space="buy", optimize=True)
# Filtre de régime : ne trade que si Choppiness Index < seuil (= marché qui tend)
buy_chop_max = IntParameter(35, 70, default=61, space="buy", optimize=True)
def leverage(self, pair, current_time, current_rate, proposed_leverage,
max_leverage, entry_tag, side, **kwargs) -> float:
return 1.0
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
high, low, close = dataframe["high"], dataframe["low"], dataframe["close"]
tenkan = (high.rolling(9).max() + low.rolling(9).min()) / 2
kijun = (high.rolling(26).max() + low.rolling(26).min()) / 2
dataframe["tenkan"] = tenkan
dataframe["kijun"] = kijun
dataframe["senkou_a"] = ((tenkan + kijun) / 2).shift(26)
dataframe["senkou_b"] = ((high.rolling(52).max() + low.rolling(52).min()) / 2).shift(26)
dataframe["cloud_top"] = dataframe[["senkou_a", "senkou_b"]].max(axis=1)
dataframe["cloud_bot"] = dataframe[["senkou_a", "senkou_b"]].min(axis=1)
dataframe["cloud_width_pct"] = (dataframe["cloud_top"] - dataframe["cloud_bot"]) / close * 100
dataframe["close_prev26"] = close.shift(26)
dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
# --- Choppiness Index (14) ---
n = 14
atr1 = ta.ATR(dataframe, timeperiod=1)
tr_sum = atr1.rolling(n).sum()
rng = high.rolling(n).max() - low.rolling(n).min()
dataframe["chop"] = 100 * np.log10(tr_sum / rng) / np.log10(n)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
adx_min = self.buy_adx_min.value
cloud_min = self.buy_cloud_min_pct.value
not_choppy = dataframe["chop"] < self.buy_chop_max.value # ← filtre régime
long_cond = (
(dataframe["close"] > dataframe["cloud_top"])
& (dataframe["tenkan"] > dataframe["kijun"])
& (dataframe["close"] > dataframe["close_prev26"])
& (dataframe["adx"] > adx_min)
& (dataframe["cloud_width_pct"] > cloud_min)
& not_choppy
& (dataframe["volume"] > 0)
)
short_cond = (
(dataframe["close"] < dataframe["cloud_bot"])
& (dataframe["tenkan"] < dataframe["kijun"])
& (dataframe["close"] < dataframe["close_prev26"])
& (dataframe["adx"] > adx_min)
& (dataframe["cloud_width_pct"] > cloud_min)
& not_choppy
& (dataframe["volume"] > 0)
)
if self.require_tk_cross.value:
long_cond &= dataframe["tenkan"].shift(1) <= dataframe["kijun"].shift(1)
short_cond &= dataframe["tenkan"].shift(1) >= dataframe["kijun"].shift(1)
dataframe.loc[long_cond, "enter_long"] = 1
dataframe.loc[short_cond, "enter_short"] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(((dataframe["tenkan"] < dataframe["kijun"]) | (dataframe["close"] < dataframe["cloud_bot"]))
& (dataframe["volume"] > 0)),
"exit_long",
] = 1
dataframe.loc[
(((dataframe["tenkan"] > dataframe["kijun"]) | (dataframe["close"] > dataframe["cloud_top"]))
& (dataframe["volume"] > 0)),
"exit_short",
] = 1
return dataframe

View File

@@ -0,0 +1,30 @@
{
"strategy_name": "IchimokuHyper",
"params": {
"max_open_trades": {
"max_open_trades": 3
},
"buy": {
"buy_adx_min": 29,
"buy_cloud_min_pct": 0.4,
"require_tk_cross": false
},
"roi": {
"0": 0.139,
"206": 0.071,
"326": 0.025,
"1563": 0
},
"stoploss": {
"stoploss": -0.299
},
"trailing": {
"trailing_stop": true,
"trailing_stop_positive": 0.014,
"trailing_stop_positive_offset": 0.042,
"trailing_only_offset_is_reached": true
}
},
"ft_stratparam_v": 1,
"export_time": "2026-06-23 15:52:37.295448+00:00"
}

View File

@@ -0,0 +1,30 @@
{
"strategy_name": "IchimokuHyper",
"params": {
"max_open_trades": {
"max_open_trades": 3
},
"buy": {
"buy_adx_min": 30,
"buy_cloud_min_pct": 1.37,
"require_tk_cross": false
},
"roi": {
"0": 0.465,
"302": 0.171,
"728": 0.08,
"2097": 0
},
"stoploss": {
"stoploss": -0.174
},
"trailing": {
"trailing_stop": true,
"trailing_stop_positive": 0.043,
"trailing_stop_positive_offset": 0.115,
"trailing_only_offset_is_reached": true
}
},
"ft_stratparam_v": 1,
"export_time": "2026-06-23 16:43:16.595085+00:00"
}

View File

@@ -0,0 +1,118 @@
# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods
"""
IchimokuHyper — version paramétrable d'IchimokuStrategy pour hyperopt (futures, long/short).
Paramètres optimisables :
- buy_adx_min : force de tendance minimale
- buy_cloud_min_pct : largeur minimale du nuage (anti-chop) en % du prix
- require_tk_cross : exiger le croisement Tenkan/Kijun (sinon simple position vs nuage)
+ espaces ROI / stoploss / trailing.
Anti-lookahead : Senkou décalés de +26 ; confirmation via close vs close[-26].
"""
from __future__ import annotations
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.strategy import (
IStrategy,
IntParameter,
DecimalParameter,
BooleanParameter,
)
class IchimokuHyper(IStrategy):
INTERFACE_VERSION = 3
timeframe = "1h"
can_short = True
minimal_roi = {"0": 0.06, "240": 0.03, "720": 0.01, "1440": 0}
stoploss = -0.08
trailing_stop = True
trailing_stop_positive = 0.025
trailing_stop_positive_offset = 0.04
trailing_only_offset_is_reached = True
startup_candle_count: int = 120
process_only_new_candles = True
use_exit_signal = True
# --- Paramètres optimisables (espace "buy", appliqués long ET short) ---
buy_adx_min = IntParameter(15, 40, default=20, space="buy", optimize=True)
buy_cloud_min_pct = DecimalParameter(
0.0, 2.0, default=0.0, decimals=2, space="buy", optimize=True
)
require_tk_cross = BooleanParameter(default=True, space="buy", optimize=True)
def leverage(self, pair, current_time, current_rate, proposed_leverage,
max_leverage, entry_tag, side, **kwargs) -> float:
return 1.0
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
high, low, close = dataframe["high"], dataframe["low"], dataframe["close"]
tenkan = (high.rolling(9).max() + low.rolling(9).min()) / 2
kijun = (high.rolling(26).max() + low.rolling(26).min()) / 2
dataframe["tenkan"] = tenkan
dataframe["kijun"] = kijun
dataframe["senkou_a"] = ((tenkan + kijun) / 2).shift(26)
dataframe["senkou_b"] = (
(high.rolling(52).max() + low.rolling(52).min()) / 2
).shift(26)
dataframe["cloud_top"] = dataframe[["senkou_a", "senkou_b"]].max(axis=1)
dataframe["cloud_bot"] = dataframe[["senkou_a", "senkou_b"]].min(axis=1)
# Largeur du nuage en % du prix (filtre anti-chop)
dataframe["cloud_width_pct"] = (
(dataframe["cloud_top"] - dataframe["cloud_bot"]) / close * 100
)
dataframe["close_prev26"] = close.shift(26)
dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
adx_min = self.buy_adx_min.value
cloud_min = self.buy_cloud_min_pct.value
long_cond = (
(dataframe["close"] > dataframe["cloud_top"])
& (dataframe["tenkan"] > dataframe["kijun"])
& (dataframe["close"] > dataframe["close_prev26"])
& (dataframe["adx"] > adx_min)
& (dataframe["cloud_width_pct"] > cloud_min)
& (dataframe["volume"] > 0)
)
short_cond = (
(dataframe["close"] < dataframe["cloud_bot"])
& (dataframe["tenkan"] < dataframe["kijun"])
& (dataframe["close"] < dataframe["close_prev26"])
& (dataframe["adx"] > adx_min)
& (dataframe["cloud_width_pct"] > cloud_min)
& (dataframe["volume"] > 0)
)
if self.require_tk_cross.value:
long_cond &= dataframe["tenkan"].shift(1) <= dataframe["kijun"].shift(1)
short_cond &= dataframe["tenkan"].shift(1) >= dataframe["kijun"].shift(1)
dataframe.loc[long_cond, "enter_long"] = 1
dataframe.loc[short_cond, "enter_short"] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
((dataframe["tenkan"] < dataframe["kijun"])
| (dataframe["close"] < dataframe["cloud_bot"]))
& (dataframe["volume"] > 0)
),
"exit_long",
] = 1
dataframe.loc[
(
((dataframe["tenkan"] > dataframe["kijun"])
| (dataframe["close"] > dataframe["cloud_top"]))
& (dataframe["volume"] > 0)
),
"exit_short",
] = 1
return dataframe

View File

@@ -0,0 +1,31 @@
{
"strategy_name": "IchimokuHyper2",
"params": {
"max_open_trades": {
"max_open_trades": 3
},
"buy": {
"buy_adx_min": 31,
"buy_cloud_min_pct": 0.48,
"require_tk_cross": true,
"use_macro": false
},
"roi": {
"0": 0.088,
"20": 0.066,
"63": 0.053,
"180": 0
},
"stoploss": {
"stoploss": -0.095
},
"trailing": {
"trailing_stop": true,
"trailing_stop_positive": 0.046,
"trailing_stop_positive_offset": 0.08,
"trailing_only_offset_is_reached": true
}
},
"ft_stratparam_v": 1,
"export_time": "2026-06-23 15:17:56.904527+00:00"
}

View File

@@ -0,0 +1,158 @@
# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods
"""
IchimokuHyper2 — Ichimoku long/short hyperoptable, AVEC contraintes de risque.
Améliorations vs IchimokuHyper :
- Filtre macro EMA200 optimisable (use_macro).
- STOP-LOSS CONTRAINT à une plage réaliste [-10% .. -2%] (le -23% précédent était le
vrai point faible : trop large pour envisager le levier).
- ROI plafonné à des valeurs réalistes.
Optimisation orientée robustesse (Sharpe) + train/test strict pour éviter l'overfit.
"""
from __future__ import annotations
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.strategy import (
IStrategy,
IntParameter,
DecimalParameter,
BooleanParameter,
)
from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal
class IchimokuHyper2(IStrategy):
INTERFACE_VERSION = 3
timeframe = "1h"
can_short = True
# Défauts (surchargés par hyperopt)
minimal_roi = {"0": 0.08, "240": 0.04, "720": 0.02, "1440": 0}
stoploss = -0.06
trailing_stop = True
trailing_stop_positive = 0.02
trailing_stop_positive_offset = 0.03
trailing_only_offset_is_reached = True
startup_candle_count: int = 220
process_only_new_candles = True
use_exit_signal = True
buy_adx_min = IntParameter(15, 40, default=25, space="buy", optimize=True)
buy_cloud_min_pct = DecimalParameter(
0.0, 2.0, default=0.3, decimals=2, space="buy", optimize=True
)
require_tk_cross = BooleanParameter(default=False, space="buy", optimize=True)
use_macro = BooleanParameter(default=True, space="buy", optimize=True)
# --- Espaces hyperopt CONTRAINTS (le coeur de cette optimisation) ---
class HyperOpt:
@staticmethod
def stoploss_space() -> list[Dimension]:
# Stop réaliste : entre -2% et -10% (fini le -23%).
return [SKDecimal(-0.10, -0.02, decimals=3, name="stoploss")]
@staticmethod
def roi_space() -> list[Dimension]:
return [
Integer(0, 120, name="roi_t1"),
Integer(0, 60, name="roi_t2"),
Integer(0, 30, name="roi_t3"),
SKDecimal(0.02, 0.12, decimals=3, name="roi_p1"),
SKDecimal(0.01, 0.06, decimals=3, name="roi_p2"),
SKDecimal(0.005, 0.03, decimals=3, name="roi_p3"),
]
@staticmethod
def generate_roi_table(params: dict) -> dict[int, float]:
roi = {}
roi[0] = params["roi_p1"] + params["roi_p2"] + params["roi_p3"]
roi[params["roi_t3"]] = params["roi_p1"] + params["roi_p2"]
roi[params["roi_t3"] + params["roi_t2"]] = params["roi_p1"]
roi[params["roi_t3"] + params["roi_t2"] + params["roi_t1"]] = 0
return roi
@staticmethod
def trailing_space() -> list[Dimension]:
return [
Categorical([True], name="trailing_stop"),
SKDecimal(0.01, 0.06, decimals=3, name="trailing_stop_positive"),
SKDecimal(0.005, 0.05, decimals=3, name="trailing_stop_positive_offset_p1"),
Categorical([True, False], name="trailing_only_offset_is_reached"),
]
def leverage(self, pair, current_time, current_rate, proposed_leverage,
max_leverage, entry_tag, side, **kwargs) -> float:
return 1.0
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
high, low, close = dataframe["high"], dataframe["low"], dataframe["close"]
tenkan = (high.rolling(9).max() + low.rolling(9).min()) / 2
kijun = (high.rolling(26).max() + low.rolling(26).min()) / 2
dataframe["tenkan"] = tenkan
dataframe["kijun"] = kijun
dataframe["senkou_a"] = ((tenkan + kijun) / 2).shift(26)
dataframe["senkou_b"] = (
(high.rolling(52).max() + low.rolling(52).min()) / 2
).shift(26)
dataframe["cloud_top"] = dataframe[["senkou_a", "senkou_b"]].max(axis=1)
dataframe["cloud_bot"] = dataframe[["senkou_a", "senkou_b"]].min(axis=1)
dataframe["cloud_width_pct"] = (
(dataframe["cloud_top"] - dataframe["cloud_bot"]) / close * 100
)
dataframe["close_prev26"] = close.shift(26)
dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
adx_min = self.buy_adx_min.value
cloud_min = self.buy_cloud_min_pct.value
long_cond = (
(dataframe["close"] > dataframe["cloud_top"])
& (dataframe["tenkan"] > dataframe["kijun"])
& (dataframe["close"] > dataframe["close_prev26"])
& (dataframe["adx"] > adx_min)
& (dataframe["cloud_width_pct"] > cloud_min)
& (dataframe["volume"] > 0)
)
short_cond = (
(dataframe["close"] < dataframe["cloud_bot"])
& (dataframe["tenkan"] < dataframe["kijun"])
& (dataframe["close"] < dataframe["close_prev26"])
& (dataframe["adx"] > adx_min)
& (dataframe["cloud_width_pct"] > cloud_min)
& (dataframe["volume"] > 0)
)
if self.require_tk_cross.value:
long_cond &= dataframe["tenkan"].shift(1) <= dataframe["kijun"].shift(1)
short_cond &= dataframe["tenkan"].shift(1) >= dataframe["kijun"].shift(1)
if self.use_macro.value:
long_cond &= dataframe["close"] > dataframe["ema200"]
short_cond &= dataframe["close"] < dataframe["ema200"]
dataframe.loc[long_cond, "enter_long"] = 1
dataframe.loc[short_cond, "enter_short"] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
((dataframe["tenkan"] < dataframe["kijun"])
| (dataframe["close"] < dataframe["cloud_bot"]))
& (dataframe["volume"] > 0)
),
"exit_long",
] = 1
dataframe.loc[
(
((dataframe["tenkan"] > dataframe["kijun"])
| (dataframe["close"] > dataframe["cloud_top"]))
& (dataframe["volume"] > 0)
),
"exit_short",
] = 1
return dataframe

View File

@@ -0,0 +1,36 @@
{
"strategy_name": "IchimokuHyper3",
"params": {
"max_open_trades": {
"max_open_trades": 3
},
"buy": {
"buy_adx_min": 29,
"buy_cloud_min_pct": 0.56,
"p_kijun": 29,
"p_mom": 27,
"p_senkou": 72,
"p_shift": 23,
"p_tenkan": 14,
"require_tk_cross": false,
"use_macro": false
},
"roi": {
"0": 0.353,
"401": 0.176,
"925": 0.079,
"2340": 0
},
"stoploss": {
"stoploss": -0.209
},
"trailing": {
"trailing_stop": true,
"trailing_stop_positive": 0.287,
"trailing_stop_positive_offset": 0.326,
"trailing_only_offset_is_reached": true
}
},
"ft_stratparam_v": 1,
"export_time": "2026-06-23 15:41:09.034742+00:00"
}

View File

@@ -0,0 +1,117 @@
# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods
"""
IchimokuHyper3 — espace de paramètres ÉLARGI (périodes Ichimoku optimisables).
Round 3 de la boucle d'optimisation : on donne plus de liberté à l'optimiseur
(Tenkan/Kijun/Senkou B + lookback momentum + filtres) pour pousser le gain.
⚠️ Plus de paramètres = plus de capacité d'overfitting (le gain in-sample monte,
mais l'OOS sera à surveiller).
"""
from __future__ import annotations
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.strategy import (
IStrategy,
IntParameter,
DecimalParameter,
BooleanParameter,
)
class IchimokuHyper3(IStrategy):
INTERFACE_VERSION = 3
timeframe = "1h"
can_short = True
minimal_roi = {"0": 0.08, "240": 0.04, "720": 0.02, "1440": 0}
stoploss = -0.08
trailing_stop = True
trailing_stop_positive = 0.02
trailing_stop_positive_offset = 0.03
trailing_only_offset_is_reached = True
startup_candle_count: int = 260
process_only_new_candles = True
use_exit_signal = True
# Périodes Ichimoku optimisables
p_tenkan = IntParameter(5, 20, default=9, space="buy", optimize=True)
p_kijun = IntParameter(15, 45, default=26, space="buy", optimize=True)
p_senkou = IntParameter(40, 90, default=52, space="buy", optimize=True)
p_shift = IntParameter(15, 40, default=26, space="buy", optimize=True)
p_mom = IntParameter(10, 40, default=26, space="buy", optimize=True)
# Filtres
buy_adx_min = IntParameter(10, 40, default=20, space="buy", optimize=True)
buy_cloud_min_pct = DecimalParameter(0.0, 2.0, default=0.3, decimals=2, space="buy", optimize=True)
require_tk_cross = BooleanParameter(default=False, space="buy", optimize=True)
use_macro = BooleanParameter(default=False, space="buy", optimize=True)
def leverage(self, pair, current_time, current_rate, proposed_leverage,
max_leverage, entry_tag, side, **kwargs) -> float:
return 1.0
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
high, low, close = dataframe["high"], dataframe["low"], dataframe["close"]
t, k, s, sh = (
self.p_tenkan.value, self.p_kijun.value,
self.p_senkou.value, self.p_shift.value,
)
tenkan = (high.rolling(t).max() + low.rolling(t).min()) / 2
kijun = (high.rolling(k).max() + low.rolling(k).min()) / 2
dataframe["tenkan"] = tenkan
dataframe["kijun"] = kijun
dataframe["senkou_a"] = ((tenkan + kijun) / 2).shift(sh)
dataframe["senkou_b"] = ((high.rolling(s).max() + low.rolling(s).min()) / 2).shift(sh)
dataframe["cloud_top"] = dataframe[["senkou_a", "senkou_b"]].max(axis=1)
dataframe["cloud_bot"] = dataframe[["senkou_a", "senkou_b"]].min(axis=1)
dataframe["cloud_width_pct"] = (
(dataframe["cloud_top"] - dataframe["cloud_bot"]) / close * 100
)
dataframe["close_prev"] = close.shift(self.p_mom.value)
dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
adx_min = self.buy_adx_min.value
cloud_min = self.buy_cloud_min_pct.value
long_cond = (
(dataframe["close"] > dataframe["cloud_top"])
& (dataframe["tenkan"] > dataframe["kijun"])
& (dataframe["close"] > dataframe["close_prev"])
& (dataframe["adx"] > adx_min)
& (dataframe["cloud_width_pct"] > cloud_min)
& (dataframe["volume"] > 0)
)
short_cond = (
(dataframe["close"] < dataframe["cloud_bot"])
& (dataframe["tenkan"] < dataframe["kijun"])
& (dataframe["close"] < dataframe["close_prev"])
& (dataframe["adx"] > adx_min)
& (dataframe["cloud_width_pct"] > cloud_min)
& (dataframe["volume"] > 0)
)
if self.require_tk_cross.value:
long_cond &= dataframe["tenkan"].shift(1) <= dataframe["kijun"].shift(1)
short_cond &= dataframe["tenkan"].shift(1) >= dataframe["kijun"].shift(1)
if self.use_macro.value:
long_cond &= dataframe["close"] > dataframe["ema200"]
short_cond &= dataframe["close"] < dataframe["ema200"]
dataframe.loc[long_cond, "enter_long"] = 1
dataframe.loc[short_cond, "enter_short"] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(((dataframe["tenkan"] < dataframe["kijun"]) | (dataframe["close"] < dataframe["cloud_bot"]))
& (dataframe["volume"] > 0)),
"exit_long",
] = 1
dataframe.loc[
(((dataframe["tenkan"] > dataframe["kijun"]) | (dataframe["close"] > dataframe["cloud_top"]))
& (dataframe["volume"] > 0)),
"exit_short",
] = 1
return dataframe

View File

@@ -0,0 +1,30 @@
{
"strategy_name": "IchimokuHyperVol",
"params": {
"max_open_trades": {
"max_open_trades": 3
},
"buy": {
"buy_adx_min": 34,
"buy_cloud_min_pct": 0.41,
"require_tk_cross": false
},
"roi": {
"0": 0.435,
"342": 0.209,
"509": 0.055,
"1579": 0
},
"stoploss": {
"stoploss": -0.192
},
"trailing": {
"trailing_stop": true,
"trailing_stop_positive": 0.251,
"trailing_stop_positive_offset": 0.255,
"trailing_only_offset_is_reached": false
}
},
"ft_stratparam_v": 1,
"export_time": "2026-06-23 17:08:14.826029+00:00"
}

View File

@@ -0,0 +1,126 @@
# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods
"""
IchimokuHyperVol — IchimokuHyper + VOLATILITY TARGETING (sizing dynamique).
Même logique d'entrée/sortie qu'IchimokuHyper. Seule différence : la TAILLE de
position s'ajuste à l'inverse de la volatilité récente (ATR%) :
vol haute → position réduite → pertes bornées (notamment dans le chop volatil)
vol normale→ pleine taille → gains préservés en tendance
Contrôle de risque DYNAMIQUE et non-prédictif (pas de filtre de régime overfit).
Référence de vol = médiane glissante de l'ATR% (trailing → pas de fuite du futur).
"""
from __future__ import annotations
import numpy as np
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.strategy import (
IStrategy,
IntParameter,
DecimalParameter,
BooleanParameter,
)
class IchimokuHyperVol(IStrategy):
INTERFACE_VERSION = 3
timeframe = "1h"
can_short = True
minimal_roi = {"0": 0.08, "240": 0.04, "720": 0.02, "1440": 0}
stoploss = -0.08
trailing_stop = True
trailing_stop_positive = 0.02
trailing_stop_positive_offset = 0.03
trailing_only_offset_is_reached = True
startup_candle_count: int = 520 # médiane ATR% sur 500 + Ichimoku
process_only_new_candles = True
use_exit_signal = True
buy_adx_min = IntParameter(15, 40, default=25, space="buy", optimize=True)
buy_cloud_min_pct = DecimalParameter(0.0, 2.0, default=0.3, decimals=2, space="buy", optimize=True)
require_tk_cross = BooleanParameter(default=False, space="buy", optimize=True)
# Bornes du facteur de sizing (réduit jusqu'à 0.4x, augmente jusqu'à 1.4x)
VOL_FACTOR_MIN = 0.4
VOL_FACTOR_MAX = 1.4
def leverage(self, pair, current_time, current_rate, proposed_leverage,
max_leverage, entry_tag, side, **kwargs) -> float:
return 1.0
def custom_stake_amount(self, pair, current_time, current_rate, proposed_stake,
min_stake, max_stake, leverage, entry_tag, side, **kwargs):
df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if df is None or len(df) == 0:
return proposed_stake
factor = df["vol_factor"].iat[-1]
if factor is None or not np.isfinite(factor):
return proposed_stake
stake = proposed_stake * float(factor)
if min_stake is not None:
stake = max(stake, min_stake)
return min(stake, max_stake)
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
high, low, close = dataframe["high"], dataframe["low"], dataframe["close"]
tenkan = (high.rolling(9).max() + low.rolling(9).min()) / 2
kijun = (high.rolling(26).max() + low.rolling(26).min()) / 2
dataframe["tenkan"] = tenkan
dataframe["kijun"] = kijun
dataframe["senkou_a"] = ((tenkan + kijun) / 2).shift(26)
dataframe["senkou_b"] = ((high.rolling(52).max() + low.rolling(52).min()) / 2).shift(26)
dataframe["cloud_top"] = dataframe[["senkou_a", "senkou_b"]].max(axis=1)
dataframe["cloud_bot"] = dataframe[["senkou_a", "senkou_b"]].min(axis=1)
dataframe["cloud_width_pct"] = (dataframe["cloud_top"] - dataframe["cloud_bot"]) / close * 100
dataframe["close_prev26"] = close.shift(26)
dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
# --- Volatility targeting ---
atr_pct = ta.ATR(dataframe, timeperiod=14) / close * 100
atr_ref = atr_pct.rolling(500).median() # vol "normale" (trailing)
factor = (atr_ref / atr_pct).clip(self.VOL_FACTOR_MIN, self.VOL_FACTOR_MAX)
dataframe["vol_factor"] = factor.fillna(1.0)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
adx_min = self.buy_adx_min.value
cloud_min = self.buy_cloud_min_pct.value
long_cond = (
(dataframe["close"] > dataframe["cloud_top"])
& (dataframe["tenkan"] > dataframe["kijun"])
& (dataframe["close"] > dataframe["close_prev26"])
& (dataframe["adx"] > adx_min)
& (dataframe["cloud_width_pct"] > cloud_min)
& (dataframe["volume"] > 0)
)
short_cond = (
(dataframe["close"] < dataframe["cloud_bot"])
& (dataframe["tenkan"] < dataframe["kijun"])
& (dataframe["close"] < dataframe["close_prev26"])
& (dataframe["adx"] > adx_min)
& (dataframe["cloud_width_pct"] > cloud_min)
& (dataframe["volume"] > 0)
)
if self.require_tk_cross.value:
long_cond &= dataframe["tenkan"].shift(1) <= dataframe["kijun"].shift(1)
short_cond &= dataframe["tenkan"].shift(1) >= dataframe["kijun"].shift(1)
dataframe.loc[long_cond, "enter_long"] = 1
dataframe.loc[short_cond, "enter_short"] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(((dataframe["tenkan"] < dataframe["kijun"]) | (dataframe["close"] < dataframe["cloud_bot"]))
& (dataframe["volume"] > 0)),
"exit_long",
] = 1
dataframe.loc[
(((dataframe["tenkan"] > dataframe["kijun"]) | (dataframe["close"] > dataframe["cloud_top"]))
& (dataframe["volume"] > 0)),
"exit_short",
] = 1
return dataframe

View File

@@ -0,0 +1,108 @@
# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods
"""
IchimokuLS — Ichimoku long/short avec FILTRE DE TENDANCE MACRO (EMA200).
Reprend les paramètres optimisés d'IchimokuHyper (figés), et ajoute :
- LONG uniquement si close > EMA200 (tendance de fond haussière)
- SHORT uniquement si close < EMA200 (tendance de fond baissière)
But : réparer le côté long, qui perdait en entrant à contre-tendance macro.
On compare A/B contre IchimokuHyper (mêmes params, sans le filtre).
"""
from __future__ import annotations
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.strategy import IStrategy
class IchimokuLS(IStrategy):
INTERFACE_VERSION = 3
timeframe = "1h"
can_short = True
# --- Paramètres figés (issus de l'hyperopt d'IchimokuHyper) ---
minimal_roi = {"0": 0.488, "213": 0.136, "639": 0.05, "2021": 0}
stoploss = -0.232
trailing_stop = True
trailing_stop_positive = 0.341
trailing_stop_positive_offset = 0.441
trailing_only_offset_is_reached = False
buy_adx_min = 36
buy_cloud_min_pct = 0.56
startup_candle_count: int = 220 # EMA200 + marge
process_only_new_candles = True
use_exit_signal = True
def leverage(self, pair, current_time, current_rate, proposed_leverage,
max_leverage, entry_tag, side, **kwargs) -> float:
return 1.0
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
high, low, close = dataframe["high"], dataframe["low"], dataframe["close"]
tenkan = (high.rolling(9).max() + low.rolling(9).min()) / 2
kijun = (high.rolling(26).max() + low.rolling(26).min()) / 2
dataframe["tenkan"] = tenkan
dataframe["kijun"] = kijun
dataframe["senkou_a"] = ((tenkan + kijun) / 2).shift(26)
dataframe["senkou_b"] = (
(high.rolling(52).max() + low.rolling(52).min()) / 2
).shift(26)
dataframe["cloud_top"] = dataframe[["senkou_a", "senkou_b"]].max(axis=1)
dataframe["cloud_bot"] = dataframe[["senkou_a", "senkou_b"]].min(axis=1)
dataframe["cloud_width_pct"] = (
(dataframe["cloud_top"] - dataframe["cloud_bot"]) / close * 100
)
dataframe["close_prev26"] = close.shift(26)
dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200) # filtre macro
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(dataframe["close"] > dataframe["cloud_top"])
& (dataframe["tenkan"] > dataframe["kijun"])
& (dataframe["close"] > dataframe["close_prev26"])
& (dataframe["adx"] > self.buy_adx_min)
& (dataframe["cloud_width_pct"] > self.buy_cloud_min_pct)
& (dataframe["close"] > dataframe["ema200"]) # ← filtre macro LONG
& (dataframe["volume"] > 0)
),
"enter_long",
] = 1
dataframe.loc[
(
(dataframe["close"] < dataframe["cloud_bot"])
& (dataframe["tenkan"] < dataframe["kijun"])
& (dataframe["close"] < dataframe["close_prev26"])
& (dataframe["adx"] > self.buy_adx_min)
& (dataframe["cloud_width_pct"] > self.buy_cloud_min_pct)
& (dataframe["close"] < dataframe["ema200"]) # ← filtre macro SHORT
& (dataframe["volume"] > 0)
),
"enter_short",
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
((dataframe["tenkan"] < dataframe["kijun"])
| (dataframe["close"] < dataframe["cloud_bot"]))
& (dataframe["volume"] > 0)
),
"exit_long",
] = 1
dataframe.loc[
(
((dataframe["tenkan"] > dataframe["kijun"])
| (dataframe["close"] > dataframe["cloud_top"]))
& (dataframe["volume"] > 0)
),
"exit_short",
] = 1
return dataframe

View File

@@ -0,0 +1,117 @@
# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods
"""
IchimokuStrategy — long/short basé sur Ichimoku Kinko Hyo (futures).
Composants :
Tenkan-sen (9), Kijun-sen (26), Senkou A/B (nuage/Kumo), confirmation type Chikou.
Signaux :
LONG : prix au-dessus du nuage + croisement Tenkan>Kijun + momentum (close > close[-26]).
SHORT : miroir exact (prix sous le nuage + Tenkan<Kijun + close < close[-26]).
⚠️ Anti-lookahead : les Senkou sont décalés de +26 (donnée passée projetée au présent),
la confirmation "Chikou" utilise close vs close d'il y a 26 bougies (pas de futur).
"""
from __future__ import annotations
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.strategy import IStrategy
class IchimokuStrategy(IStrategy):
INTERFACE_VERSION = 3
timeframe = "1h"
can_short = True
minimal_roi = {"0": 0.06, "240": 0.03, "720": 0.01, "1440": 0}
stoploss = -0.08
trailing_stop = True
trailing_stop_positive = 0.025
trailing_stop_positive_offset = 0.04
trailing_only_offset_is_reached = True
startup_candle_count: int = 120 # 52 (Senkou B) + 26 (décalage) + marge
process_only_new_candles = True
use_exit_signal = True
def leverage(self, pair, current_time, current_rate, proposed_leverage,
max_leverage, entry_tag, side, **kwargs) -> float:
return 1.0 # edge directionnel pur
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
high, low, close = dataframe["high"], dataframe["low"], dataframe["close"]
tenkan = (high.rolling(9).max() + low.rolling(9).min()) / 2
kijun = (high.rolling(26).max() + low.rolling(26).min()) / 2
dataframe["tenkan"] = tenkan
dataframe["kijun"] = kijun
# Senkou décalés de +26 : valeur du nuage au présent issue de données passées.
dataframe["senkou_a"] = ((tenkan + kijun) / 2).shift(26)
dataframe["senkou_b"] = (
(high.rolling(52).max() + low.rolling(52).min()) / 2
).shift(26)
# Bornes du nuage
dataframe["cloud_top"] = dataframe[["senkou_a", "senkou_b"]].max(axis=1)
dataframe["cloud_bot"] = dataframe[["senkou_a", "senkou_b"]].min(axis=1)
# Confirmation momentum type Chikou (close vs close d'il y a 26 bougies)
dataframe["close_prev26"] = close.shift(26)
dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# LONG : au-dessus du nuage + croisement TK haussier + momentum
dataframe.loc[
(
(dataframe["close"] > dataframe["cloud_top"])
& (dataframe["tenkan"] > dataframe["kijun"])
& (dataframe["tenkan"].shift(1) <= dataframe["kijun"].shift(1))
& (dataframe["close"] > dataframe["close_prev26"])
& (dataframe["adx"] > 20)
& (dataframe["volume"] > 0)
),
"enter_long",
] = 1
# SHORT : sous le nuage + croisement TK baissier + momentum baissier
dataframe.loc[
(
(dataframe["close"] < dataframe["cloud_bot"])
& (dataframe["tenkan"] < dataframe["kijun"])
& (dataframe["tenkan"].shift(1) >= dataframe["kijun"].shift(1))
& (dataframe["close"] < dataframe["close_prev26"])
& (dataframe["adx"] > 20)
& (dataframe["volume"] > 0)
),
"enter_short",
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Sortie LONG : Tenkan repasse sous Kijun, ou prix replonge dans le nuage
dataframe.loc[
(
(
(dataframe["tenkan"] < dataframe["kijun"])
| (dataframe["close"] < dataframe["cloud_bot"])
)
& (dataframe["volume"] > 0)
),
"exit_long",
] = 1
# Sortie SHORT : Tenkan repasse au-dessus de Kijun, ou prix remonte dans le nuage
dataframe.loc[
(
(
(dataframe["tenkan"] > dataframe["kijun"])
| (dataframe["close"] > dataframe["cloud_top"])
)
& (dataframe["volume"] > 0)
),
"exit_short",
] = 1
return dataframe

View File

@@ -0,0 +1,64 @@
# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods
"""
LeveragedStrategy — DÉMONSTRATION du risque du levier (NE PAS utiliser en réel).
Même logique technique que SampleStrategy, mais en futures avec levier 10x.
But : montrer empiriquement que le levier crée des semaines à +10 % ET des semaines
catastrophiques — donc « +10 % chaque semaine » reste impossible, et le levier ruine.
"""
from __future__ import annotations
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.strategy import IStrategy
class LeveragedStrategy(IStrategy):
INTERFACE_VERSION = 3
timeframe = "1h"
can_short = False
minimal_roi = {"0": 0.05, "120": 0.03, "360": 0.01, "720": 0}
stoploss = -0.10
trailing_stop = True
trailing_stop_positive = 0.02
trailing_stop_positive_offset = 0.03
trailing_only_offset_is_reached = True
startup_candle_count: int = 50
process_only_new_candles = True
use_exit_signal = True
def leverage(self, pair, current_time, current_rate, proposed_leverage,
max_leverage, entry_tag, side, **kwargs) -> float:
return min(5.0, max_leverage) # 5x
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["ema_fast"] = ta.EMA(dataframe, timeperiod=9)
dataframe["ema_slow"] = ta.EMA(dataframe, timeperiod=21)
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(dataframe["ema_fast"] > dataframe["ema_slow"])
& (dataframe["ema_fast"].shift(1) <= dataframe["ema_slow"].shift(1))
& (dataframe["rsi"] < 70)
& (dataframe["volume"] > 0)
),
"enter_long",
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(dataframe["ema_fast"] < dataframe["ema_slow"])
& (dataframe["ema_fast"].shift(1) >= dataframe["ema_slow"].shift(1))
& (dataframe["volume"] > 0)
),
"exit_long",
] = 1
return dataframe

View File

@@ -0,0 +1,94 @@
# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods
"""
LongShortStrategy — stratégie symétrique (futures) qui gagne dans les deux sens.
- LONG quand la tendance est haussière (EMA fast>slow, prix>EMA50, momentum +).
- SHORT quand la tendance est baissière (miroir exact).
But : ne plus subir les marchés baissiers — profiter de la baisse comme de la hausse.
Levier 1x par défaut (on isole l'edge directionnel ; le levier viendra après si robuste).
"""
from __future__ import annotations
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.strategy import IStrategy
class LongShortStrategy(IStrategy):
INTERFACE_VERSION = 3
timeframe = "1h"
can_short = True # futures requis
minimal_roi = {"0": 0.05, "120": 0.03, "360": 0.01, "720": 0}
stoploss = -0.08
trailing_stop = True
trailing_stop_positive = 0.02
trailing_stop_positive_offset = 0.03
trailing_only_offset_is_reached = True
startup_candle_count: int = 60
process_only_new_candles = True
use_exit_signal = True
def leverage(self, pair, current_time, current_rate, proposed_leverage,
max_leverage, entry_tag, side, **kwargs) -> float:
return 1.0 # 1x — edge directionnel pur, sans amplification
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["ema_fast"] = ta.EMA(dataframe, timeperiod=9)
dataframe["ema_slow"] = ta.EMA(dataframe, timeperiod=21)
dataframe["ema_trend"] = ta.EMA(dataframe, timeperiod=50)
dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# LONG : croisement haussier + tendance MT haussière + tendance forte
dataframe.loc[
(
(dataframe["ema_fast"] > dataframe["ema_slow"])
& (dataframe["ema_fast"].shift(1) <= dataframe["ema_slow"].shift(1))
& (dataframe["close"] > dataframe["ema_trend"])
& (dataframe["adx"] > 20)
& (dataframe["rsi"] > 45)
& (dataframe["rsi"] < 75)
& (dataframe["volume"] > 0)
),
"enter_long",
] = 1
# SHORT : miroir exact
dataframe.loc[
(
(dataframe["ema_fast"] < dataframe["ema_slow"])
& (dataframe["ema_fast"].shift(1) >= dataframe["ema_slow"].shift(1))
& (dataframe["close"] < dataframe["ema_trend"])
& (dataframe["adx"] > 20)
& (dataframe["rsi"] < 55)
& (dataframe["rsi"] > 25)
& (dataframe["volume"] > 0)
),
"enter_short",
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Sortie LONG : la tendance courte se retourne à la baisse
dataframe.loc[
(
(dataframe["ema_fast"] < dataframe["ema_slow"])
& (dataframe["ema_fast"].shift(1) >= dataframe["ema_slow"].shift(1))
& (dataframe["volume"] > 0)
),
"exit_long",
] = 1
# Sortie SHORT : la tendance courte se retourne à la hausse
dataframe.loc[
(
(dataframe["ema_fast"] > dataframe["ema_slow"])
& (dataframe["ema_fast"].shift(1) <= dataframe["ema_slow"].shift(1))
& (dataframe["volume"] > 0)
),
"exit_short",
] = 1
return dataframe

View File

@@ -0,0 +1,33 @@
{
"strategy_name": "MTFIchimoku",
"params": {
"max_open_trades": {
"max_open_trades": 3
},
"buy": {
"buy_adx_min": 32,
"buy_pullback_pct": 0.008,
"buy_rsi_max": 52
},
"sell": {
"sell_rsi_min": 39
},
"roi": {
"0": 0.264,
"49": 0.057,
"170": 0.035,
"468": 0
},
"stoploss": {
"stoploss": -0.154
},
"trailing": {
"trailing_stop": true,
"trailing_stop_positive": 0.232,
"trailing_stop_positive_offset": 0.309,
"trailing_only_offset_is_reached": false
}
},
"ft_stratparam_v": 1,
"export_time": "2026-06-23 16:09:25.675598+00:00"
}

View File

@@ -0,0 +1,135 @@
# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods
"""
MTFIchimoku — Ichimoku MULTI-TIMEFRAME (technique S/R inter-unités).
Idée (méthode de l'utilisateur) : trader sur une unité basse (15m) en utilisant
les Tenkan/Kijun de l'unité SUPÉRIEURE (1h) comme supports/résistances.
Règles :
- Tendance 1h donnée par tenkan_1h vs kijun_1h.
- LONG : en tendance 1h haussière, le prix 15m RECLAIME le support (croise au-dessus
de la Kijun 1h) → rebond sur support.
- SHORT : en tendance 1h baissière, le prix 15m CASSE le support (croise sous la
Kijun 1h) → rejet sur résistance.
- Sortie : perte du niveau (close repasse de l'autre côté de la Kijun 1h) + ROI/stop.
La Kijun 1h sert de S/R principal (niveau lent/fort), la Tenkan 1h de filtre de tendance.
"""
from __future__ import annotations
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.strategy import (
IStrategy,
informative,
IntParameter,
DecimalParameter,
)
class MTFIchimoku(IStrategy):
INTERFACE_VERSION = 3
timeframe = "15m" # unité de trading
can_short = True
# Paramètres optimisables
buy_adx_min = IntParameter(15, 40, default=25, space="buy", optimize=True)
buy_pullback_pct = DecimalParameter(0.004, 0.03, default=0.012, decimals=3, space="buy", optimize=True)
buy_rsi_max = IntParameter(50, 72, default=60, space="buy", optimize=True)
sell_rsi_min = IntParameter(28, 50, default=40, space="sell", optimize=True)
minimal_roi = {"0": 0.02, "60": 0.012, "180": 0.006, "360": 0}
stoploss = -0.025
trailing_stop = True
trailing_stop_positive = 0.008
trailing_stop_positive_offset = 0.014
trailing_only_offset_is_reached = True
startup_candle_count: int = 240
process_only_new_candles = True
use_exit_signal = True
def leverage(self, pair, current_time, current_rate, proposed_leverage,
max_leverage, entry_tag, side, **kwargs) -> float:
return 1.0
@informative("1h")
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Tenkan / Kijun sur l'unité supérieure (1h) → deviendront tenkan_1h / kijun_1h.
high, low = dataframe["high"], dataframe["low"]
dataframe["tenkan"] = (high.rolling(9).max() + low.rolling(9).min()) / 2
dataframe["kijun"] = (high.rolling(26).max() + low.rolling(26).min()) / 2
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# tenkan_1h / kijun_1h sont injectés automatiquement (forward-fill sur le 15m).
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
# Plus haut/bas récents (pour exiger un VRAI pullback vers le niveau)
dataframe["hh8"] = dataframe["high"].rolling(8).max()
dataframe["ll8"] = dataframe["low"].rolling(8).min()
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
kijun = dataframe["kijun_1h"]
tenkan = dataframe["tenkan_1h"]
close, op, low, high = (
dataframe["close"], dataframe["open"], dataframe["low"], dataframe["high"]
)
rsi = dataframe["rsi"]
adx = dataframe["adx"]
adx_min = self.buy_adx_min.value
pb = self.buy_pullback_pct.value
uptrend_1h = (tenkan > kijun) & (close > kijun) # biais haussier 1h
downtrend_1h = (tenkan < kijun) & (close < kijun) # biais baissier 1h
# LONG : en tendance forte, VRAI pullback vers le support Kijun 1h puis rebond + momentum.
dataframe.loc[
(
uptrend_1h
& (adx > adx_min) # tendance forte
& (dataframe["hh8"] > kijun * (1 + pb)) # le prix venait nettement au-dessus
& (low <= kijun * 1.001) # mèche teste le support
& (close > kijun) # clôture au-dessus (support tient)
& (close > op) # bougie de rebond
& (rsi > rsi.shift(1)) # momentum qui se retourne à la hausse
& (rsi < self.buy_rsi_max.value) # pas déjà suracheté
& (dataframe["volume"] > 0)
),
"enter_long",
] = 1
# SHORT : miroir exact (rejet sur résistance en tendance baissière forte).
dataframe.loc[
(
downtrend_1h
& (adx > adx_min)
& (dataframe["ll8"] < kijun * (1 - pb))
& (high >= kijun * 0.999)
& (close < kijun)
& (close < op)
& (rsi < rsi.shift(1))
& (rsi > self.sell_rsi_min.value)
& (dataframe["volume"] > 0)
),
"enter_short",
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
kijun = dataframe["kijun_1h"]
tenkan = dataframe["tenkan_1h"]
close = dataframe["close"]
# Sortie LONG : cassure NETTE du support (clôture sous la Kijun ET sous la Tenkan 1h)
dataframe.loc[
((close < kijun) & (close < tenkan) & (dataframe["volume"] > 0)),
"exit_long",
] = 1
# Sortie SHORT : reprise NETTE au-dessus de la résistance
dataframe.loc[
((close > kijun) & (close > tenkan) & (dataframe["volume"] > 0)),
"exit_short",
] = 1
return dataframe

View File

@@ -0,0 +1,75 @@
# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods
"""
SampleStrategy — stratégie de base MidasBot (Phase 1).
Croisement de moyennes mobiles exponentielles (EMA) avec filtre RSI.
Sert à valider le pipeline Freqtrade (dry-run, backtesting, FreqUI) avant
d'ajouter la couche IA (cf. AiBiasStrategy, Phase 3).
"""
from __future__ import annotations
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.strategy import IStrategy
class SampleStrategy(IStrategy):
INTERFACE_VERSION = 3
# Timeframe d'analyse
timeframe = "1h"
# Take-profit échelonné (ROI minimal par durée, en minutes)
minimal_roi = {
"0": 0.05,
"120": 0.03,
"360": 0.01,
"720": 0,
}
# Stop-loss dur
stoploss = -0.10
# Trailing stop
trailing_stop = True
trailing_stop_positive = 0.02
trailing_stop_positive_offset = 0.03
trailing_only_offset_is_reached = True
# Nombre de bougies nécessaires avant de produire un signal
startup_candle_count: int = 50
# Ordres
process_only_new_candles = True
use_exit_signal = True
exit_profit_only = False
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["ema_fast"] = ta.EMA(dataframe, timeperiod=9)
dataframe["ema_slow"] = ta.EMA(dataframe, timeperiod=21)
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(dataframe["ema_fast"] > dataframe["ema_slow"])
& (dataframe["ema_fast"].shift(1) <= dataframe["ema_slow"].shift(1))
& (dataframe["rsi"] < 70)
& (dataframe["volume"] > 0)
),
"enter_long",
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(dataframe["ema_fast"] < dataframe["ema_slow"])
& (dataframe["ema_fast"].shift(1) >= dataframe["ema_slow"].shift(1))
& (dataframe["volume"] > 0)
),
"exit_long",
] = 1
return dataframe

View File

@@ -0,0 +1,68 @@
# pragma pylint: disable=missing-docstring, invalid-name, too-few-public-methods
"""
TrendMomentumStrategy — tentative d'amélioration du rendement (vs SampleStrategy).
Idée : n'entrer que sur des tendances confirmées et fortes (filtre EMA50 + ADX),
laisser courir les gains (ROI plus haut + trailing), couper vite les perdants.
Objectif : meilleur rendement ajusté du risque — PAS de promesse de +10 %/semaine.
"""
from __future__ import annotations
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.strategy import IStrategy
class TrendMomentumStrategy(IStrategy):
INTERFACE_VERSION = 3
timeframe = "1h"
# Laisser courir : on vise des gains plus gros, on attend plus longtemps.
minimal_roi = {"0": 0.12, "240": 0.06, "720": 0.03, "1440": 0}
stoploss = -0.06 # on coupe vite les perdants
trailing_stop = True
trailing_stop_positive = 0.025
trailing_stop_positive_offset = 0.04
trailing_only_offset_is_reached = True
startup_candle_count: int = 60
process_only_new_candles = True
use_exit_signal = True
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["ema_fast"] = ta.EMA(dataframe, timeperiod=9)
dataframe["ema_slow"] = ta.EMA(dataframe, timeperiod=21)
dataframe["ema_trend"] = ta.EMA(dataframe, timeperiod=50)
dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
macd = ta.MACD(dataframe)
dataframe["macd"] = macd["macd"]
dataframe["macdsignal"] = macd["macdsignal"]
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(dataframe["ema_fast"] > dataframe["ema_slow"]) # tendance courte haussière
& (dataframe["close"] > dataframe["ema_trend"]) # au-dessus tendance MT
& (dataframe["adx"] > 25) # tendance forte
& (dataframe["macd"] > dataframe["macdsignal"]) # momentum haussier
& (dataframe["rsi"] > 50)
& (dataframe["rsi"] < 75) # pas en surachat extrême
& (dataframe["volume"] > 0)
),
"enter_long",
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(dataframe["macd"] < dataframe["macdsignal"]) # momentum se retourne
& (dataframe["close"] < dataframe["ema_slow"])
& (dataframe["volume"] > 0)
),
"exit_long",
] = 1
return dataframe