เมื่อผมเริ่มพัฒนา market making bot สำหรับ Hyperliquid ครั้งแรก ปัญหาแรกที่เจอคือ ConnectionError: timeout ขณะดึงข้อมูล history จาก exchange API โดยตรง สาเหตุคือ Hyperliquid ไม่มี public historical data endpoint แบบที่ Binance หรือ Bybit มี ทำให้ต้องพึ่งพา data provider ภายนอกอย่าง Tardis Machine แทน

บทความนี้จะสอนวิธีดึงข้อมูล Hyperliquid perpetual contracts ผ่าน Tardis API และนำไปใช้ทำ backtest สำหรับ market making strategy อย่างละเอียด พร้อมโค้ดที่พร้อมใช้งานจริง

ทำไมต้องใช้ Tardis สำหรับ Hyperliquid

Hyperliquid เป็น decentralized exchange ที่เน้นความเร็วสูงและ low latency ตอนนี้มี volume สูงมากในตลาด perpetual futures แต่ปัญหาคือ ไม่มี official historical data API ทำให้การทำ backtest ยากมาก

Tardis Machine เป็น data aggregator ที่เก็บ historical market data จากหลาย exchange รวมถึง Hyperliquid ด้วย ให้บริการข้อมูลประเภท:

การตั้งค่าเริ่มต้นและติดตั้ง dependencies

# สร้าง virtual environment
python -m venv venv
source venv/bin/activate  # Linux/Mac

venv\Scripts\activate # Windows

ติดตั้ง packages ที่จำเป็น

pip install requests pandas numpy tardis-client pyarrow asyncio aiohttp

สร้างไฟล์ config

cat > config.py << 'EOF'

Tardis API Configuration

TARDIS_API_KEY = "your_tardis_api_key_here" TARDIS_API_URL = "https://api.tardis.dev/v1"

Hyperliquid Configuration

HYPERLIQUID_WS_URL = "wss://api.hyperliquid.xyz/ws" HYPERLIQUID_REST_URL = "https://api.hyperliquid.xyz"

HolySheep AI - สำหรับ AI analysis (ราคาถูกกว่า 85%+)

HOLYSHEEP_BASE_URL = "https://api.holysheep.ai/v1" HOLYSHEEP_API_KEY = "YOUR_HOLYSHEEP_API_KEY"

Backtest Configuration

SYMBOL = "BTC-PERP" START_DATE = "2025-01-01" END_DATE = "2025-03-31" TIMEFRAME = "1m" EOF echo "Configuration file created!"

ดึงข้อมูล Historical Trades จาก Tardis

import requests
import pandas as pd
from datetime import datetime, timedelta
import time

class TardisDataFetcher:
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.base_url = "https://api.tardis.dev/v1"
        
    def get_trades(
        self, 
        exchange: str = "hyperliquid", 
        symbol: str = "BTC-PERP",
        start_date: str = None,
        end_date: str = None
    ) -> pd.DataFrame:
        """
        ดึงข้อมูล historical trades จาก Tardis API
        
        Args:
            exchange: ชื่อ exchange (hyperliquid, binance, bybit)
            symbol: ชื่อ trading pair
            start_date: วันที่เริ่มต้น (YYYY-MM-DD)
            end_date: วันที่สิ้นสุด (YYYY-MM-DD)
            
        Returns:
            DataFrame ที่มี columns: timestamp, price, side, size
        """
        # แปลงวันที่เป็น milliseconds timestamp
        start_ts = int(datetime.strptime(start_date, "%Y-%m-%d").timestamp() * 1000)
        end_ts = int(datetime.strptime(end_date, "%Y-%m-%d").timestamp() * 1000)
        
        # Tardis API endpoint สำหรับ historical data
        url = f"{self.base_url}/historical/{exchange}/trades"
        params = {
            "symbol": symbol,
            "from": start_ts,
            "to": end_ts,
            "format": "object",
            "limit": 50000  # max records per request
        }
        
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json"
        }
        
        all_trades = []
        current_start = start_ts
        
        print(f"กำลังดึงข้อมูล trades สำหรับ {symbol}...")
        print(f"ช่วงเวลา: {start_date} ถึง {end_date}")
        
        while current_start < end_ts:
            params["from"] = current_start
            
            try:
                response = requests.get(url, params=params, headers=headers, timeout=30)
                
                if response.status_code == 401:
                    raise Exception("❌ 401 Unauthorized - ตรวจสอบ Tardis API key ของคุณ")
                    
                if response.status_code == 429:
                    # Rate limited - รอแล้วลองใหม่
                    retry_after = int(response.headers.get("Retry-After", 60))
                    print(f"⏳ Rate limited. รอ {retry_after} วินาที...")
                    time.sleep(retry_after)
                    continue
                    
                response.raise_for_status()
                
                data = response.json()
                
                if not data:
                    print("ไม่มีข้อมูลเพิ่มเติม")
                    break
                    
                all_trades.extend(data)
                
                # อัพเดท timestamp สำหรับ request ถัดไป
                current_start = data[-1]["timestamp"] + 1
                
                print(f"✓ ดึงมาแล้ว {len(all_trades):,} records...")
                
                # Delay เพื่อหลีกเลี่ยง rate limit
                time.sleep(0.5)
                
            except requests.exceptions.Timeout:
                print("⚠️ Timeout error - ลองลดขนาดข้อมูลที่ขอ")
                time.sleep(5)
                continue
            except requests.exceptions.ConnectionError as e:
                print(f"❌ Connection error: {e}")
                time.sleep(10)
                continue
                
        # แปลงเป็น DataFrame
        df = pd.DataFrame(all_trades)
        df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms")
        df = df.sort_values("timestamp").reset_index(drop=True)
        
        print(f"\n✅ ดึงข้อมูลสำเร็จ: {len(df):,} trades")
        print(f"📅 ช่วงเวลาจริง: {df['timestamp'].min()} ถึง {df['timestamp'].max()}")
        
        return df

ใช้งาน

if __name__ == "__main__": fetcher = TardisDataFetcher(api_key="your_tardis_api_key") trades_df = fetcher.get_trades( exchange="hyperliquid", symbol="BTC-PERP", start_date="2025-02-01", end_date="2025-02-28" ) print(trades_df.head(10))

ดึงข้อมูล Orderbook และ Funding Rate

import asyncio
import aiohttp
import json
from typing import List, Dict
import pandas as pd
from datetime import datetime

class HyperliquidDataCollector:
    """คลาสสำหรับรวบรวมข้อมูล Hyperliquid จาก Tardis"""
    
    def __init__(self, tardis_key: str):
        self.tardis_key = tardis_key
        self.base_url = "https://api.tardis.dev/v1"
        
    async def fetch_orderbook_snapshots(
        self,
        symbol: str = "BTC-PERP",
        start_ts: int = None,
        end_ts: int = None,
        limit: int = 10000
    ) -> pd.DataFrame:
        """
        ดึงข้อมูล orderbook snapshots จาก Tardis
        
        Returns DataFrame ที่มี:
        - timestamp
        - asks (list of [price, size])
        - bids (list of [price, size])
        - spread
        - mid_price
        """
        url = f"{self.base_url}/historical/hyperliquid/orderbook_snapshots"
        
        params = {
            "symbol": symbol,
            "from": start_ts,
            "to": end_ts,
            "limit": limit,
            "format": "object"
        }
        
        headers = {"Authorization": f"Bearer {self.tardis_key}"}
        
        all_snapshots = []
        
        async with aiohttp.ClientSession() as session:
            current_start = start_ts
            
            while current_start < end_ts:
                params["from"] = current_start
                
                try:
                    async with session.get(url, params=params, headers=headers, timeout=60) as resp:
                        
                        if resp.status == 401:
                            raise ValueError("401 Unauthorized - ตรวจสอบ API key")
                            
                        if resp.status == 429:
                            await asyncio.sleep(60)
                            continue
                            
                        data = await resp.json()
                        
                        if not data:
                            break
                            
                        all_snapshots.extend(data)
                        current_start = data[-1]["timestamp"] + 1
                        
                        print(f"Orderbook: {len(all_snapshots)} snapshots")
                        await asyncio.sleep(0.3)
                        
                except asyncio.TimeoutError:
                    print("Timeout - รอแล้วลองใหม่")
                    await asyncio.sleep(5)
                    continue
                    
        # แปลงเป็น DataFrame
        df = pd.DataFrame(all_snapshots)
        df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms")
        
        # คำนวณ spread และ mid price
        if "asks" in df.columns and "bids" in df.columns:
            df["best_ask"] = df["asks"].apply(lambda x: float(x[0][0]) if x else None)
            df["best_bid"] = df["bids"].apply(lambda x: float(x[0][0]) if x else None)
            df["mid_price"] = (df["best_ask"] + df["best_bid"]) / 2
            df["spread"] = df["best_ask"] - df["best_bid"]
            df["spread_bps"] = (df["spread"] / df["mid_price"]) * 10000
            
        return df.sort_values("timestamp").reset_index(drop=True)
        
    def get_funding_rates(
        self, 
        symbol: str = "BTC-PERP",
        days: int = 30
    ) -> pd.DataFrame:
        """ดึงข้อมูล funding rate history"""
        
        end_ts = int(datetime.now().timestamp() * 1000)
        start_ts = end_ts - (days * 24 * 60 * 60 * 1000)
        
        url = f"{self.base_url}/historical/hyperliquid/funding_rate"
        params = {
            "symbol": symbol,
            "from": start_ts,
            "to": end_ts,
            "format": "object"
        }
        
        response = requests.get(
            url, 
            params=params, 
            headers={"Authorization": f"Bearer {self.tardis_key}"},
            timeout=30
        )
        
        if response.status_code != 200:
            raise Exception(f"Failed to fetch funding rates: {response.status_code}")
            
        data = response.json()
        
        df = pd.DataFrame(data)
        df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms")
        
        return df.sort_values("timestamp")

ทดสอบการใช้งาน

async def main(): collector = HyperliquidDataCollector(tardis_key="your_tardis_key") # ดึง orderbook snapshots 30 วัน end_ts = int(datetime.now().timestamp() * 1000) start_ts = end_ts - (30 * 24 * 60 * 60 * 1000) orderbook_df = await collector.fetch_orderbook_snapshots( symbol="BTC-PERP", start_ts=start_ts, end_ts=end_ts ) print(f"ดึง orderbook สำเร็จ: {len(orderbook_df)} snapshots") print(f"เฉลี่ย spread: {orderbook_df['spread_bps'].mean():.2f} bps") # ดึง funding rates funding_df = collector.get_funding_rates(days=30) print(f"\nเฉลี่ย funding rate: {funding_df['rate'].mean() * 100:.4f}%") return orderbook_df, funding_df if __name__ == "__main__": orderbook, funding = asyncio.run(main())

สร้าง Market Making Strategy Backtest Engine

import numpy as np
import pandas as pd
from dataclasses import dataclass
from typing import List, Tuple, Optional
from enum import Enum

class OrderSide(Enum):
    BUY = "buy"
    SELL = "sell"

@dataclass
class Order:
    side: OrderSide
    price: float
    size: float
    timestamp: pd.Timestamp

@dataclass
class Position:
    size: float  # positive = long, negative = short
    entry_price: float
    unrealized_pnl: float = 0.0
    
class MarketMakerBacktest:
    """
    Market Making Strategy Backtest Engine
    กลยุทธ์: Place limit orders both sides around mid price
    รายได้จาก: spread + funding (ถ้า position เปิดข้ามคืน)
    """
    
    def __init__(
        self,
        maker_fee: float = 0.0003,      # 0.03% maker fee
        taker_fee: float = 0.0005,       # 0.05% taker fee
        spread_bps: float = 5.0,         # spread ที่ตั้ง (basis points)
        order_size_pct: float = 0.001,    # % ของ volume ต่อ order
        max_position: float = 1.0,        # max position size (BTC)
        funding_rate: float = 0.0001,     # 8h funding rate (0.01%)
    ):
        self.maker_fee = maker_fee
        self.taker_fee = taker_fee
        self.spread_bps = spread_bps
        self.order_size_pct = order_size_pct
        self.max_position = max_position
        self.funding_rate = funding_rate
        
        self.position = 0.0
        self.cash = 0.0
        self.trades: List[Order] = []
        self.history: List[dict] = []
        
    def place_orders(self, mid_price: float, volume: float, timestamp: pd.Timestamp):
        """สร้างคำสั่ง buy และ sell รอที่ราคา mid ± spread"""
        
        spread = mid_price * (self.spread_bps / 10000)
        buy_price = mid_price - spread / 2
        sell_price = mid_price + spread / 2
        
        size = volume * self.order_size_pct
        
        # จำกัดขนาด position
        max_buy_size = max(0, self.max_position - self.position)
        max_sell_size = max(0, self.max_position + self.position)
        
        buy_order = Order(OrderSide.BUY, buy_price, min(size, max_buy_size), timestamp)
        sell_order = Order(OrderSide.SELL, sell_price, min(size, max_sell_size), timestamp)
        
        self.trades.extend([buy_order, sell_order])
        
    def execute_fills(
        self, 
        trades_df: pd.DataFrame, 
        orderbook_df: pd.DataFrame
    ) -> pd.DataFrame:
        """
        Simulate การ execute orders ตาม trade flow จริง
        """
        
        # Merge trades กับ orderbook
        merged = pd.merge_asof(
            trades_df.sort_values("timestamp"),
            orderbook_df.sort_values("timestamp"),
            on="timestamp",
            direction="backward",
            tolerance=pd.Timedelta("1m")
        )
        
        for idx, row in merged.iterrows():
            if pd.isna(row["mid_price"]):
                continue
                
            mid_price = row["mid_price"]
            trade_price = row["price"]
            trade_side = row["side"]
            timestamp = row["timestamp"]
            
            # ตรวจสอบว่า trade ตีที่ราคาที่เราวาง order ไว้หรือไม่
            for order in self.trades[:]:
                if order.timestamp == timestamp:
                    # Check if order would be filled
                    if trade_side == "buy" and order.side == OrderSide.BUY:
                        if trade_price >= order.price:
                            self._fill_order(order, trade_price, timestamp)
                            self.trades.remove(order)
                            
                    elif trade_side == "sell" and order.side == OrderSide.SELL:
                        if trade_price <= order.price:
                            self._fill_order(order, trade_price, timestamp)
                            self.trades.remove(order)
            
            # บันทึก state
            self.history.append({
                "timestamp": timestamp,
                "mid_price": mid_price,
                "position": self.position,
                "cash": self.cash,
                "total_pnl": self.cash + self.position * mid_price,
                "spread_bps": self.spread_bps
            })
            
    def _fill_order(self, order: Order, fill_price: float, timestamp: pd.Timestamp):
        """Execute order fill"""
        
        cost = order.size * fill_price
        
        if order.side == OrderSide.BUY:
            # ซื้อ - เพิ่ม position และหัก cash
            self.position += order.size
            self.cash -= cost
            fee = cost * self.maker_fee
            self.cash -= fee
            
        else:  # SELL
            # ขาย - ลด position และเพิ่ม cash
            self.position -= order.size
            self.cash += cost
            fee = cost * self.maker_fee
            self.cash -= fee
            
    def calculate_metrics(self) -> dict:
        """คำนวณ performance metrics"""
        
        df = pd.DataFrame(self.history)
        
        if len(df) == 0:
            return {}
            
        total_pnl = df["total_pnl"].iloc[-1] - df["total_pnl"].iloc[0]
        
        # Calculate returns
        df["returns"] = df["total_pnl"].pct_change()
        
        # Sharpe ratio (simplified)
        if df["returns"].std() != 0:
            sharpe = df["returns"].mean() / df["returns"].std() * np.sqrt(252 * 24 * 60)
        else:
            sharpe = 0
            
        # Max drawdown
        df["cummax"] = df["total_pnl"].cummax()
        df["drawdown"] = (df["total_pnl"] - df["cummax"]) / df["cummax"]
        max_drawdown = df["drawdown"].min()
        
        return {
            "total_pnl": total_pnl,
            "sharpe_ratio": sharpe,
            "max_drawdown": max_drawdown,
            "total_trades": len(self.trades),
            "avg_position": df["position"].mean(),
            "final_position": df["position"].iloc[-1],
            "final_cash": df["cash"].iloc[-1],
            "win_rate": len(df[df["returns"] > 0]) / len(df) if len(df) > 0 else 0
        }

ใช้งาน Backtest

def run_backtest(): # โหลดข้อมูลที่ดึงมาจาก Tardis trades_df = pd.read_csv("hyperliquid_trades.csv", parse_dates=["timestamp"]) orderbook_df = pd.read_csv("hyperliquid_orderbook.csv", parse_dates=["timestamp"]) # สร้าง backtest engine backtest = MarketMakerBacktest( maker_fee=0.0003, taker_fee=0.0005, spread_bps=5.0, order_size_pct=0.002, max_position=0.5, funding_rate=0.0001 ) # Run simulation backtest.execute_fills(trades_df, orderbook_df) # ดูผลลัพธ์ metrics = backtest.calculate_metrics() print("=" * 50) print("MARKET MAKING BACKTEST RESULTS") print("=" * 50) print(f"Total PnL: ${metrics['total_pnl']:.2f}") print(f"Sharpe Ratio: {metrics['sharpe_ratio']:.2f}") print(f"Max Drawdown: {metrics['max_drawdown']*100:.2f}%") print(f"Total Trades: {metrics['total_trades']}") print(f"Win Rate: {metrics['win_rate']*100:.2f}%") print("=" * 50) return metrics if __name__ == "__main__": results = run_backtest()

ข้อผิดพลาดที่พบบ่อยและวิธีแก้ไข

1. 401 Unauthorized - Tardis API Key ไม่ถูกต้อง

อาการ: เมื่อเรียก API ได้รับ response 401 พร้อม error message "Invalid API key"

# ❌ วิธีผิด - API key ไม่ถูกต้องหรือหมดอายุ
response = requests.get(url, headers={"Authorization": "Bearer wrong_key"})

✅ วิธีถูก - ตรวจสอบ key format และ environment

import os def get_tardis_key(): """ดึง API key จาก environment variable""" api_key = os.environ.get("TARDIS_API_KEY") if not api_key: raise ValueError( "❌ TARDIS_API_KEY ไม่ได้ถูกตั้งค่า\n" "กรุณาตั้งค่า environment variable:\n" "export TARDIS_API_KEY='your_key_here'" ) # ตรวจสอบ format ของ API key if len(api_key) < 20: raise ValueError(f"❌ API key สั้นเกินไป: {api_key[:5]}***") return api_key

วิธีที่แนะนำ: ใช้ .env file

สร้างไฟล์ .env

TARDIS_API_KEY=your_actual_tardis_key

HOLYSHEEP_API_KEY=your_holysheep_key

from dotenv import load_dotenv load_dotenv() API_KEY = get_tardis_key() print(f"✅ API Key loaded: {API_KEY[:5]}***")

2. ConnectionError: timeout ขณะดึงข้อมูลจำนวนมาก

อาการ: Request timeout เมื่อดึงข้อมูล period ยาวๆ หรือเมื่อ network latency สูง

# ❌ วิธีผิด - timeout default สั้นเกินไป
response = requests.get(url, timeout=10)  # แค่ 10 วินาที

✅ วิธีถูก - เพิ่ม retry logic และ exponential backoff

import time from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def create_session_with_retry(max_retries=5, backoff_factor=2): """สร้าง requests session ที่มี retry logic""" session = requests.Session() # Retry strategy retry_strategy = Retry( total=max_retries, backoff_factor=backoff_factor, status_forcelist=[429, 500, 502, 503, 504], allowed_methods=["HEAD", "GET", "OPTIONS"] ) adapter = HTTPAdapter(max_retries=retry_strategy) session.mount("http://", adapter) session.mount("https://", adapter) return session class RobustDataFetcher: def __init__(self, api_key: str): self.api_key = api_key self.session = create_session_with_retry(max_retries=5, backoff_factor=2) def fetch_with_retry(self, url: str, params: dict, timeout: int = 120): """ดึงข้อมูลพร้อม retry logic""" headers = {"Authorization": f"Bearer {self.api_key}"} for attempt in range(5): try: print(f"Attempt {attempt + 1}/5...") response = self.session.get( url, params=params, headers=headers, timeout=timeout # เพิ่มเป็น 120 วินาที ) response.raise_for_status() return response.json() except requests.exceptions.Timeout: wait_time = (2 ** attempt) * 5 # 10, 20, 40, 80, 160 วินาที print(f"⏳ Timeout - รอ {wait_time} วินาที...") time.sleep(wait_time) except requests.exceptions.ConnectionError as e: wait_time = (2 ** attempt) * 10 print(f"🔌 Connection error - รอ {wait_time} วินาที...") time.sleep(wait_time) raise Exception("❌ ดึงข้อมูลไม่สำเร็จหลังจากลอง 5 ครั้ง")

3. Rate Limit 429 - เรียก API บ่อยเกินไป

อาการ: ได้รับ 429 Too Many Requests error และถูก block ชั่วคราว

# ❌ วิธีผิด - request ต่อเนื่องโดยไม่มี delay
for i in range(1000):
    response = requests.get(url)  # จะโดน rate limit แน่นอน

✅ วิธีถูก - ใช้ rate limiter และ caching

import time import hashlib from functools import lru_cache class RateLimitedFetcher: """Fetcher ที่มี built-in rate limiting""" def __init__(self, calls_per_second: float = 2.0): self.min_interval = 1.0 / calls_per_second self.last_call = 0 def wait_if_needed(self): """รอจนกว่าจะถึงเวลาที่เรียกได้อีกครั้ง""" elapsed = time.time() - self.last_call if elapsed < self.min_interval: sleep_time = self.min_interval - elapsed print(f"⏳ Rate limited - รอ {sleep_time:.2f} วินาที...") time.sleep(sleep_time) self.last_call = time.time() def fetch(self, url: str, params: dict) -> dict: """ดึงข้อมูลพร้อม rate limiting""" # สร้าง cache key cache_key = hashlib.md5(f"{url}:{str(params)}".encode()).hexdigest() # ตรวจสอบ cache cached = self._get_cache(cache_key) if cached is not None: print("📦 ใช้ข้อมูลจาก cache") return cached # รอจนกว่าจะเรียกได้ self.wait_if_needed() # ดึงข้อมูล response = requests.get(url, params=params) if response.status_code == 429: retry_after = int(response.headers.get("Retry-After", 60)) print(f"🚫 Rate limited - รอ {retry_after} วินาที...") time.sleep(retry_after) return self.fetch(url, params) # ลองใหม่ response.raise_for_status() data = response.json() # เก็บใน cache self._set_cache(cache_key, data, ttl=300) # cache 5 นาที return data

ใช้งาน

fetcher = RateLimitedFetcher(calls_per_second=1.0) # สูงสุด 1 request ต่อวิ