สวัสดีครับนักพัฒนาทุกคน วันนี้ผมจะมาเล่าประสบการณ์จริงในการพัฒนา Trading Bot ที่ใช้ข้อมูลจากทั้ง Binance Spot Market และ Futures Market ซึ่งเป็นปัญหาที่หลายคนเจอแต่ไม่ค่อยมีคนเขียนบทความสอนอย่างละเอียด บทความนี้จะเป็นคู่มือครบถ้วนสำหรับการจัดการความแตกต่างของข้อมูลระหว่างสองตลาดนี้ ตั้งแต่พื้นฐานจนถึงเทคนิคขั้นสูง พร้อมโค้ดตัวอย่างที่รันได้จริงและวิธีแก้ปัญหาที่พบบ่อย
ทำไมข้อมูล Spot กับ Futures ถึงต่างกัน
ก่อนจะไปดูวิธีแก้ เราต้องเข้าใจก่อนว่าทำไมข้อมูลจากสองตลาดนี้ถึงไม่เหมือนกัน จากประสบการณ์ของผมที่พัฒนา Arbitrage Bot มาเกือบ 2 ปี พบว่าความแตกต่างหลักมาจากหลายส่วน
โครงสร้างของ Futures Contract แตกต่างจาก Spot ตรงที่ Futures มีวันหมดอายุของสัญญา มี Funding Rate ที่ต้องจ่ายทุก 8 ชั่วโมง และมีระบบ Mark Price ที่ใช้ในการคำนวณ Unrealized PnL ซึ่ง Mark Price นี้อาจต่างจากราคา Spot อยู่เสมอ นอกจากนี้ Futures ยังมีระบบ Leverage ที่ทำให้ข้อมูล Balance แสดงเป็น Margin ไม่ใช่จำนวนเหรียญจริง
อีกเรื่องที่สำคัญคือ ความล่าช้าของข้อมูล เพราะ Spot ใช้ Last Price ของการซื้อขายจริง แต่ Futures มีทั้ง Last Price และ Mark Price ที่คำนวณจาก Spot Index บวกลบ Basis ซึ่งทำให้ราคาที่ได้ไม่ตรงกันเป๊ะ ๆ ตลอดเวลา
ความแตกต่างหลักที่ต้องจัดการ
จากการทดสอบและใช้งานจริง ผมสรุปความแตกต่างที่สำคัญที่สุด 5 ข้อ ที่หากไม่จัดการให้ดีจะทำให้ Bot ของคุณทำงานผิดพลาดได้
1. ความแตกต่างด้าน Timestamp
ทั้ง Spot และ Futures API จะส่งค่า timestamp กลับมาเป็น Unix Time ในหน่วยมิลลิวินาที แต่ปัญหาคือ Server ของทั้งสองตลาดอาจ Sync เวลาไม่ตรงกัน 100% ดังนั้นเวลาดึงข้อมูลที่เกิดขึ้นจริงเวลาเดียวกัน คุณอาจได้ timestamp ที่ต่างกันไม่กี่มิลลิวินาที ซึ่งถ้าคุณเขียน Logic ผิด อาจทำให้การจับคู่ Order ผิดพลาด
import requests
import time
from datetime import datetime
class BinanceTimestampSyncer:
"""คลาสสำหรับ Sync timestamp ระหว่าง Spot และ Futures API"""
def __init__(self):
self.spot_base = "https://api.binance.com"
self.futures_base = "https://fapi.binance.com"
self.offset = 0 # ค่า Offset สำหรับปรับ timestamp
def get_spot_time(self):
"""ดึงเวลาจาก Spot Server"""
response = requests.get(f"{self.spot_base}/api/v3/time")
return response.json()["serverTime"]
def get_futures_time(self):
"""ดึงเวลาจาก Futures Server"""
response = requests.get(f"{self.futures_base}/fapi/v1/time")
return response.json()["serverTime"]
def sync_offset(self):
"""คำนวณ Offset ระหว่าง Spot และ Futures"""
local_time = int(time.time() * 1000)
spot_time = self.get_spot_time()
futures_time = self.get_futures_time()
# คำนวณ offset โดยเฉลี่ยจากค่าทั้งสอง
avg_server_time = (spot_time + futures_time) // 2
self.offset = avg_server_time - local_time
print(f"Spot Time: {datetime.fromtimestamp(spot_time/1000)}")
print(f"Futures Time: {datetime.fromtimestamp(futures_time/1000)}")
print(f"Offset: {self.offset} ms")
return self.offset
def get_unified_timestamp(self, use_futures=True):
"""ส่งคืน timestamp ที่ Sync แล้ว"""
base_time = self.get_futures_time() if use_futures else self.get_spot_time()
return base_time
วิธีใช้งาน
syncer = BinanceTimestampSyncer()
syncer.sync_offset()
2. ความแตกต่างของ Price Format
อีกเรื่องที่ผมเจอบ่อยมากคือ Format ของราคา Spot กับ Futures อาจแสดงทศนิยมไม่เท่ากัน บางคู่เทรด Spot มี 8 ตำแหน่งทศนิยม แต่ Futures มีแค่ 6 ตำแหน่ง หรือบางทีก็ตรงกันข้าม ถ้าคุณเขียนโค้ดเปรียบเทียบราคาโดยตรง อาจเกิด Error จากการปัดเศษได้
import decimal
from typing import Dict, Tuple
class PriceNormalizer:
"""คลาสสำหรับ Normalize ราคาจาก Spot และ Futures ให้เป็น Format เดียวกัน"""
# กำหนดจำนวนทศนิยมมาตรฐานสำหรับแต่ละคู่เทรด
SPOT_PRECISION = {
"BTCUSDT": 8,
"ETHUSDT": 8,
"BNBUSDT": 8,
"SOLUSDT": 6,
}
FUTURES_PRECISION = {
"BTCUSDT": 6,
"ETHUSDT": 6,
"BNBUSDT": 5,
"SOLUSDT": 5,
}
# กำหนดขั้นต่ำของราคา (Price Filter)
SPOT_TICK_SIZE = {
"BTCUSDT": 0.01,
"ETHUSDT": 0.01,
"BNBUSDT": 0.01,
"SOLUSDT": 0.01,
}
def __init__(self):
self.unified_precision = 8 # ใช้ 8 ตำแหน่งเป็นมาตรฐาน
def normalize_price(self, symbol: str, price: float, source: str = "spot") -> decimal.Decimal:
"""
Normalize ราคาให้เป็น Format มาตรฐาน
Args:
symbol: ชื่อคู่เทรด เช่น BTCUSDT
price: ราคาที่ต้องการ Normalize
source: 'spot' หรือ 'futures'
Returns:
Decimal ที่ Normalize แล้ว
"""
precision = (self.SPOT_PRECISION if source == "spot" else self.FUTURES_PRECISION).get(
symbol, self.unified_precision
)
# ใช้ Decimal เพื่อความแม่นยำ
d_price = decimal.Decimal(str(price))
d_price = d_price.quantize(
decimal.Decimal(10) ** -precision,
rounding=decimal.ROUND_HALF_UP
)
# ปรับให้เป็น unified precision
d_price = d_price.quantize(
decimal.Decimal(10) ** -self.unified_precision,
rounding=decimal.ROUND_HALF_UP
)
return d_price
def normalize_quantity(self, symbol: str, quantity: float, source: str = "spot") -> decimal.Decimal:
"""
Normalize จำนวนเหรียญให้เป็น Format มาตรฐาน
Args:
symbol: ชื่อคู่เทรด
quantity: จำนวนที่ต้องการ Normalize
source: 'spot' หรือ 'futures'
Returns:
Decimal ที่ Normalize แล้ว
"""
# กำหนด minimum quantity สำหรับแต่ละเหรียญ
min_qty = 0.00001 if symbol.startswith("BTC") else 0.0001
d_qty = decimal.Decimal(str(quantity))
d_qty = d_qty.quantize(
decimal.Decimal(str(min_qty)),
rounding=decimal.ROUND_DOWN
)
return d_qty
def compare_prices(self, spot_price: float, futures_price: float, symbol: str, threshold: float = 0.001) -> Tuple[bool, float]:
"""
เปรียบเทียบราคา Spot กับ Futures โดยคำนึงถึงความแตกต่าง
Args:
spot_price: ราคา Spot
futures_price: ราคา Futures
symbol: ชื่อคู่เทรด
threshold: ค่า Threshold ของความต่างที่ยอมรับได้ (default 0.1%)
Returns:
(is_valid, diff_percent)
"""
norm_spot = self.normalize_price(symbol, spot_price, "spot")
norm_futures = self.normalize_price(symbol, futures_price, "futures")
if norm_spot == 0:
return False, 0
diff = abs(norm_futures - norm_spot) / norm_spot
is_valid = diff <= threshold
return is_valid, float(diff * 100)
วิธีใช้งาน
normalizer = PriceNormalizer()
Normalize ราคา
spot_price = normalizer.normalize_price("BTCUSDT", 67234.567890123, "spot")
futures_price = normalizer.normalize_price("BTCUSDT", 67234.56, "futures")
print(f"Normalized Spot: {spot_price}")
print(f"Normalized Futures: {futures_price}")
เปรียบเทียบราคา
is_valid, diff = normalizer.compare_prices(67234.567890123, 67234.56, "BTCUSDT", threshold=0.001)
print(f"Valid difference: {is_valid}, Diff: {diff:.4f}%")
3. ความแตกต่างของ Endpoint และ Response Structure
ปัญหาที่ผมเจอบ่อยที่สุดคือ Response จาก Spot API และ Futures API มี Structure ไม่เหมือนกัน ตัวอย่างเช่น Spot ใช้ symbol เป็นตัวพิมพ์ใหญ่ แต่ Futures อาจใช้ตัวพิมพ์เล็ก หรือ Field บางตัวมีอยู่ใน Spot แต่ไม่มีใน Futures
โครงสร้างข้อมูล Unified Data Layer
จากประสบการณ์ที่พัฒนาระบบที่ต้องใช้ข้อมูลจากทั้งสองตลาด ผมแนะนำให้สร้าง Unified Data Layer ที่ทำหน้าที่ดึงข้อมูลจากทั้งสองที่แล้ว Transform ให้เป็น Format เดียวกัน
import requests
from typing import Optional, Dict, Any
from dataclasses import dataclass, field
from datetime import datetime
import decimal
@dataclass
class UnifiedTrade:
"""โครงสร้างข้อมูล Trade ที่รวมกันแล้ว"""
symbol: str
price: decimal.Decimal
quantity: decimal.Decimal
quote_quantity: decimal.Decimal
timestamp: int
is_buyer_maker: bool
is_best_match: bool
source: str # 'spot' หรือ 'futures'
market_type: str # 'spot' หรือ 'futures'
# Field เฉพาะ Futures
mark_price: Optional[decimal.Decimal] = None
index_price: Optional[decimal.Decimal] = None
funding_rate: Optional[decimal.Decimal] = None
def to_dict(self) -> Dict[str, Any]:
"""แปลงเป็น Dictionary สำหรับเก็บลง Database"""
return {
"symbol": self.symbol.upper(),
"price": float(self.price),
"quantity": float(self.quantity),
"quote_quantity": float(self.quote_quantity),
"timestamp": self.timestamp,
"datetime": datetime.fromtimestamp(self.timestamp / 1000).isoformat(),
"is_buyer_maker": self.is_buyer_maker,
"is_best_match": self.is_best_match,
"source": self.source,
"market_type": self.market_type,
"mark_price": float(self.mark_price) if self.mark_price else None,
"index_price": float(self.index_price) if self.index_price else None,
"funding_rate": float(self.funding_rate) if self.funding_rate else None,
}
class BinanceDataUnifier:
"""คลาสสำหรับ Unify ข้อมูลจาก Spot และ Futures"""
SPOT_BASE = "https://api.binance.com"
FUTURES_BASE = "https://fapi.binance.com"
def __init__(self, api_key: str = None, secret_key: str = None):
self.api_key = api_key
self.secret_key = secret_key
self.spot_session = requests.Session()
self.futures_session = requests.Session()
if api_key:
self.spot_session.headers.update({"X-MBX-APIKEY": api_key})
self.futures_session.headers.update({"X-MBX-APIKEY": api_key})
def _normalize_symbol(self, symbol: str, market: str) -> str:
"""Normalize ชื่อ Symbol ให้เป็น Format มาตรฐาน"""
symbol = symbol.upper().strip()
# Spot ใช้ format เช่น BTCUSDT
# Futures ใช้ format เช่น BTCUSDT (เหมือนกัน)
# แต่ถ้าเป็น COIN-M Futures จะใช้ BTCUSD_PERP
if market == "futures" and "_" not in symbol and "PERP" not in symbol:
# USDT-M Futures (default)
pass
elif market == "spot":
# ถ้าเป็น Spot ต้องตรวจสอบว่า Symbol ถูกต้อง
pass
return symbol
def get_spot_trades(self, symbol: str, limit: int = 100) -> list:
"""ดึงข้อมูล Trade จาก Spot"""
symbol = self._normalize_symbol(symbol, "spot")
endpoint = f"{self.SPOT_BASE}/api/v3/myTrades"
params = {"symbol": symbol, "limit": limit}
response = self.spot_session.get(endpoint, params=params)
response.raise_for_status()
trades = []
for trade_data in response.json():
trade = UnifiedTrade(
symbol=symbol,
price=decimal.Decimal(str(trade_data["price"])),
quantity=decimal.Decimal(str(trade_data["qty"])),
quote_quantity=decimal.Decimal(str(trade_data["quoteQty"])),
timestamp=trade_data["time"],
is_buyer_maker=trade_data["isBuyerMaker"],
is_best_match=trade_data["isBestMatch"],
source="binance_spot",
market_type="spot",
)
trades.append(trade)
return trades
def get_futures_trades(self, symbol: str, limit: int = 100) -> list:
"""ดึงข้อมูล Trade จาก Futures"""
symbol = self._normalize_symbol(symbol, "futures")
endpoint = f"{self.FUTURES_BASE}/fapi/v1/userTrades"
params = {"symbol": symbol, "limit": limit}
response = self.futures_session.get(endpoint, params=params)
response.raise_for_status()
trades = []
for trade_data in response.json():
trade = UnifiedTrade(
symbol=symbol,
price=decimal.Decimal(str(trade_data["price"])),
quantity=decimal.Decimal(str(trade_data["qty"])),
quote_quantity=decimal.Decimal(str(trade_data["quoteQty"])),
timestamp=trade_data["time"],
is_buyer_maker=trade_data["isBuyerMaker"],
is_best_match=True, # Futures ไม่มี field นี้
source="binance_futures",
market_type="futures",
mark_price=decimal.Decimal(str(trade_data.get("markPrice", 0))),
index_price=decimal.Decimal(str(trade_data.get("indexPrice", 0))),
)
trades.append(trade)
return trades
def get_combined_trades(self, symbol: str, limit: int = 100) -> Dict[str, list]:
"""ดึงข้อมูล Trade จากทั้ง Spot และ Futures"""
spot_trades = self.get_spot_trades(symbol, limit)
futures_trades = self.get_futures_trades(symbol, limit)
return {
"spot": [t.to_dict() for t in spot_trades],
"futures": [t.to_dict() for t in futures_trades],
"all": [t.to_dict() for t in spot_trades + futures_trades]
}
วิธีใช้งาน
unifier = BinanceDataUnifier()
ดึงข้อมูล Trade จากทั้งสองตลาด
try:
combined = unifier.get_combined_trades("BTCUSDT", limit=50)
print(f"Spot trades: {len(combined['spot'])}")
print(f"Futures trades: {len(combined['futures'])}")
# ตัวอย่างข้อมูลที่ Unify แล้ว
if combined['spot']:
print("Sample Spot Trade:", combined['spot'][0])
if combined['futures']:
print("Sample Futures Trade:", combined['futures'][0])
except Exception as e:
print(f"Error: {e}")
กลยุทธ์การ Sync ข้อมูลระหว่าง Spot และ Futures
หัวใจสำคัญของระบบที่ใช้ข้อมูลจากทั้งสองตลาดคือการ Sync ข้อมูลให้ตรงกัน จากประสบการณ์ของผม มี 3 วิธีหลักที่ใช้กัน
วิธีที่ 1: Timestamp-Based Sync
วิธีนี้ใช้ Timestamp เป็นตัวจับคู่ข้อมูล ข้อดีคือเร็วและเบา� แต่ข้อเสียคือถ้า Timestamp ไม่ตรงกันเป๊ะ ๆ อาจจับคู่ผิดได้ ผมแนะนำให้ใช้ Time Window ในการจับคู่
วิธีที่ 2: Order ID-Based Sync
วิธีนี้ใช้ Order ID เป็นตัวจับคู่ ซึ่งแม่นยำกว่า แต่ต้องมี Order ID จากทั้งสองตลาด ซึ่งบางทีถ้าเป็น Order ที่สร้างจากภายนอก อาจไม่มี ID ตรงกัน
วิธีที่ 3: Price-Based Sync
วิธีนี้ใช้ราคาและปริมาณเป็นตัวจับคู่ เหมาะสำหรับกรณีที่เป็น Arbitrage ที่มีการสร้าง Order ทั้งสองฝั่งพร้อมกัน ผมใช้วิธีนี้เป็นหลัก
ข้อผิดพลาดที่พบบ่อยและวิธีแก้ไข
จากการพัฒนาและใช้งานจริง ผมรวบรวมข้อผิดพลาดที่พบบ่อยที่สุด 5 ข้อพร้อมวิธีแก้ไข
ข้อผิดพลาดที่ 1: 403 Forbidden Error
สาเหตุ: API Key ไม่มีสิทธิ์เข้าถึง Futures API หรือ IP ถูก Block
# วิธีแก้ไข
1. ตรวจสอบว่า API Key มีสิทธิ์ Futures
2. เพิ่ม IP ที่อนุญาตใน Binance Dashboard
import requests
def check_api_permissions(api_key: str, secret_key: str):
"""ตรวจสอบสิทธิ์ของ API Key"""
# ทดสอบ Spot API
spot_headers = {"X-MBX-APIKEY": api_key}
try:
response = requests.get(
"https://api.binance.com/api/v3/account",
headers=spot_headers
)
print(f"Spot Status: {response.status_code}")
if response.status_code == 200:
print("Spot: OK")
else:
print(f"Spot Error: {response.json()}")
except Exception as e:
print(f"Spot Exception: {e}")
# ทดสอบ Futures API
try:
response = requests.get(
"https://fapi.binance.com/fapi/v1/account",
headers=spot_headers
)
print(f"Futures Status: {response.status_code}")
if response.status_code == 200:
print("Futures: OK")
else:
print(f"Futures Error: {response.json()}")
# ถ้าเป็น 403 หมายความว่า API Key ไม่มีสิทธิ์ Futures
if response.status_code == 403:
print("⚠️ API Key ไม่มีสิทธิ์เข้าถึง Futures")
print("กรุณาไปที่ Binance > API Management > สร้าง API Key ใหม่แล้วเลือก Enable Futures")
except Exception as e:
print(f"Futures Exception: {e}")
ใช้งาน
check_api_permissions("YOUR_API_KEY", "YOUR_SECRET_KEY")
ข้อผิดพลาดที่ 2: ข้อมูล Balance ไม่ตรงกัน
สาเหตุ: Spot Balance แสดงเป็นจำนวนเหรียญจริง แต่ Futures Balance แสดงเป็น USDT Margin ซึ่งเป็นคนละ Concept กัน
# วิธีแก้ไข: แยก Logic สำหรับ Balance แต่ละตลาด
from dataclasses import dataclass
from decimal import Decimal
@dataclass
class UnifiedBalance:
"""โครงสร้างข้อมูล Balance ที่รวมกันแล้ว"""
asset: str
spot_free: Decimal = Decimal("0")
spot_locked: Decimal = Decimal("0")
futures_wallet: Decimal = Decimal("0") # Futures Wallet Balance
futures_margin: Decimal = Decimal("0") # Used Margin
futures_unrealized_pnl: Decimal = Decimal("0") # Unrealized PnL
@property
def total_asset_value(self) -> Decimal:
"""คำนวณมูลค่ารวมทั้งหมดเป็น USDT"""
# สำหรับ USDT จะเท่ากับ Spot + Futures Wallet + Unrealized PnL