บทนำ: ทำไม Strategy ที่ Backtest ดีเยี่ยมถึงขาดทุนจริง
ในโลกของ การเทรดคริปโตเชิงปริมาณ (Crypto Quantitative Trading) ผมเคยเจอ Quant Developer หลายคนที่สร้าง Strategy ที่มี Sharpe Ratio เกิน 3.0 ในการ Backtest แต่พอไปรันจริงบน Mainnet ผลตอบแทนกลับติดลบอย่างน่าตกใจ ปัญหาหลักที่ผมพบบ่อยที่สุดคือ Survivorship Bias (ความเอนเอียงจากการเลือกเฉพาะผู้รอดชีวิต) — การใช้ข้อมูลเฉพาะเหรียญที่ยังมีชีวิตอยู่ในปัจจุบัน โดยไม่รวมเหรียญที่ล้มเหลวไปแล้ว บทความนี้จะสอนวิธีตรวจจับและแก้ไข Survivorship Bias อย่างเป็นระบบ พร้อมโค้ด Python ที่พร้อมใช้งานจริงSurvivorship Bias คืออะไรและทำไมมันถึงอันตราย
Survivorship Bias เกิดขึ้นเมื่อเราใช้ข้อมูลเฉพาะจาก "ผู้รอดชีวิต" ในการวิเคราะห์ โดยไม่คำนึงถึง "ผู้ที่หายไป" ตัวอย่างเช่น:# ❌ วิธีที่ใช้กันทั่วไป - ดึงข้อมูลเฉพาะเหรียญที่ยังมีอยู่
import pandas as pd
import yfinance as yf
ดึงข้อมูล BTC, ETH, SOL ที่ยังมีชีวิตอยู่
tickers = ['BTC-USD', 'ETH-USD', 'SOL-USD']
data = yf.download(tickers, start='2020-01-01', end='2024-01-01')
❌ ปัญหา: เหรียญที่ล้มไปแล้วอย่าง Luna, FTX Token,
Fantom, AVAX ในช่วง 2022 ถูกตัดออกโดยไม่รู้ตัว
ทำให้ผลตอบแทนเฉลี่ยสูงเกินจริงอย่างมาก
print(f"จำนวนเหรียญที่วิเคราะห์: {len(tickers)}")
print(f"ความเอนเอียง: ขาดหายไป 80%+ ของเหรียญที่เคยมีอยู่")
ผลกระทบในตัวเลข
ในปี 2017 มีเหรียญ ICO มากกว่า 1,500 เหรียญ แต่ปัจจุบันเหลือเพียงไม่ถึง 100 เหรียญที่ยังมีมูลค่าซื้อขายได้ หากเรา Backtest บนเฉพาะ "ผู้รอดชีวิต" จะได้ผลลัพธ์ที่บิดเบือนอย่างรุนแรงวิธีสร้าง Dataset ที่ปราศจาก Survivorship Bias
# ✅ วิธีที่ถูกต้อง - ใช้ Point-in-Time Data
ใช้บริการข้อมูลที่เก็บ Historical Constituents
from dataclasses import dataclass
from datetime import datetime
from typing import List, Dict, Optional
import pandas as pd
@dataclass
class CoinData:
symbol: str
name: str
start_date: datetime
end_date: Optional[datetime] # None = ยังมีชีวิต
is_alive: bool
class SurvivorshipBiasFreeDataset:
"""
Dataset ที่รวมเหรียญที่ตายไปแล้วด้วย
สำคัญสำหรับการ Backtest ที่แม่นยำ
"""
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://api.holysheep.ai/v1" # ตามข้อกำหนด
def get_historical_constituents(
self,
date: datetime
) -> List[CoinData]:
"""
ดึงรายชื่อเหรียญที่มีอยู่จริง ณ วันที่กำหนด
รวมถึงเหรียญที่ตายไปแล้ว
"""
response = requests.post(
f"{self.base_url}/data/historical-constituents",
headers={
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
},
json={
"date": date.isoformat(),
"universe": "crypto",
"include_delisted": True # สำคัญ: รวมเหรียญที่ถูก delist
}
)
return [
CoinData(
symbol=c['symbol'],
name=c['name'],
start_date=datetime.fromisoformat(c['start_date']),
end_date=datetime.fromisoformat(c['end_date']) if c['end_date'] else None,
is_alive=c.get('is_alive', False)
)
for c in response.json()['constituents']
]
def get_universe_returns(
self,
start: datetime,
end: datetime,
rebalance_freq: str = 'M' # M = Monthly
) -> pd.DataFrame:
"""
คำนวณผลตอบแทนของ Universe ทั้งหมด ณ เวลานั้น
"""
dates = pd.date_range(start, end, freq=rebalance_freq)
all_returns = []
for date in dates:
constituents = self.get_historical_constituents(date)
# ดึงข้อมูลราคาของเหรียญทั้งหมดที่มีอยู่ ณ วันนั้น
prices = self._get_prices(constituents, date)
# คำนวณผลตอบแทนเฉลี่ย (Equal-weighted)
if len(prices) > 0:
ret = (prices['close'].pct_change().mean()) * 100
all_returns.append({
'date': date,
'mean_return': ret,
'alive_count': sum(1 for c in constituents if c.is_alive),
'total_count': len(constituents)
})
return pd.DataFrame(all_returns)
ตัวอย่างการใช้งาน
dataset = SurvivorshipBiasFreeDataset(api_key="YOUR_HOLYSHEEP_API_KEY")
returns = dataset.get_universe_returns(
start=datetime(2022, 1, 1),
end=datetime(2024, 1, 1)
)
print(returns.head(10))
การคำนวณผลกระทบของ Survivorship Bias
import numpy as np
import matplotlib.pyplot as plt
def calculate_survivorship_bias_impact(
live_returns: np.ndarray,
all_returns: np.ndarray
) -> Dict[str, float]:
"""
คำนวณความแตกต่างระหว่างข้อมูลที่มีและไม่มี Survivorship Bias
"""
stats = {
'live_mean': np.mean(live_returns),
'all_mean': np.mean(all_returns),
'live_std': np.std(live_returns),
'all_std': np.std(all_returns),
'live_sharpe': np.mean(live_returns) / np.std(live_returns) * np.sqrt(252),
'all_sharpe': np.mean(all_returns) / np.std(all_returns) * np.sqrt(252),
'bias_percentage': ((np.mean(live_returns) - np.mean(all_returns))
/ abs(np.mean(all_returns)) * 100) if np.mean(all_returns) != 0 else 0
}
return stats
def simulate_survivorship_bias():
"""
จำลองผลกระทบของ Survivorship Bias
"""
np.random.seed(42)
# จำลอง 1000 เหรียญ ที่มี 30% ตายในปีแรก
n_coins = 1000
death_rate = 0.30
n_alive = int(n_coins * (1 - death_rate))
# ผลตอบแทนรายปี (ปีที่ตายมีผลตอบแทน -100%)
# ผลตอบแทนที่ไม่มี Bias (รวมเหรียญที่ตาย)
all_returns = []
for i in range(n_coins):
year_return = np.random.normal(0.15, 0.40) # Mean 15%, Std 40%
# เหรียญที่ตายให้ผลตอบแทน -100%
if i >= n_alive:
year_return = -1.0
all_returns.append(year_return)
# ผลตอบแทนที่มี Bias (เฉพาะเหรียญที่รอด)
live_returns = all_returns[:n_alive]
stats = calculate_survivorship_bias_impact(
np.array(live_returns),
np.array(all_returns)
)
print("=" * 50)
print("ผลกระทบของ Survivorship Bias")
print("=" * 50)
print(f"ผลตอบแทนเฉลี่ย (มี Bias): {stats['live_mean']*100:.2f}%")
print(f"ผลตอบแทนเฉลี่ย (ไม่มี Bias): {stats['all_mean']*100:.2f}%")
print(f"ความเอนเอียง: {stats['bias_percentage']:.1f}%")
print(f"Sharpe Ratio (มี Bias): {stats['live_sharpe']:.2f}")
print(f"Sharpe Ratio (ไม่มี Bias): {stats['all_sharpe']:.2f}")
print("=" * 50)
return stats
stats = simulate_survivorship_bias()
แหล่งข้อมูลที่แนะนำสำหรับ Crypto Backtest
| แหล่งข้อมูล | ราคา/เดือน | รวม Delisted | ความลึกข้อมูล | Latency | เหมาะกับ |
|---|---|---|---|---|---|
| CoinMarketCap Pro | $29 | ✓ บางส่วน | 1 นาที | >100ms | Retail Trader |
| Binance Klines | ฟรี | ✗ ไม่มี | 1 นาที | >200ms | Backtest รวดเร็ว |
| Kaiko | $500+ | ✓ เต็มรูปแบบ | Tick-level | >80ms | Institutional |
| HolySheep AI | ¥1=$1 | ✓ เต็มรูปแบบ | 1 วินาที | <50ms | Production Quant |
เหมาะกับใคร / ไม่เหมาะกับใคร
| กลุ่มผู้ใช้ | เหมาะกับ HolySheep AI | ไม่เหมาะกับ HolySheep AI |
|---|---|---|
| Quant Developer | ✓ ต้องการข้อมูลที่ปราศจาก Bias สำหรับ Research | — |
| Fund Manager | ✓ ต้องการ API ที่เสถียรและเร็ว <50ms | — |
| Retail Trader | ✓ ราคาถูกกว่าคู่แข่ง 85%+ | — |
| Academics/Research | ✓ Historical Data ครบถ้วน | — |
| Hobbyist | — | ✗ อาจใช้แหล่งฟรีก่อน |
| High-Frequency Trader | — | ✗ ต้องการ Tick-level data ที่ HolySheep ไม่มี |
ราคาและ ROI
| แพลตฟอร์ม | ราคา/MToken | ค่าใช้จ่ายต่อเดือน (100M tokens) | ประหยัด vs เฉลี่ยตลาด |
|---|---|---|---|
| OpenAI GPT-4 | $15-30 | $1,500-3,000 | Baseline |
| Anthropic Claude | $15-18 | $1,500-1,800 | Baseline |
| Google Gemini | $0.50-3.50 | $50-350 | ประหยัด 70-90% |
| HolySheep AI | $0.42-15 | $42-1,500 | ประหยัด 85%+ |
ROI สำหรับ Quant Team: หากใช้ Gemini 2.5 Flash ที่ $2.50/MToken กับ HolySheep ที่ $0.42/MToken จะประหยัดได้ 83% สำหรับ Data Processing ที่ต้องใช้ Token จำนวนมาก
ทำไมต้องเลือก HolySheep
- ประหยัด 85%+: อัตรา ¥1=$1 ทำให้ต้นทุนต่ำกว่าคู่แข่งอย่างมาก
- ชำระเงินง่าย: รองรับ WeChat Pay และ Alipay สำหรับผู้ใช้ในประเทศจีน
- API Latency ต่ำ: <50ms เหมาะสำหรับ Real-time Trading System
- เครดิตฟรีเมื่อลงทะเบียน: ทดลองใช้งานก่อนตัดสินใจ
- ข้อมูล Historical ครบถ้วน: รวมถึงเหรียญที่ถูก Delist ซึ่งสำคัญสำหรับการหลีกเลี่ยง Survivorship Bias
ข้อผิดพลาดที่พบบ่อยและวิธีแก้ไข
1. การดึงข้อมูลเฉพาะ Current Universe
# ❌ ผิดพลาด: ใช้ screener ปัจจุบันในการ Backtest
ทำให้ละเว้นเหรียญที่ตายไปแล้ว
current_tickers = ['BTC', 'ETH', 'SOL', 'BNB', 'XRP']
✅ แก้ไข: ใช้ Point-in-Time Constituent Data
historical_universe = get_historical_constituents(
date='2022-05-01', # วันที่ต้องการ
include_delisted=True
)
หรือใช้ Backtesting Library ที่รองรับ
from backtesting import Backtest
from backtesting.lib import SignalStrategy
กำหนด Universe ณ วันที่เริ่ม Backtest
bt = Backtest(
data=get_historical_data('2022-01-01', '2024-01-01'),
strategy=MyStrategy,
# สำคัญ: ตั้งค่าให้รวมเหรียญที่ถูก delist
exclusive_orders=True,
hedge='crypto' # รองรับ Short position สำหรับเหรียญที่ล้ม
)
สาเหตุ: ผู้ใช้มักดึงข้อมูลจาก Exchange API โดยตรง ซึ่งจะคืนค่าเฉพาะเหรียญที่ยังซื้อขายอยู่ผลกระทบ: Backtest Return สูงเกินจริง 30-50% ในตลาดหมี
วิธีแก้: ใช้บริการข้อมูล Historical Constituents หรือสร้าง Custom Database จาก Wayback Machine
2. Look-Ahead Bias จาก Future Data
# ❌ ผิดพลาด: ใช้ข้อมูลที่ยังไม่มี ณ เวลาที่ Backtest
def calculate_momentum_score(df):
# ผสมข้อมูล 30 วัน + 90 วัน
df['momentum'] = df['close'].pct_change(30) + df['close'].pct_change(90)
# ปัญหา: ถ้าเริ่ม Backtest วันที่ 60 จะใช้ข้อมูล 90 วันที่ยังไม่มี
return df
✅ แก้ไข: ใช้ข้อมูลเฉพาะที่มี ณ วันที่ Signal
def calculate_momentum_score_safe(df, current_date):
"""
คำนวณ Momentum Score โดยใช้เฉพาะข้อมูลที่มี ณ current_date
"""
# จำกัด lookahead ให้เป็นไปได้จริง
lookback = min(90, (current_date - df.index.min()).days)
if lookback < 30:
return None # ไม่มีข้อมูลเพียงพอ
momentum = df.loc[:current_date, 'close'].pct_change(lookback)
return momentum.iloc[-1]
หรือใช้ Pandas แบบ Vectorized
def calculate_rolling_momentum(df, lookback=30):
"""
คำนวณ Rolling Momentum โดยอัตโนมัติไม่ให้เกิน available data
"""
return df['close'].pct_change(lookback).rolling(
window=lookback,
min_periods=lookback # สำคัญ: กำหนด min_periods
).mean()
สาเหตุ: การใช้ฟังก์ชันที่มี Hardcoded Lookback ที่ยาวเกินไปผลกระทบ: Strategy ดูดีใน Backtest แต่ไม่สามารถ Implement ได้จริง
วิธีแก้: ใช้ Walk-Forward Optimization และตรวจสอบ Data Availability
3. Transaction Cost ที่ต่ำเกินไป
# ❌ ผิดพลาด: ใช้ค่า Commission ที่ต่ำเกินไป
bt = Backtest(data, MyStrategy, commission=.001) # 0.1% - ต่ำเกินไป!
✅ แก้ไข: ใช้ค่าที่สมจริงรวม Slippage
class RealisticBacktest(Backtest):
"""
Backtest ที่รวมค่าใช้จ่ายที่สมจริง
"""
def __init__(
self,
data,
strategy,
commission=0.001, # Maker fee
taker_fee=0.002, # Taker fee
slippage_bps=10, # 10 basis points slippage
funding_rate=0.0001 # สำหรับ Futures
):
super().__init__(data, strategy, commission=commission)
self.taker_fee = taker_fee
self.slippage_bps = slippage_bps
self.funding_rate = funding_rate
def _calculate_actual_cost(self, order, price):
"""
คำนวณต้นทุนที่แท้จริงรวมทุกอย่าง
"""
# Slippage
slippage = price * (self.slippage_bps / 10000)
# Taker Fee
fee = price * self.taker_fee
# Funding (สำหรับ Perpetual)
if self._is_perpetual(order):
funding = price * self.funding_rate
return slippage + fee + funding
ตัวอย่าง: คริปโตที่มี High Volatility
bt = RealisticBacktest(
data,
MyStrategy,
commission=0.001, # Maker
taker_fee=0.004, # Taker (Binance Spot)
slippage_bps=25, # สำหรับ Altcoins ที่มี Volatility สูง
funding_rate=0.0001 # รายวัน
)
สาเหตุ: ใช้ค่า Maker Fee สำหรับทุกออร์เดอร์ หรือละเลย Slippageผลกระทบ: Strategy ที่มี Turnover สูงจะดูดีเกินจริงอย่างมาก
วิธีแก้: คูณ Turnover ด้วย 2x สำหรับค่า Slippage และใช้ Taker Fee เป็น Base
4. ไม่รวม Delisted Asset PnL
# ❌ ผิดพลาด: ข้าม PnL ของเหรียญที่ถูก Delist
def calculate_portfolio_pnl(positions):
realized_pnl = []
for pos in positions:
if pos.current_price > 0: # ข้ามเหรียญที่ Delist
realized_pnl.append(pos.pnl)
return sum(realized_pnl)
✅ แก้ไข: รวม PnL ของเหรียญที่ Delist เป็น -100%
def calculate_portfolio_pnl_complete(positions, delisted_prices):
"""
คำนวณ PnL รวมเหรียญที่ถูก Delist
"""
total_pnl = 0
for pos in positions:
symbol = pos.symbol
if pos.is_alive:
# ใช้ราคาปัจจุบัน
pnl = (pos.current_price - pos.entry_price) * pos.quantity
else:
# เหรียญ Delist: ใช้ราคาสุดท้ายหรือ 0
last_price = delisted_prices.get(symbol, 0)
pnl = (last_price - pos.entry_price) * pos.quantity
total_pnl += pnl
# คำนวณ Delist Impact
delisted_pnl = sum(
-pos.entry_price * pos.quantity # ขาดทุน 100%
for pos in positions
if not pos.is_alive
)
print(f"Total PnL: ${total_pnl:,.2f}")
print(f"Delist Impact: ${delisted_pnl:,.2f}")
print(f"Net PnL (after delist): ${total_pnl - abs(delist_pnl):,.2f}")
return total_pnl - abs(delist_pnl)
สาเหตุ: การเขียนโค้ดที่ใช้ Filter ข้ามเหรียญที่ Delistผลกระทบ: ประมาณการ PnL สูงเกินจริง 10-40% ในกรณีที่มีเหรียญล้มหลายตัว