💬 Forum

HLAiMvpStrategy

office8-inc/hyperliquid-ai-trading/freqtrade/user_data/strategies/HLAiMvpStrategy.py · first seen 2026-07-16 · repo updated 2026-06-03

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)

Each tile is a different kind of check — from an instant code lint to full sandboxed backtests and forward tests on recent data. Not sure what a check actually proves? See the FAQ →

Source

Download Raw
  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