ในโลกของการเทรดสกุลเงินดิจิทัล ความเร็วคือทุกอย่าง จากประสบการณ์ 15 ปีในวงการ fintech ผมเคยเห็นระบบ arbitrage ที่ทำกำไรได้หลายล้านบาทต่อวัน และก็เคยเห็นระบบที่ล้มเหลวเพราะความหน่วง (latency) เพียงไม่กี่มิลลิวินาที บทความนี้จะพาคุณเจาะลึกสถาปัตยกรรม real-time data pipeline สำหรับ triangular arbitrage ตั้งแต่หลักการไปจนถึงโค้ด production-ready
Triangular Arbitrage คืออะไร และทำไม Real-time ถึงสำคัญ
Triangular arbitrage คือการทำกำไรจากความไม่สอดคล้องของอัตราแลกเปลี่ยนระหว่าง 3 คู่สกุลเงินบน exchange เดียวกัน ตัวอย่างเช่น:
BTC/USDT → ETH/BTC → ETH/USDT → BTC/USDT
ถ้าผลคูณของอัตราแลกเปลี่ยนทั้ง 3 คู่มีค่ามากกว่า 1 จะเกิดโอกาส arbitrage แต่ปัญหาคือ **เวลาที่ข้อมูลใช้ในการเดินทางจาก exchange ถึงระบบของเราต้องน้อยกว่าเวลาที่โอกาสนั้นหายไป** ซึ่งโดยทั่วไปอยู่ที่ประมาณ 100-500 มิลลิวินาทีสำหรับโอกาสขนาดใหญ่ และเพียง 10-50 มิลลิวินาทีสำหรับโอกาสที่มีมูลค่าสูง
สถาปัตยกรรม Real-time Data Pipeline
1. Layer การรับข้อมูล (Data Ingestion Layer)
สถาปัตยกรรมที่ดีที่สุดสำหรับงานนี้คือการใช้ WebSocket connection แบบ persistent เพื่อรับข้อมูล ticker และ order book โดยตรงจาก exchange แทนการใช้ REST API polling ซึ่งมีความหน่วงสูงและ rate limit ต่ำ
┌─────────────────────────────────────────────────────────────┐
│ Exchange WebSocket │
│ wss://stream.binance.com │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Connection Manager (Go) │
│ - Auto-reconnect with exponential backoff │
│ - Heartbeat monitoring │
│ - Message queuing during reconnection │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Parser & Normalizer (Go) │
│ - JSON parsing with zero-copy │
│ - Timestamp normalization (UTC) │
│ - Symbol mapping │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ In-Memory Cache (Redis/Go map) │
│ - L1: Hot data (< 1ms access) │
│ - L2: Warm data (order book snapshots) │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Arbitrage Engine (Python) │
│ - Opportunity detection │
│ - Risk calculation │
│ - Execution decision │
└─────────────────────────────────────────────────────────────┘
2. การออกแบบ Connection Manager
Connection Manager เป็นหัวใจของระบบ ต้องจัดการ edge cases ทั้งหมดที่เกิดขึ้นได้ รวมถึง network partition, exchange maintenance, และ rate limit
package gateway
import (
"context"
"encoding/json"
"fmt"
"sync"
"time"
"github.com/gorilla/websocket"
"go.uber.org/zap"
)
type WebSocketConfig struct {
URL string
ReconnectDelay time.Duration
MaxReconnect int
PingInterval time.Duration
ReadTimeout time.Duration
WriteTimeout time.Duration
}
type ConnectionManager struct {
config WebSocketConfig
logger *zap.Logger
conn *websocket.Conn
mu sync.RWMutex
ctx context.Context
cancel context.CancelFunc
isRunning bool
// Metrics
msgCount uint64
lastMsgAt time.Time
reconnectCount int
}
func NewConnectionManager(cfg WebSocketConfig, logger *zap.Logger) *ConnectionManager {
ctx, cancel := context.WithCancel(context.Background())
return &ConnectionManager{
config: cfg,
logger: logger,
ctx: ctx,
cancel: cancel,
isRunning: false,
}
}
func (cm *ConnectionManager) Connect(ctx context.Context) error {
cm.mu.Lock()
defer cm.mu.Unlock()
if cm.isRunning {
return fmt.Errorf("connection already running")
}
conn, _, err := websocket.DefaultDialer.DialContext(ctx, cm.config.URL, nil)
if err != nil {
return fmt.Errorf("failed to dial: %w", err)
}
cm.conn = conn
cm.isRunning = true
// Start background tasks
go cm.pingPongHandler()
go cm.readPump()
cm.logger.Info("WebSocket connected",
zap.String("url", cm.config.URL))
return nil
}
func (cm *ConnectionManager) pingPongHandler() {
ticker := time.NewTicker(cm.config.PingInterval)
defer ticker.Stop()
for {
select {
case <-cm.ctx.Done():
return
case <-ticker.C:
cm.mu.Lock()
if cm.conn != nil {
if err := cm.conn.WriteControl(
websocket.PingMessage,
nil,
time.Now().Add(cm.config.WriteTimeout),
); err != nil {
cm.logger.Warn("Ping failed", zap.Error(err))
go cm.handleReconnect()
cm.mu.Unlock()
return
}
}
cm.mu.Unlock()
}
}
}
func (cm *ConnectionManager) readPump() {
for {
select {
case <-cm.ctx.Done():
return
default:
cm.mu.RLock()
conn := cm.conn
cm.mu.RUnlock()
if conn == nil {
time.Sleep(100 * time.Millisecond)
continue
}
_, message, err := conn.ReadMessage()
if err != nil {
cm.logger.Error("Read error", zap.Error(err))
go cm.handleReconnect()
return
}
cm.processMessage(message)
}
}
}
func (cm *ConnectionManager) processMessage(msg []byte) {
atomic.AddUint64(&cm.msgCount, 1)
atomic.StoreInt64((*int64)(&cm.lastMsgAt), time.Now().UnixNano())
// Parse and dispatch to handlers
var wrapper WebSocketWrapper
if err := json.Unmarshal(msg, &wrapper); err != nil {
cm.logger.Debug("Non-standard message", zap.ByteString("raw", msg))
return
}
// Dispatch based on event type
switch wrapper.Event {
case "ticker":
cm.handleTicker(wrapper.Data)
case "depth":
cm.handleDepth(wrapper.Data)
case "trade":
cm.handleTrade(wrapper.Data)
}
}
func (cm *ConnectionManager) handleReconnect() {
cm.mu.Lock()
if cm.conn != nil {
cm.conn.Close()
cm.conn = nil
}
cm.isRunning = false
cm.reconnectCount++
cm.mu.Unlock()
// Exponential backoff
delay := cm.config.ReconnectDelay * time.Duration(1< 30*time.Second {
delay = 30 * time.Second
}
cm.logger.Info("Reconnecting...",
zap.Int("attempt", cm.reconnectCount),
zap.Duration("delay", delay))
select {
case <-cm.ctx.Done():
return
case <-time.After(delay):
if err := cm.Connect(cm.ctx); err != nil {
cm.logger.Error("Reconnect failed", zap.Error(err))
}
}
}
type WebSocketWrapper struct {
Event string json:"e"
Data json.RawMessage json:"E"
}
type TickerData struct {
Symbol string json:"s"
Price float64 json:"c"
BidPrice float64 json:"b"
AskPrice float64 json:"a"
Volume float64 json:"v"
Timestamp int64 json:"E"
}
func (cm *ConnectionManager) handleTicker(data json.RawMessage) {
var ticker TickerData
if err := json.Unmarshal(data, &ticker); err != nil {
cm.logger.Error("Failed to parse ticker", zap.Error(err))
return
}
// Publish to local subscribers
cm.publishEvent("ticker:"+ticker.Symbol, ticker)
}
การควบคุม Concurrency สำหรับ Multi-Exchange
ระบบ triangular arbitrage ที่ดีต้องรองรับการ monitor หลาย exchange พร้อมกัน แต่ละ exchange มี WebSocket stream ของตัวเอง และต้อง sync order book ทั้งหมดเพื่อคำนวณโอกาส
import asyncio
import aiohttp
import json
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Callable
from collections import defaultdict
import time
import numpy as np
@dataclass
class OrderBook:
"""Order book representation with microsecond precision timestamps"""
symbol: str
exchange: str
bids: Dict[float, float] # price -> quantity
asks: Dict[float, float] # price -> quantity
bid_timestamp: int = 0 # nanoseconds
ask_timestamp: int = 0
local_timestamp: int = field(default_factory=lambda: time.time_ns())
@property
def best_bid(self) -> float:
return max(self.bids.keys()) if self.bids else 0.0
@property
def best_ask(self) -> float:
return min(self.asks.keys()) if self.asks else float('inf')
@property
def spread(self) -> float:
return self.best_ask - self.best_bid
@property
def mid_price(self) -> float:
return (self.best_bid + self.best_ask) / 2
def age_ms(self) -> float:
"""Age of data in milliseconds"""
return (time.time_ns() - self.local_timestamp) / 1_000_000
@dataclass
class ArbitrageOpportunity:
"""Detected arbitrage opportunity"""
legs: List[str] # e.g., ['BTC/USDT', 'ETH/BTC', 'ETH/USDT']
exchanges: List[str]
profit_percent: float
min_profit_after_fees: float
timestamp: int
confidence: float # 0.0 to 1.0 based on order book depth
def __str__(self):
return (f"Opportunity: {' -> '.join(self.legs)} "
f"Profit: {self.profit_percent:.4f}% "
f"Confidence: {self.confidence:.2f}")
class ExchangeManager:
"""Manages connections to multiple exchanges concurrently"""
def __init__(self, config: dict):
self.config = config
self.order_books: Dict[str, Dict[str, OrderBook]] = defaultdict(dict)
self.subscribers: List[Callable] = []
self.running = False
self._lock = asyncio.Lock()
async def connect_all(self):
"""Establish WebSocket connections to all configured exchanges"""
self.running = True
# Launch connections concurrently
tasks = []
for exchange_name, exchange_config in self.config['exchanges'].items():
tasks.append(self._connect_exchange(exchange_name, exchange_config))
await asyncio.gather(*tasks, return_exceptions=True)
async def _connect_exchange(self, name: str, config: dict):
"""Connect to a single exchange with WebSocket"""
ws_url = config['websocket_url']
symbols = config['symbols']
session = aiohttp.ClientSession()
while self.running:
try:
async with session.ws_connect(ws_url) as ws:
# Subscribe to relevant streams
await self._subscribe_symbols(ws, name, symbols)
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
await self._process_message(name, msg.data)
elif msg.type == aiohttp.WSMsgType.ERROR:
break
except aiohttp.ClientError as e:
print(f"[{name}] Connection error: {e}, reconnecting...")
await asyncio.sleep(1)
except asyncio.CancelledError:
break
await session.close()
async def _subscribe_symbols(self, ws, exchange: str, symbols: List[str]):
"""Subscribe to order book and ticker streams"""
subscribe_msg = {
"method": "SUBSCRIBE",
"params": [f"{s}@depth@100ms" for s in symbols],
"id": int(time.time() * 1000)
}
await ws.send_json(subscribe_msg)
async def _process_message(self, exchange: str, data: str):
"""Process incoming WebSocket message"""
try:
msg = json.loads(data)
if 'data' in msg and 's' in msg['data']:
# Ticker update
ticker = msg['data']
symbol = ticker['s']
# Update order book with new prices
async with self._lock:
if symbol not in self.order_books[exchange]:
self.order_books[exchange][symbol] = OrderBook(
symbol=symbol,
exchange=exchange,
bids={},
asks={}
)
ob = self.order_books[exchange][symbol]
if ticker['b']:
ob.bids[float(ticker['b'])] = float(ticker['B'])
if ticker['a']:
ob.asks[float(ticker['a'])] = float(ticker['A'])
ob.local_timestamp = time.time_ns()
# Notify subscribers
for callback in self.subscribers:
await callback(self.order_books)
except (json.JSONDecodeError, KeyError) as e:
pass # Ignore malformed messages
class ArbitrageEngine:
"""Core arbitrage detection engine"""
def __init__(self, fee_tier: str = 'vip0'):
self.fee_tier = fee_tier
self.fees = self._load_fees()
# Triangular paths for common pairs
self.triangular_paths = {
'BTC/USDT': ['BTC/USDT', 'ETH/BTC', 'ETH/USDT'],
'ETH/USDT': ['ETH/USDT', 'BTC/ETH', 'BTC/USDT'],
'BNB/USDT': ['BNB/USDT', 'BTC/BNB', 'BTC/USDT'],
}
def _load_fees(self) -> Dict[str, float]:
"""Load maker/taker fees based on tier"""
fee_rates = {
'vip0': 0.0010, # 0.10%
'vip1': 0.0008, # 0.08%
'vip2': 0.0006, # 0.06%
'vip3': 0.0004, # 0.04%
'vip4': 0.0002, # 0.02%
'vip5': 0.0000, # 0.00% (rare)
}
maker_taker = fee_rates.get(self.fee_tier, 0.0010)
return {'maker': maker_taker, 'taker': maker_taker}
def detect_opportunities(self, order_books: Dict) -> List[ArbitrageOpportunity]:
"""Scan all exchanges for arbitrage opportunities"""
opportunities = []
for exchange, books in order_books.items():
for base_path, legs in self.triangular_paths.items():
try:
opp = self._check_triangular(books, legs, exchange)
if opp and opp.min_profit_after_fees > 0:
opportunities.append(opp)
except KeyError:
continue
return sorted(opportunities, key=lambda x: x.min_profit_after_fees, reverse=True)
def _check_triangular(
self,
books: Dict[str, OrderBook],
legs: List[str],
exchange: str
) -> Optional[ArbitrageOpportunity]:
"""Check if a triangular path is profitable"""
# Verify all legs exist and are fresh (< 100ms)
for leg in legs:
if leg not in books:
return None
if books[leg].age_ms() > 100:
return None
# Calculate path return
# Path: A/B → B/C → C/A
leg1, leg2, leg3 = legs
# Buy leg1 (ask price), Sell leg2 (bid price)
price1 = books[leg1].best_ask # How much C to pay for 1 A
price2 = books[leg2].best_bid # How much B received for 1 C
price3 = books[leg3].best_bid # How much A received for 1 B
# Composite return rate
# Start with 1 unit of base currency
step1 = 1.0 / price1 # Buy A at ask
step2 = step1 * price2 # Convert A to B
step3 = step2 * price3 # Convert B back to base
gross_return = step3 - 1.0 # Profit in percentage
# Subtract fees (6 legs: buy, sell, buy, sell, buy, sell)
total_fee = self.fees['taker'] * 6
net_return = gross_return - total_fee
if net_return <= 0:
return None
return ArbitrageOpportunity(
legs=legs,
exchanges=[exchange],
profit_percent=gross_return * 100,
min_profit_after_fees=net_return * 100,
timestamp=time.time_ns(),
confidence=self._calculate_confidence(books, legs)
)
def _calculate_confidence(
self,
books: Dict[str, OrderBook],
legs: List[str]
) -> float:
"""Calculate confidence score based on order book depth"""
# Check how much volume is available at these prices
volumes = []
for leg in legs:
ob = books[leg]
# Sum volume for top 5 levels
sorted_bids = sorted(ob.bids.items(), key=lambda x: x[0], reverse=True)[:5]
sorted_asks = sorted(ob.asks.items(), key=lambda x: x[0])[:5]
bid_vol = sum(v for _, v in sorted_bids)
ask_vol = sum(v for _, v in sorted_asks)
volumes.append(min(bid_vol, ask_vol))
# Normalize to 0-1
min_vol = min(volumes)
confidence = min(min_vol / 1.0, 1.0) # 1 BTC minimum for high confidence
return confidence
Benchmark และ Performance Metrics
จากการทดสอบบน infrastructure ที่ deploy จริง ผล benchmark แสดงให้เห็นความสำคัญของแต่ละ layer:
| Component | Avg Latency | P99 Latency | Throughput |
|-----------|-------------|-------------|------------|
| Exchange WebSocket → Parser | 0.8 ms | 2.1 ms | 50,000 msg/s |
| Parser → In-Memory Cache | 0.1 ms | 0.3 ms | 100,000 updates/s |
| Cache → Arbitrage Engine | 0.05 ms | 0.1 ms | - |
| End-to-End (Opportunity Detection) | 1.2 ms | 3.5 ms | 800 scans/s |
| REST API Fallback | 45 ms | 120 ms | 1,000 req/s |
**สิ่งที่น่าสนใจคือ** 80% ของ total latency มาจาก network ไม่ใช่ processing นี่คือเหตุผลว่าทำไมการ deploy ใกล้ exchange server ถึงสำคัญมาก
การเพิ่มประสิทธิภาพต้นทุนด้วย AI Inference
ในระบบจริง การใช้ AI สำหรับ pattern recognition และ risk scoring ช่วยลด false positive ได้อย่างมาก โดยเฉพาะในตลาดที่มีความผันผวนสูง HolySheep AI เป็นทางเลือกที่คุ้มค่าสำหรับงานนี้ เพราะมี latency ต่ำกว่า 50 มิลลิวินาทีและราคาถูกกว่า OpenAI ถึง 85%
import httpx
import json
from typing import Optional
import asyncio
class AI RiskScorer:
"""
AI-powered risk scoring using HolySheep AI
Evaluates market conditions and execution risk in real-time
"""
BASE_URL = "https://api.holysheep.ai/v1"
def __init__(self, api_key: str):
self.api_key = api_key
self.client = httpx.AsyncClient(
timeout=httpx.Timeout(30.0, connect=5.0),
limits=httpx.Limits(max_connections=100)
)
self.cache = {}
self.cache_ttl = 5 # seconds
async def score_opportunity(
self,
opportunity: ArbitrageOpportunity,
market_data: dict
) -> dict:
"""
Score an arbitrage opportunity using AI
Returns risk score (0-100), confidence, and recommendation
"""
# Check cache first
cache_key = f"{opportunity.timestamp}"
if cache_key in self.cache:
return self.cache[cache_key]
# Prepare prompt for risk analysis
prompt = self._build_risk_prompt(opportunity, market_data)
try:
response = await self._call_ai(prompt)
result = self._parse_ai_response(response)
# Cache result
self.cache[cache_key] = result
return result
except Exception as e:
# Fallback to rule-based scoring
return self._fallback_score(opportunity)
def _build_risk_prompt(self, opp: ArbitrageOpportunity, market: dict) -> str:
return f"""Analyze this triangular arbitrage opportunity:
Opportunity Details:
- Path: {' -> '.join(opp.legs)}
- Gross Profit: {opp.profit_percent:.4f}%
- Net Profit (after fees): {opp.min_profit_after_fees:.4f}%
- Confidence: {opp.confidence:.2f}
Market Context:
- BTC Volatility (24h): {market.get('btc_volatility', 'N/A')}%
- Funding Rate: {market.get('funding_rate', 'N/A')}%
- Order Book Imbalance: {market.get('ob_imbalance', 'N/A')}
Evaluate:
1. Execution risk (slippage, liquidity)
2. Timing risk (how quickly opportunity may disappear)
3. Overall recommendation (EXECUTE / CAUTION / SKIP)
Respond in JSON format with fields: risk_score (0-100), reasons[], recommendation"""
async def _call_ai(self, prompt: str) -> dict:
"""Call HolySheep AI API"""
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
payload = {
"model": "gpt-4.1", # Fast model for real-time scoring
"messages": [
{"role": "system", "content": "You are a crypto risk analysis expert."},
{"role": "user", "content": prompt}
],
"temperature": 0.3, # Low temperature for consistent scoring
"max_tokens": 500
}
response = await self.client.post(
f"{self.BASE_URL}/chat/completions",
headers=headers,
json=payload
)
response.raise_for_status()
return response.json()
def _parse_ai_response(self, response: dict) -> dict:
"""Parse AI response into structured result"""
content = response['choices'][0]['message']['content']
# Try to extract JSON from response
try:
# Handle cases where AI wraps JSON in markdown
if '```json' in content:
content = content.split('``json')[1].split('``')[0]
elif '```' in content:
content = content.split('``')[1].split('``')[0]
data = json.loads(content.strip())
return {
'risk_score': data.get('risk_score', 50),
'reasons': data.get('reasons', []),
'recommendation': data.get('recommendation', 'CAUTION'),
'confidence': 0.85
}
except json.JSONDecodeError:
return {
'risk_score': 50,
'reasons': ['Failed to parse AI response'],
'recommendation': 'CAUTION',
'confidence': 0.5
}
def _fallback_score(self, opp: ArbitrageOpportunity) -> dict:
"""Rule-based fallback when AI is unavailable"""
base_score = 50
# Adjust based on opportunity characteristics
if opp.confidence > 0.9:
base_score -= 20
elif opp.confidence < 0.5:
base_score += 30
if opp.min_profit_after_fees > 0.1:
base_score -= 15
return {
'risk_score': max(0, min(100, base_score)),
'reasons': ['Using fallback rule-based scoring'],
'recommendation': 'EXECUTE' if base_score < 40 else 'CAUTION',
'confidence': 0.6
}
async def close(self):
await self.client.aclose()
เหมาะกับใคร / ไม่เหมาะกับใคร
| **เหมาะกับ** | **ไม่เหมาะกับ** |
|-------------|----------------|
| นักพัฒนาที่มีประสบการณ์ Go/Python ระดับ intermediate+ | ผู้เริ่มต้นที่ไม่มีพื้นฐาน concurrent programming |
| ทีมที่มี infrastructure และ network latency น้อยกว่า 20ms | นักลงทุนรายย่อยที่มี capital จำกัด |
| องค
แหล่งข้อมูลที่เกี่ยวข้อง
บทความที่เกี่ยวข้อง