見出し画像

ハイリキで噂のクジラをディスコードに通知してみる

話題になっている連勝中のアカウントを追跡してみる★
あまりに大量にトレードしているなら、ミラトレしてのっかろうというお話です。ディスコード通知を作ってみました。

条件(テスト版)
PC(サーバー)つけっぱなし
Discordのwebhookと監視対象アドレスは自分で設定する
トレードがあったら通知するが、5分でまとめて通知する(調節可能)
batファイルをWクリックすると起動(pyファイルと同じフォルダに格納する)
現物保有はHYPEのみ記載
送金については通知しない
わけわからん場合はHyperscanを開いて確認
※通知がエラーで止まることがある

画像
https://hypurrscan.io/address/0xc2a30212a8ddac9e123944d6e29faddce994e5f2
画像
https://hypurrscan.io/address/0x8def9f50456c6c4e37fa5d3d57f108ed23992dae
画像
https://hypurrscan.io/address/0xc2a30212a8ddac9e123944d6e29faddce994e5f2
画像
https://hypurrscan.io/address/0x8def9f50456c6c4e37fa5d3d57f108ed23992dae

.batファイル ※コードは参考用です

start_watch.bat

@echo off
chcp 65001 >nul
echo Hyperliquid監視botを起動します...
echo.

REM ↓ここにあなたのDiscord Webhook URLを貼り付けてください
set DISCORD_WEBHOOK=https://discord.com/api/webhooks/〇〇

REM 必要なライブラリがあるか確認
pip show websockets >nul 2>&1
if errorlevel 1 (
    echo 必要なライブラリをインストール中...
    pip install websockets requests python-dateutil
)

echo.
echo 監視を開始します...
echo このウィンドウを閉じると監視が停止します。
echo.

python hl_hyperliquid_multi_watch.py

REM エラーが出た場合、ウィンドウを閉じない
if errorlevel 1 (
    echo.
    echo エラーが発生しました。上記のエラー内容を確認してください。
    pause
)

.pyファイル ※コードは参考用です

hl_hyperliquid_multi_watch.py

# -*- coding: utf-8 -*-
import os
import asyncio
import json
from datetime import datetime, timezone, timedelta
import requests
import websockets
from collections import defaultdict

# =========================
# 設定
# =========================
HL_WS_URL = "wss://api.hyperliquid.xyz/ws"
HL_API_URL = "https://api.hyperliquid.xyz/info"

# 監視対象アドレス一覧(ラベル付き)
WATCH_ADDRESSES = {
    "0xc2a30212a8DdAc9e123944d6e29FADdCe994E5f2": "インサイダーA",
    "0xb317d2bc2d3d2df5fa441b5bae0ab9d8b07283ae": "インサイダーB",
    "0x8def9f50456c6c4e37fa5d3d57f108ed23992dae": "HYPEクジラA",
}

# ↓ここに直接あなたのWebhook URLを貼り付けてください
DISCORD_WEBHOOK = "あなたのWebhook URL"

# 表示設定
USE_JST = True
DEBUG_MODE = True
SHOW_CURRENT_POSITION = True
SHOW_24H_HISTORY = True
AGGREGATE_WINDOW = 300  # 5分(300秒)
RETRY_INITIAL_DELAY = 3
RETRY_MAX_DELAY = 60
PING_INTERVAL = 20
PING_TIMEOUT = 20

# 通貨名の変換マップ
COIN_NAME_MAP = {
    "@107": "HYPE",
}

if not DISCORD_WEBHOOK or "あなたの" in DISCORD_WEBHOOK:
    raise RuntimeError("DISCORD_WEBHOOK を設定してください。")

WATCH_ADDRESSES_LOWER = {addr.lower(): label for addr, label in WATCH_ADDRESSES.items()}
first_snapshot_processed = {}
pending_fills = defaultdict(list)
pending_timers = {}

# 現在のポジションキャッシュ(アドレスごと)
current_positions_cache = {}
# 現物保有キャッシュ(アドレスごと)- HYPEのみ
spot_holdings_cache = {}

def log(msg: str):
    timestamp = datetime.now().strftime("%H:%M:%S")
    print(f"[{timestamp}] {msg}")

def debug_log(msg: str):
    if DEBUG_MODE:
        timestamp = datetime.now().strftime("%H:%M:%S")
        print(f"[DEBUG {timestamp}] {msg}")

def post_discord(content: str = None, embeds=None):
    payload = {}
    if content:
        payload["content"] = content
    if embeds:
        payload["embeds"] = embeds
    try:
        resp = requests.post(DISCORD_WEBHOOK, json=payload, timeout=10)
        resp.raise_for_status()
        log("Discord送信成功")
        return True
    except Exception as e:
        log(f"[ERROR] Discord送信失敗: {e}")
        return False

def convert_coin_name(coin: str) -> str:
    """通貨名を変換(@107 → HYPE など)"""
    return COIN_NAME_MAP.get(coin, coin)

def get_hypurrscan_url(address: str) -> str:
    """hypurrscanのアドレスURLを生成"""
    return f"https://hypurrscan.io/address/{address}"

def get_user_fills_from_api(user_address: str, hours: int = 24):
    """REST APIから過去の約定を取得"""
    try:
        payload = {
            "type": "userFills",
            "user": user_address
        }
        
        resp = requests.post(HL_API_URL, json=payload, timeout=30)
        resp.raise_for_status()
        data = resp.json()
        
        if not isinstance(data, list):
            return []
        
        cutoff_time = datetime.now(timezone.utc) - timedelta(hours=hours)
        cutoff_ms = int(cutoff_time.timestamp() * 1000)
        
        recent_fills = [f for f in data if f.get("time") and f.get("time") >= cutoff_ms]
        
        log(f"API取得: {len(recent_fills)}件(過去{hours}時間)")
        return recent_fills
        
    except Exception as e:
        log(f"[ERROR] API取得失敗: {e}")
        return []

def get_spot_holdings(user_address: str):
    """現物保有を取得(HYPEのみ)"""
    try:
        payload = {
            "type": "spotClearinghouseState",
            "user": user_address
        }
        
        resp = requests.post(HL_API_URL, json=payload, timeout=30)
        resp.raise_for_status()
        data = resp.json()
        
        hype_total = 0.0
        
        if "balances" in data:
            for balance in data["balances"]:
                coin = balance.get("coin", "")
                coin_display = convert_coin_name(coin)
                
                # HYPEのみ処理
                if coin_display == "HYPE":
                    total = safe_float(balance.get("total", 0))
                    
                    # 1枚未満は0として扱う
                    if total >= 1.0:
                        hype_total = total
                    else:
                        hype_total = 0.0
                    break
        
        # キャッシュに保存
        spot_holdings_cache[user_address.lower()] = hype_total
        
        return hype_total
        
    except Exception as e:
        log(f"[ERROR] 現物保有取得失敗: {e}")
        return 0.0

def get_current_positions(user_address: str):
    """現在のポジションを取得"""
    try:
        payload = {
            "type": "clearinghouseState",
            "user": user_address
        }
        
        resp = requests.post(HL_API_URL, json=payload, timeout=30)
        resp.raise_for_status()
        data = resp.json()
        
        positions = []
        positions_dict = {}  # 通貨ごとのポジション
        
        if "assetPositions" in data:
            for pos in data["assetPositions"]:
                position_info = pos.get("position", {})
                coin = position_info.get("coin", "不明")
                coin_display = convert_coin_name(coin)
                szi = position_info.get("szi")
                
                if szi and float(szi) != 0:
                    size = float(szi)
                    entry_px = float(position_info.get("entryPx", 0))
                    unrealized_pnl = float(position_info.get("unrealizedPnl", 0))
                    leverage = float(position_info.get("leverage", {}).get("value", 0))
                    liquidation_px = position_info.get("liquidationPx")
                    
                    pos_data = {
                        "coin": coin_display,
                        "size": size,
                        "direction": "LONG" if size > 0 else "SHORT",
                        "entry_price": entry_px,
                        "unrealized_pnl": unrealized_pnl,
                        "leverage": leverage,
                        "liquidation_px": liquidation_px
                    }
                    positions.append(pos_data)
                    positions_dict[coin_display] = size
        
        # キャッシュに保存
        current_positions_cache[user_address.lower()] = positions_dict
        
        return positions
        
    except Exception as e:
        log(f"[ERROR] ポジション取得失敗: {e}")
        return []

def get_current_position_for_coin(user_address: str, coin: str) -> float:
    """特定通貨の現在のポジションを取得(キャッシュから)"""
    coin_display = convert_coin_name(coin)
    user_addr_lower = user_address.lower()
    
    if user_addr_lower in current_positions_cache:
        return current_positions_cache[user_addr_lower].get(coin_display, 0.0)
    
    return None  # キャッシュがない場合

def get_spot_holding_for_coin(user_address: str, coin: str):
    """特定通貨の現物保有を取得(キャッシュから・HYPEのみ)"""
    coin_display = convert_coin_name(coin)
    user_addr_lower = user_address.lower()
    
    # HYPEのみ対応
    if coin_display != "HYPE":
        return None
    
    if user_addr_lower in spot_holdings_cache:
        return spot_holdings_cache[user_addr_lower]
    
    return None  # キャッシュがない場合

def build_position_embed(trader_address: str, positions: list):
    """現在のポジション情報のembedを作成"""
    trader_label = get_address_label(trader_address)
    hypurrscan_url = get_hypurrscan_url(trader_address)
    
    # 現物保有も取得(HYPEのみ)
    hype_spot = get_spot_holdings(trader_address)
    
    if not positions and hype_spot == 0:
        embed = {
            "title": f"📊 現在のポジション | {trader_label}",
            "description": "ポジション・現物保有なし",
            "url": hypurrscan_url,
            "color": 0x95a5a6,
            "timestamp": datetime.now(timezone.utc).isoformat()
        }
        return embed
    
    fields = []
    total_unrealized_pnl = 0.0
    
    # レバレッジポジション
    for pos in positions:
        direction_emoji = "🟢" if pos["direction"] == "LONG" else "🔴"
        size_abs = abs(pos["size"])
        
        field_value = f"**{direction_emoji} {pos['direction']}(レバレッジ)**\n"
        field_value += f"数量: **{size_abs:,.4f}**\n"
        field_value += f"エントリー: ${pos['entry_price']:,.4f}\n"
        
        if pos.get("leverage"):
            field_value += f"レバレッジ: {pos['leverage']:.1f}x\n"
        
        pnl = pos["unrealized_pnl"]
        pnl_emoji = "📈" if pnl >= 0 else "📉"
        field_value += f"{pnl_emoji} 含み損益: **${pnl:,.2f}**\n"
        
        if pos.get("liquidation_px"):
            field_value += f"清算価格: ${float(pos['liquidation_px']):,.4f}"
        
        fields.append({
            "name": f"💰 {pos['coin']}",
            "value": field_value,
            "inline": True
        })
        
        total_unrealized_pnl += pnl
    
    # HYPE現物保有(1枚以上の場合のみ表示)
    if hype_spot > 0:
        if hype_spot >= 1.0:
            display_amount = int(hype_spot)
        else:
            display_amount = 0
        
        field_value = f"**💎 現物保有**\n"
        field_value += f"合計: **{display_amount:,}枚**"
        
        fields.append({
            "name": f"💎 HYPE",
            "value": field_value,
            "inline": True
        })
    
    total_pnl_emoji = "📈" if total_unrealized_pnl >= 0 else "📉"
    
    description = ""
    if total_unrealized_pnl != 0:
        description = f"{total_pnl_emoji} **レバレッジ含み損益: ${total_unrealized_pnl:,.2f}**"
    
    # アドレスリンク
    fields.append({
        "name": "🔍 アドレス",
        "value": f"[Hypurrscanで確認]({hypurrscan_url})",
        "inline": False
    })
    
    position_count = len(positions)
    spot_count = 1 if hype_spot > 0 else 0
    footer_text = f"Hyperliquid"
    if position_count > 0:
        footer_text += f" | {position_count}ポジション"
    if spot_count > 0:
        footer_text += f" | HYPE現物保有"
    
    embed = {
        "title": f"📊 現在のポジション | {trader_label}",
        "url": hypurrscan_url,
        "description": description if description else None,
        "color": 0x3498db,
        "fields": fields,
        "footer": {
            "text": footer_text
        },
        "timestamp": datetime.now(timezone.utc).isoformat()
    }
    
    return embed

def safe_float(value, default=0.0):
    try:
        if value is None:
            return default
        if isinstance(value, (int, float)):
            return float(value)
        if isinstance(value, str):
            return float(value)
        return default
    except:
        return default

def fmt_ts(ts_ms):
    try:
        if isinstance(ts_ms, (int, float)) and ts_ms > 0:
            dt = datetime.fromtimestamp(float(ts_ms) / 1000, tz=timezone.utc)
            if USE_JST:
                dt = dt.astimezone(timezone(timedelta(hours=9)))
                return dt.strftime("%Y-%m-%d %H:%M:%S JST")
            else:
                return dt.strftime("%Y-%m-%d %H:%M:%S UTC")
    except Exception:
        pass
    return ""

def get_address_label(address: str) -> str:
    addr_lower = address.lower()
    label = WATCH_ADDRESSES_LOWER.get(addr_lower, "Unknown")
    addr_short = f"{address[:6]}...{address[-4:]}"
    return f"{label} ({addr_short})"

def aggregate_fills(fills_list):
    if not fills_list:
        return None
    
    groups = defaultdict(lambda: {"buy": [], "sell": []})
    
    for fill in fills_list:
        coin = fill.get("coin", "不明")
        side = fill.get("side", "").lower()
        if side in ["a", "b"]:
            side_key = "sell" if side == "a" else "buy"
            groups[coin][side_key].append(fill)
    
    return groups

def build_aggregated_embed(trader_address: str, coin: str, side: str, fills: list, is_past_data: bool = False):
    """集約された約定データからembedを作成(HYPE現物のみ対応版)"""
    
    # 通貨名を変換
    coin_display = convert_coin_name(coin)
    
    total_sz = 0.0
    total_value = 0.0
    count = len(fills)
    
    first_time = None
    last_time = None
    start_position = None
    
    # 最初のfillのstartPositionを取得
    if fills:
        start_position = safe_float(fills[0].get("startPosition"))
    
    for fill in fills:
        sz = safe_float(fill.get("sz"))
        px = safe_float(fill.get("px"))
        time_ms = fill.get("time")
        
        total_sz += sz
        total_value += sz * px
        
        if first_time is None or (time_ms and time_ms < first_time):
            first_time = time_ms
        if last_time is None or (time_ms and time_ms > last_time):
            last_time = time_ms
    
    avg_price = total_value / total_sz if total_sz > 0 else 0
    
    # 現在のポジションを取得(レバレッジ)
    current_position = get_current_position_for_coin(trader_address, coin_display)
    # 現物保有を取得(HYPEのみ)
    spot_holding = None
    if coin_display == "HYPE":
        spot_holding = get_spot_holding_for_coin(trader_address, coin_display)
    
    # ポジション情報
    position_info = None
    if start_position is not None:
        # 開始ポジション(startPositionはレバレッジポジションのみを表す)
        if start_position > 0:
            start_text = f"LONG {abs(start_position):,.2f}枚"
        elif start_position < 0:
            start_text = f"SHORT {abs(start_position):,.2f}枚"
        else:
            start_text = "ポジションなし"
        
        # 取引内容
        if side == "sell":
            action_text = f"{total_sz:,.2f}枚を売却"
            
            # 現在のポジション状態(レバレッジ)
            if current_position is not None:
                if abs(current_position) < 0.01:
                    leverage_text = "ポジションなし"
                elif current_position > 0:
                    leverage_text = f"LONG {current_position:,.2f}枚"
                else:
                    leverage_text = f"SHORT {abs(current_position):,.2f}枚"
            else:
                leverage_text = "?"
            
            # 現物保有状態(HYPEのみ)
            if coin_display == "HYPE" and spot_holding is not None:
                if spot_holding >= 1.0:
                    spot_text = f"現物 {int(spot_holding):,}枚保有"
                else:
                    spot_text = "現物保有 0"
            else:
                spot_text = None
            
            if spot_text:
                position_info = f"{start_text}{action_text}\n→ 現在: {leverage_text} / {spot_text}"
            else:
                position_info = f"{start_text}{action_text}\n→ 現在: {leverage_text}"
                
        else:  # buy
            action_text = f"{total_sz:,.2f}枚を購入"
            
            # 現在のポジション状態(レバレッジ)
            if current_position is not None:
                if abs(current_position) < 0.01:
                    leverage_text = "ポジションなし"
                elif current_position > 0:
                    leverage_text = f"LONG {current_position:,.2f}枚"
                else:
                    leverage_text = f"SHORT {abs(current_position):,.2f}枚"
            else:
                leverage_text = "?"
            
            # 現物保有状態(HYPEのみ)
            if coin_display == "HYPE" and spot_holding is not None:
                if spot_holding >= 1.0:
                    spot_text = f"現物 {int(spot_holding):,}枚保有"
                else:
                    spot_text = "現物保有 0"
            else:
                spot_text = None
            
            if spot_text:
                position_info = f"{start_text}{action_text}\n→ 現在: {leverage_text} / {spot_text}"
            else:
                position_info = f"{start_text}{action_text}\n→ 現在: {leverage_text}"
        
        # 注意書き追加
        if count >= 30:
            position_info += "\n※API制限により全取引データは取得できていません"
    
    if side == "buy":
        emoji = "🟢"
        side_text = "買い (BUY)"
        color = 0x2ecc71
    else:
        emoji = "🔴"
        side_text = "売り (SELL)"
        color = 0xe74c3c
    
    trader_label = get_address_label(trader_address)
    hypurrscan_url = get_hypurrscan_url(trader_address)
    
    title = f"{emoji} {coin_display} {side_text} | {trader_label}"
    if is_past_data:
        title = f"📜 {title}"
    
    if count >= 30:
        title += f" ×{count}件+(参考)"
    elif count > 1:
        title += f" ×{count}件"
    
    fields = []
    
    # 取引情報
    if position_info:
        fields.append({
            "name": "📊 取引情報",
            "value": position_info,
            "inline": False
        })
    
    # 平均約定価格
    fields.append({
        "name": "💰 平均約定価格",
        "value": f"**${avg_price:,.4f}**",
        "inline": True
    })
    
    # 時刻
    time_range = fmt_ts(first_time)
    if count > 1 and last_time and first_time and last_time != first_time:
        last_time_str = fmt_ts(last_time)
        if time_range.split()[0] == last_time_str.split()[0]:
            time_range += f" ~ {last_time_str.split()[1]}"
        else:
            time_range += f" ~ {last_time_str}"
    
    fields.append({
        "name": "🕐 時刻",
        "value": time_range,
        "inline": True
    })
    
    # Hash
    if fills and fills[0].get("hash"):
        hash_val = fills[0].get("hash", "")
        hash_short = hash_val[:10] + "..." + hash_val[-8:] if len(hash_val) > 20 else hash_val
        fields.append({
            "name": "🔗 Hash (最初)",
            "value": f"`{hash_short}`",
            "inline": False
        })
    
    # アドレスリンク
    fields.append({
        "name": "🔍 アドレス",
        "value": f"[Hypurrscanで確認]({hypurrscan_url})",
        "inline": False
    })
    
    embed = {
        "title": title,
        "url": hypurrscan_url,
        "color": color,
        "fields": fields,
        "footer": {
            "text": "Hyperliquid Multi-Watch" + (" | 過去24h" if is_past_data else "")
        },
        "timestamp": datetime.now(timezone.utc).isoformat()
    }
    
    return embed

async def send_initial_summary(user_addr: str):
    """起動時に現在のポジションと24時間履歴を表示"""
    
    trader_label = WATCH_ADDRESSES_LOWER[user_addr]
    hypurrscan_url = get_hypurrscan_url(user_addr)
    
    # ヘッダー送信(リンク付き)
    header = f"📊 **取引サマリー** | {trader_label}\n🔍 {hypurrscan_url}"
    post_discord(content=header)
    await asyncio.sleep(0.5)
    
    # 現在のポジションと現物保有を取得
    if SHOW_CURRENT_POSITION:
        positions = get_current_positions(user_addr)
        position_embed = build_position_embed(user_addr, positions)
        post_discord(embeds=[position_embed])
        await asyncio.sleep(1)
    
    # 24時間の履歴を取得(参考情報)
    if SHOW_24H_HISTORY:
        fills = get_user_fills_from_api(user_addr, hours=24)
        
        if fills:
            log(f"過去24時間の約定処理: {len(fills)}件 ({trader_label})")
            fills.sort(key=lambda x: x.get("time", 0))
            
            groups = aggregate_fills(fills)
            embeds = []
            
            for coin, sides in groups.items():
                for side_key, side_fills in sides.items():
                    if side_fills:
                        try:
                            embed = build_aggregated_embed(user_addr, coin, side_key, side_fills, is_past_data=True)
                            embeds.append(embed)
                        except Exception as e:
                            log(f"[ERROR] Embed作成失敗: {e}")
            
            if embeds:
                note = "📜 **過去24時間の取引履歴**"
                post_discord(content=note)
                await asyncio.sleep(0.5)
                
                for i in range(0, len(embeds), 10):
                    post_discord(embeds=embeds[i:i+10])
                    await asyncio.sleep(1)

async def flush_pending_fills(user_addr: str):
    if user_addr not in pending_fills or not pending_fills[user_addr]:
        debug_log(f"保留中の約定なし: {WATCH_ADDRESSES_LOWER[user_addr]}")
        return
    
    fills = pending_fills[user_addr]
    pending_fills[user_addr] = []
    
    log(f"リアルタイム約定集約: {len(fills)}件 ({WATCH_ADDRESSES_LOWER[user_addr]})")
    
    # 最新のポジションと現物保有を取得
    get_current_positions(user_addr)
    get_spot_holdings(user_addr)
    
    groups = aggregate_fills(fills)
    
    embeds = []
    for coin, sides in groups.items():
        for side_key, side_fills in sides.items():
            if side_fills:
                try:
                    embed = build_aggregated_embed(user_addr, coin, side_key, side_fills, is_past_data=False)
                    embeds.append(embed)
                except Exception as e:
                    log(f"[ERROR] Embed作成失敗: {e}")
    
    if embeds:
        log(f"Discord送信開始: {len(embeds)}件のembed")
        for i in range(0, len(embeds), 10):
            post_discord(embeds=embeds[i:i+10])
            await asyncio.sleep(0.5)
    else:
        log("送信するembedなし")

async def schedule_flush(user_addr: str):
    debug_log(f"schedule_flush呼び出し: {WATCH_ADDRESSES_LOWER[user_addr]}, 待機時間={AGGREGATE_WINDOW}秒")
    
    # タイマーが既に動いている場合は新規作成しない(重要な修正点)
    if user_addr in pending_timers:
        debug_log(f"既存タイマー動作中のため、新規タイマーは作成しない")
        return
    
    async def delayed_flush():
        debug_log(f"{AGGREGATE_WINDOW}秒待機開始")
        await asyncio.sleep(AGGREGATE_WINDOW)
        debug_log(f"待機完了、送信開始")
        await flush_pending_fills(user_addr)
        if user_addr in pending_timers:
            del pending_timers[user_addr]
    
    task = asyncio.create_task(delayed_flush())
    pending_timers[user_addr] = task
    debug_log(f"タイマー設定完了")

async def subscribe_user_fills(ws, address: str):
    sub = {
        "method": "subscribe",
        "subscription": {
            "type": "userFills",
            "user": address
        }
    }
    
    await ws.send(json.dumps(sub))

async def subscribe_all(ws):
    for addr in WATCH_ADDRESSES.keys():
        await subscribe_user_fills(ws, addr)
        await asyncio.sleep(0.2)

async def handle_messages(ws):
    message_count = 0
    async for raw in ws:
        message_count += 1
        try:
            data = json.loads(raw)
        except json.JSONDecodeError:
            continue

        channel = data.get("channel")
        
        if channel == "error":
            error_msg = data.get("data", "不明なエラー")
            log(f"[ERROR] Hyperliquid: {error_msg}")
            continue
        
        if channel == "subscriptionResponse":
            continue
        
        if channel == "user" or channel == "userFills":
            msg_data = data.get("data", {})
            
            if "fills" in msg_data:
                user_addr = (msg_data.get("user") or "").lower()
                is_snapshot = msg_data.get("isSnapshot", False)
                
                if user_addr in WATCH_ADDRESSES_LOWER:
                    fills = msg_data.get("fills", [])
                    
                    if is_snapshot:
                        if user_addr not in first_snapshot_processed:
                            first_snapshot_processed[user_addr] = True
                            await send_initial_summary(user_addr)
                        continue
                    
                    for fill in fills:
                        pending_fills[user_addr].append(fill)
                    
                    log(f"新規約定: {len(fills)}件 ({WATCH_ADDRESSES_LOWER[user_addr]})")
                    debug_log(f"保留中の約定数: {len(pending_fills[user_addr])}")
                    await schedule_flush(user_addr)

async def run_forever():
    delay = RETRY_INITIAL_DELAY
    first_connection = True
    
    while True:
        try:
            log("WebSocket接続中...")
            async with websockets.connect(
                HL_WS_URL,
                ping_interval=PING_INTERVAL,
                ping_timeout=PING_TIMEOUT,
                close_timeout=5,
                max_size=2**20
            ) as ws:
                log("✅ WebSocket接続成功")
                
                await subscribe_all(ws)
                
                if first_connection:
                    addr_list = "\n".join([f"• {label} (`{addr[:8]}...`)" 
                                          for addr, label in WATCH_ADDRESSES.items()])
                    post_discord(content=f"✅ **Hyperliquid監視開始**\n\n監視中のアドレス({len(WATCH_ADDRESSES)}件):\n{addr_list}\n\n• リアルタイム取引を5分集約\n• レバレッジポジション・HYPE現物保有を表示\n• 過去24時間の取引履歴を表示\n• @107はHYPEとして表示\n• Hypurrscanリンク付き")
                    first_connection = False
                
                await handle_messages(ws)
                
        except (websockets.ConnectionClosed, websockets.InvalidStatusCode) as e:
            log(f"[WARN] WS切断: {e}. {delay}秒後に再接続します。")
        except Exception as e:
            log(f"[ERROR] エラー: {e}")
            import traceback
            traceback.print_exc()
        
        await asyncio.sleep(delay)
        delay = min(RETRY_MAX_DELAY, delay * 2)

def main():
    log("Hyperliquid監視bot起動")
    log(f"監視アドレス数: {len(WATCH_ADDRESSES)}")
    log(f"約定集約時間: 5分")
    log(f"現在ポジション表示: {SHOW_CURRENT_POSITION}")
    log(f"24時間履歴表示: {SHOW_24H_HISTORY}")
    
    asyncio.run(run_forever())

if __name__ == "__main__":
    main()

大口に逆らうな、と格言があります。
指標や突発のファンダ、別の大口の存在も気になるところですが、無理のない範囲でミラトレする分には有効な戦略だと思います。

画像
https://hypurrscan.io/address/0x082e843a431aef031264dc232693dd710aedca88

🐋のロング損切にショート戦略で合わせていくことも可能。

※参考用コードを使うときは、AIに一回読み込ませたりして安全確認を行ってください。

感想
思い通りの通知にならず、苦労しましたが、動作したのでうれしいです。
20回やり直しましたが、完成版ではなく、他にも追加していきたいです。



いいなと思ったら応援しよう!

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
仮想通貨系中心に有益情報を無限に。量が多いので、検索は検索バーで「LiG★と検索したい単語」でボタンを押してください。加筆が多いため、何回も見てもらえると幸いです。 参考にさせていただいてる方々いつもありがとうございます。このアカウントはLINEグループから転生したものです。
ハイリキで噂のクジラをディスコードに通知してみる|LiG★
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1