ในช่วงตลาดคริปโตผันผวน การตรวจจับสัญญาณวิกฤตสภาพคล่องล่วงหน้าเป็นสิ่งทองคำแต่นักเทรดและสถาบันการเงินต้องการ บทความนี้จะพาคุณสร้างระบบมอนิเตอร์ Bid-Ask Spread ที่ใช้งานได้จริง พร้อมโค้ด Python และกรณีศึกษาจากประสบการณ์ตรง
สถานการณ์ข้อผิดพลาดจริง: ConnectionTimeout กับ Tardis API
ผมเคยเจอปัญหาหนักในการพัฒนาระบบมอนิเตอร์สภาพคล่องเมื่อเดือนที่แล้ว ระบบที่ทำงานอยู่ดี ๆ ก็ขึ้น ConnectionError: timeout after 30s จาก Tardis API ตอนเช้าวันที่ตลาดคริปโตล่ม ทำให้พลาดจังหวะการแจ้งเตือนวิกฤต
# โค้ดที่ทำให้เกิดปัญหา (แบบไม่มี error handling)
import tardis
client = tardis.Client()
orderbook = client.get_orderbook('BTC-USDT') # ไม่มี timeout!
print(orderbook) # ถ้า API ล่ม รอนานมากหรือ error
ปัญหา: ไม่มี retry logic, ไม่มี circuit breaker
หลังจากแก้ไขปัญหานี้ ผมได้สร้างระบบที่ทำงานเสถียรกว่าเดิมมาก มาเริ่มกันเลย
ทำความเข้าใจ Bid-Ask Spread ในตลาดคริปโต
Bid-Ask Spread คืออะไร
Bid-Ask Spread คือส่วนต่างระหว่างราคาซื้อสูงสุด (Bid) และราคาขายต่ำสุด (Ask) ใน Order Book เมื่อ Spread กว้างขึ้นผิดปกติ แสดงว่าสภาพคล่องกำลังแห้งเหี่ยว ซึ่งเป็นสัญญาณเตือนวิกฤตที่สำคัญ
สูตรคำนวณ Spread
# สูตรพื้นฐาน
spread = (ask_price - bid_price) / mid_price * 100
mid_price = (ask_price + bid_price) / 2
ตัวอย่าง
Bid: 64,500 USDT, Ask: 64,520 USDT
Spread = (64,520 - 64,500) / 64,510 * 100 = 0.031%
ปกติ BTC spread อยู่ที่ 0.01-0.05%
การติดตั้งเครื่องมือและ Setup สภาพแวดล้อม
# ติดตั้ง dependencies
pip install tardis-client pandas numpy aiohttp asyncioRateLimiter
pip install python-dotenv requests
โครงสร้างโฟลเดอร์โปรเจกต์
"""
liquidity_monitor/
├── config.py
├── tardis_client.py
├── spread_analyzer.py
├── alert_manager.py
├── main.py
└── .env
"""
การเชื่อมต่อ Tardis API อย่างปลอดภัย
# config.py
import os
from dotenv import load_dotenv
load_dotenv()
Tardis Configuration
TARDIS_API_KEY = os.getenv('TARDIS_API_KEY')
TARDIS_BASE_URL = "https://api.tardis.dev/v1"
HolySheep AI Configuration - สำหรับวิเคราะห์ขั้นสูง
HOLYSHEEP_BASE_URL = "https://api.holysheep.ai/v1"
HOLYSHEEP_API_KEY = os.getenv("HOLYSHEEP_API_KEY")
Alert Configuration
SPREAD_THRESHOLD_WARNING = 0.1 # 0.1% = warning
SPREAD_THRESHOLD_CRITICAL = 0.5 # 0.5% = critical
REQUEST_TIMEOUT = 10 # วินาที
# tardis_client.py - เวอร์ชันที่แก้ไขปัญหาแล้ว
import aiohttp
import asyncio
from typing import Optional, Dict, List
import json
from config import TARDIS_API_KEY, TARDIS_BASE_URL, REQUEST_TIMEOUT
class TardisClient:
def __init__(self):
self.base_url = TARDIS_BASE_URL
self.headers = {
"Authorization": f"Bearer {TARDIS_API_KEY}",
"Content-Type": "application/json"
}
self.session: Optional[aiohttp.ClientSession] = None
self.max_retries = 3
self.retry_delay = 5 # วินาที
async def __aenter__(self):
timeout = aiohttp.ClientTimeout(total=REQUEST_TIMEOUT)
self.session = aiohttp.ClientSession(
headers=self.headers,
timeout=timeout
)
return self
async def __aexit__(self, *args):
if self.session:
await self.session.close()
async def _make_request(
self,
method: str,
endpoint: str,
params: Optional[Dict] = None
) -> Dict:
"""HTTP request with retry logic และ circuit breaker pattern"""
url = f"{self.base_url}{endpoint}"
last_error = None
for attempt in range(self.max_retries):
try:
async with self.session.request(
method, url, params=params
) as response:
if response.status == 200:
return await response.json()
elif response.status == 401:
raise ConnectionError(
"401 Unauthorized: ตรวจสอบ TARDIS_API_KEY ของคุณ"
)
elif response.status == 429:
# Rate limited - รอแล้ว retry
wait_time = int(response.headers.get(
"Retry-After", self.retry_delay * (attempt + 1)
))
print(f"⚠️ Rate limited. รอ {wait_time} วินาที...")
await asyncio.sleep(wait_time)
else:
raise ConnectionError(
f"Tardis API Error: {response.status}"
)
except aiohttp.ClientError as e:
last_error = e
if attempt < self.max_retries - 1:
await asyncio.sleep(self.retry_delay * (attempt + 1))
print(f"🔄 Retry attempt {attempt + 1}/{self.max_retries}")
raise ConnectionError(
f"เชื่อมต่อ Tardis API ล้มเหลวหลัง {self.max_retries} ครั้ง: {last_error}"
)
async def get_orderbook_snapshot(
self,
exchange: str,
symbol: str
) -> Dict:
"""ดึงข้อมูล order book ณ จุดเวลาปัจจุบัน"""
return await self._make_request(
"GET",
f"/orderbooks/{exchange}/{symbol}",
params={"type": "snapshot"}
)
async def get_historical_orderbook(
self,
exchange: str,
symbol: str,
from_ts: int,
to_ts: int
) -> List[Dict]:
"""ดึงข้อมูล order book ในอดีต"""
return await self._make_request(
"GET",
f"/orderbooks/{exchange}/{symbol}",
params={
"from": from_ts,
"to": to_ts,
"format": "object"
}
)
การคำนวณและวิเคราะห์ Bid-Ask Spread
# spread_analyzer.py
import pandas as pd
import numpy as np
from dataclasses import dataclass
from typing import List, Optional
from datetime import datetime
@dataclass
class SpreadMetrics:
timestamp: datetime
symbol: str
bid_price: float
ask_price: float
mid_price: float
spread_bps: float # Basis points
spread_pct: float # เปอร์เซ็นต์
orderbook_depth: float # ความลึกของ orderbook
liquidity_score: float # 0-100
class SpreadAnalyzer:
"""วิเคราะห์ความผิดปกติของ Bid-Ask Spread"""
def __init__(self, warning_threshold: float = 0.1,
critical_threshold: float = 0.5):
self.warning_threshold = warning_threshold
self.critical_threshold = critical_threshold
self.history: List[SpreadMetrics] = []
def calculate_spread(self, orderbook: Dict) -> Optional[SpreadMetrics]:
"""คำนวณ spread metrics จาก orderbook data"""
try:
# Tardis format: bids และ asks เป็น list [price, amount]
bids = orderbook.get('bids', [])
asks = orderbook.get('asks', [])
if not bids or not asks:
return None
# Best bid/ask
bid_price = float(bids[0][0])
ask_price = float(asks[0][0])
bid_amount = float(bids[0][1])
ask_amount = float(asks[0][1])
# Mid price และ Spread
mid_price = (bid_price + ask_price) / 2
spread_pct = (ask_price - bid_price) / mid_price * 100
spread_bps = spread_pct * 100 # แปลงเป็น basis points
# Orderbook depth (10 levels รวม)
depth_bid = sum(float(b[0]) * float(b[1])
for b in bids[:10])
depth_ask = sum(float(a[0]) * float(a[1])
for a in asks[:10])
depth = (depth_bid + depth_ask) / 2
# Liquidity Score (ยิ่งสูงยิ่งดี)
# คำนึงถึงทั้ง spread และ depth
spread_score = max(0, 100 - spread_bps * 500)
depth_score = min(100, np.log10(depth + 1) * 10)
liquidity_score = (spread_score * 0.6 + depth_score * 0.4)
return SpreadMetrics(
timestamp=datetime.now(),
symbol=orderbook.get('symbol', 'UNKNOWN'),
bid_price=bid_price,
ask_price=ask_price,
mid_price=mid_price,
spread_bps=spread_bps,
spread_pct=spread_pct,
orderbook_depth=depth,
liquidity_score=liquidity_score
)
except (IndexError, ValueError, KeyError) as e:
print(f"❌ วิเคราะห์ orderbook ล้มเหลว: {e}")
return None
def detect_anomaly(self, metrics: SpreadMetrics) -> str:
"""ตรวจจับความผิดปกติของ spread"""
if metrics.spread_pct >= self.critical_threshold:
return "CRITICAL"
elif metrics.spread_pct >= self.warning_threshold:
return "WARNING"
return "NORMAL"
def analyze_trend(self, lookback_minutes: int = 60) -> Dict:
"""วิเคราะห์แนวโน้ม spread ในช่วงเวลาที่ผ่านมา"""
cutoff = datetime.now().timestamp() - (lookback_minutes * 60)
recent = [m for m in self.history
if m.timestamp.timestamp() > cutoff]
if len(recent) < 2:
return {"status": "INSUFFICIENT_DATA"}
spreads = [m.spread_pct for m in recent]
return {
"status": "OK",
"samples": len(recent),
"avg_spread": np.mean(spreads),
"max_spread": np.max(spreads),
"min_spread": np.min(spreads),
"std_spread": np.std(spreads),
"trend": "WIDENING" if spreads[-1] > spreads[0] else "NARROWING"
}
ตัวอย่างการใช้งาน
analyzer = SpreadAnalyzer(warning_threshold=0.1, critical_threshold=0.5)
ข้อมูล orderbook ตัวอย่าง (จาก Tardis)
sample_orderbook = {
"symbol": "BTC-USDT",
"bids": [["64400.5", "2.5"], ["64399.0", "1.8"]],
"asks": [["64405.2", "3.2"], ["64406.0", "1.5"]]
}
metrics = analyzer.calculate_spread(sample_orderbook)
if metrics:
print(f"BTC-USDT Spread: {metrics.spread_pct:.4f}%")
print(f"สถานะ: {analyzer.detect_anomaly(metrics)}")
print(f"Liquidity Score: {metrics.liquidity_score:.1f}/100")
ระบบเตือนภัยแบบ Real-time
# alert_manager.py
import asyncio
import aiohttp
from typing import List, Callable
from dataclasses import dataclass
from datetime import datetime
from config import HOLYSHEEP_API_KEY, HOLYSHEEP_BASE_URL
@dataclass
class Alert:
level: str # WARNING, CRITICAL, INFO
symbol: str
message: str
spread_value: float
timestamp: datetime
class AlertManager:
"""จัดการการแจ้งเตือนวิกฤตสภาพคล่อง"""
def __init__(self):
self.alerts: List[Alert] = []
self.handlers: List[Callable] = []
self.cooldown: dict = {} # ป้องกันแจ้งเตือนซ้ำ
def add_handler(self, handler: Callable):
"""เพิ่ม handler สำหรับส่งการแจ้งเตือน"""
self.handlers.append(handler)
async def send_ai_analysis(self, spread_data: dict) -> str:
"""
ใช้ HolySheep AI วิเคราะห์สถานการณ์
ราคาถูกมาก: $0.42/MTok สำหรับ DeepSeek V3.2
"""
if not HOLYSHEEP_API_KEY:
return "AI analysis skipped (no API key)"
prompt = f"""
วิเคราะห์สถานการณ์สภาพคล่องคริปโต:
Symbol: {spread_data.get('symbol')}
Spread: {spread_data.get('spread_pct'):.4f}%
Spread (bps): {spread_data.get('spread_bps'):.2f}
Liquidity Score: {spread_data.get('liquidity_score'):.1f}/100
Depth: ${spread_data.get('orderbook_depth', 0):,.2f}
คำแนะนำ:
1. ระดับความเสี่ยง (1-10)
2. สาเหตุที่เป็นไปได้
3. คำแนะนำเชิงกลยุทธ์
"""
async with aiohttp.ClientSession() as session:
try:
async with session.post(
f"{HOLYSHEEP_BASE_URL}/chat/completions",
headers={
"Authorization": f"Bearer {HOLYSHEEP_API_KEY}",
"Content-Type": "application/json"
},
json={
"model": "deepseek-v3.2",
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 500
},
timeout=aiohttp.ClientTimeout(total=5)
) as response:
if response.status == 200:
result = await response.json()
return result['choices'][0]['message']['content']
else:
return f"AI API Error: {response.status}"
except Exception as e:
return f"AI analysis failed: {e}"
async def trigger_alert(self, metrics, analyzer):
"""ส่งการแจ้งเตือนเมื่อตรวจพบความผิดปกติ"""
level = analyzer.detect_anomaly(metrics)
# Cooldown: อย่าแจ้งซ้ำภายใน 5 นาที
cooldown_key = f"{metrics.symbol}_{level}"
if self.cooldown.get(cooldown_key):
return
alert = Alert(
level=level,
symbol=metrics.symbol,
message=f"Spread {metrics.spread_pct:.4f}% สูงผิดปกติ",
spread_value=metrics.spread_pct,
timestamp=metrics.timestamp
)
self.alerts.append(alert)
self.cooldown[cooldown_key] = True
# ล้าง cooldown หลัง 5 นาที
asyncio.create_task(self._clear_cooldown(cooldown_key, 300))
# ขอ AI วิเคราะห์
ai_analysis = await self.send_ai_analysis({
'symbol': metrics.symbol,
'spread_pct': metrics.spread_pct,
'spread_bps': metrics.spread_bps,
'liquidity_score': metrics.liquidity_score,
'orderbook_depth': metrics.orderbook_depth
})
# เรียก handlers ทั้งหมด
for handler in self.handlers:
await handler(alert, ai_analysis)
async def _clear_cooldown(self, key: str, delay: int):
await asyncio.sleep(delay)
self.cooldown.pop(key, None)
ตัวอย่าง Handler
async def console_alert_handler(alert: Alert, ai_analysis: str):
emoji = {"WARNING": "⚠️", "CRITICAL": "🚨", "INFO": "ℹ️"}
print(f"\n{emoji.get(alert.level, '📢')} {alert.level} - {alert.symbol}")
print(f" ข้อความ: {alert.message}")
print(f" เวลา: {alert.timestamp.strftime('%H:%M:%S')}")
if ai_analysis and "AI analysis" not in ai_analysis:
print(f" 📊 AI วิเคราะห์:\n {ai_analysis}")
print("-" * 50)
Main Loop: รวมทุกอย่างเข้าด้วยกัน
# main.py
import asyncio
import signal
from datetime import datetime, timedelta
from tardis_client import TardisClient
from spread_analyzer import SpreadAnalyzer
from alert_manager import AlertManager
from config import SPREAD_THRESHOLD_WARNING, SPREAD_THRESHOLD_CRITICAL
Monitoring targets
SYMBOLS = [
("binance", "BTC-USDT"),
("binance", "ETH-USDT"),
("okx", "BTC-USDT"),
("bybit", "BTC-USDT"),
]
class LiquidityMonitor:
def __init__(self, interval: int = 30):
self.interval = interval
self.running = False
self.analyzer = SpreadAnalyzer(
warning_threshold=SPREAD_THRESHOLD_WARNING,
critical_threshold=SPREAD_THRESHOLD_CRITICAL
)
self.alert_manager = AlertManager()
async def start(self):
"""เริ่มระบบมอนิเตอร์"""
self.running = True
# เพิ่ม alert handler
self.alert_manager.add_handler(console_alert_handler)
print("🚀 เริ่มระบบมอนิเตอร์สภาพคล่อง...")
print(f"⏱️ Interval: {self.interval} วินาที")
print(f"⚠️ Warning threshold: {SPREAD_THRESHOLD_WARNING}%")
print(f"🚨 Critical threshold: {SPREAD_THRESHOLD_CRITICAL}%\n")
while self.running:
await self._check_all_symbols()
await asyncio.sleep(self.interval)
async def _check_all_symbols(self):
"""ตรวจสอบทุก symbols"""
async with TardisClient() as client:
for exchange, symbol in SYMBOLS:
try:
print(f"[{datetime.now().strftime('%H:%M:%S')}] "
f"ตรวจสอบ {exchange}/{symbol}...", end=" ")
orderbook = await client.get_orderbook_snapshot(
exchange, symbol
)
metrics = self.analyzer.calculate_spread(orderbook)
if metrics:
self.analyzer.history.append(metrics)
# แสดงผล
status = self.analyzer.detect_anomaly(metrics)
status_emoji = {"NORMAL": "✅", "WARNING": "⚠️",
"CRITICAL": "🚨"}.get(status, "❓")
print(f"{status_emoji} {metrics.spread_pct:.4f}% "
f"(Score: {metrics.liquidity_score:.0f})")
# ส่ง alert ถ้าผิดปกติ
if status != "NORMAL":
await self.alert_manager.trigger_alert(
metrics, self.analyzer
)
else:
print("❌ ดึงข้อมูลไม่ได้")
except ConnectionError as e:
print(f"❌ {e}")
except Exception as e:
print(f"❌ ข้อผิดพลาด: {e}")
def stop(self):
"""หยุดระบบมอนิเตอร์"""
print("\n🛑 หยุดระบบมอนิเตอร์...")
self.running = False
# แสดงสรุป
print(f"\n📊 สรุปการทำงาน:")
print(f" ข้อมูลที่เก็บ: {len(self.analyzer.history)} records")
print(f" การแจ้งเตือน: {len(self.alert_manager.alerts)} ครั้ง")
async def main():
monitor = LiquidityMonitor(interval=30)
# Graceful shutdown
loop = asyncio.get_event_loop()
for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(sig, monitor.stop)
try:
await monitor.start()
except KeyboardInterrupt:
monitor.stop()
if __name__ == "__main__":
asyncio.run(main())
ข้อผิดพลาดที่พบบ่อยและวิธีแก้ไข
กรณีที่ 1: 401 Unauthorized - API Key ไม่ถูกต้อง
อาการ: ได้รับ error 401 Unauthorized ทันทีที่เรียก API
# ❌ สาเหตุ: ไม่ได้ load .env ก่อนใช้งาน
หรือ API key หมดอายุ
✅ วิธีแก้ไข:
1. ตรวจสอบว่า .env อยู่ในโฟลเดอร์เดียวกับ main.py
2. ตรวจสอบ API key ใน .env
.env
TARDIS_API_KEY=your_tardis_key_here
HOLYSHEEP_API_KEY=your_holysheep_key_here
3. ทดสอบการเชื่อมต่อ
import os
from dotenv import load_dotenv
load_dotenv()
tardis_key = os.getenv("TARDIS_API_KEY")
if not tardis_key:
raise ValueError("❌ ไม่พบ TARDIS_API_KEY ใน .env")
ตรวจสอบ format key (Tardis มักเริ่มต้นด้วย "trd_")
if not tardis_key.startswith("trd_"):
print("⚠️ Warning: Tardis API key อาจไม่ถูกต้อง")
กราจสที่ 2: ConnectionTimeout - API ไม่ตอบสนอง
อาการ: โค้ดรอนานแล้วขึ้น asyncio.exceptions.TimeoutError หรือ ClientTimeout
# ❌ สาเหตุ: ไม่มี timeout setting หรือ timeout น้อยเกินไป
ขณะตลาดผันผวน API มักตอบช้า
✅ วิธีแก้ไข:
async def safe_api_call():
timeout = aiohttp.ClientTimeout(
total=30, # Timeout รวม 30 วินาที
connect=10, # Timeout เชื่อมต่อ 10 วินาที
sock_read=20 # Timeout อ่านข้อมูล 20 วินาที
)
connector = aiohttp.TCPConnector(
limit=10, # จำกัด connections
limit_per_host=5 # ต่อ host
)
async with aiohttp.ClientSession(
timeout=timeout,
connector=connector
) as session:
# พร้อม retry logic
for i in range(3):
try:
async with session.get(url) as response:
return await response.json()
except asyncio.TimeoutError:
print(f"⏰ Timeout ครั้งที่ {i+1}")
if i < 2:
await asyncio.sleep(2 ** i) # Exponential backoff
continue
raise ConnectionError("ดึงข้อมูลล้มเหลวหลัง 3 ครั้ง")
กรณีที่ 3: Data Format Error - Orderbook structure ไม่ตรงตามคาด
อาการ: IndexError: list index out of range หรือ ValueError ตอนคำนวณ spread
# ❌ สาเหตุ: Orderbook ว่างเปล่าหรือ format ผิด
เกิดได้เมื่อ exchange ยังไม่เปิด trading
✅ วิธีแก้ไข:
def safe_calculate_spread(orderbook: dict) -> Optional[SpreadMetrics]:
try:
bids = orderbook.get('bids', [])
asks = orderbook.get('asks', [])
# ตรวจสอบข้อมูลก่อนเข้าถึง
if not bids or not asks:
print("⚠️ Orderbook ว่างเปล่า")
return None
if not isinstance(bids[0], (list, tuple)) or len(bids[0]) < 2:
print(f"⚠️ Format ของ bids ผิด: {bids[0]}")
return None
# ดึงค่าอย่างปลอดภัย
def safe_float(value, default=0.0):
try:
return float(value)
except (TypeError, ValueError):
return default
bid_price = safe_float(bids[0][0])
ask_price = safe_float(asks[0][0])
if bid_price == 0 or ask_price == 0:
print("⚠️ ราคาเป็น 0 - ข้อมูลอาจผิดพลาด")
return None
# คำนวณต่อ...
except Exception as e:
print(f"❌ Unexpected error: {type(e).__name__}: {e}")
return None
ตรวจสอบ format ที่ Tardis ส่งมาจริง ๆ
print("Tardis Orderbook Keys:", orderbook.keys())
print("Sample data:", {k: orderbook.get(k)[:1] if isinstance(orderbook.get(k), list) else orderbook.get(k)} for k in orderbook)
กรณีที่ 4: Rate Limit - เรียก API บ่อยเกินไป
อาการ: ได้รับ 429 Too Many Requests หรือ API ตอบ 403 Forbidden
# ✅ วิธีแก้ไข:
import asyncio
class RateLimitedClient:
def __init__(self, calls