← กลับไปยังบทความ
March 7, 2026
อ่าน 5 นาที

Backtest-live parity: ทำไมบอทของคุณถึงเทรดต่างจาก Backtest

Backtest-live parity: ทำไมบอทของคุณถึงเทรดต่างจาก Backtest
#algotrading
#backtest
#การเทรดสด
#backtest-live parity
#การส่งคำสั่ง
#NautilusTrader

คุณรัน Strategy ผ่าน Backtest แล้ว Sharpe 2.1, MaxDD -8%, PnL +67% คุณเปิดบอท หนึ่งเดือนต่อมาเปรียบเทียบ: สัญญาณเดิม ช่วงเวลาเดิม — แต่ PnL จริงต่ำกว่า 40% Drawdown ลึกกว่าหนึ่งเท่าครึ่ง สองจากสิบคำสั่งไม่ถูกส่งเลย

นี่ไม่ใช่ Bug นี่คือ Backtest-live divergence — ความไม่สอดคล้องอย่างเป็นระบบระหว่างผล Backtest และการเทรดจริง ทุกคนเจอปัญหานี้ คำถามคือคุณรู้เรื่องนี้หรือไม่ และสามารถควบคุมได้หรือเปล่า

บทความนี้นำเสนออนุกรมวิธานฉบับสมบูรณ์ของความแตกต่าง รูปแบบสถาปัตยกรรมเพื่อลดความแตกต่างเหล่านั้น และ Checklist ในทางปฏิบัติสำหรับตรวจสอบ Parity ในโปรดักชัน

อาการ "มันใช้งานได้ใน Backtest"

Backtest vs live trading divergence — ideal equity curve versus real volatile results

นักเทรดอัลโก้ทุกคนผ่านวงจรนี้:

  1. เขียน Strategy ใน Jupyter Notebook
  2. รัน Backtest บน CSV ประวัติ — ผลลัพธ์ดีมาก
  3. เขียน Logic ใหม่เป็นบอท (มักใช้ภาษาหรือ Framework ต่างกัน)
  4. เปิดใช้งาน — ผลลัพธ์ไม่ตรงกัน
  5. เริ่มหา Bug แต่ไม่เจอ — "ตลาดเปลี่ยนแล้ว"

ปัญหาไม่ได้อยู่ที่ตลาด ปัญหาคือ Backtest และบอทคือซอฟต์แวร์สองตัวที่ต่างกัน ซึ่งจำลองความเป็นจริงเดียวกันต่างกัน ความแตกต่างหลีกเลี่ยงไม่ได้ แต่สามารถจัดระบบและลดให้น้อยลงได้

อนุกรมวิธานของความแตกต่าง (Taxonomy of Divergences)

Taxonomy of backtest-live divergences

แหล่งที่มาของความแตกต่างทั้งหมดแบ่งเป็นสี่หมวดหมู่ แต่ละหมวดมีคะแนนความรุนแรง (1 ถึง 5) และการมีส่วนร่วมในความแตกต่าง PnL ทั่วไป

1. ความแตกต่างด้านข้อมูล (ความรุนแรง: 3/5)

ข้อมูลที่ Backtest เห็นและข้อมูลที่บอทเห็นแบบเรียลไทม์ไม่เหมือนกัน

Timestamp Exchange ส่ง Candle พร้อมกฎการกำหนด Timestamp ที่ต่างกัน Exchange หนึ่งติดป้าย Candle ด้วยเวลาเริ่มต้นของช่วง อีก Exchange ด้วยเวลาสิ้นสุด REST API อาจส่งคืน Candle ล่าช้า 1-3 วินาทีหลัง Close จริง Backtest ทำงานกับ Timestamp "อุดมคติ" จากไฟล์ประวัติ

การรวม OHLCV ข้อมูลประวัติมักถูกรวมโดย Provider ต่างจากที่ Exchange ทำแบบเรียลไทม์ ความแตกต่างอยู่ที่ตัวเลขท้าย — แต่กับสัญญาณเกณฑ์ (MA Crossover, การทะลุระดับ) สิ่งนี้กำหนดว่า Strategy จะเข้า Position หรือไม่

ช่องว่างและข้อมูลที่หายไป ข้อมูลประวัติมักสะอาด — Candle ที่หายไปถูกเติมด้วยการ Interpolation ในเวลาจริง WebSocket อาจหลุด และบอทพลาดข้อมูล 30 วินาที

การมีส่วนร่วมในความแตกต่าง PnL ทั่วไป: 2-5% ของ PnL รายปี

2. ความแตกต่างด้านการส่งคำสั่ง (ความรุนแรง: 5/5)

Order execution divergences — orderbook slippage, latency, and partial fills visualization

ความแตกต่างประเภทที่อันตรายที่สุด Backtest จำลองการส่งคำสั่งได้อย่างสมบูรณ์แบบ — ความเป็นจริงอยู่ไกลจากอุดมคติ

Slippage Backtest เติมคำสั่งที่ราคา Close (หรือราคาสัญญาณ) ในความเป็นจริง คำสั่ง Market ถูกส่งที่ Bid/Ask ดีที่สุดบวก Slippage ที่ขึ้นอยู่กับปริมาณและสภาพคล่อง สำหรับ Position ขนาด $10K บน Altcoin สภาพคล่องปานกลาง Slippage อาจเป็น 0.05-0.3%

สูตรสำหรับ Slippage สะสมตลอด NN การเทรด:

Slippagetotal=i=1Nsizei×si\text{Slippage}_{total} = \sum_{i=1}^{N} \text{size}_i \times s_i

โดยที่ sis_i คือ Slippage ของการเทรดที่ ii ขึ้นอยู่กับความลึกของ Orderbook:

sisizeiLiquidity(ti)×ks_i \approx \frac{\text{size}_i}{\text{Liquidity}(t_i)} \times k

Latency ตั้งแต่เวลาที่สัญญาณถูกสร้างจนถึงการส่งคำสั่ง เวลาผ่านไป: การคำนวณสัญญาณ (1-50 ms), การส่งคำขอ (10-200 ms), การจับคู่บน Exchange (1-10 ms) ใน Backtest, Latency = 0 ในการเทรดสด — ราคาสามารถเคลื่อนไหวได้

การเติมบางส่วน (Partial fills) Backtest สมมติว่าคำสั่ง 100% ถูกเติมทันที ในความเป็นจริง คำสั่ง Limit อาจถูกเติมบางส่วน — หรือไม่ถูกเติมเลยหากราคาย้อนกลับ สำหรับคำสั่ง Market บนตลาดที่ไม่มีสภาพคล่อง คำสั่ง "ไหล" ผ่านหลายระดับ Orderbook

ลำดับคิว (Queue priority) คำสั่ง Limit ที่วางที่ราคา Bid ดีที่สุดจะไม่ถูกเติมทันที — มันต่อคิวหลังคำสั่งทั้งหมดที่วางก่อนหน้าที่ระดับนั้น Backtest ที่พิจารณา "ราคาสัมผัส = คำสั่งถูกเติม" ประเมิน Fill Rate เกินจริงอย่างเป็นระบบ

การมีส่วนร่วมในความแตกต่าง PnL ทั่วไป: 10-30% ของ PnL รายปี

3. ความแตกต่างด้าน Logic (ความรุนแรง: 4/5)

นี่คือความแตกต่างใน Code ของ Strategy เองระหว่าง Backtest และบอท

Codebase แยกกัน Anti-pattern คลาสสิก: backtests/strategy_a.py และ bot/strategy_a.py — สองไฟล์แยกกันที่ "ทำสิ่งเดียวกัน" หลังจากแก้ไขสามเดือน พวกมันย่อมแตกต่างกัน มีคนเพิ่ม Filter ใน Backtest แล้วลืม Replicate ไปยังบอท หรือตรงข้าม — Bug ถูกแก้ในบอทแต่ยังอยู่ใน Backtest

Framework ต่างกัน Backtest บน Pandas พร้อม Vectorized Operations, บอทบน Asyncio พร้อม Event-driven Logic แม้จะมี Strategy เดียวกัน Edge Cases ถูกจัดการต่างกัน: การปัดเศษ, ลำดับการตรวจสอบเงื่อนไข, การจัดการ NaN

การจัดการ State Backtest มักไม่มี State — มันวนซ้ำบนอาร์เรย์ข้อมูล บอทมี State — จัดเก็บ Position, ยอดดุล, ประวัติคำสั่ง การรีสตาร์ทบอท การสูญเสีย State, การขาดประสานกับ Exchange — ล้วนเป็นแหล่งความแตกต่าง

การมีส่วนร่วมในความแตกต่าง PnL ทั่วไป: 5-20% ของ PnL รายปี

4. ความแตกต่างด้านต้นทุน (ความรุนแรง: 3/5)

ความแตกต่างในการจำลองต้นทุนการเทรด

Funding Rates Backtest ส่วนใหญ่ของ Perpetual Futures ไม่คำนึงถึง Funding Rates เลย ที่ Leverage 10x และอัตราเฉลี่ย 0.01% ต่อ 8 ชั่วโมง นี่คือ 0.01%×3×365×10=109.5%0.01\% \times 3 \times 365 \times 10 = 109.5\% ต่อปี — มากกว่า PnL ของ Strategy ส่วนใหญ่ การวิเคราะห์โดยละเอียดอยู่ในบทความ Funding rates ทำลาย Leverage ของคุณ

Commissions Maker/Taker Commission มักถูกจำลองแต่บ่อยครั้งด้วยอัตราที่ผิด VIP Tiers, ส่วนลด BNB, Rebates — ทั้งหมดนี้ส่งผลต่อผลลัพธ์สุดท้าย

Spread Backtest ที่ใช้ Candle ไม่เห็น Bid-Ask Spread บน Candle 1 นาที, Close = 3000 แต่ในความเป็นจริง Bid = 2999.5 และ Ask = 3000.5 การเทรดแต่ละครั้ง "มีต้นทุน" ครึ่งหนึ่งของ Spread

การมีส่วนร่วมในความแตกต่าง PnL ทั่วไป: 5-15% ของ PnL รายปี

ผลกระทบสะสม

ทั้งสี่หมวดหมู่ทำงาน พร้อมกัน และโดยปกติในทิศทางเดียว — ต่อต้านนักเทรด:

PnLlivePnLbacktestΔdataΔexecutionΔlogicΔcosts\text{PnL}_{live} \approx \text{PnL}_{backtest} - \Delta_{data} - \Delta_{execution} - \Delta_{logic} - \Delta_{costs}

ความแตกต่างรวม 20-50% จาก PnL ของ Backtest เป็นเรื่องปกติสำหรับระบบที่ไม่ได้ปรับปรุง ด้วย Leverage ผลกระทบจะถูกคูณ

รูปแบบสถาปัตยกรรมเพื่อ Parity

Pattern 1: Shared Core (การแยก Core ร่วม)

Shared Core architecture — a single strategy module powering both backtest and live trading engines

แนวคิด: แยก Strategy Core — Logic การสร้างสัญญาณและการส่งคำสั่ง — ออกเป็นโมดูลแยกต่างหากที่ใช้ทั้งโดย Backtest และบอท มีแค่โครงสร้างพื้นฐานโดยรอบที่แตกต่างกัน: แหล่งข้อมูลและกลไกการส่งคำสั่ง

┌─────────────────────────────────────┐
│         strategy_core.py            │
│  ┌─────────────┐ ┌───────────────┐  │
│  │ SignalEngine │ │ OrderManager  │  │
│  └──────┬──────┘ └──────┬────────┘  │
│         │               │           │
│    generate_signal()  create_order()│
└─────────┬───────────────┬───────────┘
          │               │
    ┌─────┴─────┐   ┌─────┴──────┐
    │ Backtest   │   │ Live       │
    │ DataFeed   │   │ DataFeed   │
    │ FillModel  │   │ Exchange   │
    └────────────┘   └────────────┘

from dataclasses import dataclass
from typing import Optional
import numpy as np

@dataclass
class Signal:
    side: str          # 'long' | 'short'
    entry_price: float
    sl_price: float
    tp_price: float
    size: float
    timestamp: int

@dataclass
class OrderRequest:
    side: str
    order_type: str    # 'market' | 'limit'
    price: float
    size: float

class StrategyCore:
    """
    Strategy core. Identical code for backtest and live.
    Depends only on data, not on infrastructure.
    """
    def __init__(self, params: dict):
        self.fast_period = params.get('fast_ma', 20)
        self.slow_period = params.get('slow_ma', 50)
        self.sl_pct = params.get('sl_pct', 0.02)
        self.tp_pct = params.get('tp_pct', 0.04)
        self.position: Optional[Signal] = None
        self._closes: list[float] = []

    def on_candle(self, timestamp: int, o: float, h: float,
                  l: float, c: float, v: float) -> Optional[OrderRequest]:
        """
        Process a new candle. Returns an OrderRequest or None.
        This method is called identically from the backtest and the bot.
        """
        self._closes.append(c)

        if len(self._closes) < self.slow_period:
            return None

        fast_ma = np.mean(self._closes[-self.fast_period:])
        slow_ma = np.mean(self._closes[-self.slow_period:])

        if self.position is not None:
            exit_order = self._check_exit(h, l, c)
            if exit_order:
                self.position = None
                return exit_order

        if self.position is None:
            if fast_ma > slow_ma and self._prev_fast_ma <= self._prev_slow_ma:
                self.position = Signal(
                    side='long', entry_price=c,
                    sl_price=c * (1 - self.sl_pct),
                    tp_price=c * (1 + self.tp_pct),
                    size=1.0, timestamp=timestamp,
                )
                return OrderRequest('buy', 'market', c, 1.0)

        self._prev_fast_ma = fast_ma
        self._prev_slow_ma = slow_ma
        return None

    def _check_exit(self, high: float, low: float,
                    close: float) -> Optional[OrderRequest]:
        pos = self.position
        if pos.side == 'long':
            if low <= pos.sl_price:
                return OrderRequest('sell', 'market', pos.sl_price, pos.size)
            if high >= pos.tp_price:
                return OrderRequest('sell', 'market', pos.tp_price, pos.size)
        return None

ตอนนี้ Backtest และบอทใช้ StrategyCore เดียวกัน:


from strategy_core import StrategyCore

def run_backtest(candles, params, fill_model):
    core = StrategyCore(params)
    trades = []

    for candle in candles:
        order = core.on_candle(
            candle['timestamp'], candle['open'], candle['high'],
            candle['low'], candle['close'], candle['volume'],
        )
        if order:
            fill_price = fill_model.simulate_fill(order, candle)
            trades.append({'price': fill_price, 'side': order.side})

    return trades

from strategy_core import StrategyCore

async def run_live(exchange, symbol, params):
    core = StrategyCore(params)

    async for candle in exchange.stream_candles(symbol, '1m'):
        order = core.on_candle(
            candle['timestamp'], candle['open'], candle['high'],
            candle['low'], candle['close'], candle['volume'],
        )
        if order:
            await exchange.place_order(symbol, order.side,
                                       order.order_type, order.size)

กฎสำคัญ: StrategyCore ไม่รู้ว่าข้อมูลมาจากไหนหรือคำสั่งถูกส่งไปที่ไหน มันรับ OHLCV และส่งคืน OrderRequest ทุกอย่างอื่นเป็นความรับผิดชอบของเลเยอร์โครงสร้างพื้นฐาน

Pattern 2: Event-driven Unification (แนวทาง NautilusTrader)

Event-driven trading architecture with cascading event pipeline — market data, signals, orders, fills

NautilusTrader ใช้ Parity ผ่าน NautilusKernel แบบรวม — Engine ที่ใช้ Rust แบบเนทีฟพร้อม Core ที่ขับเคลื่อนด้วย Event แบบ Deterministic และความละเอียดระดับนาโนวินาที Implementation ของ Strategy เดียวกันทำงานทั้งใน Backtest และการเทรดสด

สถาปัตยกรรมสร้างบน Pattern Ports and Adapters (Hexagonal Architecture):

┌──────────────────────────────────┐
│        NautilusKernel            │
│  ┌───────────┐  ┌─────────────┐  │
│  │ Strategy   │  │ RiskEngine  │  │
│  │ (Python)   │  │ (Rust)      │  │
│  └─────┬─────┘  └──────┬──────┘  │
│        │               │         │
│  ┌─────┴───────────────┴──────┐  │
│  │      Message Bus (Rust)    │  │
│  └─────┬───────────────┬──────┘  │
└────────┼───────────────┼─────────┘
         │               │
   ┌─────┴─────┐   ┌─────┴──────┐
   │ Backtest   │   │ Live       │
   │ Adapter    │   │ Adapter    │
   │ FillModel  │   │ Exchange   │
   │ (L2 book)  │   │ Gateway    │
   └────────────┘   └────────────┘

ข้อดี:

  • การ Replay แบบ Deterministic Event ถูกประมวลผลในลำดับที่กำหนดไว้อย่างเคร่งครัด — ผล Backtest สามารถ Reproduce ได้ทีละ Bit
  • Custom FillModel การจำลอง L2 Orderbook สำหรับทุกการส่งคำสั่ง — Slippage ถูกจำลองตามความลึก Orderbook จริง
  • ประสิทธิภาพ ถึง 5 ล้านแถว/วินาที ประมวลผลข้อมูลที่ไม่พอดีกับ RAM
  • Redis + PostgreSQL Cache และ Message Bus ผ่าน Redis, Persistence ผ่าน PostgreSQL — โครงสร้างพื้นฐานเดียวกันสำหรับ Backtest และการเทรดสด

Pattern 3: Strategy Interface (แนวทาง Freqtrade)

Freqtrade ใช้ Interface IStrategy แบบรวม: Class ของ Strategy เดียวกันทำงานทั้งใน Backtest และการเทรดสด ความแตกต่างเดียวคือเลเยอร์ Persistence


class IStrategy:
    """Unified interface — the implementation does not know if this is a backtest or live."""

    def populate_indicators(self, dataframe, metadata):
        """Compute indicators."""
        dataframe['fast_ma'] = dataframe['close'].rolling(20).mean()
        dataframe['slow_ma'] = dataframe['close'].rolling(50).mean()
        return dataframe

    def populate_entry_trend(self, dataframe, metadata):
        """Determine entry signals."""
        dataframe.loc[
            (dataframe['fast_ma'] > dataframe['slow_ma']) &
            (dataframe['fast_ma'].shift(1) <= dataframe['slow_ma'].shift(1)),
            'enter_long'
        ] = 1
        return dataframe

    def populate_exit_trend(self, dataframe, metadata):
        """Determine exit signals."""
        dataframe.loc[
            (dataframe['fast_ma'] < dataframe['slow_ma']),
            'exit_long'
        ] = 1
        return dataframe

Freqtrade ยังมีให้เพิ่มเติม:

  • Hyperopt ผ่าน Optuna — การปรับพารามิเตอร์ Strategy
  • --timeframe-detail — Drill-down ไปยัง Timeframe ที่ละเอียดกว่าสำหรับการปรับแต่ง Fill (คล้ายกับ Adaptive drill-down)

การเปรียบเทียบ Pattern

Shared Core Event-driven (NautilusTrader) Strategy Interface (Freqtrade)
ความซับซ้อนของการ Implement ต่ำ สูง ปานกลาง
ระดับ Parity ปานกลาง สูงสุด สูง
การจำลอง Fill FillModel แยก L2 Orderbook --timeframe-detail
ภาษา Core Python Rust + Python Python
เหมาะสำหรับ Engine แบบ Custom การเทรดระดับสถาบัน เริ่มต้นอย่างรวดเร็ว

ความแม่นยำในการจำลอง Fill

Fill simulation accuracy levels

การจำลอง Fill คือแหล่งหลักของความแตกต่างในการส่งคำสั่ง สามระดับความแม่นยำ:

ระดับ 1: Naive (เติมที่ราคา Close)

fill_price = candle['close']

ข้อผิดพลาด: ไม่คำนึงถึง Slippage, Spread, หรือ Partial Fills ประเมิน PnL เกินจริงอย่างเป็นระบบ

ระดับ 2: Slippage Model

def simulate_fill(order, candle, slippage_bps=5):
    """Fill with slippage."""
    base_price = candle['close']
    slip = base_price * slippage_bps / 10000

    if order.side == 'buy':
        return base_price + slip  # Buy at a higher price
    else:
        return base_price - slip  # Sell at a lower price

ข้อผิดพลาด: Slippage แบบคงที่ไม่คำนึงถึงสภาพคล่องและขนาดคำสั่ง ดีกว่า Naive แต่ยังเป็นโมเดลที่หยาบ

ระดับ 3: Adaptive Drill-down ด้วยข้อมูล 1s/100ms

ตัวเลือกที่ดีที่สุด: ใช้ข้อมูลความละเอียดสูงที่แท้จริงเพื่อกำหนด SL/TP Fill Order อย่างแม่นยำ อธิบายโดยละเอียดในบทความ Adaptive drill-down: การ Backtesting ด้วยความละเอียดแปรผัน

class RealisticFillModel:
    """
    Combined fill model: slippage + spread + volume impact.
    """
    def __init__(self, avg_spread_bps=3, impact_coeff=0.1):
        self.avg_spread_bps = avg_spread_bps
        self.impact_coeff = impact_coeff

    def simulate_fill(self, order, candle, order_size_usd):
        base_price = candle['close']

        spread_cost = base_price * self.avg_spread_bps / 20000

        candle_volume_usd = candle['volume'] * candle['close']
        participation_rate = order_size_usd / max(candle_volume_usd, 1)
        impact = base_price * self.impact_coeff * np.sqrt(participation_rate)

        if order.side == 'buy':
            return base_price + spread_cost + impact
        else:
            return base_price - spread_cost - impact

สูตร Market Impact (โมเดล Almgren-Chriss แบบง่าย):

Δp=σkVorderVmarket\Delta p = \sigma \cdot k \cdot \sqrt{\frac{V_{order}}{V_{market}}}

โดยที่ σ\sigma คือ Volatility, kk คือสัมประสิทธิ์ Impact, VorderV_{order} คือปริมาณคำสั่ง และ VmarketV_{market} คือปริมาณตลาดสำหรับช่วงเวลา

Checklist Parity ในทางปฏิบัติ

Holographic parity validation checklist organized by category — data, execution, timing, fees

ก่อนเปิดบอทสด ตรวจสอบแต่ละรายการ:

Code:

  • Strategy ใช้ Core ร่วม (โมดูลเดียวสำหรับ Backtest และการเทรดสด)
  • ไม่มีการซ้ำซ้อนของ Logic สัญญาณสองที่
  • Unit Tests ยืนยัน Output ของ Core ที่เหมือนกันสำหรับ Input ที่เหมือนกัน
  • ลำดับการตรวจสอบเงื่อนไขเหมือนกัน (SL ก่อน TP? TP ก่อน SL?)

ข้อมูล:

  • รูปแบบ Timestamp เหมือนกัน (UTC, Provider เดียวกัน)
  • การรวม OHLCV ใช้กฎเดียวกัน
  • การจัดการ Candle ที่หายไปเหมือนกัน
  • ไม่มี Look-ahead Bias — Backtest ไม่มองไปยังอนาคต

การส่งคำสั่ง:

  • Slippage Model ถูก Calibrate บนข้อมูลจริง
  • Partial Fills ถูกจำลอง (หรืออย่างน้อยประมาณการอย่างอนุรักษ์นิยม)
  • คำสั่ง Limit มีโมเดลลำดับคิว
  • Latency ถูกนำมาคิด (ล่าช้า 100-500 ms จากสัญญาณถึง Fill)

ต้นทุน:

  • Maker/Taker Commission รวมอยู่กับอัตราปัจจุบัน
  • Funding Rates คำนึงถึงกับ Perpetual Futures
  • Spread ถูกจำลอง (อย่างน้อยค่าเฉลี่ย)

โครงสร้างพื้นฐาน:

  • State Persistence: บอทกู้คืน Position หลังรีสตาร์ท
  • Logic การเชื่อมต่อใหม่: WebSocket เชื่อมต่อใหม่โดยไม่สูญเสียข้อมูล
  • การ Log: คำสั่งและ Fill ทั้งหมดถูก Log สำหรับการวิเคราะห์ Post-mortem

การตรวจสอบความแตกต่างในโปรดักชัน

Parity ไม่ใช่การตรวจสอบครั้งเดียวแต่เป็นกระบวนการต่อเนื่อง หลังเปิดบอท ความแตกต่างต้องถูกติดตามแบบเรียลไทม์

Shadow Mode (Paper Trading)

Shadow trading mode — live market data and simulated orders running in parallel

รันบอทควบคู่กับ Backtest บนข้อมูลเดียวกัน บอทสร้างสัญญาณแต่ไม่ส่งคำสั่ง — มันแค่ Log เท่านั้น ในเวลาเดียวกัน Backtest ประมวลผลข้อมูลเดียวกัน เปรียบเทียบ:

class DivergenceMonitor:
    """
    Compares backtest and live bot signals in real time.
    """
    def __init__(self, tolerance_pct=0.5):
        self.tolerance = tolerance_pct / 100
        self.divergences = []

    def compare_signal(self, backtest_signal, live_signal, timestamp):
        """Compare backtest and live signals."""
        if backtest_signal is None and live_signal is None:
            return  # Both silent — OK

        if (backtest_signal is None) != (live_signal is None):
            self.divergences.append({
                'timestamp': timestamp,
                'type': 'signal_mismatch',
                'backtest': backtest_signal,
                'live': live_signal,
                'severity': 'HIGH',
            })
            return

        price_diff = abs(
            backtest_signal.entry_price - live_signal.entry_price
        ) / backtest_signal.entry_price

        if price_diff > self.tolerance:
            self.divergences.append({
                'timestamp': timestamp,
                'type': 'price_divergence',
                'diff_pct': price_diff * 100,
                'severity': 'MEDIUM',
            })

    def compare_fill(self, backtest_fill, live_fill, timestamp):
        """Compare execution."""
        if backtest_fill and live_fill:
            slippage = (live_fill['price'] - backtest_fill['price']
                        ) / backtest_fill['price']
            self.divergences.append({
                'timestamp': timestamp,
                'type': 'fill_divergence',
                'slippage_bps': slippage * 10000,
                'severity': 'LOW' if abs(slippage) < 0.001 else 'MEDIUM',
            })

    def report(self):
        """Weekly divergence report."""
        from collections import Counter
        severity_counts = Counter(d['severity'] for d in self.divergences)
        return {
            'total_divergences': len(self.divergences),
            'by_severity': dict(severity_counts),
            'avg_slippage_bps': np.mean([
                d['slippage_bps'] for d in self.divergences
                if d['type'] == 'fill_divergence'
            ]) if any(d['type'] == 'fill_divergence'
                      for d in self.divergences) else 0,
        }

เมตริก Dashboard

เมตริก สูตร เกณฑ์แจ้งเตือน
Signal match rate matchestotal signals\frac{\text{matches}}{\text{total signals}} < 95%
Avg slippage 1Nsi\frac{1}{N}\sum s_i (bps) > 10 bps
Fill rate filledsent\frac{\text{filled}}{\text{sent}} < 90%
PnL divergence PnLlivePnLbtPnLbt\frac{PnL_{live} - PnL_{bt}}{PnL_{bt}} > 20%
Latency p99 เปอร์เซ็นไทล์ที่ 99 signal-to-fill > 500 ms

การ Calibrate Slippage Model

Slippage model calibration — order book depth with price impact curve showing expected vs actual fills

หลังจากสะสมข้อมูลเป็นเวลา 2-4 สัปดาห์ คุณสามารถ Calibrate Slippage Model ของ Backtest บนข้อมูลจริง:

def calibrate_slippage(live_fills: list[dict]) -> dict:
    """
    Calibrate slippage model using real fills.

    live_fills: [{'expected_price': ..., 'actual_price': ..., 'size_usd': ..., 'volume_usd': ...}]
    """
    slippages = []
    participation_rates = []

    for fill in live_fills:
        slip = abs(fill['actual_price'] - fill['expected_price']
                   ) / fill['expected_price']
        part = fill['size_usd'] / max(fill['volume_usd'], 1)
        slippages.append(slip)
        participation_rates.append(part)

    slippages = np.array(slippages)
    participation_rates = np.array(participation_rates)

    from scipy.optimize import curve_fit

    def model(x, k, base):
        return k * np.sqrt(x) + base

    popt, _ = curve_fit(model, participation_rates, slippages,
                        p0=[0.1, 0.0001])

    return {
        'impact_coeff': popt[0],
        'base_slippage': popt[1],
        'mean_slippage_bps': np.mean(slippages) * 10000,
        'p95_slippage_bps': np.percentile(slippages, 95) * 10000,
    }

การเชื่อมโยงกับเครื่องมืออื่น

Backtest-live parity ไม่ใช่งานที่แยกต่างหาก มันตัดกับเครื่องมืออื่นๆ จากซีรีส์ "Backtests Without Illusions":

  • Adaptive drill-down — ปรับปรุงความแม่นยำของการจำลอง Fill เป็นส่วนประกอบสำคัญของ Execution Parity
  • Funding rates — ถ้า Backtest ไม่จำลอง Funding, Parity เป็นไปไม่ได้ที่ Leverage > 3x
  • Parquet cache — Timeframe และ Indicator ที่คำนวณล่วงหน้าช่วยให้ Backtest เห็นข้อมูลเดียวกับบอท RunningCandleBuffer Emulation = การอัปเดตแบบเรียลไทม์
  • Polars vs Pandas — เมื่อสลับจาก Pandas (Backtest) ไปยัง Polars (สด) คุณต้องมั่นใจว่าผลลัพธ์ตัวเลขตรงกัน
  • Walk-Forward — Walk-forward บนข้อมูล Out-of-sample แสดงว่า Strategy เสื่อมลงอย่างไร — ใกล้กับการเทรดสดมากกว่า Backtest แบบ In-sample

ข้อแนะนำ

  1. Shared Core เป็นสิ่งจำเป็น Codebase เดียวสำหรับการสร้างสัญญาณคือข้อกำหนดขั้นต่ำสำหรับ Parity สองไฟล์ที่มี Logic เหมือนกันรับประกันความแตกต่างภายในหนึ่งเดือน

  2. Calibrate Fill Model Slippage แบบคงที่ 5 bps ดีกว่าไม่มีเลย Slippage Model ที่ Calibrate บนข้อมูลจริงดีกว่าอย่างมีนัยสำคัญ

  3. ใช้ Shadow Mode ในช่วง 2-4 สัปดาห์แรก อย่าเทรดด้วยเงินจริงจนกว่า Signal Match Rate จะถึง 95%+

  4. จำลอง Funding Rates สำหรับ Perpetual Futures สิ่งนี้ไม่ใช่ตัวเลือก — เป็นสิ่งจำเป็น Funding สามารถกินกำไร PnL ทั้งหมดที่ Leverage > 5x

  5. Log ทุกอย่าง ทุกสัญญาณ ทุกคำสั่ง ทุก Fill — พร้อม Timestamp โดยไม่มี Log การวิเคราะห์ Post-mortem เป็นไปไม่ได้

  6. ทำให้การเปรียบเทียบเป็นอัตโนมัติ รายงาน DivergenceMonitor รายสัปดาห์ควรมาถึงโดยอัตโนมัติ อย่ารอจนกว่า PnL จะเป็นลบ

  7. Backtest แบบอนุรักษ์นิยมเป็นค่าเริ่มต้น ดีกว่าที่จะประเมินความคาดหวังต่ำใน Backtest และรู้สึกประหลาดใจในทางที่ดีในการเทรดสดมากกว่าตรงกันข้าม Slippage Model ควรเป็นแบบอนุรักษ์นิยม

สรุป

Trading system maturity levels — from basic backtesting to full production

Backtest-live parity ไม่ใช่คุณสมบัติของระบบแต่เป็น กระบวนการ Parity ที่สมบูรณ์แบบไม่มีอยู่จริง: Backtest คือโดยนิยามแบบจำลองของความเป็นจริง และโมเดลมักจะทำให้ง่ายขึ้นเสมอ แต่ความแตกต่างระหว่าง "โมเดลต่างกัน 5%" และ "โมเดลต่างกัน 50%" ถูกกำหนดโดยสถาปัตยกรรม

สามระดับความเป็นผู้ใหญ่:

  1. ขั้นพื้นฐาน Shared Core, Slippage แบบคงที่, Commission ความแตกต่าง: 10-20%
  2. ขั้นสูง สถาปัตยกรรม Event-driven, Adaptive Drill-down, Funding Model, Shadow Mode ความแตกต่าง: 5-10%
  3. ระดับสถาบัน การจำลอง L2 Orderbook, Calibrated Impact Model, การตรวจสอบความแตกต่างแบบเรียลไทม์ ความแตกต่าง: 2-5%

งานของคุณคือกำหนดว่าคุณอยู่ระดับใดและทำความเข้าใจ ว่าความแตกต่างใดที่คุณถือว่ายอมรับได้ สำหรับขนาด Position และ Leverage ของคุณ


ลิงก์ที่เป็นประโยชน์

  1. NautilusTrader — High-Performance Algorithmic Trading Platform
  2. Freqtrade — Free, open source crypto trading bot
  3. Almgren, R., Chriss, N. — Optimal Execution of Portfolio Transactions (2001)
  4. Lopez de Prado — Advances in Financial Machine Learning, Chapter 12: Backtesting
  5. Ernest Chan — Quantitative Trading: How to Build Your Own Algorithmic Trading Business
  6. Hexagonal Architecture (Ports and Adapters) — Alistair Cockburn
  7. Optuna — Hyperparameter Optimization Framework

การอ้างอิง

@article{soloviov2026backtestliveparity,
  author = {Soloviov, Eugen},
  title = {Backtest-live parity: why your bot trades differently from the backtest},
  year = {2026},
  url = {https://marketmaker.cc/ru/blog/post/backtest-live-parity},
  description = {Complete taxonomy of divergences between backtesting and live trading: from slippage and partial fills to codebase desynchronization. Architectural patterns for achieving parity and a production monitoring checklist.}
}
ข้อจำกัดความรับผิดชอบ: ข้อมูลที่ให้ไว้ในบทความนี้มีไว้เพื่อการศึกษาและให้ข้อมูลเท่านั้น และไม่ถือเป็นคำแนะนำทางการเงิน การลงทุน หรือการเทรด การเทรดสกุลเงินดิจิทัลมีความเสี่ยงสูงที่จะขาดทุน

ผู้เขียน

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

ก้าวนำหน้าตลาด

สมัครรับจดหมายข่าวของเราเพื่อรับข้อมูลเชิงลึกการเทรดด้วย AI เฉพาะ การวิเคราะห์ตลาด และการอัปเดตแพลตฟอร์ม

เราเคารพความเป็นส่วนตัวของคุณ ยกเลิกการสมัครได้ทุกเมื่อ