เมื่อผมเริ่มพัฒนา 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 ด้วย ให้บริการข้อมูลประเภท:
- Trades - ข้อมูลการซื้อขายทุกรายการ
- Orderbook snapshots - ภาพรวมคำสั่งซื้อ-ขาย at time interval
- Funding rate - อัตราค่า funding
- Positions - ข้อมูล position ของ traders
การตั้งค่าเริ่มต้นและติดตั้ง 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 ต่อวิ