บทความนี้สอนวิธีดึงข้อมูลตลาดคริปโตแบบ High-Frequency จาก Tardis.dev เข้าสู่ Python Pandas เพื่อใช้สร้างระบบ Backtesting สำหรับ Quantitative Trading โดยเน้นการประมวลผล Order Book, Trade Data และ Candlestick อย่างมีประสิทธิภาพ เหมาะสำหรับนักเทรดและนักพัฒนาที่ต้องการทดสอบกลยุทธ์ด้วยข้อมูลจริงจาก Exchange ยอดนิยม
Tardis.dev คืออะไร ทำไมต้องใช้สำหรับ Backtesting
Tardis.dev เป็นบริการ Historical Market Data API ที่รวบรวมข้อมูลตลาดคริปโตครบถ้วนที่สุดรายการหนึ่ง ครอบคลุม Exchange กว่า 50 แห่ง รวมถึง Binance, Bybit, OKX, BitMEX และ Deribit มีจุดเด่นด้านคุณภาพข้อมูลระดับ Tick-by-Tick พร้อม WebSocket Streaming และ REST API สำหรับดึงข้อมูลย้อนหลังหลายปี
ประเภทข้อมูลที่รองรับ
- Trades — รายการซื้อขายที่เกิดขึ้นจริงในตลาด พร้อม Timestamp, Price, Volume, Side
- Order Book Snapshots — สถานะลิมิตออร์เดอร์ ณ ช่วงเวลาใดเวลาหนึ่ง
- Order Book Deltas — การเปลี่ยนแปลงของ Order Book แบบ Real-time
- Candlesticks/OHLCV — ข้อมูลแท่งเทียนหลาย Timeframe
- Funding Rate — อัตราดอกเบี้ยต่อเนื่องของ Futures
- Liquidations — รายการ Liquidation ที่เกิดขึ้นในตลาด
ติดตั้งและเตรียม Environment
# สร้าง Virtual Environment แยกสำหรับโปรเจกต์
python -m venv quant_backtest
source quant_backtest/bin/activate # Windows: quant_backtest\Scripts\activate
ติดตั้ง Dependencies
pip install pandas numpy requests asyncio aiohttp
pip install pandas>=2.0.0 numpy>=1.24.0
pip install tardis-client # Official Python Client ของ Tardis.dev
ตรวจสอบเวอร์ชัน
python -c "import pandas; import numpy; print(f'Pandas: {pandas.__version__}')"
วิธีที่ 1: ดึงข้อมูล Trades ผ่าน REST API
วิธีนี้เหมาะสำหรับดึงข้อมูล Trade History ย้อนหลังในช่วงเวลาที่กำหนด เช่น ดึงข้อมูล 1 เดือนของ BTCUSDT Perpetual
import pandas as pd
import requests
from datetime import datetime, timedelta
import time
class TardisDataFetcher:
"""คลาสสำหรับดึงข้อมูลตลาดจาก Tardis.dev API"""
BASE_URL = "https://api.tardis.dev/v1"
def __init__(self, api_key: str):
self.api_key = api_key
self.session = requests.Session()
self.session.headers.update({"Authorization": api_key})
def get_trades(
self,
exchange: str,
symbol: str,
start_date: datetime,
end_date: datetime,
limit: int = 100000
) -> pd.DataFrame:
"""
ดึงข้อมูล Trade History จาก Exchange ที่กำหนด
Args:
exchange: ชื่อ Exchange เช่น 'binance', 'bybit'
symbol: สัญลักษณ์คู่เทรด เช่น 'BTCUSDT'
start_date: วันที่เริ่มต้น
end_date: วันที่สิ้นสุด
limit: จำนวน Records สูงสุดต่อ Request
Returns:
DataFrame ที่มีคอลัมน์: timestamp, price, amount, side
"""
url = f"{self.BASE_URL}/ trades"
params = {
"exchange": exchange,
"symbol": symbol,
"from": int(start_date.timestamp()),
"to": int(end_date.timestamp()),
"limit": limit
}
all_trades = []
cursor = None
print(f"กำลังดึงข้อมูล {exchange}:{symbol} จาก {start_date} ถึง {end_date}")
while True:
if cursor:
params["cursor"] = cursor
response = self.session.get(url, params=params)
response.raise_for_status()
data = response.json()
if not data.get("trades"):
break
all_trades.extend(data["trades"])
cursor = data.get("nextCursor")
if not cursor:
break
# Tardis.dev มี Rate Limit ดังนั้นต้องหน่วงเวลา
time.sleep(0.5)
print(f"ดึงได้แล้ว {len(all_trades)} records...")
# แปลงเป็น DataFrame
df = pd.DataFrame(all_trades)
if not df.empty:
df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms")
df["price"] = df["price"].astype(float)
df["amount"] = df["amount"].astype(float)
# เรียงลำดับตามเวลา
df = df.sort_values("timestamp").reset_index(drop=True)
print(f"สรุป: ดึงข้อมูลได้ทั้งหมด {len(df)} trades")
return df
ตัวอย่างการใช้งาน
API_KEY = "YOUR_TARDIS_API_KEY" # สมัครที่ https://tardis.dev
fetcher = TardisDataFetcher(API_KEY)
ดึงข้อมูล BTCUSDT Perpetual 7 วันย้อนหลัง
end_date = datetime.now()
start_date = end_date - timedelta(days=7)
btc_trades = fetcher.get_trades(
exchange="binance",
symbol="BTCUSDT",
start_date=start_date,
end_date=end_date
)
print(btc_trades.head(10))
print(f"\nข้อมูลครอบคลุม: {btc_trades['timestamp'].min()} ถึง {btc_trades['timestamp'].max()}")
วิธีที่ 2: ดึงข้อมูล Order Book ผ่าน WebSocket Streaming
สำหรับข้อมูล Order Book ที่ละเอียดกว่า ต้องใช้ WebSocket Streaming ซึ่งให้ข้อมูลแบบ Real-time แต่สามารถ Replay ได้เช่นกัน
import asyncio
import aiohttp
import pandas as pd
from dataclasses import dataclass, field
from typing import List, Dict
from datetime import datetime
import json
@dataclass
class OrderBookLevel:
"""โครงสร้างข้อมูลระดับราคาของ Order Book"""
price: float
amount: float
side: str # 'bid' หรือ 'ask'
class TardisWebSocketCollector:
"""คลาสสำหรับรวบรวมข้อมูล Order Book ผ่าน WebSocket"""
def __init__(self, api_key: str):
self.api_key = api_key
self.trades: List[Dict] = []
self.orderbook_snapshots: List[Dict] = []
self.liquidations: List[Dict] = []
async def fetch_replayed_data(
self,
exchange: str,
symbols: List[str],
start_date: datetime,
end_date: datetime
):
"""
ดึงข้อมูลย้อนหลังผ่าน Replay API
วิธีนี้เหมาะสำหรับดึงข้อมูลปริมาณมากๆ โดยไม่ต้องรอ Real-time
"""
replay_url = f"https://api.tardis.dev/v1/feeds/{exchange}:{','.join(symbols)}"
params = {
"from": int(start_date.timestamp()),
"to": int(end_date.timestamp()),
"channels": "trade,book"
}
headers = {"Authorization": self.api_key}
print(f"เริ่ม Replay ข้อมูล {exchange}:{symbols}")
print(f"ช่วงเวลา: {start_date} ถึง {end_date}")
async with aiohttp.ClientSession() as session:
# ใช้ SSE (Server-Sent Events) สำหรับดึงข้อมูล
async with session.get(
replay_url,
params=params,
headers=headers
) as response:
async for line in response.content:
line = line.decode("utf-8").strip()
if not line or not line.startswith("data:"):
continue
try:
data = json.loads(line[5:])
await self._process_message(data)
except json.JSONDecodeError:
continue
# แสดงความคืบหน้า
if len(self.trades) % 10000 == 0 and len(self.trades) > 0:
print(f"ประมวลผลได้ {len(self.trades)} trades, "
f"{len(self.orderbook_snapshots)} snapshots")
print(f"เสร็จสิ้น! รวม {len(self.trades)} trades, "
f"{len(self.orderbook_snapshots)} orderbook snapshots")
async def _process_message(self, data: Dict):
"""ประมวลผลข้อความจาก WebSocket"""
msg_type = data.get("type") or data.get("channel")
if msg_type == "trade":
self.trades.append({
"timestamp": pd.to_datetime(data["timestamp"], unit="ms"),
"symbol": data.get("symbol"),
"price": float(data["price"]),
"amount": float(data["amount"]),
"side": data.get("side", "buy" if data.get("is_buyer_maker") else "sell")
})
elif msg_type == "book" or msg_type == "snapshot":
self.orderbook_snapshots.append({
"timestamp": pd.to_datetime(data["timestamp"], unit="ms"),
"symbol": data.get("symbol"),
"bids": [[float(p), float(a)] for p, a in data.get("bids", [])],
"asks": [[float(p), float(a)] for p, a in data.get("asks", [])]
})
elif msg_type == " liquidation":
self.liquidations.append({
"timestamp": pd.to_datetime(data["timestamp"], unit="ms"),
"symbol": data.get("symbol"),
"side": data.get("side"),
"price": float(data["price"]),
"amount": float(data["amount"])
})
def to_dataframes(self) -> Dict[str, pd.DataFrame]:
"""แปลงข้อมูลที่รวบรวมเป็น DataFrame"""
return {
"trades": pd.DataFrame(self.trades) if self.trades else pd.DataFrame(),
"orderbook": pd.DataFrame(self.orderbook_snapshots) if self.orderbook_snapshots else pd.DataFrame(),
"liquidations": pd.DataFrame(self.liquidations) if self.liquidations else pd.DataFrame()
}
async def main():
"""ตัวอย่างการใช้งาน WebSocket Replay"""
api_key = "YOUR_TARDIS_API_KEY"
collector = TardisWebSocketCollector(api_key)
# ดึงข้อมูล 1 ชั่วโมงของ BTCUSDT Perpetual
end_time = datetime.now()
start_time = end_time - timedelta(hours=1)
await collector.fetch_replayed_data(
exchange="binance",
symbols=["BTCUSDT"],
start_date=start_time,
end_date=end_time
)
# แปลงเป็น DataFrame
dfs = collector.to_dataframes()
if not dfs["trades"].empty:
trades_df = dfs["trades"]
print("\n=== Trade Data Sample ===")
print(trades_df.head())
print(f"\nTime Range: {trades_df['timestamp'].min()} - {trades_df['timestamp'].max()}")
print(f"Total Volume: {trades_df['amount'].sum():,.2f} BTC")
print(f"Avg Trade Size: {trades_df['amount'].mean():,.6f} BTC")
รัน Asyncio
if __name__ == "__main__":
asyncio.run(main())
วิธีที่ 3: ดึงข้อมูล OHLCV/Candlestick พร้อมสร้าง Features
สำหรับการสร้างกลยุทธ์ Trading ที่ใช้ Technical Indicators ข้อมูล Candlestick เป็นสิ่งจำเป็น ตัวอย่างนี้แสดงการดึงข้อมูลและคำนวณ Features สำหรับ Machine Learning
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
class TradingFeatureEngineer:
"""คลาสสำหรับสร้าง Features สำหรับ Quant Trading"""
def __init__(self, trades_df: pd.DataFrame):
"""
Args:
trades_df: DataFrame ที่มีคอลัมน์ timestamp, price, amount, side
"""
self.trades = trades_df.copy()
self.candles = None
def resample_to_candles(self, timeframe: str = "1T") -> pd.DataFrame:
"""
แปลง Trade Data เป็น Candlestick
Args:
timeframe: ระยะเวลาของแท่งเทียน เช่น '1T' = 1 นาที, '5T' = 5 นาที, '1H' = 1 ชั่วโมง
"""
self.trades.set_index("timestamp", inplace=True)
# Resample OHLCV
self.candles = self.trades["price"].resample(timeframe).ohlc()
self.candles["volume"] = self.trades["amount"].resample(timeframe).sum()
self.candles["trade_count"] = self.trades["price"].resample(timeframe).count()
# คำนวณ VWAP (Volume Weighted Average Price)
self.candles["vwap"] = (
(self.trades["price"] * self.trades["amount"])
.resample(timeframe).sum()
/ self.candles["volume"]
)
# คำนวณ Buy/Sell Volume
buy_vol = self.trades[self.trades["side"] == "buy"]["amount"].resample(timeframe).sum()
sell_vol = self.trades[self.trades["side"] == "sell"]["amount"].resample(timeframe).sum()
self.candles["buy_volume"] = buy_vol.fillna(0)
self.candles["sell_volume"] = sell_vol.fillna(0)
self.candles["buy_ratio"] = self.candles["buy_volume"] / self.candles["volume"]
# คำนวณ Volume Imbalance
self.candles["volume_imbalance"] = (
(self.candles["buy_volume"] - self.candles["sell_volume"])
/ self.candles["volume"]
)
# Reset Index
self.candles = self.candles.reset_index()
self.candles.rename(columns={"timestamp": "datetime"}, inplace=True)
# ลบ NaN rows
self.candles.dropna(inplace=True)
return self.candles
def add_technical_indicators(self) -> pd.DataFrame:
"""เพิ่ม Technical Indicators พื้นฐาน"""
if self.candles is None:
raise ValueError("ต้องเรียก resample_to_candles() ก่อน")
df = self.candles.copy()
# Simple Moving Averages
for period in [5, 10, 20, 50, 200]:
df[f"SMA_{period}"] = df["close"].rolling(window=period).mean()
# Exponential Moving Averages
for period in [12, 26]:
df[f"EMA_{period}"] = df["close"].ewm(span=period, adjust=False).mean()
# MACD
df["MACD"] = df["EMA_12"] - df["EMA_26"]
df["MACD_signal"] = df["MACD"].ewm(span=9, adjust=False).mean()
df["MACD_histogram"] = df["MACD"] - df["MACD_signal"]
# RSI (Relative Strength Index)
delta = df["close"].diff()
gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
rs = gain / loss
df["RSI_14"] = 100 - (100 / (1 + rs))
# Bollinger Bands
df["BB_middle"] = df["close"].rolling(window=20).mean()
bb_std = df["close"].rolling(window=20).std()
df["BB_upper"] = df["BB_middle"] + (bb_std * 2)
df["BB_lower"] = df["BB_middle"] - (bb_std * 2)
df["BB_width"] = (df["BB_upper"] - df["BB_lower"]) / df["BB_middle"]
df["BB_position"] = (df["close"] - df["BB_lower"]) / (df["BB_upper"] - df["BB_lower"])
# ATR (Average True Range)
high_low = df["high"] - df["low"]
high_close = np.abs(df["high"] - df["close"].shift())
low_close = np.abs(df["low"] - df["close"].shift())
tr = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
df["ATR_14"] = tr.rolling(window=14).mean()
# Volume Profile Features
df["volume_SMA_20"] = df["volume"].rolling(window=20).mean()
df["volume_ratio"] = df["volume"] / df["volume_SMA_20"]
# Momentum
df["momentum_5"] = df["close"] / df["close"].shift(5) - 1
df["momentum_10"] = df["close"] / df["close"].shift(10) - 1
# Returns
df["returns"] = df["close"].pct_change()
df["log_returns"] = np.log(df["close"] / df["close"].shift(1))
df["volatility_20"] = df["returns"].rolling(window=20).std()
return df
ตัวอย่างการใช้งาน
if __name__ == "__main__":
# สมมติว่ามี trades_df จาก TardisDataFetcher
# trades_df = fetcher.get_trades("binance", "BTCUSDT", start_date, end_date)
# สร้าง Features
engineer = TradingFeatureEngineer(trades_df)
# แปลงเป็น Candlestick 15 นาที
candles = engineer.resample_to_candles("15T")
# เพิ่ม Technical Indicators
features_df = engineer.add_technical_indicators()
print("=== Sample Features ===")
print(features_df.tail(10))
print(f"\nจำนวน Features: {len(features_df.columns)}")
print(f"Features ที่สร้าง: {list(features_df.columns)}")
สร้างระบบ Backtesting พื้นฐาน
หลังจากมีข้อมูลและ Features แล้ว มาสร้างระบบ Backtesting อย่างง่ายสำหรับทดสอบกลยุทธ์
import pandas as pd
import numpy as np
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class Trade:
"""โครงสร้างข้อมูลการเทรด"""
entry_time: pd.Timestamp
exit_time: pd.Timestamp
side: str # 'long' หรือ 'short'
entry_price: float
exit_price: float
size: float
pnl: float
pnl_pct: float
class SimpleBacktester:
"""ระบบ Backtesting แบบง่ายสำหรับ Mean Reversion Strategy"""
def __init__(self, initial_capital: float = 10000, commission: float = 0.0004):
"""
Args:
initial_capital: ทุนเริ่มต้น (USD)
commission: ค่าคอมมิชชันต่อครั้ง (0.04% = 4 bps)
"""
self.initial_capital = initial_capital
self.commission = commission
self.trades: List[Trade] = []
self.equity_curve = []
def run_mean_reversion(
self,
df: pd.DataFrame,
entry_threshold: float = -1.5,
exit_threshold: float = 0.5,
lookback: int = 20,
stop_loss: float = 0.02
):
"""
รัน Mean Reversion Strategy
Strategy Logic:
- เข้า Long เมื่อ RSI < entry_threshold (Oversold)
- ออกเมื่อ RSI > exit_threshold หรือ ถึง Stop Loss
"""
position = None
entry_price = 0
entry_time = None
size = 0
equity = self.initial_capital
for i in range(lookback, len(df)):
row = df.iloc[i]
prev_rows = df.iloc[i-lookback:i]
# คำนวณ Position Size
if position is None:
size = equity * 0.95 # ใช้ 95% ของ Equity
else:
size = equity # Maintain Position Size
# Entry Signal: RSI Oversold
if position is None and row["RSI_14"] < entry_threshold:
position = "long"
entry_price = row["close"]
entry_time = row["datetime"]
# คำนวณค่าคอมมิชชัน
equity -= size * self.commission
# Exit Conditions
elif position == "long":
pnl_pct = (row["close"] - entry_price) / entry_price
# Stop Loss
if pnl_pct <= -stop_loss:
exit_price = entry_price * (1 - stop_loss)
self._close_trade(entry_time, row["datetime"], "long",
entry_price, exit_price, size)
equity = size * (1 + pnl_pct) - (size * self.commission)
position = None
# Take Profit: RSI Overbought หรือ Mean Reversion
elif row["RSI_14"] > exit_threshold or pnl_pct > -pnl_pct * 2:
exit_price = row["close"]
self._close_trade(entry_time, row["datetime"], "long",
entry_price, exit_price, size)
equity = size * (1 + pnl_pct) - (size * self.commission)
position = None
self.equity_curve.append({
"datetime": row["datetime"],
"equity": equity,
"position": position
})
return self._generate_report()
def _close_trade(self, entry_time, exit_time, side, entry, exit, size):
"""บันทึกการเทรด"""
pnl_pct = (exit - entry) / entry if side == "long" else (entry - exit) / entry
pnl = size * pnl_pct
trade = Trade(
entry_time=entry_time,
exit_time=exit_time,
side=side,
entry_price=entry,
exit_price=exit,
size=size,
pnl=pnl,
pnl_pct=pnl_pct
)
self.trades.append(trade)
def _generate_report(self) -> dict:
"""สร้างรายงานผล Backtest"""
if not self.trades:
return {"error": "No trades executed"}
equity_df = pd.DataFrame(self.equity_curve)
trades_df = pd.DataFrame([vars(t) for t in self.trades])
winning_trades = trades_df[trades_df["pnl"] > 0]
losing_trades = trades_df[trades_df["pnl"] <= 0]
final_equity = equity_df["equity"].iloc[-1]
total_return = (final_equity - self.initial_capital) / self.initial_capital
# Calculate Sharpe Ratio
returns = equity_df["equity"].pct_change().dropna()
sharpe = returns.mean() / returns.std() * np.sqrt(252 * 390) # แท่งเทียน 1 นาที
# Max Drawdown
cumulative = equity_df["equity"]
running_max = cumulative.expanding().max()
drawdown = (cumulative - running_max) / running_max
max_drawdown = drawdown.min()
return {
"initial_capital": self.initial_capital,
"final_equity": final_equity,
"total_return": total_return,
"total_trades": len(self.trades),
"winning_trades": len(winning_trades),
"losing_trades": len(losing_trades),
"win_rate": len(winning_trades) / len(self.trades) * 100,
"avg_win": winning_trades["pnl"].mean() if len(winning_trades) > 0 else 0,
"avg_loss": losing_trades["pnl"].mean() if len(losing_trades) > 0 else 0,
"profit_factor": (
abs(winning_trades["pnl"].sum() / losing_trades["pnl"].sum())
if len(losing_trades) > 0 and losing_trades["pnl"].sum() != 0
else float("inf")
),
"sharpe_ratio": sharpe,
"max_drawdown": max_drawdown,
"equity_curve": equity_df
}
ตัวอย่างการใช้งาน
if __name__ == "__main__":
# 假设มี features_df จากตัวอย่างก่อนหน้า
backtester = SimpleBacktester(initial_capital=10000)
results = backtester.run_mean_reversion(
features_df,
entry_threshold=30,
exit_threshold=50,
lookback=20,
stop_loss=0.02
)
print("=== Backtest Results ===")
print(f"Initial Capital: ${results['initial_capital']:,.2f}")
print(f"Final Equity: ${results['final_equity']:,.2f}")
print(f"Total Return: {results['total_return']*100:.2f}%")
print(f"Total Trades: {results['total_trades']}")
print(f"Win Rate: {results['win_rate']:.2f}%")
print(f"Profit Factor: {results['profit_factor']:.2f}")
print(f"Sharpe Ratio: {results['sharpe_ratio']:.3f}")
print(f"Max Drawdown: {results['max_drawdown']*100:.2f}%")
เหมาะกับใคร / ไม่เหมาะกับใคร
เหมาะกับ
- นักพัฒนาระบบเทรดอัตโนมัติ ที่ต้องการข้อมูลตลาดคุณภาพ