HLAiMvpStrategy
♡
Basics
mode: spot
timeframe: 5m
interface version: 3
Settings
stoploss: -0.025
has minimal roi
process only new candles
startup candle count: 50
Concepts
mean_reversion
15 related strategies (⧉ identical code, ≈ similar name)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | from __future__ import annotations import json from datetime import datetime, timezone from pathlib import Path from typing import Any from freqtrade.strategy import IStrategy from pandas import DataFrame class HLAiMvpStrategy(IStrategy): """Long-only DIP-BUY strategy for Hyperliquid dry-run. 横原さんのスタイル(急落で買い・戻りで利確=逆張り/mean reversion)。 エントリー: RSI過売り + 直近急落 + 反発の兆し(RSI上昇転換 & 陽線)。 決済: 戻り(RSI回復 or 平均回帰) + 早めのminimal_ROIで回転。 AIフィルタ(claude): ai_signals/latest.json の bias=long&conf>=0.55 を「押し目買いGO」として通す。 可視化: AIシグナルのconfidence/biasとスマートマネーnetをFreqUIチャートのsubplotに出す(現在値)。 """ INTERFACE_VERSION = 3 timeframe = "5m" startup_candle_count = 50 process_only_new_candles = True can_short = False minimal_roi = {"0": 0.012, "30": 0.006, "90": 0} stoploss = -0.025 trailing_stop = False use_exit_signal = True exit_profit_only = False ignore_roi_if_entry_signal = False order_types = { "entry": "limit", "exit": "limit", "emergency_exit": "limit", "force_entry": "limit", "force_exit": "limit", "stoploss": "limit", "stoploss_on_exchange": True, "stoploss_on_exchange_interval": 60, "stoploss_on_exchange_limit_ratio": 0.99, } ai_filter_enabled = True ai_signal_path = Path("/freqtrade/user_data/ai_signals/latest.json") plot_config = { "main_plot": { "ema_fast": {"color": "blue"}, "ema_slow": {"color": "orange"}, }, "subplots": { "RSI": {"rsi": {"color": "purple"}}, "Drop% 30m": {"drop_30m": {"color": "red"}}, "AI confidence": { "ai_confidence": {"color": "green"}, "ai_long": {"color": "lightgreen"}, }, "SmartMoney net $M": {"sm_net_musd": {"color": "teal"}}, }, } def populate_indicators(self, dataframe: DataFrame, metadata: dict[str, Any]) -> DataFrame: dataframe["ema_fast"] = dataframe["close"].ewm(span=12, adjust=False).mean() dataframe["ema_slow"] = dataframe["close"].ewm(span=26, adjust=False).mean() dataframe["rsi"] = self._rsi(dataframe, period=14) dataframe["volume_mean"] = dataframe["volume"].rolling(20).mean() dataframe["drop_30m"] = (dataframe["close"] / dataframe["close"].shift(6) - 1) * 100 # --- 可視化用: AIシグナル + スマートマネー(現在値を全行へ。FreqUIチャートで確認用) --- sig = self._load_signal_for_pair(metadata["pair"]) dataframe["ai_confidence"] = float(sig.get("confidence", 0)) if sig else 0.0 dataframe["ai_long"] = 1.0 if (sig and sig.get("bias") == "long") else 0.0 sm = self._load_smart_money() coin = metadata["pair"].split("/")[0] if sm and coin in sm: dataframe["sm_net_musd"] = round(sm[coin].get("net_usd", 0) / 1_000_000, 2) else: dataframe["sm_net_musd"] = 0.0 return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict[str, Any]) -> DataFrame: dip_entry = ( (dataframe["rsi"] < 35) & (dataframe["rsi"] > dataframe["rsi"].shift(1)) & (dataframe["close"] > dataframe["open"]) & (dataframe["drop_30m"] < -1.0) & (dataframe["volume"] > 0) & (dataframe["volume"] >= dataframe["volume_mean"] * 0.5) ) dataframe.loc[dip_entry, ["enter_long", "enter_tag"]] = (1, "dip_buy") if self.ai_filter_enabled and not self._ai_allows_long(metadata["pair"]): dataframe.loc[:, ["enter_long", "enter_tag"]] = (0, None) return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict[str, Any]) -> DataFrame: rebound_exit = ( (dataframe["rsi"] > 55) | (dataframe["close"] >= dataframe["ema_slow"]) ) & (dataframe["volume"] > 0) dataframe.loc[rebound_exit, ["exit_long", "exit_tag"]] = (1, "rebound_exit") return dataframe def leverage(self, pair, current_time, current_rate, proposed_leverage, max_leverage, entry_tag, side, **kwargs) -> float: return 1.0 @staticmethod def _rsi(dataframe: DataFrame, period: int = 14): delta = dataframe["close"].diff() gain = delta.clip(lower=0) loss = -delta.clip(upper=0) avg_gain = gain.ewm(alpha=1 / period, min_periods=period, adjust=False).mean() avg_loss = loss.ewm(alpha=1 / period, min_periods=period, adjust=False).mean() rs = avg_gain / avg_loss.replace(0, 1e-10) return 100 - (100 / (1 + rs)) def _ai_allows_long(self, pair: str) -> bool: signal = self._load_signal_for_pair(pair) if not signal: return False expires_at = signal.get("expires_at") if expires_at: try: expires = datetime.fromisoformat(expires_at.replace("Z", "+00:00")) if expires < datetime.now(timezone.utc): return False except ValueError: return False return ( signal.get("bias") == "long" and float(signal.get("confidence", 0)) >= 0.55 and signal.get("risk") in {"low", "medium"} ) def _resolve_signal_path(self) -> Path: user_dir = self.config.get("user_data_dir") if user_dir: return Path(user_dir) / "ai_signals" / "latest.json" return self.ai_signal_path def _read_payload(self) -> dict[str, Any] | None: signal_path = self._resolve_signal_path() if not signal_path.exists(): return None try: return json.loads(signal_path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): return None def _load_signal_for_pair(self, pair: str) -> dict[str, Any] | None: payload = self._read_payload() if not payload: return None for signal in payload.get("signals", []): if signal.get("pair") == pair: return signal return None def _load_smart_money(self) -> dict[str, Any] | None: payload = self._read_payload() return payload.get("smart_money") if payload else None |
Strategy League — fixed backtest that feeds the ranking
No trades. This strategy never entered a position over the League window (33 pairs · 20210101-20260101), so there's nothing to backtest or rank — its entry conditions didn't trigger on the tested pairs and timeframe.
Backtests — over a market period
Backtest this strategy over a chosen crypto-cycle period. These don't affect the League ranking, and need that period's candle data downloaded.
Log in or sign up to run backtests.
| Period | Range | Total % | Win % | Max DD | Trades | |
|---|---|---|---|---|---|---|
| 2020 · DeFi Summer & Pre-Halving Rally | 20200101-20210101 | not run | ||||
| 2021 · Institutional Bull Market | 20210101-20220101 | not run | ||||
| 2022 · Post-Bull Crash & Macro Tightening | 20220101-20230101 | not run | ||||
| 2023–2024 · Recovery & ETF Anticipation | 20230101-20250101 | not run | ||||
| 2025–2026 · Current Cycle | 20250101-20260101 | not run | ||||
Walk forward
Out-of-sample backtest on recent data · 33 pairs · 20260101-20260701.
Backtest trust check
no lookahead patterns · 2 thing(s) worth reviewing before trusting the numbers
| Line | Pattern | Detail | |
|---|---|---|---|
| 38 | review | backtest_vs_live | order_types sets stoploss_on_exchange, but backtesting forces it to False -- the backtest exits on candle data while live trading would rest a stop order on the exchange. The two don't fill at the same price |
| 97 | review | enter_tag_overwrite | enter_tag/exit_tag is written by 3 separate assignments -- they share one column and run in source order, so a row matching more than one condition keeps only the LAST tag. Per-tag statistics won't mean what they appear to |
ran by Ron · took s
Lookahead analysis
freqtrade lookahead-analysis: detects strategies peeking at future candles.