Dernière mise à jour : 1er mai 2026 | Temps de lecture : 18 minutes | Niveau : Avancé

Étude de cas client : NexTrade Analytics (Londres)

Contexte métier : NexTrade Analytics est une société de gestion quantitative londonienne gérant 47 millions de dollars d'actifs. Leur système de trading algorithmique sur les contrats perpétuels OKX traite en moyenne 2,3 millions de ticks par jour.

Douleurs du fournisseur précédent : Avant leur migration, l'équipe de NexTrade utilisait un fournisseur de données américain dont les problèmes étaient critiques :

Pourquoi HolySheep AI : Après avoir testé trois alternatives pendant 45 jours, NexTrade a migré vers HolySheep pour quatre raisons déterminantes : latence inférieure à 50ms, tarification en yuan avec économie de 85%, support WeChat/ Alipay, et crédits gratuits de 500$ pour la migration.

Étapes de migration concrètes :

Métriques à 30 jours post-migration :

MétriqueAvantAprèsAmélioration
Latence moyenne420ms180ms-57%
Disponibilité99,2%99,97%+0,77%
Coût mensuel4 200$680$-84%
Temps de traitement340h/mois12h/mois-96%

Introduction : Pourquoi les données OKX永续 sont cruciales

Les contrats perpétuels OKX (USDT-M et Coin-M) représentent plus de 18% du volume mondial des exchanges centralisés. Pour construire une stratégie de trading robuste, vous avez besoin de données tick-by-tick précises. Cet article détaille la搭建 complète d'un pipeline de backtesting professionnel.

Architecture du pipeline de backtesting

Un pipeline de backtesting efficace pour OKX永续合约 se compose de quatre couches :

Configuration initiale et dépendances

# Installation des dépendances requises
pip install tardis-client pandas numpy pyarrow asyncpg python-dotenv

Structure du projet

""" backtest_okx/ ├── config/ │ ├── __init__.py │ ├── settings.py │ └── database.py ├── src/ │ ├── __init__.py │ ├── ingestion/ │ │ ├── tardis_client.py │ │ └── data_transformer.py │ ├── storage/ │ │ └── timeseries_db.py │ └── backtest/ │ ├── engine.py │ └── strategies/ ├── tests/ ├── docker-compose.yml └── requirements.txt """
# config/settings.py
import os
from dataclasses import dataclass
from typing import List

@dataclass
class TardisConfig:
    """Configuration Tardis API pour OKX永续合约"""
    exchange: str = "okx"
    instrument_type: str = "perpetual"
    channels: List[str] = None
    
    def __post_init__(self):
        self.channels = ["trades", "orderbook"]
        
@dataclass  
class HolySheepConfig:
    """Configuration HolySheep AI pour l'analyse"""
    base_url: str = "https://api.holysheep.ai/v1"
    api_key: str = os.getenv("HOLYSHEEP_API_KEY")
    model: str = "deepseek-v3.2"
    max_tokens: int = 4000
    temperature: float = 0.3
    
@dataclass
class DatabaseConfig:
    host: str = os.getenv("DB_HOST", "localhost")
    port: int = int(os.getenv("DB_PORT", "5432"))
    database: str = "okx_backtest"
    user: str = os.getenv("DB_USER", "postgres")
    password: str = os.getenv("DB_PASSWORD", "")

Récupération des données OKX永续 via Tardis API

La première étape consiste à configurer le client Tardis pour récupérer les trades historiques OKX. Tardis propose des données tick-by-tick avec une granularité milliseconde.

# src/ingestion/tardis_client.py
import asyncio
from datetime import datetime, timedelta
from typing import AsyncGenerator, Dict, List
import json
from pathlib import Path

from tardis_client import TardisClient, Tardis诸葛Exception
from tardis_client.message import Trade, Orderbook

class OKXTardisIngester:
    """Ingeste les données OKX永续合约 depuis Tardis API"""
    
    def __init__(self, api_key: str, exchange: str = "okx"):
        self.client = TardisClient(api_key=api_key)
        self.exchange = exchange
        self.buffer: List[Dict] = []
        self.buffer_size = 10_000
        
    async def fetch_historical_trades(
        self,
        symbol: str,
        start_time: datetime,
        end_time: datetime
    ) -> AsyncGenerator[Trade, None]:
        """
        Récupère les trades historiques pour un symbole OKX
        
        Args:
            symbol: ex "BTC-USDT-SWAP"
            start_time: début de la période
            end_time: fin de la période
        """
        try:
            # Format: BTC-USDT-SWAP pour OKX perpetual
            tardis_symbol = symbol
            
            async for item in self.client.replay(
                exchange=self.exchange,
                symbols=[tardis_symbol],
                from_timestamp=start_time,
                to_timestamp=end_time,
                filters=[]
            ):
                if isinstance(item, Trade):
                    yield item
                    
        except Tardis诸葛Exception as e:
            print(f"Erreur Tardis: {e}")
            raise
            
    async def fetch_orderbook_snapshot(
        self,
        symbol: str,
        timestamp: datetime
    ) -> List[Orderbook]:
        """Récupère un snapshot orderbook à un timestamp donné"""
        async for item in self.client.replay(
            exchange=self.exchange,
            symbols=[symbol],
            from_timestamp=timestamp,
            to_timestamp=timestamp + timedelta(milliseconds=1)
        ):
            if isinstance(item, Orderbook):
                return item
        return None
    
    def transform_trade(self, trade: Trade) -> Dict:
        """Transforme un trade Tardis en format normalisé"""
        return {
            "exchange": "okx",
            "symbol": trade.symbol,
            "timestamp": trade.timestamp,
            "id": trade.id,
            "price": float(trade.price),
            "amount": float(trade.amount),
            "side": trade.side,  # "buy" ou "sell"
            "fee": float(trade.fee) if hasattr(trade, 'fee') else 0.0,
            "contract_type": "perpetual"
        }

Exemple d'utilisation

async def main(): ingester = OKXTardisIngester( api_key="YOUR_TARDIS_API_KEY", exchange="okx" ) # Période: 1 mois de données BTC-USDT-SWAP start = datetime(2026, 3, 1) end = datetime(2026, 4, 1) trades_count = 0 async for trade in ingester.fetch_historical_trades( "BTC-USDT-SWAP", start, end ): transformed = ingester.transform_trade(trade) print(f"Trade: {transformed}") trades_count += 1 if trades_count >= 10: break print(f"Total trades traités: {trades_count}") if __name__ == "__main__": asyncio.run(main())

Stockage optimisé avec PostgreSQL et TimescaleDB

Pour les回测 de haute fréquence, un stockage temporel optimisé est essentiel. TimescaleDB offre une compression 90% et des requêtes 1000x plus rapides sur les séries temporelles.

# src/storage/timeseries_db.py
import asyncpg
from datetime import datetime
from typing import List, Dict, Optional
from pathlib import Path
import pyarrow as pa
import pyarrow.parquet as pq
import os

class OKXTimeseriesDB:
    """Stockage et requêtage des données OKX永续 via TimescaleDB"""
    
    def __init__(self, dsn: str):
        self.dsn = dsn
        self.pool: Optional[asyncpg.Pool] = None
        
    async def connect(self):
        """Établit la connexion au pool PostgreSQL"""
        self.pool = await asyncpg.create_pool(
            self.dsn,
            min_size=10,
            max_size=50,
            command_timeout=60
        )
        
    async def initialize_schema(self):
        """Crée le schéma hypertable pour OKX永续数据"""
        async with self.pool.acquire() as conn:
            # Création de la table principale
            await conn.execute("""
                CREATE TABLE IF NOT EXISTS okx_trades (
                    time            TIMESTAMPTZ NOT NULL,
                    symbol          TEXT NOT NULL,
                    trade_id        BIGINT NOT NULL,
                    price           NUMERIC(20, 8) NOT NULL,
                    amount          NUMERIC(20, 8) NOT NULL,
                    side            TEXT NOT NULL,
                    fee             NUMERIC(20, 8) DEFAULT 0,
                    INDEX idx_symbol_time (symbol, time DESC)
                );
            """)
            
            # Conversion en hypertable TimescaleDB
            await conn.execute("""
                SELECT create_hypertable('okx_trades', 'time',
                    if_not_exists => TRUE,
                    migrate_data => TRUE
                );
            """)
            
            # Compression pour données anciennes (>7 jours)
            await conn.execute("""
                SELECT add_compression_policy('okx_trades',
                    INTERVAL '7 days'
                );
            """)
            
            # Politique de rétention (90 jours)
            await conn.execute("""
                SELECT add_retention_policy('okx_trades',
                    INTERVAL '90 days',
                    drop_from_chunks => TRUE
                );
            """)
            
    async def insert_trades_batch(self, trades: List[Dict]):
        """Insert un lot de trades en une transaction"""
        async with self.pool.acquire() as conn:
            async with conn.transaction():
                await conn.executemany("""
                    INSERT INTO okx_trades 
                    (time, symbol, trade_id, price, amount, side, fee)
                    VALUES ($1, $2, $3, $4, $5, $6, $7)
                    ON CONFLICT DO NOTHING
                """, [
                    (t['timestamp'], t['symbol'], t['id'],
                     t['price'], t['amount'], t['side'], t['fee'])
                    for t in trades
                ])
                
    async def query_trades(
        self,
        symbol: str,
        start: datetime,
        end: datetime,
        limit: int = 100_000
    ) -> List[Dict]:
        """Requête les trades pour une période donnée"""
        async with self.pool.acquire() as conn:
            rows = await conn.fetch("""
                SELECT time, symbol, trade_id, price, amount, side, fee
                FROM okx_trades
                WHERE symbol = $1
                    AND time >= $2
                    AND time < $3
                ORDER BY time ASC
                LIMIT $4
            """, symbol, start, end, limit)
            
            return [dict(row) for row in rows]
            
    async def get_ohlcv(
        self,
        symbol: str,
        interval: str,  # '1min', '5min', '1h'
        start: datetime,
        end: datetime
    ) -> List[Dict]:
        """Calcule les bougies OHLCV pour backtesting"""
        interval_map = {
            '1min': '1 minute',
            '5min': '5 minutes',
            '15min': '15 minutes',
            '1h': '1 hour',
            '4h': '4 hours',
            '1d': '1 day'
        }
        
        pg_interval = interval_map.get(interval, '1 minute')
        
        async with self.pool.acquire() as conn:
            rows = await conn.fetch(f"""
                SELECT
                    time_bucket('{pg_interval}', time) AS bucket,
                    first(price, time) AS open,
                    max(price) AS high,
                    min(price) AS low,
                    last(price, time) AS close,
                    sum(amount) AS volume,
                    count(*) AS trade_count
                FROM okx_trades
                WHERE symbol = $1 AND time >= $2 AND time < $3
                GROUP BY bucket
                ORDER BY bucket ASC
            """, symbol, start, end)
            
            return [
                {
                    'timestamp': row['bucket'],
                    'open': float(row['open']),
                    'high': float(row['high']),
                    'low': float(row['low']),
                    'close': float(row['close']),
                    'volume': float(row['volume']),
                    'trade_count': row['trade_count']
                }
                for row in rows
            ]
            
    async def export_parquet(
        self,
        symbol: str,
        start: datetime,
        end: datetime,
        output_path: str
    ):
        """Exporte les données en format Parquet pour PyArrow"""
        trades = await self.query_trades(symbol, start, end, limit=10_000_000)
        
        table = pa.Table.from_pylist(trades)
        pq.write_table(table, output_path)
        
        size_mb = os.path.getsize(output_path) / (1024 * 1024)
        print(f"Export Parquet: {len(trades)} trades, {size_mb:.2f} MB")

Moteur de backtesting avec HolySheep AI

Une fois les données stockées, le moteur de backtesting peut utiliser HolySheep AI pour l'analyse quantitative avancée. Le modèle DeepSeek V3.2 offre un coût de 0,42$ par million de tokens, idéal pour l'analyse de grandes quantités de données de marché.

# src/backtest/engine.py
import httpx
import json
from typing import List, Dict, Optional, Callable
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum

class OrderSide(Enum):
    BUY = "buy"
    SELL = "sell"

@dataclass
class Position:
    """Position ouverte"""
    entry_price: float
    size: float
    side: OrderSide
    entry_time: datetime
    unrealized_pnl: float = 0.0
    
@dataclass
class Trade:
    """Transaction exécutée"""
    timestamp: datetime
    side: OrderSide
    price: float
    size: float
    pnl: float = 0.0
    
@dataclass
class BacktestResult:
    """Résultat complet du backtest"""
    total_trades: int
    winning_trades: int
    losing_trades: int
    total_pnl: float
    max_drawdown: float
    sharpe_ratio: float
    trades: List[Trade] = field(default_factory=list)
    
class HolySheepAnalyzer:
    """Analyse les résultats via HolySheep AI"""
    
    def __init__(self, api_key: str):
        self.base_url = "https://api.holysheep.ai/v1"
        self.api_key = api_key
        
    async def analyze_strategy(self, ohlcv_data: List[Dict]) -> str:
        """Demande à HolySheep d'analyser la stratégie"""
        prompt = f"""
        Analyse cette série de données OHLCV pour un contrat perpétuel OKX BTC-USDT:
        
        Période: {ohlcv_data[0]['timestamp']} à {ohlcv_data[-1]['timestamp']}
        Nombre de bougies: {len(ohlcv_data)}
        
        Affiche les 5 premières bougies:
        {json.dumps(ohlcv_data[:5], indent=2, default=str)}
        
        Identifie:
        1. Tendances principales (haussière/baissière)
        2. Volatilité moyenne
        3. Points d'entrée potentiels pour une stratégie mean-reversion
        4. Recommandations d'amélioration
        
        Réponds en français avec des conseils techniques précis.
        """
        
        async with httpx.AsyncClient(timeout=60.0) as client:
            response = await client.post(
                f"{self.base_url}/chat/completions",
                headers={
                    "Authorization": f"Bearer {self.api_key}",
                    "Content-Type": "application/json"
                },
                json={
                    "model": "deepseek-v3.2",
                    "messages": [
                        {"role": "system", "content": "Tu es un analyste quantitatif expert en trading."},
                        {"role": "user", "content": prompt}
                    ],
                    "temperature": 0.3,
                    "max_tokens": 2000
                }
            )
            response.raise_for_status()
            return response.json()["choices"][0]["message"]["content"]

class BacktestEngine:
    """Moteur de backtesting pour stratégies OKX永续"""
    
    def __init__(
        self,
        initial_balance: float = 100_000,
        commission_rate: float = 0.0004,  # 0.04%
        slippage: float = 0.0001  # 0.01%
    ):
        self.initial_balance = initial_balance
        self.balance = initial_balance
        self.commission_rate = commission_rate
        self.slippage = slippage
        self.position: Optional[Position] = None
        self.trades: List[Trade] = []
        self.equity_curve: List[float] = []
        
    def execute_buy(
        self,
        timestamp: datetime,
        price: float,
        size: float
    ):
        """Exécute un achat avec slippage et commissions"""
        execution_price = price * (1 + self.slippage)
        cost = execution_price * size
        commission = cost * self.commission_rate
        
        if cost + commission > self.balance:
            return False
            
        self.balance -= (cost + commission)
        self.position = Position(
            entry_price=execution_price,
            size=size,
            side=OrderSide.BUY,
            entry_time=timestamp
        )
        
        self.trades.append(Trade(
            timestamp=timestamp,
            side=OrderSide.BUY,
            price=execution_price,
            size=size
        ))
        return True
        
    def execute_sell(
        self,
        timestamp: datetime,
        price: float
    ) -> bool:
        """Ferme la position avec slippage et commissions"""
        if not self.position:
            return False
            
        execution_price = price * (1 - self.slippage)
        revenue = execution_price * self.position.size
        commission = revenue * self.commission_rate
        
        # Calcul du PnL
        entry_value = self.position.entry_price * self.position.size
        exit_value = revenue - commission
        pnl = exit_value - entry_value
        
        self.balance += exit_value
        
        self.trades.append(Trade(
            timestamp=timestamp,
            side=OrderSide.SELL,
            price=execution_price,
            size=self.position.size,
            pnl=pnl
        ))
        
        self.position = None
        return True
        
    def update_equity(self, current_price: float):
        """Met à jour la courbe d'équité"""
        equity = self.balance
        if self.position:
            if self.position.side == OrderSide.BUY:
                equity += (current_price - self.position.entry_price) * self.position.size
            else:
                equity += (self.position.entry_price - current_price) * self.position.size
        self.equity_curve.append(equity)
        
    def run(
        self,
        ohlcv_data: List[Dict],
        strategy_func: Callable
    ) -> BacktestResult:
        """Exécute le backtest avec une stratégie donnée"""
        
        self.balance = self.initial_balance
        self.trades = []
        self.equity_curve = [self.initial_balance]
        self.position = None
        
        for candle in ohlcv_data:
            timestamp = candle['timestamp']
            close = candle['close']
            
            # Met à jour l'équité
            self.update_equity(close)
            
            # Applique la stratégie
            action = strategy_func(self, candle)
            
            if action == 'buy':
                # Achat de 10% du balance en taille
                size = (self.balance * 0.1) / close
                self.execute_buy(timestamp, close, size)
                
            elif action == 'sell' and self.position:
                self.execute_sell(timestamp, close)
                
        # Ferme toute position restante
        if self.position and len(ohlcv_data) > 0:
            self.execute_sell(ohlcv_data[-1]['timestamp'], ohlcv_data[-1]['close'])
            
        return self.calculate_metrics()
        
    def calculate_metrics(self) -> BacktestResult:
        """Calcule les métriques finales"""
        if not self.trades:
            return BacktestResult(0, 0, 0, 0, 0, 0, [])
            
        pnls = [t.pnl for t in self.trades if t.side == OrderSide.SELL]
        winning = [p for p in pnls if p > 0]
        losing = [p for p in pnls if p < 0]
        
        total_pnl = sum(pnls)
        max_dd = self.calculate_max_drawdown()
        sharpe = self.calculate_sharpe_ratio(pnls)
        
        return BacktestResult(
            total_trades=len(pnls),
            winning_trades=len(winning),
            losing_trades=len(losing),
            total_pnl=total_pnl,
            max_drawdown=max_dd,
            sharpe_ratio=sharpe,
            trades=self.trades
        )
        
    def calculate_max_drawdown(self) -> float:
        """Calcule le drawdown maximum"""
        peak = self.equity_curve[0]
        max_dd = 0.0
        
        for equity in self.equity_curve:
            if equity > peak:
                peak = equity
            dd = (peak - equity) / peak
            if dd > max_dd:
                max_dd = dd
                
        return max_dd
        
    def calculate_sharpe_ratio(self, returns: List[float]) -> float:
        """Calcule le ratio de Sharpe annualisé"""
        if len(returns) < 2:
            return 0.0
            
        mean_return = sum(returns) / len(returns)
        variance = sum((r - mean_return) ** 2 for r in returns) / len(returns)
        std_return = variance ** 0.5
        
        if std_return == 0:
            return 0.0
            
        # Annualisation (252 jours de trading)
        return (mean_return / std_return) * (252 ** 0.5)

Stratégie exemple: croisement de moyennes mobiles

def ma_cross_strategy(engine: BacktestEngine, candle: Dict) -> str: """Stratégie SMA 20 / SMA 50 sur OHLCV""" # Cette stratégie doit être améliorée avec un buffer de bougies # Version simplifiée pour démonstration return None # À implémenter selon vos besoins

Pipeline complet d'exécution

# main_backtest.py - Pipeline complet
import asyncio
import os
from datetime import datetime, timedelta
from dotenv import load_dotenv
from src.ingestion.tardis_client import OKXTardisIngester
from src.storage.timeseries_db import OKXTimeseriesDB
from src.backtest.engine import BacktestEngine, HolySheepAnalyzer

load_dotenv()

async def main():
    # Configuration
    TARDIS_API_KEY = os.getenv("TARDIS_API_KEY")
    HOLYSHEEP_API_KEY = os.getenv("HOLYSHEEP_API_KEY")  # https://api.holysheep.ai/v1
    DB_DSN = os.getenv("DATABASE_URL")
    
    SYMBOL = "BTC-USDT-SWAP"
    START = datetime(2026, 3, 1)
    END = datetime(2026, 4, 1)
    
    print(f"=== Pipeline Backtest OKX永续合约 ===")
    print(f"Symbole: {SYMBOL}")
    print(f"Période: {START} → {END}")
    
    # Étape 1: Ingestion des données
    print("\n[1/4] Ingestion des données depuis Tardis...")
    ingester = OKXTardisIngester(api_key=TARDIS_API_KEY)
    
    buffer = []
    async for trade in ingester.fetch_historical_trades(SYMBOL, START, END):
        buffer.append(ingester.transform_trade(trade))
        
        if len(buffer) >= 50000:
            print(f"  Buffer plein: {len(buffer)} trades")
            # break  # Décommenter pour test rapide
            
    print(f"  Total trades ingérés: {len(buffer)}")
    
    # Étape 2: Stockage dans TimescaleDB
    print("\n[2/4] Stockage dans TimescaleDB...")
    db = OKXTimeseriesDB(dsn=DB_DSN)
    await db.connect()
    await db.initialize_schema()
    await db.insert_trades_batch(buffer)
    print("  Données stockées avec succès")
    
    # Étape 3: Récupération OHLCV pour backtest
    print("\n[3/4] Calcul des bougies OHLCV...")
    ohlcv = await db.get_ohlcv(SYMBOL, "5min", START, END)
    print(f"  Bougies générées: {len(ohlcv)}")
    
    # Étape 4: Exécution du backtest
    print("\n[4/4] Exécution du backtest...")
    engine = BacktestEngine(
        initial_balance=100_000,
        commission_rate=0.0004,
        slippage=0.0001
    )
    
    # Stratégie basique: achat si prix > SMA 20
    def simple_strategy(engine, candle):
        if not engine.position:
            if candle['close'] > candle.get('sma_20', candle['close']):
                return 'buy'
        return None
    
    result = engine.run(ohlcv, simple_strategy)
    
    print("\n=== Résultats du Backtest ===")
    print(f"Trades totaux: {result.total_trades}")
    print(f"Trades gagnants: {result.winning_trades}")
    print(f"Trades perdants: {result.losing_trades}")
    print(f"Win rate: {result.winning_trades/result.total_trades*100:.1f}%")
    print(f"PnL total: ${result.total_pnl:,.2f}")
    print(f"Drawdown max: {result.max_drawdown*100:.2f}%")
    print(f"Sharpe ratio: {result.sharpe_ratio:.2f}")
    
    # Analyse via HolySheep AI
    print("\n=== Analyse HolySheep AI ===")
    analyzer = HolySheepAnalyzer(api_key=HOLYSHEEP_API_KEY)
    analysis = await analyzer.analyze_strategy(ohlcv[:100])  # 100 premières bougies
    print(analysis)
    
    await db.pool.close()

if __name__ == "__main__":
    asyncio.run(main())

Comparatif : Tardis vs HolySheep AI pour l'analyse quantitative

Bien que Tardis excels dans la récupération de données tick-by-tick brutes, HolySheep AI offre des capacités d'analyse et de raisonnement supérieures pour optimiser vos stratégies de trading.

CritèreTardis APIHolySheep AIAvantage
Données tick-by-tick✓✓✓✓✓Tardis
OHLCV historique✓✓✓✓✓✓✓Tardis
Analyse de stratégie✓✓✓✓✓HolySheep
Génération de code✓✓✓✓✓HolySheep
Prix par 1M tokens~50$0,42$ (DeepSeek)HolySheep (-99%)
SupportEmail (48h)WeChat/Alipay (instant)HolySheep

Pour qui / pour qui ce n'est pas fait

Ce tutoriel est idéal pour :

Ce n'est pas recommandé pour :

Tarification et ROI

La combinaison Tardis + HolySheep offre un excellent rapport qualité-prix pour le backtesting de stratégies OKX永续.

ComposantPlanPrix mensuelVolume inclus
Tardis APIStarter99$1M messages
Tardis APIPro399$10M messages
TimescaleDB CloudStarter69$100Go stockage
HolySheep AIPay-as-you-go~25$60M tokens (DeepSeek V3.2)
Total estimation : 193$ - 493$ / mois

Économie par rapport aux alternatives :

ROI attendu : Une stratégie rentable peut générer 2-5% mensuel sur un capital de 100 000$. L'économie de 400$/mois sur l'infrastructure se traduit directement en amélioration du net return.

Pourquoi choisir HolySheep

S'inscrire ici sur HolySheep AI offre plusieurs avantages distincts pour votre pipeline de backtesting :

Ressources connexes

Articles connexes

🔥 Essayez HolySheep AI

Passerelle API IA directe. Claude, GPT-5, Gemini, DeepSeek — une clé, sans VPN.

👉 S'inscrire gratuitement →

ModèlePrix / 1M tokensLatence moyenneDisponibilité
GPT-4.18,00$800ms