3 tháng trước, mình gặp một lỗi kinh hoàng lúc 2 giờ sáng: ConnectionError: timeout - k89fh2j4. Hệ thống trading tự động của mình ngừng hoạt động hoàn toàn vì không lấy được dữ liệu lịch sử từ Binance. 47 giao dịch bị bỏ lỡ, thiệt hại khoảng 2,300 USD. Kể từ đó, mình đã nghiên cứu kỹ Binance Historical Data API và chia sẻ lại toàn bộ kiến thức thực chiến trong bài viết này.

Binance Historical Data API Là Gì?

Binance cung cấp API miễn phí để truy cập dữ liệu lịch sử, bao gồm:

Yêu Cầu Ban Đầu

Trước khi bắt đầu, bạn cần:

Code Mẫu Hoàn Chỉnh - Python

#!/usr/bin/env python3
"""
Binance Historical Data API - Hướng dẫn toàn diện
Author: HolySheep AI Blog
Version: 2026.01
"""

import requests
import time
import json
from datetime import datetime, timedelta
from typing import List, Dict, Optional

class BinanceHistoricalData:
    """Class truy cập dữ liệu lịch sử từ Binance API"""
    
    BASE_URL = "https://api.binance.com"
    MAX_REQUESTS_PER_MINUTE = 1200  # Giới hạn rate limit
    
    def __init__(self, api_key: str = None, secret_key: str = None):
        self.api_key = api_key
        self.secret_key = secret_key
        self.session = requests.Session()
        if api_key:
            self.session.headers.update({"X-MBX-APIKEY": api_key})
    
    def get_klines(
        self,
        symbol: str,
        interval: str,
        start_time: int = None,
        end_time: int = None,
        limit: int = 500
    ) -> List[Dict]:
        """
        Lấy dữ liệu nến (candlestick) từ Binance
        
        Args:
            symbol: Cặp tiền (VD: 'BTCUSDT')
            interval: Khung thời gian ('1m', '5m', '1h', '1d'...)
            start_time: Thời gian bắt đầu (milliseconds timestamp)
            end_time: Thời gian kết thúc (milliseconds timestamp)
            limit: Số lượng nến tối đa (1-1000)
        
        Returns:
            List chứa dữ liệu nến
        """
        endpoint = "/api/v3/klines"
        params = {
            "symbol": symbol.upper(),
            "interval": interval,
            "limit": min(limit, 1000)
        }
        
        if start_time:
            params["startTime"] = start_time
        if end_time:
            params["endTime"] = end_time
        
        try:
            response = self.session.get(
                f"{self.BASE_URL}{endpoint}",
                params=params,
                timeout=10
            )
            response.raise_for_status()
            
            data = response.json()
            
            # Chuyển đổi sang dict format
            return self._parse_klines(data)
            
        except requests.exceptions.Timeout:
            raise ConnectionError(f"Timeout khi lấy dữ liệu {symbol}")
        except requests.exceptions.HTTPError as e:
            if response.status_code == 429:
                raise RateLimitError("Rate limit exceeded. Chờ 60 giây...")
            elif response.status_code == 401:
                raise AuthError("API Key không hợp lệ")
            else:
                raise APIError(f"HTTP Error: {e}")
        except Exception as e:
            raise APIError(f"Lỗi không xác định: {e}")
    
    def _parse_klines(self, data: List) -> List[Dict]:
        """Parse dữ liệu kline từ array sang dict format"""
        parsed = []
        for k in data:
            parsed.append({
                "open_time": k[0],
                "open": float(k[1]),
                "high": float(k[2]),
                "low": float(k[3]),
                "close": float(k[4]),
                "volume": float(k[5]),
                "close_time": k[6],
                "quote_volume": float(k[7]),
                "trades": int(k[8]),
                "taker_buy_volume": float(k[9]),
                "taker_buy_quote_volume": float(k[10]),
            })
        return parsed
    
    def get_historical_klines(
        self,
        symbol: str,
        interval: str,
        start_date: str,
        end_date: str = None
    ) -> List[Dict]:
        """
        Lấy dữ liệu lịch sử dài (tự động paginate)
        Hỗ trợ ngày tháng dạng '2023-01-01'
        """
        all_klines = []
        
        # Chuyển đổi ngày sang timestamp
        start_ts = int(datetime.strptime(start_date, "%Y-%m-%d").timestamp() * 1000)
        end_ts = int(
            datetime.strptime(end_date, "%Y-%m-%d").timestamp() * 1000
        ) if end_date else int(time.time() * 1000)
        
        current_ts = start_ts
        
        while current_ts < end_ts:
            try:
                klines = self.get_klines(
                    symbol=symbol,
                    interval=interval,
                    start_time=current_ts,
                    end_time=end_ts,
                    limit=500
                )
                
                if not klines:
                    break
                
                all_klines.extend(klines)
                current_ts = klines[-1]["close_time"] + 1
                
                print(f"Đã lấy {len(all_klines)} nến... ({symbol})")
                
                # Tránh rate limit
                time.sleep(0.2)
                
            except RateLimitError:
                print("Rate limit - chờ 60 giây...")
                time.sleep(60)
            except Exception as e:
                print(f"Lỗi: {e}")
                break
        
        return all_klines
    
    def get_aggregated_trades(
        self,
        symbol: str,
        from_id: int = None,
        start_time: int = None,
        end_time: int = None,
        limit: int = 500
    ) -> List[Dict]:
        """Lấy dữ liệu giao dịch tổng hợp"""
        endpoint = "/api/v3/aggTrades"
        params = {
            "symbol": symbol.upper(),
            "limit": min(limit, 1000)
        }
        
        if from_id:
            params["fromId"] = from_id
        if start_time:
            params["startTime"] = start_time
        if end_time:
            params["endTime"] = end_time
        
        response = self.session.get(
            f"{self.BASE_URL}{endpoint}",
            params=params,
            timeout=10
        )
        response.raise_for_status()
        
        return response.json()


class RateLimitError(Exception):
    """Exception khi vượt rate limit"""
    pass

class AuthError(Exception):
    """Exception khi xác thực thất bại"""
    pass

class APIError(Exception):
    """Exception chung của API"""
    pass


================== VÍ DỤ SỬ DỤNG ==================

if __name__ == "__main__": # Khởi tạo client (không cần API key cho dữ liệu public) client = BinanceHistoricalData() # Lấy 500 nến 1 giờ gần nhất của BTCUSDT print("Lấy dữ liệu BTCUSDT 1h...") btc_1h = client.get_klines( symbol="BTCUSDT", interval="1h", limit=500 ) print(f"Tổng cộng: {len(btc_1h)} nến") print(f"Nến mới nhất: {btc_1h[-1]}") # Lấy dữ liệu 30 ngày gần đây print("\nLấy dữ liệu ETHUSDT 1 ngày trong 30 ngày...") eth_daily = client.get_historical_klines( symbol="ETHUSDT", interval="1d", start_date="2025-11-01", end_date="2025-12-01" ) print(f"Tổng cộng: {len(eth_daily)} nến")

Code Mẫu - Node.js (TypeScript)

/**
 * Binance Historical Data API - Node.js/TypeScript Implementation
 * Author: HolySheep AI Blog
 */

interface Kline {
  openTime: number;
  open: number;
  high: number;
  low: number;
  close: number;
  volume: number;
  closeTime: number;
  quoteVolume: number;
  trades: number;
  takerBuyBaseVolume: number;
  takerBuyQuoteVolume: number;
}

interface ApiError {
  code: number;
  msg: string;
}

class BinanceApiError extends Error {
  constructor(
    message: string,
    public code?: number,
    public statusCode?: number
  ) {
    super(message);
    this.name = 'BinanceApiError';
  }
}

class BinanceHistoricalClient {
  private baseUrl = 'https://api.binance.com';
  private requestCount = 0;
  private lastReset = Date.now();
  
  private async request(
    endpoint: string,
    params: Record = {}
  ): Promise {
    // Rate limit check
    this.requestCount++;
    if (this.requestCount >= 1200) {
      const elapsed = Date.now() - this.lastReset;
      if (elapsed < 60000) {
        await new Promise(r => setTimeout(r, 60000 - elapsed));
      }
      this.requestCount = 0;
      this.lastReset = Date.now();
    }
    
    // Build query string
    const queryParams = Object.entries(params)
      .filter(([_, v]) => v !== undefined)
      .map(([k, v]) => ${encodeURIComponent(k)}=${encodeURIComponent(v)})
      .join('&');
    
    const url = ${this.baseUrl}${endpoint}${queryParams ? '?' + queryParams : ''};
    
    try {
      const response = await fetch(url, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
        },
      });
      
      if (!response.ok) {
        if (response.status === 429) {
          throw new BinanceApiError('Rate limit exceeded', undefined, 429);
        }
        if (response.status === 401) {
          throw new BinanceApiError('Unauthorized - Invalid API Key', undefined, 401);
        }
        if (response.status === 418) {
          throw new BinanceApiError('IP banned - Too many requests', undefined, 418);
        }
        throw new BinanceApiError(HTTP Error: ${response.status}, undefined, response.status);
      }
      
      const data = await response.json();
      
      // Check for API error response
      if ('code' in data && 'msg' in data) {
        throw new BinanceApiError(data.msg, data.code);
      }
      
      return data as T;
      
    } catch (error) {
      if (error instanceof BinanceApiError) {
        throw error;
      }
      if (error instanceof TypeError && error.message.includes('fetch')) {
        throw new BinanceApiError('Connection failed - Check internet', undefined, undefined);
      }
      throw error;
    }
  }
  
  async getKlines(
    symbol: string,
    interval: '1m' | '3m' | '5m' | '15m' | '30m' | '1h' | '2h' | '4h' | '6h' | '8h' | '12h' | '1d' | '3d' | '1w' | '1M',
    limit: number = 500,
    startTime?: number,
    endTime?: number
  ): Promise {
    const rawData = await this.request(
      '/api/v3/klines',
      {
        symbol: symbol.toUpperCase(),
        interval,
        limit: Math.min(limit, 1000),
        startTime,
        endTime,
      }
    );
    
    return rawData.map(k => ({
      openTime: k[0],
      open: parseFloat(k[1]),
      high: parseFloat(k[2]),
      low: parseFloat(k[3]),
      close: parseFloat(k[4]),
      volume: parseFloat(k[5]),
      closeTime: k[6],
      quoteVolume: parseFloat(k[7]),
      trades: parseInt(k[8]),
      takerBuyBaseVolume: parseFloat(k[9]),
      takerBuyQuoteVolume: parseFloat(k[10]),
    }));
  }
  
  async getHistoricalKlines(
    symbol: string,
    interval: string,
    startDate: string,
    endDate?: string
  ): Promise {
    const allKlines: Kline[] = [];
    const startTs = new Date(startDate).getTime();
    const endTs = endDate ? new Date(endDate).getTime() : Date.now();
    
    let currentTs = startTs;
    
    while (currentTs < endTs) {
      try {
        const klines = await this.getKlines(
          symbol,
          interval as any,
          500,
          currentTs,
          endTs
        );
        
        if (klines.length === 0) break;
        
        allKlines.push(...klines);
        currentTs = klines[klines.length - 1].closeTime + 1;
        
        console.log(Progress: ${allKlines.length} klines loaded...);
        
        // Delay to avoid rate limit
        await new Promise(r => setTimeout(r, 200));
        
      } catch (error) {
        if (error instanceof BinanceApiError && error.statusCode === 429) {
          console.log('Rate limited. Waiting 60 seconds...');
          await new Promise(r => setTimeout(r, 60000));
        } else {
          throw error;
        }
      }
    }
    
    return allKlines;
  }
  
  async getAggregatedTrades(
    symbol: string,
    limit: number = 500,
    startTime?: number,
    endTime?: number
  ): Promise {
    return this.request('/api/v3/aggTrades', {
      symbol: symbol.toUpperCase(),
      limit: Math.min(limit, 1000),
      startTime,
      endTime,
    });
  }
  
  async getSymbolPrice(symbol: string): Promise {
    const data = await this.request<{ price: string }>('/api/v3/ticker/price', {
      symbol: symbol.toUpperCase(),
    });
    return parseFloat(data.price);
  }
}

// ================== VÍ DỤ SỬ DỤNG ==================

async function main() {
  const client = new BinanceHistoricalClient();
  
  try {
    // Lấy 500 nến 15 phút của BTCUSDT
    console.log('Fetching BTCUSDT 15m data...');
    const btcKlines = await client.getKlines('BTCUSDT', '15m', 500);
    console.log(Received: ${btcKlines.length} candles);
    console.log('Latest candle:', btcKlines[btcKlines.length - 1]);
    
    // Lấy dữ liệu 1 năm
    console.log('\nFetching ETHUSDT daily for 1 year...');
    const ethKlines = await client.getHistoricalKlines(
      'ETHUSDT',
      '1d',
      '2025-01-01',
      '2025-12-31'
    );
    console.log(Total: ${ethKlines.length} candles);
    
    // Tính toán indicator đơn giản
    const closes = ethKlines.map(k => k.close);
    const sma20 = calculateSMA(closes, 20);
    console.log(ETH SMA(20): ${sma20[sma20.length - 1]});
    
  } catch (error) {
    if (error instanceof BinanceApiError) {
      console.error(API Error [${error.statusCode}]: ${error.message});
    } else {
      console.error('Unexpected error:', error);
    }
  }
}

function calculateSMA(prices: number[], period: number): number[] {
  const sma: number[] = [];
  for (let i = period - 1; i < prices.length; i++) {
    const sum = prices.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
    sma.push(sum / period);
  }
  return sma;
}

main();

Bảng So Sánh Các Khung Thời Gian

Khung thời gian Mã viết tắt Limit tối đa/lần gọi Dữ liệu có sẵn Phù hợp cho
1 phút 1m 1000 ~7 ngày gần nhất Scalping, phân tích intra-day
5 phút 5m 1000 ~60 ngày Day trading
15 phút 15m 1000 ~120 ngày Swing trading ngắn
1 giờ 1h 1000 ~500 ngày Swing trading
4 giờ 4h 1000 ~2 năm Phân tích trung hạn
1 ngày 1d 1000 Toàn bộ lịch sử Investment, backtesting dài hạn
1 tuần 1w 1000 Toàn bộ lịch sử Phân tích xu hướng dài hạn

Xử Lý Dữ Liệu Với Pandas (Python)

import pandas as pd
import matplotlib.pyplot as plt
from datetime import datetime

class BinanceDataProcessor:
    """Xử lý và phân tích dữ liệu từ Binance"""
    
    def __init__(self, klines: list):
        """
        Khởi tạo với dữ liệu klines từ BinanceHistoricalData
        
        Args:
            klines: List các dict chứa dữ liệu nến
        """
        self.df = pd.DataFrame(klines)
        self._prepare_dataframe()
    
    def _prepare_dataframe(self):
        """Chuẩn bị DataFrame với các cột cần thiết"""
        # Chuyển đổi timestamp
        self.df['datetime'] = pd.to_datetime(self.df['open_time'], unit='ms')
        self.df.set_index('datetime', inplace=True)
        
        # Đảm bảo các cột numeric
        numeric_cols = ['open', 'high', 'low', 'close', 'volume']
        for col in numeric_cols:
            self.df[col] = pd.to_numeric(self.df[col])
    
    def add_sma(self, periods: list) -> 'BinanceDataProcessor':
        """Thêm Simple Moving Average"""
        for period in periods:
            self.df[f'sma_{period}'] = self.df['close'].rolling(window=period).mean()
        return self
    
    def add_ema(self, periods: list) -> 'BinanceDataProcessor':
        """Thêm Exponential Moving Average"""
        for period in periods:
            self.df[f'ema_{period}'] = self.df['close'].ewm(span=period, adjust=False).mean()
        return self
    
    def add_rsi(self, period: int = 14) -> 'BinanceDataProcessor':
        """Thêm RSI (Relative Strength Index)"""
        delta = self.df['close'].diff()
        gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
        loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
        
        rs = gain / loss
        self.df['rsi'] = 100 - (100 / (1 + rs))
        return self
    
    def add_macd(
        self,
        fast: int = 12,
        slow: int = 26,
        signal: int = 9
    ) -> 'BinanceDataProcessor':
        """Thêm MACD (Moving Average Convergence Divergence)"""
        exp1 = self.df['close'].ewm(span=fast, adjust=False).mean()
        exp2 = self.df['close'].ewm(span=slow, adjust=False).mean()
        
        self.df['macd'] = exp1 - exp2
        self.df['macd_signal'] = self.df['macd'].ewm(span=signal, adjust=False).mean()
        self.df['macd_hist'] = self.df['macd'] - self.df['macd_signal']
        return self
    
    def add_bollinger_bands(
        self,
        period: int = 20,
        std_dev: float = 2.0
    ) -> 'BinanceDataProcessor':
        """Thêm Bollinger Bands"""
        self.df['bb_middle'] = self.df['close'].rolling(window=period).mean()
        std = self.df['close'].rolling(window=period).std()
        self.df['bb_upper'] = self.df['bb_middle'] + (std * std_dev)
        self.df['bb_lower'] = self.df['bb_middle'] - (std * std_dev)
        return self
    
    def get_technical_summary(self) -> dict:
        """Trả về tóm tắt các chỉ báo kỹ thuật"""
        latest = self.df.iloc[-1]
        return {
            'symbol': 'BTCUSDT',  # Cần truyền thêm symbol
            'current_price': latest['close'],
            'sma_20': latest.get('sma_20', None),
            'sma_50': latest.get('sma_50', None),
            'sma_200': latest.get('sma_200', None),
            'rsi_14': latest.get('rsi', None),
            'macd': latest.get('macd', None),
            'macd_signal': latest.get('macd_signal', None),
            'bb_upper': latest.get('bb_upper', None),
            'bb_middle': latest.get('bb_middle', None),
            'bb_lower': latest.get('bb_lower', None),
            'volume_24h': latest['volume'],
        }
    
    def plot_chart(self, show_bb: bool = True, show_sma: bool = True):
        """Vẽ biểu đồ giá với các chỉ báo"""
        fig, axes = plt.subplots(2, 1, figsize=(14, 10), 
                                   gridspec_kw={'height_ratios': [3, 1]})
        
        # Biểu đồ giá
        ax1 = axes[0]
        ax1.plot(self.df.index, self.df['close'], label='Close', linewidth=1.5)
        
        if show_sma:
            if 'sma_20' in self.df.columns:
                ax1.plot(self.df.index, self.df['sma_20'], label='SMA 20', alpha=0.7)
            if 'sma_50' in self.df.columns:
                ax1.plot(self.df.index, self.df['sma_50'], label='SMA 50', alpha=0.7)
        
        if show_bb:
            if 'bb_upper' in self.df.columns:
                ax1.fill_between(
                    self.df.index, 
                    self.df['bb_lower'], 
                    self.df['bb_upper'], 
                    alpha=0.1, 
                    label='Bollinger Bands'
                )
                ax1.plot(self.df.index, self.df['bb_upper'], 'r--', alpha=0.5)
                ax1.plot(self.df.index, self.df['bb_lower'], 'r--', alpha=0.5)
        
        ax1.set_title('Giá và Chỉ báo Kỹ thuật', fontsize=14)
        ax1.set_ylabel('Giá (USDT)')
        ax1.legend()
        ax1.grid(True, alpha=0.3)
        
        # Biểu đồ RSI
        ax2 = axes[1]
        if 'rsi' in self.df.columns:
            ax2.plot(self.df.index, self.df['rsi'], 'purple', linewidth=1)
            ax2.axhline(y=70, color='r', linestyle='--', alpha=0.5)
            ax2.axhline(y=30, color='g', linestyle='--', alpha=0.5)
            ax2.fill_between(self.df.index, 70, 100, alpha=0.2, color='red')
            ax2.fill_between(self.df.index, 0, 30, alpha=0.2, color='green')
            ax2.set_ylabel('RSI')
            ax2.set_ylim(0, 100)
            ax2.grid(True, alpha=0.3)
        
        plt.tight_layout()
        plt.savefig('chart.png', dpi=150)
        plt.show()
        print("Đã lưu biểu đồ vào chart.png")
    
    def export_to_csv(self, filename: str = 'data.csv'):
        """Xuất dữ liệu ra file CSV"""
        self.df.to_csv(filename)
        print(f"Đã xuất {len(self.df)} dòng vào {filename}")
    
    def export_to_json(self, filename: str = 'data.json'):
        """Xuất dữ liệu ra file JSON"""
        self.df.to_json(filename, orient='records', date_format='iso')
        print(f"Đã xuất {len(self.df)} dòng vào {filename}")


================== VÍ DỤ SỬ DỤNG ==================

if __name__ == "__main__": from binance_api import BinanceHistoricalData # Lấy dữ liệu client = BinanceHistoricalData() klines = client.get_historical_klines( symbol="BTCUSDT", interval="1h", start_date="2025-10-01", end_date="2025-12-01" ) # Xử lý và phân tích processor = BinanceDataProcessor(klines) processor.add_sma([20, 50, 200]) processor.add_rsi(14) processor.add_macd(12, 26, 9) processor.add_bollinger_bands(20, 2) # In tóm tắt summary = processor.get_technical_summary() print("=== TÓM TẮT PHÂN TÍCH KỸ THUẬT ===") for key, value in summary.items(): if value is not None: print(f"{key}: {value:.2f}" if isinstance(value, float) else f"{key}: {value}") # Xuất dữ liệu processor.export_to_csv('btcusdt_1h.csv') processor.export_to_json('btcusdt_1h.json') # Vẽ biểu đồ # processor.plot_chart(show_bb=True, show_sma=True)

Tối Ưu Hóa Hiệu Suất Với Batch Processing

#!/usr/bin/env python3
"""
Batch Processing cho nhiều cặp tiền cùng lúc
Sử dụng asyncio cho hiệu suất cao
"""

import asyncio
import aiohttp
import time
from datetime import datetime
from typing import List, Dict, Tuple
from dataclasses import dataclass

@dataclass
class TradingPair:
    symbol: str
    interval: str
    start_date: str
    end_date: str

class BatchBinanceFetcher:
    """Fetch dữ liệu cho nhiều cặp tiền song song"""
    
    BASE_URL = "https://api.binance.com"
    MAX_CONCURRENT = 5  # Số request đồng thời tối đa
    RATE_LIMIT = 1200   # requests/phút
    
    def __init__(self):
        self.semaphore = asyncio.Semaphore(self.MAX_CONCURRENT)
        self.request_times: List[float] = []
    
    async def fetch_klines_async(
        self,
        session: aiohttp.ClientSession,
        symbol: str,
        interval: str,
        limit: int = 1000
    ) -> Dict:
        """Fetch klines cho một cặp tiền"""
        async with self.semaphore:
            url = f"{self.BASE_URL}/api/v3/klines"
            params = {
                "symbol": symbol.upper(),
                "interval": interval,
                "limit": limit
            }
            
            try:
                async with session.get(url, params=params) as response:
                    if response.status == 429:
                        await asyncio.sleep(60)
                        return await self.fetch_klines_async(
                            session, symbol, interval, limit
                        )
                    
                    data = await response.json()
                    self.request_times.append(time.time())
                    
                    return {
                        "symbol": symbol,
                        "interval": interval,
                        "count": len(data),
                        "data": data,
                        "success": True
                    }
                    
            except aiohttp.ClientError as e:
                return {
                    "symbol": symbol,
                    "error": str(e),
                    "success": False
                }
    
    async def fetch_multiple(
        self,
        pairs: List[Tuple[str, str]]
    ) -> List[Dict]:
        """Fetch nhiều cặp tiền song song"""
        async with aiohttp.ClientSession() as session:
            tasks = [
                self.fetch_klines_async(session, symbol, interval)
                for symbol, interval in pairs
            ]
            
            results = await asyncio.gather(*tasks)
            return results
    
    def get_rate_limit_status(self) -> Dict:
        """Kiểm tra trạng thái rate limit"""
        now = time.time()
        # Lọc các request trong 1 phút gần nhất
        recent = [t for t in self.request_times if now - t < 60]
        
        return {
            "requests_last_minute": len(recent),
            "limit": self.RATE_LIMIT,
            "remaining": self.RATE_LIMIT - len(recent),
            "reset_needed": len(recent) >= self.RATE_LIMIT
        }


class DataAggregator:
    """Tổng hợp dữ liệu từ nhiều nguồn"""
    
    def __init__(self):
        self.data_cache: Dict[str, List] = {}
    
    def add_data(self, key: str, data: List):
        """Thêm dữ liệu vào cache"""
        self.data_cache[key] = data
    
    def calculate_correlation(
        self,
        symbol1: str,
        symbol2: str,
        metric: str = "close"
    ) -> float:
        """Tính correlation giữa 2 symbol"""
        if symbol1 not in self.data_cache or symbol2 not in self.data_cache:
            raise ValueError("Symbol not found in cache")
        
        data1 = self.data_cache[symbol1]
        data2 = self.data_cache[symbol2]
        
        # Lấy