บทนำ: ทำไมต้อง Backtest ด้วย Historical Orderbook

การ backtest คือการทดสอบกลยุทธ์การเทรดด้วยข้อมูลในอดีตก่อนนำไปใช้จริง และ orderbook data เป็นข้อมูลที่มีคุณค่ามากเพราะแสดง liquidity, market depth และพฤติกรรมของ market makers อย่างละเอียด ผมใช้ Tardis.dev API มากว่า 2 ปีสำหรับโปรเจกต์ quantitative trading และต้องบอกว่า coverage ของ orderbook data ครอบคลุมมากที่สุดในตลาด crypto Tardis.dev มี data ตั้งแต่ปี 2019 สำหรับ Binance Futures และรองรับ granular levels หลายระดับ ตั้งแต่ raw trades ไปจนถึง aggregated orderbook snapshots

สถาปัตยกรรมของระบบ Backtest

ก่อนเข้าสู่โค้ด มาดู architecture ของระบบที่เราจะสร้างกัน:
┌─────────────────────────────────────────────────────────────────┐
│                    BACKTEST ARCHITECTURE                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   ┌──────────────┐    ┌──────────────┐    ┌──────────────┐     │
│   │  Tardis.dev  │───▶│   Python     │───▶│   Strategy   │     │
│   │    API       │    │  Data Loader │    │   Engine     │     │
│   │              │    │              │    │              │     │
│   │ • Orderbook  │    │ • Streaming  │    │ • Signals    │     │
│   │ • Trades     │    │ • Caching    │    │ • Execution  │     │
│   │ • Kline      │    │ • Normalize  │    │ • Metrics    │     │
│   └──────────────┘    └──────────────┘    └──────────────┘     │
│                             │                    │             │
│                             ▼                    ▼             │
│                      ┌──────────────┐    ┌──────────────┐      │
│                      │   SQLite/    │    │   Results    │      │
│                      │   Parquet    │    │   Analysis   │      │
│                      └──────────────┘    └──────────────┘      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

การติดตั้งและ Setup Environment

# สร้าง virtual environment
python -m venv venv
source venv/bin/activate  # Linux/Mac หรือ venv\Scripts\activate บน Windows

ติดตั้ง dependencies ที่จำเป็น

pip install tardis-client pandas numpy pyarrow sqlalchemy aiohttp pip install asyncpg # สำหรับ PostgreSQL (optional) pip install python-dotenv redis # สำหรับ caching

สร้างไฟล์ .env

cat > .env << 'EOF' TARDIS_API_KEY=your_tardis_api_key_here DATA_DIR=./backtest_data CACHE_ENABLED=true REDIS_URL=redis://localhost:6379 EOF

การใช้งาน Tardis.dev API Client

import os
from dotenv import load_dotenv
from tardis_client import TardisClient, Channel, MessageType
import asyncio
import pandas as pd
from typing import Dict, List, Optional
from dataclasses import dataclass, field
from datetime import datetime, timedelta
import json
import hashlib

load_dotenv()

@dataclass
class OrderbookLevel:
    """โครงสร้างข้อมูล orderbook level"""
    price: float
    quantity: float
    side: str  # 'bid' หรือ 'ask'
    
@dataclass
class OrderbookSnapshot:
    """Orderbook snapshot ณ จุดเวลาใดเวลาหนึ่ง"""
    timestamp: datetime
    symbol: str
    bids: List[OrderbookLevel]  # sorted descending by price
    asks: List[OrderbookLevel]  # sorted ascending by price
    local_timestamp: datetime = field(default_factory=datetime.now)
    
    @property
    def best_bid(self) -> float:
        return self.bids[0].price if self.bids else 0.0
    
    @property
    def best_ask(self) -> float:
        return self.asks[0].price if self.asks else 0.0
    
    @property
    def mid_price(self) -> float:
        return (self.best_bid + self.best_ask) / 2
    
    @property
    def spread(self) -> float:
        return self.best_ask - self.best_bid
    
    @property
    def spread_bps(self) -> float:
        """Spread ในหน่วย basis points"""
        return (self.spread / self.mid_price) * 10000 if self.mid_price > 0 else 0


class TardisDataLoader:
    """
    Data loader สำหรับดึง historical orderbook data จาก Tardis.dev
    รองรับ both replay mode และ historical query mode
    """
    
    def __init__(self, api_key: str, exchange: str = "binance-futures"):
        self.api_key = api_key
        self.exchange = exchange
        self.client = None
        self._orderbook_cache: Dict[str, List[OrderbookSnapshot]] = {}
        
    async def __aenter__(self):
        self.client = TardisClient(api_key=self.api_key)
        return self
    
    async def __aexit__(self, *args):
        if self.client:
            await self.client.close()
            
    async def fetch_orderbook_range(
        self,
        symbol: str,
        start_time: datetime,
        end_time: datetime,
        symbols_filter: Optional[List[str]] = None
    ) -> List[OrderbookSnapshot]:
        """
        ดึง orderbook snapshots ในช่วงเวลาที่กำหนด
        
        Args:
            symbol: เช่น 'BTCUSDT'
            start_time: วันที่เริ่มต้น
            end_time: วันที่สิ้นสุด
            symbols_filter: list of symbols to filter (for efficiency)
            
        Returns:
            List of OrderbookSnapshot objects
        """
        cache_key = f"{symbol}_{start_time.isoformat()}_{end_time.isoformat()}"
        
        # Check cache first
        if cache_key in self._orderbook_cache:
            print(f"📦 Using cached data for {symbol}")
            return self._orderbook_cache[cache_key]
        
        print(f"📥 Fetching orderbook data for {symbol} from {start_time} to {end_time}")
        
        # Convert to milliseconds
        from_ms = int(start_time.timestamp() * 1000)
        to_ms = int(end_time.timestamp() * 1000)
        
        orderbook_data = []
        
        # Replay mode - iterates through historical data
        async for message in self.client.replay(
            exchange=self.exchange,
            from_timestamp=from_ms,
            to_timestamp=to_ms,
            filters=[Channel(name=f"{symbol}@orderbook", type="orderbook")]
        ):
            if message.type == MessageType.Snapshot:
                snapshot = self._parse_orderbook_message(message, symbol)
                if snapshot:
                    orderbook_data.append(snapshot)
        
        # Cache the results
        self._orderbook_cache[cache_key] = orderbook_data
        print(f"✅ Loaded {len(orderbook_data)} orderbook snapshots")
        
        return orderbook_data
    
    def _parse_orderbook_message(self, message, symbol: str) -> Optional[OrderbookSnapshot]:
        """Parse Tardis message เป็น OrderbookSnapshot"""
        try:
            data = message.data
            
            # Binance Futures orderbook structure
            bids = [
                OrderbookLevel(price=float(b[0]), quantity=float(b[1]), side='bid')
                for b in data.get('b', data.get('bids', []))
            ]
            asks = [
                OrderbookLevel(price=float(a[0]), quantity=float(a[1]), side='ask')
                for a in data.get('a', data.get('asks', []))
            ]
            
            # Sort bids descending, asks ascending
            bids.sort(key=lambda x: x.price, reverse=True)
            asks.sort(key=lambda x: x.price)
            
            return OrderbookSnapshot(
                timestamp=datetime.fromtimestamp(message.timestamp / 1000),
                symbol=symbol,
                bids=bids,
                asks=asks
            )
        except Exception as e:
            print(f"⚠️ Error parsing orderbook message: {e}")
            return None
    
    async def fetch_trades(
        self,
        symbol: str,
        start_time: datetime,
        end_time: datetime
    ) -> pd.DataFrame:
        """ดึง trade data สำหรับ analysis"""
        from_ms = int(start_time.timestamp() * 1000)
        to_ms = int(end_time.timestamp() * 1000)
        
        trades = []
        
        async for message in self.client.replay(
            exchange=self.exchange,
            from_timestamp=from_ms,
            to_timestamp=to_ms,
            filters=[Channel(name=f"{symbol}@trade", type="trade")]
        ):
            trades.append({
                'timestamp': datetime.fromtimestamp(message.timestamp / 1000),
                'price': float(message.data['p']),
                'quantity': float(message.data['q']),
                'side': message.data.get('m', None),  # maker sell = True
                'trade_id': message.data.get('t')
            })
        
        return pd.DataFrame(trades)


ตัวอย่างการใช้งาน

async def main(): api_key = os.getenv("TARDIS_API_KEY") async with TardisDataLoader(api_key) as loader: # ดึงข้อมูล 1 ชั่วโมงล่าสุด end_time = datetime.now() start_time = end_time - timedelta(hours=1) snapshots = await loader.fetch_orderbook_range( symbol="BTCUSDT", start_time=start_time, end_time=end_time ) if snapshots: print(f"\n📊 Orderbook Analysis:") print(f" Best Bid Range: {min(s.mid_price for s in snapshots):.2f} - {max(s.mid_price for s in snapshots):.2f}") print(f" Avg Spread: {sum(s.spread for s in snapshots) / len(snapshots):.4f} USDT") if __name__ == "__main__": asyncio.run(main())

การสร้าง Backtest Engine

import pandas as pd
import numpy as np
from typing import Dict, List, Optional, Callable
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
import statistics

class OrderSide(Enum):
    BUY = "BUY"
    SELL = "SELL"
    
@dataclass
class Position:
    """ข้อมูล position ปัจจุบัน"""
    symbol: str
    side: OrderSide
    entry_price: float
    quantity: float
    entry_time: datetime
    unrealized_pnl: float = 0.0
    
@dataclass
class Trade:
    """Record ของ trade ที่เกิดขึ้น"""
    timestamp: datetime
    side: OrderSide
    price: float
    quantity: float
    pnl: float = 0.0
    commission: float = 0.0
    slippage: float = 0.0
    
@dataclass
class BacktestResult:
    """ผลลัพธ์ของ backtest"""
    total_trades: int = 0
    winning_trades: int = 0
    losing_trades: int = 0
    total_pnl: float = 0.0
    max_drawdown: float = 0.0
    sharpe_ratio: float = 0.0
    win_rate: float = 0.0
    avg_win: float = 0.0
    avg_loss: float = 0.0
    profit_factor: float = 0.0
    trades: List[Trade] = field(default_factory=list)
    
    def summary(self) -> str:
        return f"""
╔══════════════════════════════════════════════════════════╗
║              BACKTEST RESULTS SUMMARY                     ║
╠══════════════════════════════════════════════════════════╣
║  Total Trades:        {self.total_trades:>10}                      ║
║  Win Rate:            {self.win_rate*100:>10.2f}%                     ║
║  Total P&L:           {self.total_pnl:>10.2f} USDT                ║
║  Max Drawdown:        {self.max_drawdown:>10.2f}%                     ║
║  Sharpe Ratio:        {self.sharpe_ratio:>10.2f}                      ║
║  Profit Factor:       {self.profit_factor:>10.2f}                      ║
║  Avg Win:             {self.avg_win:>10.2f} USDT                ║
║  Avg Loss:            {self.avg_loss:>10.2f} USDT                ║
╚══════════════════════════════════════════════════════════╝
"""


class MeanReversionStrategy:
    """
    Mean Reversion Strategy โดยใช้ orderbook data
    
    Logic:
    1. คำนวณ mid price และ moving average
    2. เมื่อ price ห่างจาก MA เกิน threshold → signal
    3. ซื้อเมื่อ underpriced (ราคาต่ำกว่า MA มาก)
    4. ขายเมื่อ overpriced (ราคาสูงกว่า MA มาก)
    """
    
    def __init__(
        self,
        window: int = 20,
        entry_threshold: float = 2.0,  # ห่างกี่ std
        exit_threshold: float = 0.5,
        position_size: float = 0.1  # 10% ของ capital
    ):
        self.window = window
        self.entry_threshold = entry_threshold
        self.exit_threshold = exit_threshold
        self.position_size = position_size
        self.price_history: List[float] = []
        self.ma: float = 0
        self.std: float = 0
        
    def update(self, mid_price: float) -> Optional[str]:
        """อัพเดท strategy และ return signal"""
        self.price_history.append(mid_price)
        
        if len(self.price_history) < self.window:
            return None
            
        # Keep window size
        if len(self.price_history) > self.window:
            self.price_history.pop(0)
            
        # Calculate stats
        self.ma = statistics.mean(self.price_history)
        self.std = statistics.stdev(self.price_history)
        
        if self.std == 0:
            return None
            
        z_score = (mid_price - self.ma) / self.std
        
        # Signals
        if z_score < -self.entry_threshold:
            return "LONG"   # Underpriced - คาดว่าราคาจะกลับขึ้น
        elif z_score > self.entry_threshold:
            return "SHORT"  # Overpriced - คาดว่าราคาจะลง
        elif abs(z_score) < self.exit_threshold:
            if z_score < 0:
                return "CLOSE_SHORT"
            else:
                return "CLOSE_LONG"
                
        return None


class BacktestEngine:
    """
    Backtest engine สำหรับทดสอบกลยุทธ์ด้วย historical data
    """
    
    def __init__(
        self,
        initial_capital: float = 10000.0,
        commission_rate: float = 0.0004,  # Binance Futures: 0.04%
        slippage_bps: float = 2.0  # 2 basis points
    ):
        self.initial_capital = initial_capital
        self.capital = initial_capital
        self.commission_rate = commission_rate
        self.slippage_bps = slippage_bps
        self.position: Optional[Position] = None
        self.trades: List[Trade] = []
        self.equity_curve: List[float] = [initial_capital]
        self.timestamps: List[datetime] = []
        
    def execute_signal(
        self,
        signal: str,
        price: float,
        timestamp: datetime
    ) -> Optional[Trade]:
        """Execute trade based on signal"""
        trade = None
        
        if signal == "LONG" and self.position is None:
            # Open long position
            quantity = (self.capital * 0.1) / price  # 10% position size
            slippage = price * (self.slippage_bps / 10000)
            execution_price = price + slippage
            commission = execution_price * quantity * self.commission_rate
            
            self.position = Position(
                symbol="BTCUSDT",
                side=OrderSide.BUY,
                entry_price=execution_price,
                quantity=quantity,
                entry_time=timestamp
            )
            
            trade = Trade(
                timestamp=timestamp,
                side=OrderSide.BUY,
                price=execution_price,
                quantity=quantity,
                commission=commission,
                slippage=slippage
            )
            
        elif signal == "SHORT" and self.position is None:
            # Open short position
            quantity = (self.capital * 0.1) / price
            slippage = price * (self.slippage_bps / 10000)
            execution_price = price - slippage  # Short: execute below market
            commission = execution_price * quantity * self.commission_rate
            
            self.position = Position(
                symbol="BTCUSDT",
                side=OrderSide.SELL,
                entry_price=execution_price,
                quantity=quantity,
                entry_time=timestamp
            )
            
            trade = Trade(
                timestamp=timestamp,
                side=OrderSide.SELL,
                price=execution_price,
                quantity=quantity,
                commission=commission,
                slippage=slippage
            )
            
        elif signal in ["CLOSE_LONG", "CLOSE_SHORT"] and self.position:
            # Close position
            is_long = self.position.side == OrderSide.BUY
            
            if (signal == "CLOSE_LONG" and is_long) or \
               (signal == "CLOSE_SHORT" and not is_long):
                
                slippage = price * (self.slippage_bps / 10000)
                exit_price = price - slippage if is_long else price + slippage
                commission = exit_price * self.position.quantity * self.commission_rate
                
                # Calculate P&L
                if is_long:
                    pnl = (exit_price - self.position.entry_price) * self.position.quantity
                else:
                    pnl = (self.position.entry_price - exit_price) * self.position.quantity
                    
                pnl -= commission
                
                trade = Trade(
                    timestamp=timestamp,
                    side=OrderSide.SELL if is_long else OrderSide.BUY,
                    price=exit_price,
                    quantity=self.position.quantity,
                    pnl=pnl,
                    commission=commission,
                    slippage=slippage
                )
                
                self.capital += pnl
                self.position = None
                
        if trade:
            self.trades.append(trade)
            self.equity_curve.append(self.capital)
            self.timestamps.append(timestamp)
            
        return trade
    
    def run(
        self,
        strategy: MeanReversionStrategy,
        orderbook_snapshots: List
    ) -> BacktestResult:
        """Run backtest with orderbook data"""
        print(f"🚀 Starting backtest with {len(orderbook_snapshots)} data points")
        
        for snapshot in orderbook_snapshots:
            mid_price = snapshot.mid_price
            timestamp = snapshot.timestamp
            
            signal = strategy.update(mid_price)
            if signal:
                trade = self.execute_signal(signal, mid_price, timestamp)
                if trade:
                    print(f"📋 {timestamp} | {signal} @ {trade.price:.2f} | P&L: {trade.pnl:.2f}")
        
        # Close any remaining position at last price
        if self.position:
            last_snapshot = orderbook_snapshots[-1]
            self.execute_signal("CLOSE_LONG", last_snapshot.mid_price, last_snapshot.timestamp)
            
        return self.calculate_results()
    
    def calculate_results(self) -> BacktestResult:
        """Calculate backtest metrics"""
        result = BacktestResult()
        
        if not self.trades:
            return result
            
        closed_trades = [t for t in self.trades if t.pnl != 0]
        result.total_trades = len(closed_trades)
        
        winning = [t for t in closed_trades if t.pnl > 0]
        losing = [t for t in closed_trades if t.pnl <= 0]
        
        result.winning_trades = len(winning)
        result.losing_trades = len(losing)
        result.total_pnl = sum(t.pnl for t in closed_trades)
        
        if result.total_trades > 0:
            result.win_rate = result.winning_trades / result.total_trades
            
        if winning:
            result.avg_win = sum(t.pnl for t in winning) / len(winning)
        if losing:
            result.avg_loss = abs(sum(t.pnl for t in losing) / len(losing))
            
        if result.avg_loss > 0:
            result.profit_factor = result.avg_win * result.winning_trades / (result.avg_loss * result.losing_trades)
            
        # Calculate max drawdown
        peak = self.equity_curve[0]
        max_dd = 0
        for equity in self.equity_curve:
            if equity > peak:
                peak = equity
            dd = (peak - equity) / peak * 100
            if dd > max_dd:
                max_dd = dd
        result.max_drawdown = max_dd
        
        # Calculate Sharpe ratio (annualized)
        if len(self.equity_curve) > 1:
            returns = np.diff(self.equity_curve) / self.equity_curve[:-1]
            if np.std(returns) > 0:
                result.sharpe_ratio = np.mean(returns) / np.std(returns) * np.sqrt(252 * 24)  # Assuming hourly data
                
        result.trades = closed_trades
        
        return result


ตัวอย่างการรัน backtest

async def run_backtest_example(): from tardis_data_loader import TardisDataLoader api_key = os.getenv("TARDIS_API_KEY") async with TardisDataLoader(api_key) as loader: # ดึงข้อมูล 7 วัน end_time = datetime.now() start_time = end_time - timedelta(days=7) print("📥 Fetching orderbook data...") snapshots = await loader.fetch_orderbook_range( symbol="BTCUSDT", start_time=start_time, end_time=end_time ) if len(snapshots) < 100: print("⚠️ Not enough data for backtest, using sample data") # Generate sample data for demonstration snapshots = generate_sample_orderbook_data(days=7) # Initialize strategy and engine strategy = MeanReversionStrategy( window=50, entry_threshold=1.5, exit_threshold=0.3, position_size=0.1 ) engine = BacktestEngine( initial_capital=10000, commission_rate=0.0004, slippage_bps=2.0 ) # Run backtest results = engine.run(strategy, snapshots) # Print results print(results.summary()) # Save detailed results df = pd.DataFrame([ { 'timestamp': t.timestamp, 'side': t.side.value, 'price': t.price, 'quantity': t.quantity, 'pnl': t.pnl, 'commission': t.commission } for t in results.trades ]) df.to_csv('backtest_results.csv', index=False) print("💾 Results saved to backtest_results.csv") return results def generate_sample_orderbook_data(days: int = 1) -> List: """Generate synthetic orderbook data for testing""" snapshots = [] base_price = 45000.0 now = datetime.now() # Generate hourly snapshots for i in range(days * 24): timestamp = now - timedelta(hours=days * 24 - i) # Random walk with mean reversion tendency change = np.random.normal(0, 10) base_price = base_price * 0.999 + (base_price + change) * 0.001 spread = 0.5 + np.random.exponential(0.5) mid = base_price bids = [ OrderbookLevel(price=mid - spread/2 - j * 0.1, quantity=1 + np.random.random(), side='bid') for j in range(10) ] asks = [ OrderbookLevel(price=mid + spread/2 + j * 0.1, quantity=1 + np.random.random(), side='ask') for j in range(10) ] snapshots.append(OrderbookSnapshot( timestamp=timestamp, symbol="BTCUSDT", bids=bids, asks=asks )) return snapshots if __name__ == "__main__": asyncio.run(run_backtest_example())

การ Optimize Performance สำหรับ Large Dataset

เมื่อต้องทำ backtest กับข้อมูลหลายเดือน ประสิทธิภาพของโค้ดมีความสำคัญมาก ผมได้ทำ benchmark และพบว่ามีหลายจุดที่ต้อง optimize:
"""
Performance Optimization Module สำหรับ Large-scale Backtesting
รวบรวมเทคนิคที่ใช้ลดเวลา processing ลง 10-50 เท่า
"""

import numpy as np
import pandas as pd
from numba import jit
import pyarrow as pa
import pyarrow.parquet as pq
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
from functools import lru_cache
import mmap
import struct
from typing import Generator
import time

============================================================

1. NUMBA JIT COMPILATION สำหรับ Calculation-Intensive Tasks

============================================================

@jit(nopython=True, cache=True) def calculate_z_score_numba(prices: np.ndarray, window: int) -> np.ndarray: """ Calculate rolling z-score using Numba for 10-50x speedup ใช้ Numba JIT compile เป็น machine code โดยตรง """ n = len(prices) z_scores = np.full(n, np.nan) for i in range(window, n): window_data = prices[i-window:i] mean = np.mean(window_data) std = np.std(window_data) if std > 0: z_scores[i] = (prices[i] - mean) / std return z_scores @jit(nopython=True, cache=True, parallel=True) def calculate_orderbook_metrics_numba( bids: np.ndarray, asks: np.ndarray, quantities_bid: np.ndarray, quantities_ask: np.ndarray ) -> np.ndarray: """ Calculate multiple orderbook metrics in parallel - VWAP - Market depth - Order flow imbalance """ n = len(bids) results = np.zeros((n, 5)) # mid, spread, depth, imbalance, vwap for i in prange(n): if bids[i] > 0 and asks[i] > 0: # Mid price results[i, 0] = (bids[i] + asks[i]) / 2 # Spread results[i, 1] = asks[i] - bids[i] # Depth (sum of top 10 levels) depth_bid = 0 depth_ask = 0 for j in range(min(10, len(quantities_bid[i]))): depth_bid += quantities_bid[i, j] depth_ask += quantities_ask[i, j] results[i, 2] = depth_bid + depth_ask # Order flow imbalance total_bid_qty = np.sum(quantities_bid[i]) total_ask_qty = np.sum(quantities_ask[i]) if total_bid_qty + total_ask_qty > 0: results[i, 3] = (total_bid_qty - total_ask_qty) / (total_bid_qty + total_ask_qty) # VWAP bid_volume = bids[i] * quantities_bid[i] ask_volume = asks[i] * quantities_ask[i] total_volume = np.sum(quantities_bid[i]) + np.sum(quantities_ask[i]) if total_volume > 0: results[i, 4] = np.sum(bid_volume + ask_volume) / total_volume return results

============================================================

2. VECTORIZED OPERATIONS ด้วย NumPy

============================================================

class VectorizedBacktest: """ Backtest engine ที่ใช้ vectorized operations แทน loops เร็วกว่า loop-based ถึง 100 เท่าสำหรับ large datasets """ def __init__(self, initial_capital: float = 10000.0): self.initial_capital = initial_capital def preprocess_orderbook_to_arrays( self, orderbook_snapshots: list ) -> dict: """ แปลง orderbook snapshots เป็น numpy arrays สำหรับ vectorized operations """ n = len(orderbook_snapshots) # Initialize arrays timestamps = np.zeros(n, dtype=np.int64) mid_prices = np.zeros(n, dtype=np.float64) spreads = np.zeros(n, dtype=np.float64) bid_depths = np.zeros(n, dtype=np.float64) ask_depths = np.zeros(n, dtype=np.float64) for i, snapshot in enumerate(orderbook_snapshots): timestamps[i] = int(snapshot.timestamp.timestamp() * 1000) mid_prices[i] = snapshot.mid_price spreads[i] = snapshot.spread # Calculate depth bid_depths[i] = sum(b.quantity for b in snapshot.bids[:10]) ask_depths[i] = sum(a.quantity for a in snapshot.asks[:10]) return { 'timestamps': timestamps, 'mid_prices': mid_prices, 'spreads': spreads, 'bid_depths': bid_depths, 'ask_depths': ask_depths } def generate_signals_vectorized( self, mid_prices: np.ndarray, window: int = 20, entry_threshold: float = 2.0, exit_threshold: float = 0.5 ) -> np.ndarray: """ Generate trading signals using vectorized z-score calculation แทนที่จะ loop ผ่านแต่ละ timestamp ใช้ NumPy vectorized operations """ n = len(mid_prices) # Use Numba-optimized function z_scores = calculate_z_score_numba(mid_prices, window) # Generate signals (0 = no signal, 1 = long, -1 = short, 2 = close) signals = np.zeros(n, dtype=np.int8) signals[z_scores < -entry_threshold] = 1 # LONG signals[z_scores > entry_threshold] = -1 # SHORT signals[np.abs(z_scores) < exit_threshold] = 2 # CLOSE return signals def run_vectorized_backtest(