巴基斯坦证券交易所(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,
params=params,
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开启新兴市场量化之旅吧。
延伸阅读: