Giới Thiệu Tổng Quan

Khi xây dựng các hệ thống giao dịch algorithm (algo trading), backtesting, hoặc phân tích thị trường, dữ liệu order book là yếu tố quyết định độ chính xác của chiến lược. Bài viết này sẽ hướng dẫn chi tiết cách sử dụng Tardis.dev Python API để tải dữ liệu order book theo tick-by-tick từ Binance Futures, bao gồm cách parse dữ liệu, lưu trữ hiệu quả, và mô phỏng撮合 (matching engine) để tái tạo trạng thái thị trường.

Bảng So Sánh: HolySheep vs API Chính Thức vs Các Dịch Vụ Relay

Trước khi đi vào chi tiết kỹ thuật, hãy cùng xem bảng so sánh toàn diện giữa các phương án tiếp cận dữ liệu thị trường hiện nay:
Tiêu chí HolySheep AI Binance API Chính Thức Tardis.dev ccxt
Chi phí hàng tháng $15-200/tháng Miễn phí (rate limited) $99-999/tháng Miễn phí
Chi phí per token (GPT-4.1) $8/MTok $8/MTok $8/MTok $8/MTok
Độ trễ trung bình <50ms 100-300ms 60-150ms 150-400ms
Dữ liệu lịch sử Limited 7 ngày (klines) Full history Phụ thuộc exchange
WebSocket real-time
Hỗ trợ thanh toán WeChat/Alipay/Visa Chỉ card quốc tế Card quốc tế N/A
Tín dụng miễn phí đăng ký Có ($5-20) Không Có ($5) N/A
Setup ban đầu 5 phút 30-60 phút 15-30 phút 20-40 phút

Tardis.dev Là Gì và Tại Sao Cần Nó?

Tardis.dev là dịch vụ cung cấp dữ liệu thị trường cryptocurrency dưới dạng normalized format, hỗ trợ replay historical data với độ chính xác cao. Khác với API chính thức của sàn, Tardis.dev cung cấp:

Phù Hợp Với Ai

Nên dùng Tardis.dev Python API khi:

Không phù hợp khi:

Cài Đặt Môi Trường và Dependencies

# Cài đặt các thư viện cần thiết
pip install tardis-dev pandas numpy asyncio aiohttp msgpack

Hoặc sử dụng poetry

poetry add tardis-dev pandas numpy aiohttp msgpack

Kiểm tra phiên bản

python -c "import tardis; print(tardis.__version__)"

Output: 3.2.1 hoặc newer

Kết Nối và Tải Dữ Liệu Order Book Từ Binance

import asyncio
import aiohttp
import json
from datetime import datetime, timedelta
from dataclasses import dataclass
from typing import List, Dict, Optional
import pandas as pd

Cấu hình Tardis.dev API

TARDIS_API_KEY = "YOUR_TARDIS_API_KEY" BASE_URL = "https://api.tardis.dev/v1" @dataclass class OrderBookEntry: """Cấu trúc một entry trong order book""" price: float size: float side: str # 'bid' hoặc 'ask' timestamp: int # microseconds local_timestamp: int @dataclass class OrderBookSnapshot: """Snapshot đầy đủ của order book tại một thời điểm""" symbol: str bids: List[OrderBookEntry] asks: List[OrderBookEntry] timestamp: int exchange: str = "binance-futures" class BinanceOrderBookDownloader: """Download và xử lý dữ liệu order book từ Tardis.dev""" def __init__(self, api_key: str): self.api_key = api_key self.base_url = BASE_URL async def get_order_book_replay( self, symbol: str = "BTC-USDT-PERP", start_date: datetime = None, end_date: datetime = None, channels: List[str] = None ): """ Tải dữ liệu order book replay từ Tardis.dev Args: symbol: Cặp giao dịch (format: BTC-USDT-PERP) start_date: Thời điểm bắt đầu end_date: Thời điểm kết thúc channels: Các kênh dữ liệu cần subscribe """ if channels is None: channels = ["book_snapshot", "book_update"] # Format thời gian theo ISO 8601 start_str = start_date.isoformat() if start_date else None end_str = end_date.isoformat() if end_date else None params = { "symbol": symbol, "channels": ",".join(channels), "from": start_str, "to": end_str, "format": "json" } headers = { "Authorization": f"Bearer {self.api_key}", "Accept": "application/x-ndjson" } all_data = [] async with aiohttp.ClientSession() as session: async with session.get( f"{self.base_url}/replay", params=params, headers=headers ) as response: if response.status != 200: error = await response.text() raise Exception(f"API Error {response.status}: {error}") # Đọc dữ liệu dạng newline-delimited JSON async for line in response.content: if line.strip(): try: data = json.loads(line) all_data.append(data) except json.JSONDecodeError: continue return all_data

Sử dụng

downloader = BinanceOrderBookDownloader(TARDIS_API_KEY)

Tải dữ liệu 1 ngày

start = datetime(2024, 3, 15, 0, 0, 0) end = datetime(2024, 3, 16, 0, 0, 0) data = await downloader.get_order_book_replay( symbol="BTC-USDT-PERP", start_date=start, end_date=end ) print(f"Đã tải {len(data)} records")

Xây Dựng Order Book Manager với Chức Năng Mô Phỏng撮合

import heapq
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Dict, List, Tuple
import time

@dataclass
class Order:
    """Cấu trúc một lệnh trong hệ thống"""
    order_id: str
    price: float
    size: float
    side: str  # 'buy' hoặc 'sell'
    timestamp: int
    status: str = "pending"  # pending, filled, partially_filled, cancelled

class MatchingEngine:
    """
    Mô phỏng撮合 engine của Binance Futures
    Xử lý matching giữa buy và sell orders
    """
    
    def __init__(self, symbol: str):
        self.symbol = symbol
        # Heap cho bids (max-heap simulation bằng negative prices)
        self.bids = []  # List of (price, order)
        # Heap cho asks (min-heap)
        self.asks = []
        self.order_map: Dict[str, Order] = {}
        self.trade_history: List[Dict] = []
        self.sequence = 0
        
    def apply_snapshot(self, snapshot: Dict):
        """
        Áp dụng order book snapshot từ Tardis.dev
        """
        self.bids = []
        self.asks = []
        
        # Parse bids
        for price, size in snapshot.get('b', []):
            order = Order(
                order_id=f"snap_b_{self.sequence}",
                price=float(price),
                size=float(size),
                side="buy",
                timestamp=snapshot.get('E', 0)
            )
            self.sequence += 1
            heapq.heappush(self.bids, (-order.price, order))
            self.order_map[order.order_id] = order
            
        # Parse asks  
        for price, size in snapshot.get('a', []):
            order = Order(
                order_id=f"snap_a_{self.sequence}",
                price=float(price),
                size=float(size),
                side="sell",
                timestamp=snapshot.get('E', 0)
            )
            self.sequence += 1
            heapq.heappush(self.asks, (order.price, order))
            self.order_map[order.order_id] = order
    
    def apply_update(self, update: Dict):
        """
        Áp dụng incremental update
        """
        updates = update.get('u', [])
        
        for price, size in updates:
            price = float(price)
            size = float(size)
            
            if size == 0:
                # Remove order
                self._remove_order(price)
            else:
                # Add or update order
                self._add_or_update_order(price, size)
                
    def _remove_order(self, price: float):
        """Xóa order ở mức giá xác định"""
        # Tìm và remove từ bids
        new_bids = []
        for neg_p, order in self.bids:
            if abs(order.price - price) > 1e-8:
                new_bids.append((neg_p, order))
        self.bids = new_bids
        heapq.heapify(self.bids)
        
        # Tìm và remove từ asks
        new_asks = []
        for p, order in self.asks:
            if abs(order.price - price) > 1e-8:
                new_asks.append((p, order))
        self.asks = new_asks
        heapq.heapify(self.asks)
    
    def _add_or_update_order(self, price: float, size: float):
        """Thêm hoặc cập nhật order"""
        side = "buy" if price < self.get_mid_price() else "sell"
        
        # Tìm xem đã có order ở mức giá này chưa
        existing = self._find_order(price)
        
        if existing:
            existing.size = size
            if size == 0:
                self._remove_order(price)
        else:
            order = Order(
                order_id=f"upd_{self.sequence}",
                price=price,
                size=size,
                side=side,
                timestamp=int(time.time() * 1000000)
            )
            self.sequence += 1
            self.order_map[order.order_id] = order
            
            if side == "buy":
                heapq.heappush(self.bids, (-price, order))
            else:
                heapq.heappush(self.asks, (price, order))
    
    def _find_order(self, price: float) -> Optional[Order]:
        """Tìm order ở mức giá xác định"""
        for neg_p, order in self.bids:
            if abs(order.price - price) < 1e-8:
                return order
        for p, order in self.asks:
            if abs(order.price - price) < 1e-8:
                return order
        return None
    
    def get_mid_price(self) -> float:
        """Lấy giá giữa bid-ask"""
        best_bid = self.get_best_bid()
        best_ask = self.get_best_ask()
        if best_bid and best_ask:
            return (best_bid + best_ask) / 2
        return 0.0
    
    def get_best_bid(self) -> float:
        """Lấy best bid price"""
        if self.bids:
            return -self.bids[0][0]
        return 0.0
    
    def get_best_ask(self) -> float:
        """Lấy best ask price"""
        if self.asks:
            return self.asks[0][0]
        return 0.0
    
    def get_spread(self) -> float:
        """Tính spread"""
        return self.get_best_ask() - self.get_best_bid()
    
    def get_top_n_levels(self, n: int = 10) -> Dict:
        """Lấy top N levels của order book"""
        # Sort bids descending
        sorted_bids = sorted(self.bids, key=lambda x: -x[0])[:n]
        # Sort asks ascending  
        sorted_asks = sorted(self.asks, key=lambda x: x[0])[:n]
        
        return {
            'bids': [(order.price, order.size) for _, order in sorted_bids],
            'asks': [(order.price, order.size) for _, order in sorted_asks],
            'mid_price': self.get_mid_price(),
            'spread': self.get_spread(),
            'spread_bps': (self.get_spread() / self.get_mid_price() * 10000) if self.get_mid_price() else 0
        }
    
    def submit_order(self, price: float, size: float, side: str) -> List[Dict]:
        """
        Submit một order mới và thực hiện matching
        Trả về list các trades được tạo ra
        """
        trades = []
        order = Order(
            order_id=f"ord_{self.sequence}",
            price=price,
            size=size,
            side=side,
            timestamp=int(time.time() * 1000000)
        )
        self.sequence += 1
        self.order_map[order.order_id] = order
        
        if side == "buy":
            # Match với các sell orders
            remaining = size
            while remaining > 0 and self.asks:
                best_ask_price, best_ask_order = self.asks[0]
                
                if price >= best_ask_price:
                    fill_size = min(remaining, best_ask_order.size)
                    
                    trade = {
                        'order_id': order.order_id,
                        'counter_order_id': best_ask_order.order_id,
                        'price': best_ask_price,
                        'size': fill_size,
                        'side': side,
                        'timestamp': order.timestamp
                    }
                    trades.append(trade)
                    self.trade_history.append(trade)
                    
                    best_ask_order.size -= fill_size
                    remaining -= fill_size
                    
                    if best_ask_order.size == 0:
                        heapq.heappop(self.asks)
                else:
                    break
        
        elif side == "sell":
            # Match với các buy orders
            remaining = size
            while remaining > 0 and self.bids:
                neg_best_bid_price, best_bid_order = self.bids[0]
                best_bid_price = -neg_best_bid_price
                
                if price <= best_bid_price:
                    fill_size = min(remaining, best_bid_order.size)
                    
                    trade = {
                        'order_id': order.order_id,
                        'counter_order_id': best_bid_order.order_id,
                        'price': best_bid_price,
                        'size': fill_size,
                        'side': side,
                        'timestamp': order.timestamp
                    }
                    trades.append(trade)
                    self.trade_history.append(trade)
                    
                    best_bid_order.size -= fill_size
                    remaining -= fill_size
                    
                    if best_bid_order.size == 0:
                        heapq.heappop(self.bids)
                else:
                    break
        
        # Cập nhật remaining size
        order.size = remaining
        order.status = "filled" if remaining == 0 else "partially_filled"
        
        # Thêm phần còn lại vào order book
        if remaining > 0:
            if side == "buy":
                heapq.heappush(self.bids, (-price, order))
            else:
                heapq.heappush(self.asks, (price, order))
                
        return trades

Ví dụ sử dụng

engine = MatchingEngine("BTC-USDT-PERP")

Tạo snapshot giả lập

snapshot = { 'b': [['50000.0', '10.5'], ['49999.5', '8.2'], ['49999.0', '15.0']], 'a': [['50000.5', '12.0'], ['50001.0', '9.5'], ['50001.5', '20.0']], 'E': 1710500000000000 } engine.apply_snapshot(snapshot) print(f"Best Bid: {engine.get_best_bid()}") print(f"Best Ask: {engine.get_best_ask()}") print(f"Spread: {engine.get_spread()} ({engine.get_spread() / engine.get_mid_price() * 10000:.2f} bps)")

Submit một buy order

trades = engine.submit_order(50001.0, 5.0, 'buy') print(f"Tạo {len(trades)} trades") for trade in trades: print(f" Trade: price={trade['price']}, size={trade['size']}")

Xây Dựng Data Pipeline Hoàn Chỉnh

import asyncio
import json
import msgpack
from pathlib import Path
from typing import Generator, AsyncGenerator
import pandas as pd
from datetime import datetime

class OrderBookDataPipeline:
    """
    Pipeline xử lý dữ liệu order book từ Tardis.dev
    Bao gồm: download, parse, validate, store
    """
    
    def __init__(self, tardis_key: str, cache_dir: str = "./data_cache"):
        self.downloader = BinanceOrderBookDownloader(tardis_key)
        self.cache_dir = Path(cache_dir)
        self.cache_dir.mkdir(exist_ok=True)
        
    async def download_and_process(
        self,
        symbol: str,
        date: datetime,
        batch_size: int = 10000
    ) -> pd.DataFrame:
        """
        Download và xử lý dữ liệu cho một ngày
        """
        start = datetime(date.year, date.month, date.day, 0, 0, 0)
        end = start + timedelta(days=1)
        
        print(f"Bắt đầu download {symbol} cho ngày {date.date()}")
        
        # Check cache
        cache_file = self.cache_dir / f"{symbol}_{date.strftime('%Y%m%d')}.parquet"
        if cache_file.exists():
            print(f"Đọc từ cache: {cache_file}")
            return pd.read_parquet(cache_file)
        
        # Download
        raw_data = await self.downloader.get_order_book_replay(
            symbol=symbol,
            start_date=start,
            end_date=end
        )
        
        print(f"Đã download {len(raw_data)} records raw")
        
        # Process thành DataFrame
        records = self._parse_raw_data(raw_data)
        df = pd.DataFrame(records)
        
        # Validate
        df = self._validate_and_clean(df)
        
        # Lưu cache
        df.to_parquet(cache_file)
        print(f"Đã lưu {len(df)} records vào cache")
        
        return df
    
    def _parse_raw_data(self, raw_data: List[Dict]) -> List[Dict]:
        """Parse raw data thành normalized format"""
        records = []
        
        for item in raw_data:
            channel = item.get('channel', '')
            data = item.get('data', item)
            
            if channel == 'book_snapshot' or 'b' in data:
                # Snapshot
                record = {
                    'type': 'snapshot',
                    'timestamp': data.get('E', data.get('ts', 0)),
                    'local_timestamp': data.get('T', 0),
                    'symbol': data.get('s', ''),
                    'bids': json.dumps(data.get('b', [])),
                    'asks': json.dumps(data.get('a', [])),
                    'bid_count': len(data.get('b', [])),
                    'ask_count': len(data.get('a', [])),
                    'best_bid': float(data['b'][0][0]) if data.get('b') else None,
                    'best_ask': float(data['a'][0][0]) if data.get('a') else None,
                    'mid_price': self._calc_mid(data),
                    'spread': self._calc_spread(data),
                    'spread_bps': self._calc_spread_bps(data)
                }
                records.append(record)
                
            elif channel == 'book_update' or 'u' in data:
                # Update
                record = {
                    'type': 'update',
                    'timestamp': data.get('E', data.get('ts', 0)),
                    'local_timestamp': data.get('T', 0),
                    'symbol': data.get('s', ''),
                    'update_id': data.get('u', data.get('seq', 0)),
                    'updates': json.dumps(data.get('u', [])),
                    'update_count': len(data.get('u', []))
                }
                records.append(record)
                
            elif 'trade' in channel:
                # Trade
                record = {
                    'type': 'trade',
                    'timestamp': data.get('E', data.get('ts', 0)),
                    'local_timestamp': data.get('T', 0),
                    'symbol': data.get('s', ''),
                    'price': float(data.get('p', 0)),
                    'size': float(data.get('q', 0)),
                    'side': data.get('m', 'unknown'),  # m=true means buyer is maker
                    'trade_id': data.get('t', data.get('i', ''))
                }
                records.append(record)
        
        return records
    
    def _calc_mid(self, data: Dict) -> float:
        """Tính mid price"""
        bids = data.get('b', [])
        asks = data.get('a', [])
        if bids and asks:
            return (float(bids[0][0]) + float(asks[0][0])) / 2
        return 0.0
    
    def _calc_spread(self, data: Dict) -> float:
        """Tính spread tuyệt đối"""
        bids = data.get('b', [])
        asks = data.get('a', [])
        if bids and asks:
            return float(asks[0][0]) - float(bids[0][0])
        return 0.0
    
    def _calc_spread_bps(self, data: Dict) -> float:
        """Tính spread theo basis points"""
        bids = data.get('b', [])
        asks = data.get('a', [])
        if bids and asks:
            mid = (float(bids[0][0]) + float(asks[0][0])) / 2
            if mid > 0:
                return (float(asks[0][0]) - float(bids[0][0])) / mid * 10000
        return 0.0
    
    def _validate_and_clean(self, df: pd.DataFrame) -> pd.DataFrame:
        """Validate và clean data"""
        # Remove duplicates
        df = df.drop_duplicates(subset=['timestamp', 'type'], keep='last')
        
        # Sort by timestamp
        df = df.sort_values('timestamp').reset_index(drop=True)
        
        # Handle missing values
        if 'mid_price' in df.columns:
            df['mid_price'] = df['mid_price'].fillna(method='ffill')
            
        return df
    
    def analyze_order_book(self, df: pd.DataFrame) -> Dict:
        """Phân tích order book"""
        snapshot_df = df[df['type'] == 'snapshot'].copy()
        
        if snapshot_df.empty:
            return {}
            
        analysis = {
            'total_snapshots': len(snapshot_df),
            'avg_spread_bps': snapshot_df['spread_bps'].mean(),
            'median_spread_bps': snapshot_df['spread_bps'].median(),
            'max_spread_bps': snapshot_df['spread_bps'].max(),
            'min_spread_bps': snapshot_df['spread_bps'].min(),
            'avg_bid_count': snapshot_df['bid_count'].mean(),
            'avg_ask_count': snapshot_df['ask_count'].mean(),
            'time_span_ms': snapshot_df['timestamp'].max() - snapshot_df['timestamp'].min()
        }
        
        return analysis

Sử dụng pipeline

async def main(): pipeline = OrderBookDataPipeline( tardis_key="YOUR_TARDIS_API_KEY", cache_dir="./orderbook_data" ) # Download một ngày dữ liệu date = datetime(2024, 3, 15) df = await pipeline.download_and_process( symbol="BTC-USDT-PERP", date=date ) # Phân tích analysis = pipeline.analyze_order_book(df) print("=== Order Book Analysis ===") for key, value in analysis.items(): print(f"{key}: {value:.4f}" if isinstance(value, float) else f"{key}: {value}") # Replay với matching engine engine = MatchingEngine("BTC-USDT-PERP") for _, row in df[df['type'] == 'snapshot'].head(100).iterrows(): snapshot = { 'b': json.loads(row['bids']), 'a': json.loads(row['asks']), 'E': row['timestamp'] } engine.apply_snapshot(snapshot) # Log trạng thái if _ % 10 == 0: levels = engine.get_top_n_levels(5) print(f"\nSnapshot {_}:") print(f" Mid: {levels['mid_price']:.2f}, Spread: {levels['spread_bps']:.2f} bps") if __name__ == "__main__": asyncio.run(main())

Tối Ưu Hóa Hiệu Suất cho Dữ Liệu Lớn

import numpy as np
from numba import jit
import numba

@jit(nopython=True)
def calculate_vwap_numba(prices: np.ndarray, volumes: np.ndarray) -> float:
    """Tính VWAP sử dụng numba JIT"""
    total_pv = 0.0
    total_vol = 0.0
    for i in range(len(prices)):
        total_pv += prices[i] * volumes[i]
        total_vol += volumes[i]
    return total_pv / total_vol if total_vol > 0 else 0.0

@jit(nopython=True)
def calculate_order_imbalance_numba(
    bid_prices: np.ndarray, bid_sizes: np.ndarray,
    ask_prices: np.ndarray, ask_sizes: np.ndarray
) -> float:
    """
    Tính Order Book Imbalance (OBI)
    OBI = (BidVolume - AskVolume) / (BidVolume + AskVolume)
    Giá trị dương = bullish, giá trị âm = bearish
    """
    total_bid_vol = 0.0
    total_ask_vol = 0.0
    
    for i in range(len(bid_sizes)):
        total_bid_vol += bid_sizes[i]
    
    for i in range(len(ask_sizes)):
        total_ask_vol += ask_sizes[i]
    
    total = total_bid_vol + total_ask_vol
    if total == 0:
        return 0.0
    
    return (total_bid_vol - total_ask_vol) / total

class OrderBookAnalyzer:
    """Phân tích order book với hiệu suất cao"""
    
    def __init__(self):
        self.history = []
        
    def analyze_snapshot_fast(self, bids: List[List], asks: List[List]) -> Dict:
        """Phân tích nhanh một snapshot"""
        # Convert sang numpy arrays
        bid_prices = np.array([float(b[0]) for b in bids])
        bid_sizes = np.array([float(b[1]) for b in bids])
        ask_prices = np.array([float(a[0]) for a in asks])
        ask_sizes = np.array([float(a[1]) for a in asks])
        
        # Tính các metrics
        obi = calculate_order_imbalance_numba(
            bid_prices, bid_sizes, ask_prices, ask_sizes
        )
        
        best_bid = bid_prices[0] if len(bid_prices) > 0 else 0
        best_ask = ask_prices[0] if len(ask_prices) > 0 else 0
        mid_price = (best_bid + best_ask) / 2
        
        # Tính volume weighted metrics
        bid_volumes = bid_sizes
        ask_volumes = ask_sizes
        
        # Volume ở các mức giá khác nhau
        depth_levels = 10
        bid_cumvol = np.cumsum(bid_sizes[:depth_levels])
        ask_cumvol = np.cumsum(ask_sizes[:depth_levels])
        
        return {
            'best_bid': best_bid,
            'best_ask': best_ask,
            'spread': best_ask - best_bid,
            'spread_bps': (best_ask - best_bid) / mid_price * 10000 if mid_price > 0 else 0,
            'order_imbalance': obi,
            'total_bid_volume': np.sum(bid_sizes),
            'total_ask_volume': np.sum(ask_sizes),
            'bid_depth_1pct': self._calc_depth_at_level(bid_prices, bid_sizes, mid_price, 0.01),
            'ask_depth_1pct': self._calc_depth_at_level(ask_prices, ask_sizes, mid_price, 0.01),
            'bid_vwap_levels': np.sum(bid_prices[:5] * bid_sizes[:5]) / np.sum(bid_sizes[:5]) if np.sum(bid_sizes[:5]) > 0 else 0,
            'ask_vwap_levels': np.sum(ask_prices[:5] * ask_sizes[:5]) / np.sum(ask_sizes[:5]) if np.sum(ask_sizes[:5]) > 0 else 0
        }
    
    @staticmethod
    @jit(nopython=True)
    def _calc_depth_at_level(
        prices: np.ndarray, 
        sizes: np.ndarray, 
        mid_price: float, 
        level_pct: float
    ) -> float:
        """Tính volume tích lũy trong khoảng level_pct từ mid price"""
        threshold = mid_price * level_pct
        total = 0.0
        
        for i in range(len(prices)):
            if abs(prices[i] - mid_price) <= threshold:
                total += sizes[i]
                
        return total
    
    def add_to_history(self, analysis: Dict):
        """Thêm kết quả phân tích vào lịch sử"""
        self.history.append(analysis)
        
    def get_momentum_signal(self, window: int = 100) -> float:
        """
        Tính momentum signal dựa trên OBI history
        """
        if len(self.history) < window:
            return 0.0
            
        recent_obi = [h['order_imbalance'] for h in self.history[-window:]]
        return np.mean(recent_obi)
    
    def detect_liquidity_events(self, threshold_bps: float = 50.0) -> List[Dict]:
        """Phát hiện các sự kiện liquidity"""
        events = []
        
        for i, h in enumerate(self.history):