Pakistan Stock Exchange (PSX) Data via API — Real-Time Market Data & Quantitative Trading Implementation Guide

Introduction: Why Is PSX the Next Blue Ocean in Quantitative Trading?
As South Asia's third-largest stock exchange, the Pakistan Stock Exchange (PSX) has attracted significant attention from international institutional investors in recent years. The KSE-100 Index encompasses core blue-chip stocks listed on exchanges in Karachi, Lahore, and Islamabad, with active trading volumes steadily increasing in energy, banking, and cement sectors. However, quantitative developers have long faced two major pain points regarding PSX:
- Data Interface Scarcity: Traditional data vendors inadequately cover emerging markets, making real-time market data acquisition costly.
- Localization Challenges: PSX trading hours (Pakistan Standard Time, PST) and ticker symbol conventions differ significantly from mainstream markets.
The advent of iTick API is changing this landscape. Leveraging a unified multi-market data architecture, iTick not only covers mature markets such as U.S. equities, Hong Kong stocks, and A-shares but also reserves scalable space for emerging markets through standardized interfaces. This article will walk you through step-by-step:
- ✅ How to encapsulate PSX data requests using iTick-style APIs
- ✅ Building a quantitative backtesting framework adapted to Karachi trading hours
- ✅ Practical dual moving average strategy implementation for PSX constituent stocks
- ✅ Full workflow deployment from historical backtesting to simulated trading
All code is plug-and-play—copy-paste to launch your first PSX quantitative strategy backtest.
I: Core Advantages of iTick API – Why Choose It as the Bridge to PSX?
Before diving into code, let’s briefly review several key features of iTick API that directly serve PSX market strategy development:
| Feature | Value to PSX Quant Strategies |
|---|---|
| Unified REST/WebSocket Interface | Eliminates need for multiple data parsing logic across different markets |
| Millisecond-Level Historical K-Line Queries | Supports multi-year backtesting of KSE-100 index constituents |
| Free Tier Available | Enables zero-cost strategy validation for individual developers |
| Multi-Language SDK Support | Prioritizes Python, seamlessly integrating with Pandas/NumPy |
| Extensible Market Identifier | Customizable region parameter adapts to non-native supported markets |
💡 Core Concept: Although iTick currently does not natively expose dedicated PSX endpoints, we can implement a “market adapter layer” design pattern, mapping PSX tickers to custom identifiers within iTick’s existing architecture, achieving decoupling between data retrieval and strategy logic.
II: Environment Setup and API Credential Acquisition
2.1 Install Required Dependencies
pip install requests pandas numpy matplotlib ta-lib
# Official iTick data interface library (supports unified data format)
pip install itrade
2.2 Obtain Free API Key
- Visit iTick Official Website and click “Get Started”
- Complete OAuth login via GitHub/Google account
- Enter the dashboard and copy your API Key (e.g.,
bb42e247...)
⚠️ Please safeguard your key. All subsequent code uses environment variable injection—never hardcode credentials in scripts.
import os
from dotenv import load_dotenv
load_dotenv()
ITICK_API_KEY = os.getenv("ITICK_API_KEY", "your_key_here")
III: PSX Data Adapter Layer – Making iTick Understand the Karachi Exchange
Since PSX is currently not on iTick’s native market list, we adopt a “protocol compatibility + local mapping” approach, encoding PSX tickers into a custom format and retrieving structured market data through iTick’s generic data interface.
3.1 Retrieve Full PSX Ticker List
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 authenticates via token header
}
try:
print(f"🔄 Requesting PSX ticker list...")
response = requests.get(
url,
headers=headers,
timeout=15
)
# Handle HTTP status codes
if response.status_code == 200:
data = response.json()
if data.get("code") == 200:
symbol_list = data.get("data", [])
print(f"✅ Successfully retrieved {len(symbol_list)} PSX tickers")
return symbol_list
else:
print(f"⚠️ API business error: {data.get('msg')} (Code: {data.get('code')})")
return None
elif response.status_code == 401:
print("❌ Authentication failed (401): Check if API key is valid/expired or if the endpoint is covered by your plan")
print(" - Log in to https://itick.org dashboard to verify key status")
print(" - Confirm whether your plan includes 'basic stock data' permissions")
return None
elif response.status_code == 403:
print("❌ Insufficient permissions (403): Your account lacks access to PSX regional data")
print(" - PSX may currently be in restricted beta; contact sales to enable")
return None
elif response.status_code == 429:
print("❌ Rate limit exceeded (429): Retry later or upgrade plan for higher limits")
return None
else:
print(f"❌ Unknown error: HTTP {response.status_code}")
return None
except Exception as e:
print(f"❌ Request exception: {str(e)}")
return None
def save_to_dataframe(symbol_list: List[Dict]) -> pd.DataFrame:
"""Save ticker list as Pandas DataFrame for analysis"""
if not symbol_list:
return pd.DataFrame()
df = pd.DataFrame(symbol_list)
print("\n📋 Preview of PSX Ticker List:")
print(df.head())
print(f"\nTotal records: {len(df)}")
return df
# ===== Usage Example =====
if __name__ == "__main__":
# ⚠️ Important: Load key from environment variables or config files; never hardcode
YOUR_API_KEY = os.environ.get("ITICK_API_KEY", "")
if not YOUR_API_KEY:
print("Please set environment variable ITICK_API_KEY or assign directly (testing only)")
# YOUR_API_KEY = "your_key_here" # Temporarily uncomment for testing
else:
# Step 1: Fetch PSX ticker list
symbols = fetch_psx_symbols(YOUR_API_KEY)
# Step 2: Convert to DataFrame
if symbols:
df = save_to_dataframe(symbols)
# Optional: Save to CSV
df.to_csv("psx_symbols.csv", index=False)
print("\n💾 Saved to psx_symbols.csv")
# Print all ticker symbols
print("\n🔹 PSX Ticker Symbols:")
for code in df.get("symbol", [])[:20]: # Show top 20
print(f" {code}")
3.2 Build PSX Data Fetcher
We encapsulate a PSXDataFetcher class that internally reuses iTick’s stock/kline interface format, identifying the PSX market via the PK region parameter and fulfilling requests through a locally maintained ticker pool.
import requests
import pandas as pd
from datetime import datetime, timedelta
class PSXDataFetcher:
"""Pakistan PSX Market Data Fetcher (based on iTick API protocol adaptation)"""
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):
"""
Retrieve PSX stock historical K-line data
:param symbol: PSX ticker symbol (e.g., 'OGDC')
:param ktype: K-line period (1=min, 2=5min, 3=15min, 4=30min, 5=1hr, 6=2hr, 7=4hr, 8=daily, 9=weekly, 10=monthly)
:param limit: Number of K-lines returned (max 500)
:return: Pandas DataFrame
"""
# Adapt PSX market via custom region parameter
params = {
"region": "PK", # Market identifier
"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 error: {data.get('msg')}")
return None
except Exception as e:
print(f"Request failed for {symbol}: {str(e)}")
# Mock data generation (for demo purposes only—remove in production)
return self._generate_mock_data(symbol, limit)
def _parse_kline(self, kline_list):
"""Convert iTick K-line JSON to standard OHLCV DataFrame"""
df = pd.DataFrame(kline_list)
if df.empty:
return df
# Field mapping: iTick response fields → standard column names
df.rename(columns={
'o': 'open',
'h': 'high',
'l': 'low',
'c': 'close',
'v': 'volume',
't': 'timestamp'
}, inplace=True)
# Convert timestamps and set as index
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
df.set_index('timestamp', inplace=True)
# Ensure numeric types
for col in ['open', 'high', 'low', 'close', 'volume']:
df[col] = pd.to_numeric(df[col])
return df
def _generate_mock_data(self, symbol, limit):
"""Generate mock PSX data (demo only—replace with real API responses in production)"""
print(f"[Mock Mode] Generating {limit} test records for {symbol}")
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 Real-Time Quote Simulation (WebSocket Extension)
For intraday strategies, we can implement PSX real-time market data streaming based on iTick’s WebSocket protocol:
import websocket
import json
import threading
class PSXRealtimeFeed:
"""PSX Real-Time Market Feed (WebSocket Adapter Layer)"""
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" # Reuse stock WebSocket endpoint
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
)
# Run asynchronously
threading.Thread(target=self.ws.run_forever, daemon=True).start()
def subscribe(self, symbols):
"""Subscribe to PSX real-time market data"""
if not self.ws:
self.connect()
# Wrap PSX tickers in custom format
params = ",".join([f"{sym}$PK" for sym in symbols])
sub_msg = {
"ac": "subscribe",
"params": params,
"types": "quote,tick" # Quotes + tick-by-tick trades
}
self.ws.send(json.dumps(sub_msg))
print(f"Subscribed to PSX real-time feed: {symbols}")
def _on_message(self, ws, message):
data = json.loads(message)
# Dispatch to registered callback functions
for cb in self.subscribers:
cb(data)
IV: Practical Case Study – Dual Moving Average Trend Strategy on PSX
4.1 Strategy Logic
Using OGDC (Oil & Gas Development Company Limited), a leading energy sector stock on PSX, we deploy a classic dual moving average strategy:
- Fast Window: 20 periods
- Slow Window: 60 periods
- Entry Signal: Fast MA crosses above slow MA → Buy
- Exit Signal: Fast MA crosses below slow MA → Sell
- Position Control: Invest 90% of available capital per trade, rounded to whole lots (1 lot = 100 shares)
4.2 Complete Strategy Code
import pandas as pd
import numpy as np
import talib
class PSXDualMovingAverageStrategy:
"""Dual Moving Average Strategy for PSX Market (adapted to Karachi Exchange rules)"""
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):
"""Generate trading signals"""
# Calculate moving averages
df['MA_FAST'] = talib.SMA(df['close'], self.fast_period)
df['MA_SLOW'] = talib.SMA(df['close'], self.slow_period)
# Determine crossover conditions
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))
# Encode signals: 1=Buy, -1=Sell, 0=No Action
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):
"""
Execute backtest (in Pakistani Rupees PKR)
PSX trading rules: Minimum trade size 100 shares, T+2 settlement
"""
df = self.calculate_signals(df.copy())
# Initialize portfolio
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)):
# Default inheritance from previous value
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']
# Buy signal
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) # Round to full lots
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))
# Sell signal
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))
# Update total assets
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 Backtest Execution: Ten-Year Validation for OGDC
# Initialize data fetcher
fetcher = PSXDataFetcher(api_key=ITICK_API_KEY)
# Retrieve OGDC historical daily data (simulated ~3 years)
df_ogdc = fetcher.get_historical_data("OGDC", ktype=7, limit=750)
# Run strategy backtest
strategy = PSXDualMovingAverageStrategy(fast_period=20, slow_period=60)
portfolio = strategy.run_backtest(df_ogdc, initial_capital=1000000)
# Compute performance metrics
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 Dual Moving Average Strategy Backtest Report")
print("="*50)
print(f"Asset: OGDC (Oil & Gas Development Company Ltd.)")
print(f"Backtest Period: {df_ogdc.index[0].date()} → {df_ogdc.index[-1].date()}")
print(f"Initial Capital: 1,000,000 PKR")
print(f"Final Equity: {portfolio['total'].iloc[-1]:,.0f} PKR")
print(f"Total Return: {total_return:.2f}%")
print(f"Annualized Sharpe Ratio: {sharpe_ratio:.2f}")
print(f"Max Drawdown: {max_drawdown:.2f}%")
print("="*50)
# Output trade log
print("\n📝 Trade Details:")
for i, trade in enumerate(strategy.trades[:10], 1):
action, date, price, shares = trade
print(f" {i}. {action} {date.date()} @ {price:.2f} PKR, {shares} shares")
Sample Backtest Results (Simulated Data):
==================================================
📊 PSX Dual Moving Average Strategy Backtest Report
==================================================
Asset: OGDC (Oil & Gas Development Company Ltd.)
Backtest Period: 2023-01-09 → 2025-12-10
Initial Capital: 1,000,000 PKR
Final Equity: 1,487,200 PKR
Total Return: 48.72%
Annualized Sharpe Ratio: 1.34
Max Drawdown: -12.85%
==================================================
📝 Trade Details:
1. BUY 2023-02-15 @ 124.50 PKR, 7000 shares
2. SELL 2023-03-22 @ 136.80 PKR, 7000 shares
3. BUY 2023-05-08 @ 118.20 PKR, 7600 shares
...
4.4 Visualize Strategy Performance
import matplotlib.pyplot as plt
plt.figure(figsize=(14, 8))
# Subplot 1: Price and Moving Averages
plt.subplot(2, 1, 1)
plt.plot(df_ogdc.index, df_ogdc['close'], label='OGDC Closing Price', alpha=0.7)
ma_values = strategy.calculate_signals(df_ogdc)
plt.plot(ma_values.index, ma_values['MA_FAST'], label=f'{strategy.fast_period}-Day MA', linestyle='--')
plt.plot(ma_values.index, ma_values['MA_SLOW'], label=f'{strategy.slow_period}-Day MA', linestyle='--')
# Mark buy/sell signals
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='Buy Signal')
plt.scatter(portfolio.index[sell_signals], portfolio['price'].iloc[sell_signals],
marker='v', color='red', s=100, label='Sell Signal')
plt.title('OGDC Dual Moving Average Trading Signals (PSX)')
plt.legend()
plt.grid(alpha=0.3)
# Subplot 2: Equity Curve
plt.subplot(2, 1, 2)
plt.plot(portfolio.index, portfolio['total'], label='Portfolio Value', color='navy')
plt.fill_between(portfolio.index, portfolio['total'], alpha=0.2)
plt.title('Equity Curve (PKR)')
plt.legend()
plt.grid(alpha=0.3)
plt.tight_layout()
plt.show()
V: From Backtesting to Simulated Trading – PSX Quant Pipeline
Once backtest results meet expectations, you can deploy the strategy to a PSX simulation environment via iTick WebSocket real-time feeds plus simulated trading interfaces.
5.1 Real-Time Signal Monitor
class PSXLiveTrader:
"""PSX Real-Time Trading Monitor (Simulated Execution)"""
def __init__(self, strategy, fetcher):
self.strategy = strategy
self.fetcher = fetcher
self.current_position = 0
def on_bar_update(self, symbol, new_bar):
"""Process newly arrived K-line"""
# Retrieve latest data window
df = self.fetcher.get_historical_data(symbol, ktype=5, limit=100)
df = self.strategy.calculate_signals(df)
# Check latest signal
latest_signal = df['signal'].iloc[-1]
last_price = df['close'].iloc[-1]
if latest_signal == 1 and self.current_position == 0:
print(f"🟢 Buy Signal: {symbol} @ {last_price:.2f} PKR")
# Insert broker API execution here
self.current_position = 1
elif latest_signal == -1 and self.current_position == 1:
print(f"🔴 Sell Signal: {symbol} @ {last_price:.2f} PKR")
self.current_position = 0
5.2 Integrating with Local Pakistani Broker APIs (Architecture Sketch)
Although iTick does not provide order execution itself, generated trading signals can be routed to local Pakistani brokers via middleware:
Signal JSON → Middleware → Broker Trading Gateway (e.g., KATS, ODIN) → PSX Order Routing
# Pseudocode example: Convert signals to broker instructions
order_payload = {
"broker_code": "XYZ",
"symbol": "OGDC",
"side": "BUY",
"order_type": "LIMIT",
"price": 125.50,
"quantity": 5000,
"validity": "DAY"
}
# Submit via broker-provided REST API
VI: Strategy Optimization – Enhancing Robustness for PSX
6.1 Parameter Sensitivity Analysis
# Iterate over different MA combinations
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})
# Output optimal parameter combination
best = max(results, key=lambda x: x['return'])
print(f"Optimal Parameters: Fast MA {best['fast']}, Slow MA {best['slow']}, Return {best['return']:.2f}%")
6.2 PSX-Specific Risk Management Module
Given PSX’s characteristics of high volatility and concentrated liquidity, consider adding the following risk controls:
class PSXRiskManager:
"""PSX Market Risk Rules"""
@staticmethod
def check_circuit_breaker(price_change_pct):
"""PSX circuit breaker limit (±5% for index constituents)"""
return abs(price_change_pct) <= 5.0
@staticmethod
def position_sizing(equity, atr, risk_per_trade=0.02):
"""Dynamic position sizing based on ATR"""
risk_amount = equity * risk_per_trade
shares = int(risk_amount / atr / 100) * 100
return max(shares, 100)
@staticmethod
def trading_hours_filter(dt):
"""Filter for PSX trading hours (Mon-Fri 09:30–15:30 PST)"""
if dt.weekday() >= 5:
return False
pst_hour = dt.hour + 5 # UTC+5 conversion
return (9 <= pst_hour <= 15) and not (pst_hour == 15 and dt.minute > 30)
VII: FAQs and Solutions
Q1: iTick doesn’t natively support PSX—how to get real data?
A: This article uses simulated data layers for demonstration. In practice:
- Contact iTick enterprise team to request PSX data source integration for institutional clients.
- Scrape PSX official site or third-party sources, clean, and convert to iTick-compatible DataFrame format.
Q2: How to handle fluctuations in Pakistani Rupee (PKR) exchange rates?
A: For portfolios denominated in foreign currencies, simultaneously subscribe to iTick forex API for USD/PKR rates to convert local returns to benchmark currency for risk assessment.
# Retrieve USD/PKR exchange rate
def get_usdpkr_rate():
url = "https://api.itick.org/forex/quote?region=GB&code=USDPKR"
# Actual request code...
return rate
Q3: Will PSX ticker suffixes (.PSX) be rejected by iTick systems?
A: iTick strictly validates the region parameter. In production environments:
- Use iTick-supported regions (e.g.,
region=PX) but map symbols to mock codes. - Or export full PSX historical data via iTick’s custom data upload feature (enterprise version).
- For unsupported symbols or customization needs, contact iTick Support.
Conclusion: The "Adapter Mindset" in Emerging Market Quantitative Finance
Through this hands-on case study of the Pakistan Stock Exchange (PSX), we demonstrate an important methodology: When mainstream APIs do not yet natively cover target markets, skilled developers should not wait but instead design elegant adapter layers to project existing tool capabilities into emerging scenarios.
iTick API’s unified JSON data structure, standardized field naming, and stable REST/WebSocket gateways form an ideal foundation for such adapter patterns. Whether targeting PSX, Vietnam’s Ho Chi Minh Stock Exchange, or Johannesburg Stock Exchange in Africa, the same code framework can be reused simply by replacing symbol mappings and trading rule modules.
Now, log in to iTick Official Site to claim your free API key and embark on your quantitative journey in emerging markets with Python.
Further Reading:
- Dual Moving Average Quant Strategy Guide: Python Implementation Based on iTick Forex/Stock APIs
- iTick & Futu Quant Trading Python Integration Guide
- Quantitative Chanlun Technical Indicators Combined with Real-Time Stock Quotes API for Strategy Trading
- Global Stock Real-Time Quotes API: The Ultimate Data Engine for Quant Trading
- iTick Official Documentation
- iTick GitHub