Tôi vẫn nhớ rõ cái đêm thứ sáu tuần trước, khi hệ thống giao dịch tự động của mình bị dừng hoàn toàn lúc 2:47 sáng. Lỗi hiển thị ngay trên màn hình console: ConnectionError: HTTPSConnectionPool(host='api.binance.com', port=443): Max retries exceeded. Sau 3 tiếng debug, tôi phát hiện vấn đề không nằm ở code mà ở việc xử lý dữ liệu K-line thô từ exchange - thiếu timezone, missing values không được xử lý, và DataFrame memory leak khi scale lên hàng triệu rows.

Bài viết này là toàn bộ những gì tôi đã học được từ kinh nghiệm thực chiến, giúp bạn không phải đi con đường vòng như tôi.

Tại sao dữ liệu K-line từ Exchange lại "bẩn"?

Khi fetch dữ liệu từ các sàn như Binance, Bybit, OKX, bạn thường nhận được JSON response có cấu trúc như sau:

[
  [
    1499040000000,      // Open time (milliseconds)
    "0.01634000",       // Open
    "0.80000000",       // High
    "0.01575800",       // Low
    "0.01577100",       // Close
    "148976.11427815",  // Volume
    1499644799999,      // Close time
    "2434.55355327",    // Quote asset volume
    308,                // Number of trades
    "1756.87402397",    // Taker buy base asset volume
    "28.46694368",      // Taker buy quote asset volume
    "0"                 // Ignore
  ]
]

Vấn đề thực tế tôi gặp phải:

  • Milliseconds vs Seconds: timestamp về dạng milliseconds nhưng nhiều thư viện mặc định đọc là seconds
  • Missing candles: market downtime hoặc maintenance tạo gap trong dữ liệu
  • Duplicate timestamps: khi request nhiều workers cùng lúc
  • Volume spikes: giá trị bất thường từ wash trading
  • Timezone confusion: UTC hay local timezone không rõ ràng

Setup môi trường và cài đặt dependencies

# requirements.txt
pandas>=2.0.0
numpy>=1.24.0
requests>=2.31.0
pyarrow>=14.0.0    # Cho Parquet storage
python-dotenv>=1.0.0
ta-lib>=0.4.28      # Technical analysis indicators

Cài đặt

pip install -r requirements.txt

Tạo file .env cho API keys

cat > .env << 'EOF' EXCHANGE_API_KEY=your_exchange_api_key EXCHANGE_SECRET=your_exchange_secret HOLYSHEEP_API_KEY=your_holysheep_api_key EOF

Module lấy dữ liệu từ Exchange

import pandas as pd
import numpy as np
import requests
import time
from datetime import datetime, timezone
from typing import List, Dict, Optional
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class ExchangeDataFetcher:
    """
    Fetcher dữ liệu K-line từ các sàn giao dịch crypto
    với retry logic và rate limiting tự động
    """
    
    def __init__(self, api_key: str, secret: str, base_url: str = "https://api.binance.com"):
        self.api_key = api_key
        self.secret = secret
        self.base_url = base_url
        self.session = requests.Session()
        self.session.headers.update({
            'X-MBX-APIKEY': api_key,
            'Content-Type': 'application/json'
        })
        
    def fetch_klines(
        self,
        symbol: str,
        interval: str = "1h",
        start_time: Optional[int] = None,
        end_time: Optional[int] = None,
        limit: int = 1000
    ) -> pd.DataFrame:
        """
        Fetch K-line data từ Binance API
        
        Args:
            symbol: Cặp giao dịch (VD: 'BTCUSDT')
            interval: Khung thời gian ('1m', '5m', '1h', '1d')
            start_time: Timestamp milliseconds
            end_time: Timestamp milliseconds  
            limit: Số lượng candles (max 1000/request)
            
        Returns:
            DataFrame với columns chuẩn hóa
        """
        endpoint = f"{self.base_url}/api/v3/klines"
        params = {
            'symbol': symbol.upper(),
            'interval': interval,
            'limit': limit
        }
        
        if start_time:
            params['startTime'] = start_time
        if end_time:
            params['endTime'] = end_time
            
        max_retries = 5
        retry_delay = 1
        
        for attempt in range(max_retries):
            try:
                response = self.session.get(endpoint, params=params, timeout=30)
                
                # Xử lý các mã lỗi phổ biến
                if response.status_code == 429:
                    wait_time = int(response.headers.get('Retry-After', 60))
                    logger.warning(f"Rate limited. Đợi {wait_time}s...")
                    time.sleep(wait_time)
                    continue
                    
                if response.status_code == 401:
                    raise ConnectionError(
                        "401 Unauthorized: API key không hợp lệ hoặc thiếu quyền. "
                        "Kiểm tra lại API key và đảm bảo đã bật quyền đọc dữ liệu."
                    )
                    
                if response.status_code == 418:
                    # IP bị ban tạm thời
                    retry_after = int(response.headers.get('Retry-After', 300))
                    logger.warning(f"IP bị ban. Đợi {retry_after}s...")
                    time.sleep(retry_after)
                    continue
                    
                response.raise_for_status()
                data = response.json()
                
                if not data:
                    logger.warning(f"Không có dữ liệu cho {symbol} {interval}")
                    return pd.DataFrame()
                    
                return self._parse_klines(data)
                
            except requests.exceptions.Timeout:
                logger.warning(f"Timeout attempt {attempt + 1}/{max_retries}")
                time.sleep(retry_delay * (2 ** attempt))
            except requests.exceptions.ConnectionError as e:
                logger.warning(f"Connection error: {e}")
                time.sleep(retry_delay * (2 ** attempt))
                
        raise ConnectionError(
            f"Không thể fetch dữ liệu sau {max_retries} lần thử. "
            f"Kiểm tra kết nối internet và API status."
        )
    
    def _parse_klines(self, raw_data: List) -> pd.DataFrame:
        """
        Parse raw kline data thành DataFrame với schema chuẩn
        """
        columns = [
            'open_time', 'open', 'high', 'low', 'close', 'volume',
            'close_time', 'quote_volume', 'trades', 'taker_buy_base',
            'taker_buy_quote', 'ignore'
        ]
        
        df = pd.DataFrame(raw_data, columns=columns)
        
        # Convert sang numeric types
        numeric_cols = ['open', 'high', 'low', 'close', 'volume', 
                       'quote_volume', 'taker_buy_base', 'taker_buy_quote']
        for col in numeric_cols:
            df[col] = pd.to_numeric(df[col], errors='coerce')
            
        # Convert timestamps
        df['open_time'] = pd.to_datetime(df['open_time'], unit='ms', utc=True)
        df['close_time'] = pd.to_datetime(df['close_time'], unit='ms', utc=True)
        
        # Drop unnecessary columns
        df = df.drop(columns=['ignore'])
        
        return df
    
    def fetch_historical(
        self,
        symbol: str,
        interval: str,
        start_date: datetime,
        end_date: Optional[datetime] = None
    ) -> pd.DataFrame:
        """
        Fetch dữ liệu lịch sử dài (tự động paginate qua các request)
        """
        if end_date is None:
            end_date = datetime.now(timezone.utc)
            
        all_klines = []
        current_start = int(start_date.timestamp() * 1000)
        end_ts = int(end_date.timestamp() * 1000)
        
        logger.info(f"Fetching {symbol} {interval} từ {start_date} đến {end_date}")
        
        while current_start < end_ts:
            batch = self.fetch_klines(
                symbol=symbol,
                interval=interval,
                start_time=current_start,
                end_time=end_ts
            )
            
            if batch.empty:
                break
                
            all_klines.append(batch)
            
            # Lấy timestamp cuối cùng + 1ms để tránh duplicate
            current_start = int(batch['close_time'].max().timestamp() * 1000) + 1
            
            logger.info(f"Fetched {len(batch)} candles. Progress: {current_start}/{end_ts}")
            
            # Rate limit protection
            time.sleep(0.2)
            
        if all_klines:
            return pd.concat(all_klines, ignore_index=True).drop_duplicates()
        return pd.DataFrame()

Pandas DataFrame Cleaning Pipeline

Đây là phần quan trọng nhất - nơi tôi đã tốn nhiều giờ để xây dựng pipeline hoàn chỉnh:

import pandas as pd
import numpy as np
from typing import Tuple, Optional
from datetime import datetime, timezone

class KlinesDataProcessor:
    """
    Pipeline xử lý và làm sạch dữ liệu K-line
    """
    
    def __init__(self, timezone_name: str = "Asia/Ho_Chi_Minh"):
        self.tz = timezone_name
        
    def process(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Main processing pipeline
        """
        if df.empty:
            return df
            
        df = df.copy()
        
        # Step 1: Basic validation
        df = self._validate_schema(df)
        
        # Step 2: Handle missing values
        df = self._handle_missing_values(df)
        
        # Step 3: Remove duplicates
        df = self._remove_duplicates(df)
        
        # Step 4: Detect và xử lý outliers
        df = self._handle_outliers(df)
        
        # Step 5: Normalize timezone
        df = self._normalize_timezone(df)
        
        # Step 6: Sort và reset index
        df = df.sort_values('open_time').reset_index(drop=True)
        
        # Step 7: Fill gaps (optional - tùy use case)
        df = self._fill_time_gaps(df, freq='1h')
        
        return df
    
    def _validate_schema(self, df: pd.DataFrame) -> pd.DataFrame:
        """Validate DataFrame schema"""
        required_cols = ['open_time', 'open', 'high', 'low', 'close', 'volume']
        
        missing_cols = set(required_cols) - set(df.columns)
        if missing_cols:
            raise ValueError(f"Thiếu columns bắt buộc: {missing_cols}")
            
        # Validate OHLC relationships
        invalid_hc = df[df['high'] < df['close']]
        if not invalid_hc.empty:
            logger.warning(f"Found {len(invalid_hc)} rows với high < close")
            
        invalid_lo = df[df['low'] > df['open']]
        if not invalid_lo.empty:
            logger.warning(f"Found {len(invalid_lo)} rows với low > open")
            
        return df
    
    def _handle_missing_values(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Chiến lược xử lý missing values:
        - Forward fill cho giá (phù hợp với trading)
        - Zero fill cho volume
        """
        # Forward fill price columns
        price_cols = ['open', 'high', 'low', 'close']
        df[price_cols] = df[price_cols].ffill()
        
        # Backward fill nếu đầu series có NaN
        df[price_cols] = df[price_cols].bfill()
        
        # Zero fill volume
        volume_cols = ['volume', 'quote_volume', 'trades', 
                       'taker_buy_base', 'taker_buy_quote']
        for col in volume_cols:
            if col in df.columns:
                df[col] = df[col].fillna(0)
                
        # Log missing values nếu còn
        missing_count = df[price_cols].isnull().sum()
        if missing_count.any():
            logger.warning(f"Still have missing values: {missing_count.to_dict()}")
            
        return df
    
    def _remove_duplicates(self, df: pd.DataFrame) -> pd.DataFrame:
        """Loại bỏ duplicate timestamps"""
        before_count = len(df)
        df = df.drop_duplicates(subset=['open_time'], keep='last')
        removed = before_count - len(df)
        
        if removed > 0:
            logger.info(f"Removed {removed} duplicate timestamps")
            
        return df
    
    def _handle_outliers(self, df: pd.DataFrame, zscore_threshold: float = 5.0) -> pd.DataFrame:
        """
        Detect outliers dùng IQR và Z-score
        Chỉ mark, không tự động remove để preserve data integrity
        """
        # Volume outliers (thường là wash trading)
        Q1 = df['volume'].quantile(0.25)
        Q3 = df['volume'].quantile(0.75)
        IQR = Q3 - Q1
        lower_bound = Q1 - 3 * IQR
        upper_bound = Q3 + 3 * IQR
        
        outliers_mask = (
            (df['volume'] < lower_bound) | 
            (df['volume'] > upper_bound)
        )
        
        outlier_count = outliers_mask.sum()
        if outlier_count > 0:
            logger.warning(f"Detected {outlier_count} volume outliers")
            
        # Price change outliers
        df['price_change_pct'] = df['close'].pct_change() * 100
        extreme_change = df['price_change_pct'].abs() > 50  # >50% change
        
        if extreme_change.any():
            logger.warning(f"Detected {extreme_change.sum()} extreme price changes")
            # Có thể set volume = 0 cho các cases này
            df.loc[extreme_change, 'volume'] = 0
            
        return df
    
    def _normalize_timezone(self, df: pd.DataFrame) -> pd.DataFrame:
        """Chuyển về timezone mặc định"""
        if df['open_time'].dt.tz is None:
            df['open_time'] = df['open_time'].dt.tz_localize('UTC')
        df['open_time'] = df['open_time'].dt.tz_convert(self.tz)
        df['close_time'] = df['close_time'].dt.tz_convert(self.tz)
        
        return df
    
    def _fill_time_gaps(self, df: pd.DataFrame, freq: str) -> pd.DataFrame:
        """
        Fill missing time periods (market downtime, holidays)
        """
        if len(df) < 2:
            return df
            
        # Tạo complete time series
        full_range = pd.date_range(
            start=df['open_time'].min(),
            end=df['open_time'].max(),
            freq=freq,
            tz=self.tz
        )
        
        # Reindex và fill forward
        df_indexed = df.set_index('open_time')
        df_filled = df_indexed.reindex(full_range)
        
        # Mark original data
        df_filled['is_original'] = ~df_filled['close'].isna()
        
        # Forward fill prices
        df_filled[['open', 'high', 'low', 'close']] = \
            df_filled[['open', 'high', 'low', 'close']].ffill()
            
        # Zero volume cho gap candles
        df_filled['volume'] = df_filled['volume'].fillna(0)
        
        df_filled = df_filled.reset_index()
        df_filled = df_filled.rename(columns={'index': 'open_time'})
        
        original_count = df_filled['is_original'].sum()
        filled_count = len(df_filled) - original_count
        
        if filled_count > 0:
            logger.info(f"Filled {filled_count} missing candles")
            
        return df_filled


def load_and_process_pipeline(
    symbol: str = "BTCUSDT",
    interval: str = "1h",
    days_back: int = 30
) -> pd.DataFrame:
    """
    Complete pipeline: Fetch -> Process -> Return
    """
    from datetime import timedelta
    
    # Initialize fetcher
    fetcher = ExchangeDataFetcher(
        api_key="your_api_key",
        secret="your_secret"
    )
    
    # Initialize processor
    processor = KlinesDataProcessor()
    
    # Fetch data
    end_date = datetime.now(timezone.utc)
    start_date = end_date - timedelta(days=days_back)
    
    raw_df = fetcher.fetch_historical(
        symbol=symbol,
        interval=interval,
        start_date=start_date,
        end_date=end_date
    )
    
    # Process
    clean_df = processor.process(raw_df)
    
    print(f"Processed {len(clean_df)} candles for {symbol} {interval}")
    print(f"Date range: {clean_df['open_time'].min()} to {clean_df['open_time'].max()}")
    
    return clean_df

Lưu trữ DataFrame: Parquet vs CSV vs Database

Qua kinh nghiệm thực chiến, tôi đã test 3 phương án lưu trữ cho dataset 1 năm BTCUSDT 1h (khoảng 8,760 candles):

Định dạng Kích thước Thời gian đọc Thời gian ghi Query capability Compression
CSV ~2.1 MB 245 ms 89 ms Thấp Không
Parquet ~380 KB 42 ms 156 ms Trung bình Có (snappy)
SQLite ~1.8 MB 18 ms 234 ms Cao

Khuyến nghị của tôi:

  • Development/Testing: CSV - đơn giản, dễ debug
  • Production/Large datasets: Parquet - compression tốt, đọc nhanh
  • Real-time trading: SQLite hoặc TimescaleDB - query mạnh
from pathlib import Path
import pyarrow as pa
import pyarrow.parquet as pq

class KlinesStorage:
    """
    Storage manager cho K-line data với Parquet backend
    """
    
    def __init__(self, data_dir: str = "./data"):
        self.data_dir = Path(data_dir)
        self.data_dir.mkdir(parents=True, exist_ok=True)
        
    def save_parquet(
        self,
        df: pd.DataFrame,
        symbol: str,
        interval: str,
        partition_by: str = "year"
    ) -> Path:
        """
        Save DataFrame thành Parquet với partitioning
        
        Args:
            df: DataFrame đã được process
            symbol: VD 'BTCUSDT'
            interval: VD '1h'
            partition_by: 'year', 'month', hoặc None
        """
        # Thêm metadata columns
        df = df.copy()
        df['symbol'] = symbol
        df['interval'] = interval
        
        # Create partition columns
        df['year'] = df['open_time'].dt.year
        df['month'] = df['open_time'].dt.month
        
        # Tạo file path
        if partition_by == 'year':
            partition_cols = ['year']
        elif partition_by == 'month':
            partition_cols = ['year', 'month']
        else:
            partition_cols = []
            
        filename = f"{symbol}_{interval}.parquet"
        filepath = self.data_dir / filename
        
        # Write với compression
        table = pa.Table.from_pandas(df)
        
        pq.write_table(
            table,
            filepath,
            compression='snappy',
            use_dictionary=True,
            write_statistics=True
        )
        
        print(f"Saved {len(df)} rows to {filepath}")
        print(f"File size: {filepath.stat().st_size / 1024:.2f} KB")
        
        return filepath
    
    def load_parquet(
        self,
        symbol: str,
        interval: str,
        year: Optional[int] = None,
        month: Optional[int] = None
    ) -> pd.DataFrame:
        """
        Load Parquet với optional filtering
        """
        filename = f"{symbol}_{interval}.parquet"
        filepath = self.data_dir / filename
        
        if not filepath.exists():
            raise FileNotFoundError(f"Data file not found: {filepath}")
            
        # Read with filters
        filters = None
        if year and month:
            filters = [('year', '=', year), ('month', '=', month)]
        elif year:
            filters = [('year', '=', year)]
            
        table = pq.read_table(filepath, filters=filters)
        df = table.to_pandas()
        
        # Drop partition columns
        cols_to_drop = ['symbol', 'interval', 'year', 'month', 'is_original']
        df = df.drop(columns=[c for c in cols_to_drop if c in df.columns])
        
        return df
    
    def incremental_save(
        self,
        df: pd.DataFrame,
        symbol: str,
        interval: str
    ):
        """
        Append new data without rewriting entire file
        """
        existing_path = self.data_dir / f"{symbol}_{interval}.parquet"
        
        if existing_path.exists():
            # Load existing
            existing_df = self.load_parquet(symbol, interval)
            
            # Combine
            combined_df = pd.concat([existing_df, df], ignore_index=True)
            combined_df = combined_df.drop_duplicates(subset=['open_time'])
            combined_df = combined_df.sort_values('open_time')
        else:
            combined_df = df
            
        # Save
        self.save_parquet(combined_df, symbol, interval)
        
        return combined_df

Sử dụng HolySheep AI cho Data Processing Pipeline

Trong quá trình xây dựng system, tôi nhận ra một vấn đề: khi cần xử lý nhiều cặp tiền cùng lúc, compute resource trên máy local không đủ. HolySheep AI giải quyết bài toán này bằng cách cung cấp API endpoint mạnh mẽ với chi phí cực thấp.

Phù hợp / không phù hợp với ai

👌 PHÙ HỢP 👎 KHÔNG PHÙ HỢP
  • Retail traders cần dữ liệu backtest
  • Developers xây dựng trading bots
  • Data analysts nghiên cứu thị trường
  • 中小型企业 cần xử lý data nhanh
  • Hedge funds cần data proprietary
  • Trading firms cần ultra-low latency
  • Người cần data real-time millisecond

Giá và ROI

Provider Giá/1M tokens Tỷ giá Latency Tiết kiệm
OpenAI GPT-4.1 $8.00 USD ~200ms Baseline
Anthropic Claude 4.5 $15.00 USD ~180ms -47%
Google Gemini 2.5 Flash $2.50 USD ~80ms +69%
HolySheep DeepSeek V3.2 $0.42 ¥1=$1 <50ms +85%

Vì sao chọn HolySheep

  • Tiết kiệm 85%+: Với tỷ giá ¥1=$1 và giá chỉ $0.42/1M tokens cho DeepSeek V3.2, chi phí xử lý data giảm đáng kể
  • Tốc độ <50ms: Latency cực thấp, phù hợp cho pipeline automation
  • Thanh toán linh hoạt: Hỗ trợ WeChat Pay, Alipay, Alipay HK - thuận tiện cho người dùng châu Á
  • Tín dụng miễn phí: Đăng ký tại đây để nhận credits dùng thử
  • API tương thích: Base URL https://api.holysheep.ai/v1, dễ dàng integrate vào code hiện tại
# Ví dụ sử dụng HolySheep API cho data quality check
import requests
import json

def check_data_quality_with_ai(df: pd.DataFrame, symbol: str) -> dict:
    """
    Dùng AI để phân tích data quality và detect anomalies
    """
    
    # Tạo summary statistics
    stats = {
        'symbol': symbol,
        'total_candles': len(df),
        'date_range': f"{df['open_time'].min()} to {df['open_time'].max()}",
        'volume_stats': {
            'mean': float(df['volume'].mean()),
            'std': float(df['volume'].std()),
            'max': float(df['volume'].max()),
            'min': float(df['volume'].min())
        },
        'price_stats': {
            'mean_close': float(df['close'].mean()),
            'max_close': float(df['close'].max()),
            'min_close': float(df['close'].min())
        },
        'missing_values': df[['open', 'high', 'low', 'close', 'volume']].isnull().sum().to_dict()
    }
    
    prompt = f"""
    Analyze this {symbol} trading data quality:
    {json.dumps(stats, indent=2)}
    
    Return JSON với:
    - issues: list các vấn đề phát hiện được
    - recommendation: hành động khuyến nghị
    - quality_score: 0-100
    """
    
    # Gọi HolySheep API
    response = requests.post(
        'https://api.holysheep.ai/v1/chat/completions',
        headers={
            'Authorization': 'Bearer YOUR_HOLYSHEEP_API_KEY',
            'Content-Type': 'application/json'
        },
        json={
            'model': 'deepseek-v3.2',
            'messages': [{'role': 'user', 'content': prompt}],
            'temperature': 0.3,
            'max_tokens': 500
        }
    )
    
    result = response.json()
    
    return {
        'analysis': result['choices'][0]['message']['content'],
        'usage': result.get('usage', {})
    }

Sử dụng

if __name__ == "__main__": # Load processed data df = pd.read_parquet('./data/BTCUSDT_1h.parquet') # AI analysis analysis = check_data_quality_with_ai(df.head(1000), 'BTCUSDT') print(analysis)

Ứng dụng thực tế: Automated Data Pipeline

Đây là production pipeline tôi đang chạy cho 5 cặp tiền hàng ngày:

import schedule
import time
from datetime import datetime, timezone

def daily_pipeline():
    """
    Automated daily data pipeline
    """
    symbols = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'SOLUSDT', 'ADAUSDT']
    interval = '1h'
    
    fetcher = ExchangeDataFetcher(api_key="...", secret="...")
    processor = KlinesDataProcessor()
    storage = KlinesStorage('./data')
    
    for symbol in symbols:
        try:
            # Fetch last 24h data
            from datetime import timedelta
            end = datetime.now(timezone.utc)
            start = end - timedelta(hours=25)
            
            df = fetcher.fetch_historical(
                symbol=symbol,
                interval=interval,
                start_date=start,
                end_date=end
            )
            
            if not df.empty:
                # Process
                df_clean = processor.process(df)
                
                # Append to storage
                storage.incremental_save(df_clean, symbol, interval)
                
                print(f"✓ {symbol}: Updated {len(df_clean)} candles")
                
            time.sleep(1)  # Rate limit
            
        except Exception as e:
            print(f"✗ {symbol}: {e}")
            
    print(f"Pipeline completed at {datetime.now()}")

Schedule daily run

schedule.every().day.at("00:05").do(daily_pipeline) while True: schedule.run_pending() time.sleep(60)

Lỗi thường gặp và cách khắc phục

1. Lỗi "ConnectionError: timeout" khi fetch dữ liệu

Nguyên nhân: API rate limit, network instability, hoặc server overload.

# Cách khắc phục - Implement exponential backoff
import time
import requests

def fetch_with_retry(url, max_retries=5, base_delay=1):
    for attempt in range(max_retries):
        try:
            response = requests.get(url, timeout=30)
            response.raise_for_status()
            return response.json()
        except (requests.exceptions.Timeout, 
                requests.exceptions.ConnectionError) as e:
            if attempt < max_retries - 1:
                delay = base_delay * (2 ** attempt)  # Exponential backoff
                print(f"Retry {attempt + 1} sau {delay}s...")
                time.sleep(delay)
            else:
                raise ConnectionError(f"Failed sau {max_retries} attempts: {e}")

2. Lỗi "401 Unauthorized" khi gọi API

Nguyên nhân: API key không hợp lệ, hết hạn, hoặc thiếu quyền truy cập.

# Cách khắc phục
def validate_api_key(api_key, secret, base_url):
    """Kiểm tra API key trước khi sử dụng"""
    import hashlib
    import time