บทนำ: ทำไมข้อมูลถึงต้อง "สะอาด" ก่อน Backtest
การทำ
Quantitative Backtesting ที่แม่นยำเริ่มต้นจากข้อมูลที่บริสุทธิ์ หลายคนเริ่มต้นสร้างระบบเทรดด้วย Python แต่พบปัญหาว่าผลลัพธ์จาก Backtest ไม่ตรงกับตลาดจริง — สาเหตุหลักคือ
Garbage In, Garbage Out ข้อมูลที่มี NaN, Outlier ผิดปกติ, หรือ Missing Bar จะทำให้ทุกการคำนวณผิดเพี้ยน
บทความนี้จะพาทุกท่านสร้าง
Data Pipeline สำหรับล้างข้อมูล Tardis อย่างเป็นระบบ โดยใช้ API จาก
HolySheep AI ซึ่งมี Latency ต่ำกว่า 50ms และราคาประหยัดกว่า 85% เมื่อเทียบกับ OpenAI API
Tardis API คืออะไร
Tardis คือบริการที่รวบรวมข้อมูล Historical Market Data จากหลาย Exchange ทั่วโลก ให้บริการทั้ง:
- Historical K-line Data — OHLCV (Open, High, Low, Close, Volume) รายนาที, ชั่วโมง, วัน
- Tick/Trade Data — ข้อมูลการซื้อขายทีละ Transaction พร้อม Timestamp และ Side
- Orderbook Snapshot — ข้อมูลคำสั่งซื้อ-ขาย ณ ช่วงเวลาต่างๆ
สำหรับนักพัฒนาระบบเทรดแบบ Quant, Tardis เป็นแหล่งข้อมูลที่น่าเชื่อถือและครอบคลุม Exchange ยอดนิยมอย่าง Binance, Bybit, OKX, Bitget และอื่นๆ
โครงสร้าง Pipeline ภาพรวม
┌─────────────────────────────────────────────────────────────┐
│ DATA PIPELINE │
├─────────────────────────────────────────────────────────────┤
│ 1. Extract: ดึงข้อมูลจาก Tardis API │
│ ↓ │
│ 2. Validate: ตรวจสอบความครบถ้วน ความถูกต้องของ Timestamp │
│ ↓ │
│ 3. Clean: จัดการ NaN, Outlier, Missing Bar │
│ ↓ │
│ 4. Transform: คำนวณ Indicator, Resample, Merge │
│ ↓ │
│ 5. Load: เก็บลง Database / Parquet / CSV │
└─────────────────────────────────────────────────────────────┘
การตั้งค่าเริ่มต้นและการเชื่อมต่อ API
import requests
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from typing import List, Dict, Optional
import json
import time
การตั้งค่า HolySheep AI API สำหรับเรียกใช้งาน AI ในการวิเคราะห์ข้อมูล
HOLYSHEEP_API_KEY = "YOUR_HOLYSHEEP_API_KEY"
HOLYSHEEP_BASE_URL = "https://api.holysheep.ai/v1"
def call_holysheep_ai(prompt: str) -> str:
"""เรียกใช้ AI จาก HolySheep สำหรับวิเคราะห์ข้อมูลอัตโนมัติ"""
response = requests.post(
f"{HOLYSHEEP_BASE_URL}/chat/completions",
headers={
"Authorization": f"Bearer {HOLYSHEEP_API_KEY}",
"Content-Type": "application/json"
},
json={
"model": "gpt-4.1",
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.3
}
)
return response.json()["choices"][0]["message"]["content"]
การตั้งค่า Tardis API
TARDIS_API_KEY = "YOUR_TARDIS_API_KEY" # สมัครได้ที่ tardis.dev
TARDIS_BASE_URL = "https://api.tardis.dev/v1"
def get_tardis_headers():
return {"Authorization": f"Token {TARDIS_API_KEY}"}
print("✅ การตั้งค่าเริ่มต้นเสร็จสมบูรณ์")
print(f"📡 HolySheep API: {HOLYSHEEP_BASE_URL}")
print(f"📡 Tardis API: {TARDIS_BASE_URL}")
ขั้นตอนที่ 1: Extract — ดึงข้อมูล Historical K-line
def fetch_kline_data(
exchange: str,
symbol: str,
start_date: str,
end_date: str,
timeframe: str = "1m"
) -> pd.DataFrame:
"""
ดึงข้อมูล K-line จาก Tardis API
Parameters:
- exchange: ชื่อ Exchange เช่น 'binance', 'bybit'
- symbol: สัญลักษณ์คู่เทรด เช่น 'BTCUSDT'
- start_date: วันที่เริ่มต้น format 'YYYY-MM-DD'
- end_date: วันที่สิ้นสุด format 'YYYY-MM-DD'
- timeframe: '1m', '5m', '1h', '4h', '1d'
"""
url = f"{TARDIS_BASE_URL}/ahara/historical/klines"
params = {
"exchange": exchange,
"symbol": symbol,
"date": f"{start_date},{end_date}",
"interval": timeframe
}
print(f"🔄 กำลังดึงข้อมูล {symbol} บน {exchange} ({timeframe})...")
response = requests.get(
url,
headers=get_tardis_headers(),
params=params
)
if response.status_code != 200:
raise Exception(f"Tardis API Error: {response.status_code} - {response.text}")
data = response.json()
# แปลงเป็น DataFrame
df = pd.DataFrame(data)
# ตรวจสอบว่ามีข้อมูลหรือไม่
if df.empty:
raise ValueError(f"ไม่พบข้อมูลสำหรับ {symbol} ในช่วงวันที่ที่ระบุ")
# แปลงชื่อคอลัมน์ให้เป็นมาตรฐาน
df.columns = ['timestamp', 'open', 'high', 'low', 'close', 'volume', 'close_time', 'quote_volume', 'trades', 'taker_buy_base', 'taker_buy_quote']
# แปลง timestamp เป็น datetime
df['datetime'] = pd.to_datetime(df['timestamp'], unit='ms')
df.set_index('datetime', inplace=True)
# แปลงข้อมูลเป็น float
numeric_cols = ['open', 'high', 'low', 'close', 'volume']
df[numeric_cols] = df[numeric_cols].astype(float)
print(f"✅ ดึงข้อมูลสำเร็จ: {len(df):,} bars")
return df
ตัวอย่างการใช้งาน
df_btc = fetch_kline_data(
exchange='binance',
symbol='BTCUSDT',
start_date='2024-01-01',
end_date='2024-03-01',
timeframe='5m'
)
print(df_btc.head())
print(f"\n📊 ข้อมูลครอบคลุม: {df_btc.index.min()} ถึง {df_btc.index.max()}")
ขั้นตอนที่ 2: Extract — ดึงข้อมูล Tick/Trade Data
def fetch_tick_data(
exchange: str,
symbol: str,
start_date: str,
end_date: str,
limit: int = 100000
) -> pd.DataFrame:
"""
ดึงข้อมูล Tick/Trade จาก Tardis API
Parameters:
- limit: จำนวน records สูงสุดต่อครั้ง (Tardis มี rate limit)
"""
url = f"{TARDIS_BASE_URL}/ahara/historical/trades"
params = {
"exchange": exchange,
"symbol": symbol,
"date": f"{start_date},{end_date}",
"limit": limit
}
print(f"🔄 กำลังดึงข้อมูล Tick สำหรับ {symbol}...")
all_trades = []
offset = 0
while True:
params["offset"] = offset
response = requests.get(
url,
headers=get_tardis_headers(),
params=params
)
if response.status_code != 200:
print(f"⚠️ API Error: {response.status_code}, รอ 5 วินาที...")
time.sleep(5)
continue
data = response.json()
if not data:
break
all_trades.extend(data)
offset += limit
print(f" 📥 ดึงได้ {len(all_trades):,} trades แล้ว...")
# หยุดถ้าได้ข้อมูลครบตามช่วงเวลา
if len(data) < limit:
break
time.sleep(0.5) # เคารพ Rate Limit
df_trades = pd.DataFrame(all_trades)
# ตรวจสอบและแปลงข้อมูล
if df_trades.empty:
raise ValueError(f"ไม่พบข้อมูล Tick สำหรับ {symbol}")
# แปลง timestamp
df_trades['datetime'] = pd.to_datetime(df_trades['timestamp'], unit='ms')
df_trades.set_index('datetime', inplace=True)
# แปลงข้อมูลตัวเลข
df_trades['price'] = df_trades['price'].astype(float)
df_trades['amount'] = df_trades['amount'].astype(float)
print(f"✅ ดึงข้อมูล Tick สำเร็จ: {len(df_trades):,} trades")
return df_trades
ตัวอย่างการใช้งาน
df_trades = fetch_tick_data(
exchange='binance',
symbol='BTCUSDT',
start_date='2024-01-01',
end_date='2024-01-02',
limit=50000
)
print(df_trades.head(10))
print(f"\n📊 ข้อมูล Tick: {df_trades.index.min()} ถึง {df_trades.index.max()}")
ขั้นตอนที่ 3: Validate — ตรวจสอบความครบถ้วนของข้อมูล
def validate_kline_data(df: pd.DataFrame, expected_timeframe: str = "5T") -> Dict:
"""
ตรวจสอบความครบถ้วนของข้อมูล K-line
- ตรวจหา Missing Bars (เวลาขาดหาย)
- ตรวจหา Duplicate Bars
- ตรวจหา Outlier ใน OHLC
- ตรวจสอบความสมเหตุสมผลของราคา
"""
validation_report = {
"total_bars": len(df),
"missing_bars": 0,
"duplicate_bars": 0,
"outliers": [],
"invalid_ohlc": 0,
"warnings": []
}
# 1. ตรวจสอบ Missing Bars
expected_index = pd.date_range(
start=df.index.min(),
end=df.index.max(),
freq=expected_timeframe
)
missing_times = expected_index.difference(df.index)
validation_report["missing_bars"] = len(missing_times)
if len(missing_times) > 0:
validation_report["warnings"].append(
f"พบ {len(missing_times)} bars ที่หายไป (Missing)"
)
print(f"⚠️ พบ {len(missing_times)} Missing Bars")
print(f" ตัวอย่างเวลาที่ขาด: {missing_times[:5].tolist()}")
# 2. ตรวจสอบ Duplicate Bars
duplicates = df.index[df.index.duplicated()]
validation_report["duplicate_bars"] = len(duplicates)
if len(duplicates) > 0:
validation_report["warnings"].append(
f"พบ {len(duplicates)} bars ที่ซ้ำกัน"
)
print(f"⚠️ พบ {len(duplicates)} Duplicate Bars")
# 3. ตรวจสอบความสมเหตุสมผลของ OHLC
invalid_ohlc = (
(df['high'] < df['low']) |
(df['high'] < df['open']) |
(df['high'] < df['close']) |
(df['low'] > df['open']) |
(df['low'] > df['close'])
)
validation_report["invalid_ohlc"] = invalid_ohlc.sum()
if invalid_ohlc.sum() > 0:
print(f"⚠️ พบ {invalid_ohlc.sum()} bars ที่มี OHLC ไม่ถูกต้อง")
# 4. ตรวจหา Outlier (ราคาผิดปกติ)
# ใช้ IQR Method
price_change = df['close'].pct_change()
Q1 = price_change.quantile(0.25)
Q3 = price_change.quantile(0.75)
IQR = Q3 - Q1
outlier_threshold_upper = Q3 + 3 * IQR
outlier_threshold_lower = Q1 - 3 * IQR
outliers = price_change[(price_change > outlier_threshold_upper) |
(price_change < outlier_threshold_lower)]
validation_report["outliers"] = outliers.index.tolist()
if len(outliers) > 0:
print(f"⚠️ พบ {len(outliers)} Outliers ที่อาจเป็นข้อมูลผิดพลาด")
print(f" Outlier ที่ใหญ่ที่สุด: {price_change.max():.2%}")
print(f" Outlier ที่เล็กที่สุด: {price_change.min():.2%}")
print("\n" + "="*50)
print("📋 VALIDATION REPORT")
print("="*50)
print(f"Total Bars: {validation_report['total_bars']:,}")
print(f"Missing Bars: {validation_report['missing_bars']}")
print(f"Duplicate Bars: {validation_report['duplicate_bars']}")
print(f"Invalid OHLC: {validation_report['invalid_ohlc']}")
print(f"Outliers: {len(validation_report['outliers'])}")
return validation_report
ทดสอบการ Validate
validation = validate_kline_data(df_btc, expected_timeframe="5T")
ขั้นตอนที่ 4: Clean — ล้างข้อมูลและแก้ไขปัญหา
def clean_kline_data(df: pd.DataFrame, fill_method: str = "forward") -> pd.DataFrame:
"""
ล้างข้อมูล K-line โดย:
1. ลบ Duplicate Bars (เเที่ยงวันที่ซ้ำ)
2. สร้าง Missing Bars (เติมเวลาที่ขาด)
3. แก้ไข OHLC ที่ผิดปกติ
4. เติมค่าที่ขาดหายด้วย Forward Fill
"""
print("🧹 เริ่มล้างข้อมูล...")
df_clean = df.copy()
# 1. ลบ Duplicate ถ้ามี
before_count = len(df_clean)
df_clean = df_clean[~df_clean.index.duplicated(keep='first')]
duplicates_removed = before_count - len(df_clean)
if duplicates_removed > 0:
print(f" ✅ ลบ {duplicates_removed} Duplicate Bars")
# 2. สร้าง Missing Bars (Resample)
df_clean = df_clean.resample('5T').agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum'
})
# 3. แก้ไข OHLC ที่ผิดปกติ
# High ต้องสูงสุด, Low ต้องต่ำสุด
df_clean['high'] = df_clean[['open', 'high', 'low', 'close']].max(axis=1)
df_clean['low'] = df_clean[['open', 'high', 'low', 'close']].min(axis=1)
# 4. เติมค่าที่ขาดด้วย Forward Fill
if fill_method == "forward":
df_clean = df_clean.ffill()
elif fill_method == "interpolate":
df_clean = df_clean.interpolate(method='time')
# ลบแถวที่ยังมี NaN (ช่วงแรกที่ไม่มีข้อมูล)
df_clean = df_clean.dropna()
print(f" ✅ ล้างข้อมูลเสร็จสมบูรณ์: {len(df_clean):,} bars")
return df_clean
def clean_tick_data(df_trades: pd.DataFrame, price_outlier_threshold: float = 0.1) -> pd.DataFrame:
"""
ล้างข้อมูล Tick/Trade โดย:
1. ลบ Duplicate Timestamps
2. กรอง Outlier ที่ราคาผิดปกติ
3. ตรวจสอบ Side (buy/sell)
"""
print("🧹 ล้างข้อมูล Tick...")
df_clean = df_trades.copy()
# 1. ลบ Duplicate Timestamps
before = len(df_clean)
df_clean = df_clean[~df_clean.index.duplicated(keep='first')]
print(f" ✅ ลบ {before - len(df_clean)} Duplicate Trades")
# 2. กรอง Outlier (ราคาเปลี่ยนแปลงเกิน threshold)
if 'price' in df_clean.columns:
price_change = df_clean['price'].pct_change().abs()
valid_trades = price_change < price_outlier_threshold
valid_trades.iloc[0] = True # Keep first trade
outliers = ~valid_trades
print(f" ✅ กรอง {outliers.sum()} Outlier Trades")
df_clean = df_clean[valid_trades]
# 3. ตรวจสอบ Side
if 'side' in df_clean.columns:
valid_sides = df_clean['side'].isin(['buy', 'sell'])
invalid = (~valid_sides).sum()
if invalid > 0:
print(f" ⚠️ พบ {invalid} Trades ที่มี Side ไม่ถูกต้อง")
print(f" ✅ ล้างข้อมูล Tick เสร็จ: {len(df_clean):,} trades")
return df_clean
ใช้งาน
df_btc_clean = clean_kline_data(df_btc)
df_trades_clean = clean_tick_data(df_trades)
print(f"\n📊 ข้อมูลหลังล้าง: {len(df_btc_clean):,} bars")
print(df_btc_clean.head())
ขั้นตอนที่ 5: Transform — คำนวณ Indicator และ Resample
def add_technical_indicators(df: pd.DataFrame) -> pd.DataFrame:
"""เพิ่ม Technical Indicators สำหรับ Backtesting"""
df_ta = df.copy()
# Simple Moving Averages
df_ta['sma_20'] = df_ta['close'].rolling(window=20).mean()
df_ta['sma_50'] = df_ta['close'].rolling(window=50).mean()
df_ta['sma_200'] = df_ta['close'].rolling(window=200).mean()
# Exponential Moving Averages
df_ta['ema_12'] = df_ta['close'].ewm(span=12, adjust=False).mean()
df_ta['ema_26'] = df_ta['close'].ewm(span=26, adjust=False).mean()
# MACD
df_ta['macd'] = df_ta['ema_12'] - df_ta['ema_26']
df_ta['macd_signal'] = df_ta['macd'].ewm(span=9, adjust=False).mean()
df_ta['macd_hist'] = df_ta['macd'] - df_ta['macd_signal']
# RSI
delta = df_ta['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_ta['rsi'] = 100 - (100 / (1 + rs))
# Bollinger Bands
df_ta['bb_middle'] = df_ta['close'].rolling(window=20).mean()
bb_std = df_ta['close'].rolling(window=20).std()
df_ta['bb_upper'] = df_ta['bb_middle'] + (bb_std * 2)
df_ta['bb_lower'] = df_ta['bb_middle'] - (bb_std * 2)
# ATR (Average True Range)
high_low = df_ta['high'] - df_ta['low']
high_close = (df_ta['high'] - df_ta['close'].shift()).abs()
low_close = (df_ta['low'] - df_ta['close'].shift()).abs()
true_range = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
df_ta['atr'] = true_range.rolling(window=14).mean()
# Volume indicators
df_ta['volume_sma_20'] = df_ta['volume'].rolling(window=20).mean()
df_ta['volume_ratio'] = df_ta['volume'] / df_ta['volume_sma_20']
print("✅ เพิ่ม Technical Indicators เรียบร้อย")
return df_ta
def resample_to_higher_timeframe(df: pd.DataFrame, timeframe: str) -> pd.DataFrame:
"""Resample ข้อมูลเป็น Timeframe ที่สูงกว่า"""
df_resampled = df.resample(timeframe).agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum'
})
df_resampled = df_resampled.dropna()
print(f"✅ Resample เป็น {timeframe}: {len(df_resampled):,} bars")
return df_resampled
ใช้งาน
df_with_ta = add_technical_indicators(df_btc_clean)
Resample เป็น 1 ชั่วโมง
df_hourly = resample_to_higher_timeframe(df_btc_clean, '1H')
print("\n📊 ข้อมูลพร้อมใช้งาน:")
print(df_with_ta[['close', 'sma_20', 'rsi', 'macd']].tail())
ขั้นตอนที่ 6: Load — บันทึกข้อมูลลง Storage
import pyarrow as pa
import pyarrow.parquet as pq
import os
def save_data_to_storage(
df: pd.DataFrame,
symbol: str,
timeframe: str,
data_type: str = "kline",
storage_path: str = "./data"
):
"""
บันทึกข้อมูลลง Storage ในรูปแบบต่างๆ
- Parquet: สำหรับ Backtesting ที่ต้องการความเร็ว
- CSV: สำหรับการตรวจสอบด้วยสายตา
"""
os.makedirs(storage_path, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# 1. บันทึกเป็น Parquet
parquet_path = f"{storage_path}/{symbol}_{timeframe}_{data_type}_{timestamp}.parquet"
table = pa.Table.from_pandas(df.reset_index())
pq.write_table(table, parquet_path)
parquet_size = os.path.getsize(parquet_path) / (1024 * 1024)
print(f"💾 บันทึก Parquet: {parquet_path} ({parquet_size:.2f} MB)")
# 2. บันทึกเป็น CSV (สำหรับตรวจสอบ)
csv_path = f"{storage_path}/{symbol}_{timeframe}_{data_type}_{timestamp}.csv"
df.to_csv(csv_path)
csv_size = os.path.getsize(csv_path) / (1024 * 1024)
print(f"💾 บันทึก CSV: {csv_path} ({csv_size:.2f} MB)")
# 3. สร้าง Metadata
metadata = {
"symbol": symbol,
"timeframe": timeframe,
"data_type": data_type,
"start_date": str(df.index.min()),
"end_date": str(df.index.max()),
"total_bars": len(df),
"created_at": timestamp,
"indicators": list(df.columns)
}
metadata_path = f"{storage_path}/{symbol}_{timeframe}_{data_type}_{timestamp}_metadata.json"
with open(metadata_path, 'w') as f:
json.dump(metadata, f, indent=2)
print(f"💾 บันทึก Metadata: {metadata_path}")
return parquet_path, csv_path
บันทึกข้อมูล
parquet_file, csv_file = save_data_to_storage(
df_with_ta,
symbol="BTCUSDT",
timeframe="5m",
data_type="kline",
storage_path="./btc_data"
)
Pipeline สมบูรณ์: รวมทุกขั้นตอนเป็น Automated Workflow
def run_backtest_data_pipeline(
exchange: str,
symbol: str,
start_date: str,
end_date: str,
timeframe: str = "5m",
add_indicators: bool = True,
storage_path: str = "./backtest_data"
):
"""
Pipeline สมบูรณ์
แหล่งข้อมูลที่เกี่ยวข้อง
บทความที่เกี่ยวข้อง