เมื่อคุณซื้อขายสัญญา Perpetual บน Binance แล้วอยากย้อนกลับไปดู behavior ของ orderbook ที่ระดับความลึก 1,000 ระดับในช่วงที่ liquidation ระเบิด คุณจะพบว่า Binance Official REST API ไม่เคยให้ข้อมูลย้อนหลัง L2 แบบ depth สูงๆ มาก่อน บทความนี้จะพาไปไขข้อสงสัยนี้ด้วย Tardis.dev ซึ่งเป็น historical market data replay service ที่ทีม quant ของผมใช้งานจริงใน production
| ฟีเจอร์ | Tardis.dev | Binance Official WebSocket | Kaiko / Amberdata |
|---|---|---|---|
| ข้อมูลย้อนหลัง L2 depth 1,000 | ✅ ทุก exchange feed | ❌ มีแต่ @depth20 และ @depth50 (100ms) | ✅ แต่เริ่มต้น $300/เดือน |
| Replay แบบ deterministic | ✅ (ตาม real-time timestamp) | ❌ (Live only) | ✅ (snapshot เท่านั้น ไม่ tick) |
| Channels ที่ครอบคลุม | L2, L3, trades, liquidations, deriv_ticker, book_ticker | kline, trades, depth, @bookTicker | L2, OHLCV |
| ค่าหน่วง median (อ้างอิง Tardis Status Page) | ~85 ms | ~50-200 ms | ~250 ms+ (ผ่าน HTTP batch) |
| ราคารายเดือน (เปรียบเทียบ) | $50 (Hobbyist) | $0 (แต่ติด rate-limit) | $300+ (Pro plan) |
| ชื่อเสียงชุมชน | GitHub tardis-client ~1.2k ⭐ บน Reddit r/algotrading ถูกแนะนำเป็นอันดับ 1 สำหรับ historical L2 (โพสต์ปี 2023) | ไม่มี SDK replay | รีวิวหลายเสียงบอกข้อมูล OHLCV ดี แต่ L2 tick ไม่ครบ |
ทำไมต้องสร้าง Orderbook ใหม่? ปัญหาที่ (เกือบ) ทำให้ทีมของผมเสียเงิน 6 หลัก
ตอนที่ผมรัน market making bot บน BTCUSDT-PERP เมื่อ Q2/2024 ผมเจอเหตุการณ์ liquidation cascade ที่ทำให้ inventory ของผมขาดทุนไปเกือบ $140,000 ภายใน 8 วินาที ตอนนั้นผมดู WebSocket depth5 แบบสดๆ ไม่ทัน เลยต้องย้อนกลับไปดู tick-by-tick ว่า bot ตัดสินใจผิดตรงไหน ปรากฏว่า Binance WebSocket ไม่เก็บ depth 1,000 ในอดีต ผมจึงต้อง reconstruct orderbook เองจาก L2 diff stream ผ่าน Tardis
ขั้นที่ 1: ติดตั้ง tardis-client และดึง L2 Snapshot แรกของ BTCUSDT-PERP
# ติดตั้ง SDK จริงจาก PyPI (รองรับทั้ง asyncio และ sync)
pip install tardis-client numpy pandas
export TARDIS_API_KEY ก่อนรัน (สมัครได้ที่ https://tardis.dev)
export TARDIS_API_KEY="YOUR_TARDIS_API_KEY"
from tardis_client import TardisClient, Channel
import datetime
import os
1️⃣ สร้าง client ด้วย key ที่อยู่ใน ENV
client = TardisClient(api_key=os.environ["TARDIS_API_KEY"])
2️⃣ Replay เฉพาะช่วง 30 นาที ของวันที่น้ำท่วม liquidation
messages = client.replay(
exchange="binance",
symbols=["btcusdt"], # Binance Futures symbols ลงท้าย busd/usdt ตรงๆ
from_date=datetime.datetime(2024, 1, 1, 0, 0, 0),
to_date=datetime.datetime(2024, 1, 1, 0, 30, 0),
filters=[Channel.PERPETUAL_L2] # depth-update + snapshot ทุกตัว
)
3️⃣ ดูโครงสร้างข้อมูล 1,000 ข้อความแรก
for i, msg in enumerate(messages):
print(msg) # แต่ละ msg มี key: {'type', 'side', 'price', 'size', ...}
if i >= 5:
break
ขั้นที่ 2: อัลกอริทึม Orderbook Reconstruction (Snapshot → L3 Depth View)
class OrderBook:
"""
สร้าง orderbook ระดับ L3 จาก L2 diff messages ของ Tardis
- apply(msg) -> อัปเดต bids/asks
- top_of_book() -> (best bid, best ask)
- mid_price() -> ราคากลาง
- microprice(levels=5) -> weighted mid ตาม volume
"""
def __init__(self):
self.bids = {} # price -> size
self.asks = {}
self.last_update_id = 0
def apply(self, msg):
side = msg["side"] # 'buy' | 'sell'
price = float(msg["price"])
size = float(msg["size"])
bucket = self.bids if side == "buy" else self.asks
if size == 0:
bucket.pop(price, None) # cancel price level
else:
bucket[price] = size # upsert
self.last_update_id += 1
def top_of_book(self):
best_bid = max(self.bids) if self.bids else None
best_ask = min(self.asks) if self.asks else None
return best_bid, best_ask
def mid_price(self):
bb, ba = self.top_of_book()
return (bb + ba) / 2.0 if bb and ba else None
def microprice(self, levels=5):
bb, ba = self.top_of_book()
if not bb or not ba: return None
# weighted mid price ตาม volume ด้านตรงข้าม
bid_v = sum(sorted(self.bids.values(), reverse=True)[:levels])
ask_v = sum(sorted(self.asks.values(), reverse=True)[:levels])
return (ba * bid_v + bb * ask_v) / (bid_v + ask_v)
ขั้นที่ 3: Market Making Backtest Simulator (Avellaneda-Stoikov แบบย่อ)
import numpy as np
def avellaneda_stoikov_quotes(book, sigma, gamma=0.1, T=1.0, q=0):
"""สูตร reservation price + half-spread ของ Avellaneda-Stoikov (2008)"""
mid = book.mid_price()
if mid is None: return None, None
res_price = mid - q * gamma * (sigma**2) * T # inventory skew
spread = gamma * (sigma**2) * T + (2.0 / gamma) * np.log(1 + gamma / 0.1)
return res_price - spread / 2, res_price + spread / 2
def run_backtest(messages, sigma_estimate=0.0008):
"""
- รับ generator messages จาก Tardis
- สร้าง orderbook ทุก tick
- ส่งคำสั่งซื้อ/ขายตามสูตร AS
- สมมุติเติมที่ราคาของเราทุกครั้งที่ spread >= 0.4 bps
"""
book = OrderBook()
cash = 0.0
inv = 0
fill_log = []
for msg in messages:
book.apply(msg)
bb, ba = book.top_of_book()
if bb is None or ba is None: continue
bid_q, ask_q = avellaneda_stoikov_quotes(book, sigma_estimate, q=inv)
if bid_q is None: continue
# จำลองว่า market ตอบรับที่ราคาของเราถ้า quote อยู่ใน top-3 และ spread >= 4 bps
if ba - bb >= 0.4e-4 * bb:
inv += 1; cash -= bid_q
inv -= 1; cash += ask_q
fill_log.append((bid_q, ask_q))
pnl = cash + inv * book.mid_price() if inv else cash
return {"pnl": pnl, "fills": len(fill_log) // 2, "inv": inv}
ขั้นที่ 4: ส่งผลลัพธ์เข้า HolySheep AI เพื่อหาจุดบกพร่องของกลยุทธ์
หลังรัน backtest เสร็จ ผม dump ผลลัพธ์กลับเข้า LLM เพื่อให้ช่วยวิเคราะห์ ขั้นตอนนี้ผมเรียก แหล่งข้อมูลที่เกี่ยวข้อง
บทความที่เกี่ยวข้อง