巴基斯坦證券交易所(PSX)股票API對接:即時行情與量化交易實踐指南

  1. iTick
  2. 教程
巴基斯坦證券交易所(PSX)股票API對接:即時行情與量化交易實踐指南 - iTick
巴基斯坦證券交易所(PSX)股票API對接:即時行情與量化交易實踐指南

引言:為什麼PSX是量化交易的下一片藍海?

巴基斯坦證券交易所(PSX)作為南亞第三大股票市場,近年來吸引了大量國際投資機構的關注。KSE-100指數涵蓋卡拉奇、拉合爾、伊斯蘭堡三地交易所的核心藍籌股,能源、銀行、水泥板塊交易活躍度持續攀升。然而,對於量化開發者而言,PSX長期面臨兩大痛點:

  1. 數據接口匱乏:傳統數據商對新興市場覆蓋不足,即時行情獲取成本高昂
  2. 本地化適配困難:PSX的交易時段(巴基斯坦標準時間,PST)、股票代碼規則與主流市場存在差異

iTick API的誕生正在改變這一局面。依託統一的多市場數據架構,iTick不僅覆蓋美股、港股、A股等成熟市場,更通過標準化接口為新興市場留出擴展空間。本文將手把手教你:

  • ✅ 如何用iTick風格API封裝PSX數據請求
  • ✅ 構建適配卡拉奇交易時段的量化回測框架
  • ✅ 基於雙均線策略的PSX成分股實戰代碼
  • ✅ 從歷史回測到模擬交易的全流程落地

所有代碼開箱即用,複製粘貼即可啟動你的第一筆PSX量化策略回測。


一、iTick API核心優勢:為什麼選擇它作為PSX數據橋樑?

在深入代碼之前,我們先快速梳理iTick API的幾個關鍵特性,這些能力將直接服務於PSX市場的策略開發:

特性對PSX量化的價值
統一REST/WebSocket接口無需為不同市場維護多套數據解析邏輯
毫秒級歷史K線查詢支持KSE-100指數成分股多年數據回測
免費套餐可用個人開發者0成本啟動策略驗證
多語言SDKPython優先支持,無縫對接Pandas/NumPy
可擴展市場標識自定義region參數適配非原生支持市場

💡 核心思路:儘管iTick目前尚未原生開放PSX專用端點,但我們可以通過**「市場適配層」設計模式**,將PSX股票映射為iTick現有架構中的自定義標的,實現數據獲取與策略邏輯的解耦。

二、環境準備與API憑證獲取

2.1 安裝必要依賴庫

      pip install requests pandas numpy matplotlib ta-lib
# iTick官方數據接口庫(支持統一數據格式)
pip install itrade

    

2.2 獲取免費API密鑰

  1. 訪問 iTick官網 點擊「立即獲取」
  2. 使用GitHub/Google帳號完成OAuth登錄
  3. 進入控制台,複製您的API Key(如:bb42e247...

⚠️ 請妥善保管密鑰,本文後續代碼均使用環境變量注入方式,切勿硬編碼在腳本中。

      import os
from dotenv import load_dotenv
load_dotenv()

ITICK_API_KEY = os.getenv("ITICK_API_KEY", "your_key_here")

    

三、PSX數據適配層:讓iTick讀懂卡拉奇交易所

由於PSX目前不在iTick原生市場列表中,我們採用**「協議兼容+本地映射」**策略,將PSX股票編碼為自定義格式,通過iTick通用數據接口獲取結構化行情。

3.1 獲取PSX全部股票列表

      import requests
import pandas as pd
from typing import Optional, List, Dict
import os

def fetch_psx_symbols(api_key: str) -> Optional[List[Dict]]:
    url = "https://api.itick.org/symbol/list?type=stock&region=PK"
    headers = {
        "accept": "application/json",
        "token": api_key  # iTick使用token頭進行認證
    }
    try:
        print(f"🔄 正在請求PSX股票列表...")
        response = requests.get(
            url, 
            headers=headers, 
            timeout=15
        )
        
        # 處理HTTP狀態碼
        if response.status_code == 200:
            data = response.json()
            if data.get("code") == 200:
                symbol_list = data.get("data", [])
                print(f"✅ 成功獲取 {len(symbol_list)} 只PSX股票")
                return symbol_list
            else:
                print(f"⚠️ API業務錯誤: {data.get('msg')} (代碼: {data.get('code')})")
                return None
                
        elif response.status_code == 401:
            print("❌ 認證失敗 (401): 請檢查API密鑰是否有效、過期,或該接口未被您的套餐覆蓋")
            print("   - 登錄 https://itick.org 控制台查看密鑰狀態")
            print("   - 確認您的套餐是否包含'股票基礎數據'權限")
            return None
            
        elif response.status_code == 403:
            print("❌ 權限不足 (403): 您的帳號無權訪問PSX區域數據")
            print("   - PSX目前可能是受限測試狀態,需聯繫商務開通")
            return None
            
        elif response.status_code == 429:
            print("❌ 請求過於頻繁 (429): 請稍後重試,或升級套餐提高速率限制")
            return None
            
        else:
            print(f"❌ 未知錯誤: HTTP {response.status_code}")
            return None
    except Exception as e:
        print(f"❌ 請求異常: {str(e)}")
        return None


def save_to_dataframe(symbol_list: List[Dict]) -> pd.DataFrame:
    """將股票列表保存為Pandas DataFrame便於分析"""
    if not symbol_list:
        return pd.DataFrame()
    
    df = pd.DataFrame(symbol_list)
    print("\n📋 PSX股票列表預覽:")
    print(df.head())
    print(f"\n總記錄數: {len(df)}")
    return df


# ===== 使用示例 =====
if __name__ == "__main__":
    # ⚠️ 重要: 請從環境變量或配置文件中讀取密鑰,切勿硬編碼
    YOUR_API_KEY = os.environ.get("ITICK_API_KEY", "")
    
    if not YOUR_API_KEY:
        print("請先設置環境變量 ITICK_API_KEY,或直接賦值(僅測試)")
        # YOUR_API_KEY = "your_key_here"  # 測試時可臨時取消註釋
    else:
        # 步驟1: 獲取PSX股票列表
        symbols = fetch_psx_symbols(YOUR_API_KEY)
        
        # 步驟2: 轉換為DataFrame
        if symbols:
            df = save_to_dataframe(symbols)
            
            # 可選: 保存到CSV
            df.to_csv("psx_symbols.csv", index=False)
            print("\n💾 已保存至 psx_symbols.csv")
            
            # 打印所有股票代碼
            print("\n🔹 PSX股票代碼列表:")
            for code in df.get("symbol", [])[:20]:  # 顯示前20個
                print(f"  {code}")

    

3.2 構建PSX數據獲取器

我們封裝一個PSXDataFetcher類,內部復用iTick的stock/kline接口格式,將region參數標識PK,通過本地維護的股票池完成請求。

      import requests
import pandas as pd
from datetime import datetime, timedelta

class PSXDataFetcher:
    """巴基斯坦PSX市場數據獲取器(基於iTick API協議適配)"""
    
    BASE_URL = "https://api.itick.org/stock/kline"
    
    def __init__(self, api_key):
        self.headers = {
            "accept": "application/json",
            "token": api_key
        }
    
    def get_historical_data(self, symbol, ktype=5, limit=100):
        """
        獲取PSX股票歷史K線數據
        :param symbol: PSX股票代碼(如'OGDC')
        :param ktype: K線週期(1=1分鐘, 2=5分鐘, 3=15分鐘, 4=30分鐘, 5=1小時, 6=2小時, 7=4小時, 8=日線, 9=週線, 10=月線)
        :param limit: 返回K線數量(最大500)
        :return: Pandas DataFrame
        """
        # PSX市場通過自定義region參數適配
        params = {
            "region": "PK",      # 市場標識
            "code": symbol,
            "kType": ktype,
            "limit": limit
        }
        
        try:
            response = requests.get(
                self.BASE_URL, 
                headers=self.headers, 
                params=params,
                timeout=10
            )
            response.raise_for_status()
            data = response.json()
            
            if data.get("code") == 200:
                return self._parse_kline(data.get("data", []))
            else:
                print(f"API錯誤: {data.get('msg')}")
                return None
                
        except Exception as e:
            print(f"請求失敗 {symbol}: {str(e)}")
            # 模擬數據生成(用於演示,實際使用時應刪除)
            return self._generate_mock_data(symbol, limit)
    
    def _parse_kline(self, kline_list):
        """將iTick K線JSON轉換為標準OHLCV DataFrame"""
        df = pd.DataFrame(kline_list)
        if df.empty:
            return df
        
        # 字段映射:iTick響應字段 -> 標準列名
        df.rename(columns={
            'o': 'open',
            'h': 'high', 
            'l': 'low',
            'c': 'close',
            'v': 'volume',
            't': 'timestamp'
        }, inplace=True)
        
        # 轉換時間戳並設為索引
        df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
        df.set_index('timestamp', inplace=True)
        
        # 確保數值類型
        for col in ['open', 'high', 'low', 'close', 'volume']:
            df[col] = pd.to_numeric(df[col])
        
        return df
    
    def _generate_mock_data(self, symbol, limit):
        """生成模擬PSX數據(僅供本教程演示,實際使用時請替換為真實API響應)"""
        print(f"[模擬模式] 為 {symbol} 生成{limit}條測試數據")
        dates = pd.date_range(
            end=datetime.now(), 
            periods=limit, 
            freq='D'
        )
        np.random.seed(hash(symbol) % 100)
        price = 100 + np.cumsum(np.random.randn(limit) * 0.5)
        df = pd.DataFrame({
            'open': price * (1 + np.random.randn(limit)*0.01),
            'high': price * (1 + np.abs(np.random.randn(limit)*0.02)),
            'low': price * (1 - np.abs(np.random.randn(limit)*0.02)), 
            'close': price,
            'volume': np.random.randint(100000, 500000, limit)
        }, index=dates)
        return df

    

3.3 即時報價模擬(WebSocket擴展)

對於日內策略,我們可以基於iTick WebSocket協議實現PSX即時行情推送:

      import websocket
import json
import threading

class PSXRealtimeFeed:
    """PSX即時行情訂閱(WebSocket適配層)"""
    
    def __init__(self, api_key):
        self.api_key = api_key
        self.ws = None
        self.subscribers = []
    
    def connect(self):
        ws_url = "wss://api.itick.org/stock"  # 復用股票WS端點
        headers = {"token": self.api_key}
        self.ws = websocket.WebSocketApp(
            ws_url,
            header=headers,
            on_open=self._on_open,
            on_message=self._on_message,
            on_error=self._on_error
        )
        # 異步運行
        threading.Thread(target=self.ws.run_forever, daemon=True).start()
    
    def subscribe(self, symbols):
        """訂閱PSX股票即時行情"""
        if not self.ws:
            self.connect()
        
        # PSX股票按自定義格式包裝
        params = ",".join([f"{sym}$PK" for sym in symbols])
        sub_msg = {
            "ac": "subscribe",
            "params": params,
            "types": "quote,tick"  # 報價+逐筆成交
        }
        self.ws.send(json.dumps(sub_msg))
        print(f"已訂閱PSX即時行情: {symbols}")
    
    def _on_message(self, ws, message):
        data = json.loads(message)
        # 分發至已註冊的回調函數
        for cb in self.subscribers:
            cb(data)

    

四、實戰案例:PSX雙均線趨勢策略

4.1 策略邏輯

我們以PSX能源板塊龍頭股OGDC(巴基斯坦油氣開發公司)為例,部署經典雙均線策略:

  • 快線窗口:20週期
  • 慢線窗口:60週期
  • 開倉信號:快線上穿慢線 → 買入
  • 平倉信號:快線下穿慢線 → 賣出
  • 倉位控制:每次投入可用資金的90%,按整手數交易(1手=100股)

4.2 完整策略代碼

      import pandas as pd
import numpy as np
import talib

class PSXDualMovingAverageStrategy:
    """PSX市場雙均線策略(適配卡拉奇交易所交易規則)"""
    
    def __init__(self, fast_period=20, slow_period=60):
        self.fast_period = fast_period
        self.slow_period = slow_period
        self.position = 0
        self.trades = []
    
    def calculate_signals(self, df):
        """生成交易信號"""
        # 計算移動平均線
        df['MA_FAST'] = talib.SMA(df['close'], self.fast_period)
        df['MA_SLOW'] = talib.SMA(df['close'], self.slow_period)
        
        # 判斷均線位置
        df['cross_above'] = (df['MA_FAST'] > df['MA_SLOW']) & \
                            (df['MA_FAST'].shift(1) <= df['MA_SLOW'].shift(1))
        df['cross_below'] = (df['MA_FAST'] < df['MA_SLOW']) & \
                            (df['MA_FAST'].shift(1) >= df['MA_SLOW'].shift(1))
        
        # 信號編碼:1=買入,-1=賣出,0=無操作
        df['signal'] = 0
        df.loc[df['cross_above'], 'signal'] = 1
        df.loc[df['cross_below'], 'signal'] = -1
        
        return df
    
    def run_backtest(self, df, initial_capital=1000000):
        """
        執行回測(巴基斯坦盧比PKR)
        PSX交易規則:最小交易單位100股,T+2結算
        """
        df = self.calculate_signals(df.copy())
        
        # 初始化投資組合
        portfolio = pd.DataFrame(index=df.index)
        portfolio['price'] = df['close']
        portfolio['signal'] = df['signal']
        portfolio['cash'] = initial_capital
        portfolio['holdings'] = 0
        portfolio['total'] = initial_capital
        
        current_position = 0
        
        for i in range(1, len(portfolio)):
            # 默認繼承前值
            portfolio.loc[portfolio.index[i], 'cash'] = portfolio.loc[portfolio.index[i-1], 'cash']
            portfolio.loc[portfolio.index[i], 'holdings'] = portfolio.loc[portfolio.index[i-1], 'holdings']
            
            # 買入信號
            if portfolio['signal'].iloc[i] == 1 and current_position == 0:
                price = portfolio['price'].iloc[i]
                max_spend = portfolio['cash'].iloc[i-1] * 0.9
                shares = int(max_spend // price // 100 * 100)  # 整手交易
                
                if shares >= 100:
                    cost = shares * price
                    portfolio.loc[portfolio.index[i], 'cash'] = portfolio['cash'].iloc[i-1] - cost
                    portfolio.loc[portfolio.index[i], 'holdings'] = shares
                    current_position = shares
                    self.trades.append(('BUY', portfolio.index[i], price, shares))
            
            # 賣出信號
            elif portfolio['signal'].iloc[i] == -1 and current_position > 0:
                price = portfolio['price'].iloc[i]
                shares = current_position
                proceeds = shares * price
                portfolio.loc[portfolio.index[i], 'cash'] = portfolio['cash'].iloc[i-1] + proceeds
                portfolio.loc[portfolio.index[i], 'holdings'] = 0
                current_position = 0
                self.trades.append(('SELL', portfolio.index[i], price, shares))
            
            # 更新總資產
            portfolio.loc[portfolio.index[i], 'total'] = \
                portfolio['cash'].iloc[i] + portfolio['holdings'].iloc[i] * portfolio['price'].iloc[i]
        
        self.portfolio = portfolio
        return portfolio

    

4.3 執行回測:OGDC十年數據驗證

      # 初始化數據獲取器
fetcher = PSXDataFetcher(api_key=ITICK_API_KEY)

# 獲取OGDC歷史日線數據(模擬近3年)
df_ogdc = fetcher.get_historical_data("OGDC", ktype=7, limit=750)

# 運行策略回測
strategy = PSXDualMovingAverageStrategy(fast_period=20, slow_period=60)
portfolio = strategy.run_backtest(df_ogdc, initial_capital=1000000)

# 計算績效指標
total_return = (portfolio['total'].iloc[-1] / portfolio['total'].iloc[0] - 1) * 100
sharpe_ratio = np.sqrt(252) * (portfolio['total'].pct_change().mean() / portfolio['total'].pct_change().std())
max_drawdown = (portfolio['total'] / portfolio['total'].cummax() - 1).min() * 100

print("="*50)
print("📊 PSX雙均線策略回測報告")
print("="*50)
print(f"標的資產: OGDC (巴基斯坦油氣開發公司)")
print(f"回測區間: {df_ogdc.index[0].date()}{df_ogdc.index[-1].date()}")
print(f"初始資本: 1,000,000 PKR")
print(f"最終權益: {portfolio['total'].iloc[-1]:,.0f} PKR")
print(f"總收益率: {total_return:.2f}%")
print(f"年化夏普: {sharpe_ratio:.2f}")
print(f"最大回撤: {max_drawdown:.2f}%")
print("="*50)

# 輸出交易記錄
print("\n📝 交易明細:")
for i, trade in enumerate(strategy.trades[:10], 1):
    action, date, price, shares = trade
    print(f"  {i}. {action} {date.date()} @ {price:.2f} PKR, {shares}股")

    

回測結果示例(模擬數據)

      ==================================================
📊 PSX雙均線策略回測報告
==================================================
標的資產: OGDC (巴基斯坦油氣開發公司)
回測區間: 2023-01-09 → 2025-12-10
初始資本: 1,000,000 PKR
最終權益: 1,487,200 PKR
總收益率: 48.72%
年化夏普: 1.34
最大回撤: -12.85%
==================================================

📝 交易明細:
  1. BUY 2023-02-15 @ 124.50 PKR, 7000股
  2. SELL 2023-03-22 @ 136.80 PKR, 7000股
  3. BUY 2023-05-08 @ 118.20 PKR, 7600股
  ...

    

4.4 可視化策略表現

      import matplotlib.pyplot as plt

plt.figure(figsize=(14, 8))

# 子圖1:價格與均線
plt.subplot(2, 1, 1)
plt.plot(df_ogdc.index, df_ogdc['close'], label='OGDC收盤價', alpha=0.7)
ma_values = strategy.calculate_signals(df_ogdc)
plt.plot(ma_values.index, ma_values['MA_FAST'], label=f'{strategy.fast_period}日均線', linestyle='--')
plt.plot(ma_values.index, ma_values['MA_SLOW'], label=f'{strategy.slow_period}日均線', linestyle='--')

# 標註買賣點
buy_signals = [i for i in range(len(portfolio)) if portfolio['signal'].iloc[i] == 1]
sell_signals = [i for i in range(len(portfolio)) if portfolio['signal'].iloc[i] == -1]
plt.scatter(portfolio.index[buy_signals], portfolio['price'].iloc[buy_signals], 
            marker='^', color='green', s=100, label='買入信號')
plt.scatter(portfolio.index[sell_signals], portfolio['price'].iloc[sell_signals], 
            marker='v', color='red', s=100, label='賣出信號')
plt.title('OGDC 雙均線策略交易信號 (PSX)')
plt.legend()
plt.grid(alpha=0.3)

# 子圖2:權益曲線
plt.subplot(2, 1, 2)
plt.plot(portfolio.index, portfolio['total'], label='投資組合價值', color='navy')
plt.fill_between(portfolio.index, portfolio['total'], alpha=0.2)
plt.title('權益曲線 (PKR)')
plt.legend()
plt.grid(alpha=0.3)

plt.tight_layout()
plt.show()

    

五、從回測到模擬交易:PSX量化流水線

當回測結果符合預期後,你可以通過iTick WebSocket即時行情 + 模擬交易接口,將策略部署至PSX仿真環境。

5.1 即時信號監控器

      class PSXLiveTrader:
    """PSX即時交易監控器(模擬執行)"""
    
    def __init__(self, strategy, fetcher):
        self.strategy = strategy
        self.fetcher = fetcher
        self.current_position = 0
    
    def on_bar_update(self, symbol, new_bar):
        """處理新到達的K線"""
        # 獲取最新數據窗口
        df = self.fetcher.get_historical_data(symbol, ktype=5, limit=100)
        df = self.strategy.calculate_signals(df)
        
        # 檢查最新信號
        latest_signal = df['signal'].iloc[-1]
        last_price = df['close'].iloc[-1]
        
        if latest_signal == 1 and self.current_position == 0:
            print(f"🟢 買入信號: {symbol} @ {last_price:.2f} PKR")
            # 此處可接入券商API執行實盤下單
            self.current_position = 1
            
        elif latest_signal == -1 and self.current_position == 1:
            print(f"🔴 賣出信號: {symbol} @ {last_price:.2f} PKR")
            self.current_position = 0

    

5.2 對接巴基斯坦本地券商API(架構示意)

雖然iTick本身不提供交易執行,但你可以將生成的交易信號通過以下方式對接巴基斯坦本地券商:

      信號JSON → 中間件 → 券商交易網關(如KATS、ODIN)→ PSX訂單路由

    
      # 偽代碼示例:轉換信號為券商指令
order_payload = {
    "broker_code": "XYZ",
    "symbol": "OGDC",
    "side": "BUY",
    "order_type": "LIMIT",
    "price": 125.50,
    "quantity": 5000,
    "validity": "DAY"
}
# 通過券商提供的REST API提交

    

六、策略優化:讓PSX策略更穩健

6.1 參數敏感性分析

      # 遍歷不同均線組合
results = []
for fast in [10, 20, 30, 50]:
    for slow in [50, 60, 120, 200]:
        if fast >= slow: continue
        strat = PSXDualMovingAverageStrategy(fast, slow)
        port = strat.run_backtest(df_ogdc, initial_capital=1000000)
        ret = (port['total'].iloc[-1] / 1000000 - 1) * 100
        results.append({'fast': fast, 'slow': slow, 'return': ret})

# 輸出最佳參數組合
best = max(results, key=lambda x: x['return'])
print(f"最優參數: 快線{best['fast']}, 慢線{best['slow']}, 收益率{best['return']:.2f}%")

    

6.2 PSX專屬風控模塊

巴基斯坦市場具有波動性高、流動性集中的特點,建議增加以下風控:

      class PSXRiskManager:
    """PSX市場風控規則"""
    
    @staticmethod
    def check_circuit_breaker(price_change_pct):
        """PSX漲跌停限制(指數成分股±5%)"""
        return abs(price_change_pct) <= 5.0
    
    @staticmethod
    def position_sizing(equity, atr, risk_per_trade=0.02):
        """基於ATR的動態倉位計算"""
        risk_amount = equity * risk_per_trade
        shares = int(risk_amount / atr / 100) * 100
        return max(shares, 100)
    
    @staticmethod
    def trading_hours_filter(dt):
        """PSX交易時段過濾(週一至週五 09:30-15:30 PST)"""
        if dt.weekday() >= 5:
            return False
        pst_hour = dt.hour + 5  # UTC+5轉換
        return (9 <= pst_hour <= 15) and not (pst_hour == 15 and dt.minute > 30)

    

七、常見問題與解決方案

Q1:iTick原生不支持PSX,獲取不到真實數據怎麼辦?

A:本文示例採用模擬數據層做演示。真實場景中,您有兩種選擇:

  1. 聯繫iTick商務團隊,提供PSX數據源需求,他們可為機構客戶定制新市場接入
  2. 自行爬取PSX官網或第三方數據源,清洗後轉換為iTick兼容的DataFrame格式

Q2:巴基斯坦盧比(PKR)匯率波動如何處理?

A:對於以外幣計價的投資組合,建議同時訂閱iTick外匯API中的USDPKR匯率,將本地收益折算為基準貨幣進行風險評估。

      # 獲取美元/巴基斯坦盧比匯率
def get_usdpkr_rate():
    url = "https://api.itick.org/forex/quote?region=GB&code=USDPKR"
    # 實際請求代碼...
    return rate

    

Q3:PSX股票代碼後綴(.PSX)會被iTick系統拒絕嗎?

A:iTick對region參數進行嚴格校驗,在生產環境中,您應:

  • 使用iTick支持的region(如region=PX)但將symbol映射為模擬代碼
  • 或通過iTick的自定義數據上傳功能(企業版)導出PSX全量歷史數據
  • 如遇具體symbol不支持或需定制,可以聯繫 iTick客服 進行定制開發

結語:新興市場量化的「適配器思維」

本文通過 巴基斯坦證券交易所(PSX) 的實戰案例,展示了一條重要方法論:當主流API尚未原生覆蓋目標市場時,優秀的開發者不應等待,而是通過設計精巧的適配層,將現有工具的能力「投射」到新興場景中

iTick API提供的統一JSON數據結構、標準化字段命名、穩定的REST/WebSocket網關,正是這種適配器模式的理想基座。無論你的目標是PSX、越南胡志明交易所,還是非洲的約翰內斯堡證交所,同樣的代碼框架只需替換symbol映射和交易規則模塊即可復用

現在,登錄 iTick官網 領取你的免費API密鑰,用Python開啟新興市場量化之旅吧。

延伸閱讀: