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

引言:為什麼PSX是量化交易的下一片藍海?
巴基斯坦證券交易所(PSX)作為南亞第三大股票市場,近年來吸引了大量國際投資機構的關注。KSE-100指數涵蓋卡拉奇、拉合爾、伊斯蘭堡三地交易所的核心藍籌股,能源、銀行、水泥板塊交易活躍度持續攀升。然而,對於量化開發者而言,PSX長期面臨兩大痛點:
- 數據接口匱乏:傳統數據商對新興市場覆蓋不足,即時行情獲取成本高昂
- 本地化適配困難:PSX的交易時段(巴基斯坦標準時間,PST)、股票代碼規則與主流市場存在差異
iTick API的誕生正在改變這一局面。依託統一的多市場數據架構,iTick不僅覆蓋美股、港股、A股等成熟市場,更通過標準化接口為新興市場留出擴展空間。本文將手把手教你:
- ✅ 如何用iTick風格API封裝PSX數據請求
- ✅ 構建適配卡拉奇交易時段的量化回測框架
- ✅ 基於雙均線策略的PSX成分股實戰代碼
- ✅ 從歷史回測到模擬交易的全流程落地
所有代碼開箱即用,複製粘貼即可啟動你的第一筆PSX量化策略回測。
一、iTick API核心優勢:為什麼選擇它作為PSX數據橋樑?
在深入代碼之前,我們先快速梳理iTick API的幾個關鍵特性,這些能力將直接服務於PSX市場的策略開發:
| 特性 | 對PSX量化的價值 |
|---|---|
| 統一REST/WebSocket接口 | 無需為不同市場維護多套數據解析邏輯 |
| 毫秒級歷史K線查詢 | 支持KSE-100指數成分股多年數據回測 |
| 免費套餐可用 | 個人開發者0成本啟動策略驗證 |
| 多語言SDK | Python優先支持,無縫對接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密鑰
- 訪問 iTick官網 點擊「立即獲取」
- 使用GitHub/Google帳號完成OAuth登錄
- 進入控制台,複製您的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®ion=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:本文示例採用模擬數據層做演示。真實場景中,您有兩種選擇:
- 聯繫iTick商務團隊,提供PSX數據源需求,他們可為機構客戶定制新市場接入
- 自行爬取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開啟新興市場量化之旅吧。
延伸閱讀: