Pulling candlestick data one ticker at a time doesn’t scale. Whether you’re building a backtesting pipeline, training a price-prediction model, or simply archiving market history, you need to download OHLCV data for dozens or hundreds of instruments in a single run reliably, without hitting rate limits, and ideally across more than one market.
This guide shows how to do that using Infoway API, which covers equities, forex, futures, and crypto across multiple global exchanges. All examples are in Python and ready to run.
Market Coverage
One API key gives you candlestick data across the following markets:
| Market | Exchange | Instruments |
|---|---|---|
| US Equities | NYSE, NASDAQ | 8,000+ stocks, ETFs |
| Hong Kong Equities | HKEX | 2,600+ stocks |
| A-Share (China) | SSE, SZSE, BSE | 5,000+ stocks |
| Japan Equities | TSE | 3,700+ stocks |
| South Korea Equities | KRX (KOSPI / KOSDAQ) | 2,600+ stocks |
| India Equities | NSE, BSE | 5,300+ stocks |
| Forex | / | 40+ currency pairs |
| Crypto | 30+ exchanges | BTC, ETH, and more |
| Commodities / Futures | / | Gold, oil, major futures |
All markets share the same endpoint structure and response format, so the same download script works everywhere, you only change the ticker symbols.
Supported Candlestick Timeframes
klineType | Timeframe | klineType | Timeframe |
|---|---|---|---|
| 1 | 1-minute | 6 | 2-hour |
| 2 | 5-minute | 7 | 4-hour |
| 3 | 15-minute | 8 | Daily |
| 4 | 30-minute | 9 | Weekly |
| 5 | 1-hour | 10 | Monthly |
Minute-level history goes back 3 years. Daily and above have no lookback limit.
Installation
There are two ways to connect: the official Python SDK (recommended) or direct HTTP requests. The SDK handles authentication, serialization, and error wrapping for you.
Python SDK:
pip install infoway-sdkOr with just requests (no extra dependencies):
pip install requests pandasRegister at infoway.io to get an API key. New accounts include a 7-day free trial, no credit card required.
Quick Start with the Python SDK
from infoway import InfowayClient, KlineType
client = InfowayClient(api_key="YOUR_API_KEY")
# Or set INFOWAY_API_KEY in your environment and call InfowayClient() with no arguments
# Fetch 250 daily candles for Apple
df = client.stock.get_kline("AAPL.US", kline_type=KlineType.DAY, count=250)
print(df.head())The SDK returns a pandas DataFrame directly, no manual parsing required. The kline_type parameter accepts either the KlineType enum or a plain integer (see the timeframe table below).
KlineType enum reference:
| Enum | Value | Timeframe | Enum | Value | Timeframe |
|---|---|---|---|---|---|
KlineType.MIN_1 | 1 | 1-minute | KlineType.HOUR_2 | 6 | 2-hour |
KlineType.MIN_5 | 2 | 5-minute | KlineType.HOUR_4 | 7 | 4-hour |
KlineType.MIN_15 | 3 | 15-minute | KlineType.DAY | 8 | Daily |
KlineType.MIN_30 | 4 | 30-minute | KlineType.WEEK | 9 | Weekly |
KlineType.HOUR_1 | 5 | 1-hour | KlineType.MONTH | 10 | Monthly |
The Candlestick Endpoint (Raw HTTP)
If you prefer not to install the SDK, all examples below also work with plain requests. The endpoint is:
POST https://data.infoway.io/stock/v2/batch_kline
| Parameter | Type | Description |
|---|---|---|
codes | string | Comma-separated ticker symbols |
klineType | int | Timeframe (see table above) |
klineNum | int | Number of candles to return (max 500 per request) |
timestamp | int | Optional. Unix seconds, fetch candles older than this timestamp (for pagination) |
Ticker symbol format by market:
| Market | Format | Example |
|---|---|---|
| US | {TICKER}.US | AAPL.US |
| Hong Kong | {5-digit code}.HK | 00700.HK |
| A-Share | {6-digit code}.SH or .SZ | 600519.SH |
| Japan | {4-digit code}.JP | 7203.JP |
| South Korea | {6-digit code}.KS | 005930.KS |
| India | {symbol}.IN | RELIANCE.IN |
Example 1: Batch Download Daily Candles
Fetch 250 daily candles (roughly one year) for a list of stocks in one request.
Using the Python SDK:
from infoway import InfowayClient, KlineType
client = InfowayClient(api_key="YOUR_API_KEY")
symbols = ["AAPL.US", "TSLA.US", "NVDA.US", "MSFT.US", "GOOGL.US"]
for sym in symbols:
df = client.stock.get_kline(sym, kline_type=KlineType.DAY, count=250)
latest = df.iloc[-1]
print(f"{sym:12s} bars={len(df)} latest close={latest['close']:.2f} date={latest['datetime'].date()}")Using raw HTTP (requests):
import requests
import pandas as pd
import json
API_KEY = "YOUR_API_KEY"
ENDPOINT = "https://data.infoway.io/stock/v2/batch_kline"
HEADERS = {"Content-Type": "application/json", "apiKey": API_KEY}
def fetch_candles(symbols: list[str], kline_type: int = 8, count: int = 250) -> dict[str, pd.DataFrame]:
payload = {
"codes": ",".join(symbols),
"klineType": kline_type,
"klineNum": count,
}
resp = requests.post(ENDPOINT, headers=HEADERS, data=json.dumps(payload), timeout=30)
resp.raise_for_status()
result = {}
for item in resp.json().get("data", []):
candles = item.get("respList", [])
if not candles:
continue
df = pd.DataFrame(candles,
columns=["time", "open", "high", "low", "close",
"volume", "amount", "pct_chg", "chg"])
df[["open", "high", "low", "close", "volume", "amount"]] = \
df[["open", "high", "low", "close", "volume", "amount"]].astype(float)
df["datetime"] = pd.to_datetime(df["time"].astype(int), unit="s")
df.sort_values("datetime", inplace=True)
result[item["s"]] = df[["datetime", "open", "high", "low", "close", "volume"]].reset_index(drop=True)
return result
symbols = ["AAPL.US", "TSLA.US", "NVDA.US", "MSFT.US", "GOOGL.US"]
data = fetch_candles(symbols, kline_type=8, count=250)
for sym, df in data.items():
latest = df.iloc[-1]
print(f"{sym:12s} bars={len(df)} latest close={latest['close']:.2f} date={latest['datetime'].date()}")Output:
AAPL.US bars=250 latest close=211.45 date=2026-06-27
TSLA.US bars=250 latest close=328.90 date=2026-06-27
NVDA.US bars=250 latest close=137.22 date=2026-06-27
MSFT.US bars=250 latest close=471.83 date=2026-06-27
GOOGL.US bars=250 latest close=192.14 date=2026-06-27Example 2: Multi-Market Batch Export to CSV
The same API call handles tickers from different markets simultaneously. This example downloads daily candles for US, Hong Kong, A-share, Japan, and South Korea stocks and saves each to a separate CSV file.
Using the Python SDK:
from infoway import InfowayClient, KlineType
import os
client = InfowayClient(api_key="YOUR_API_KEY")
OUTPUT_DIR = "candlestick_export"
os.makedirs(OUTPUT_DIR, exist_ok=True)
WATCHLIST = {
"US": ["AAPL.US", "TSLA.US", "NVDA.US", "MSFT.US", "META.US"],
"HK": ["00700.HK", "09988.HK", "03690.HK"],
"CN": ["600519.SH", "300750.SZ", "000858.SZ"],
"JP": ["7203.JP", "6758.JP", "9984.JP"],
"KR": ["005930.KS", "000660.KS"],
}
all_symbols = [sym for group in WATCHLIST.values() for sym in group]
print(f"Downloading daily candles for {len(all_symbols)} instruments across 5 markets...\n")
for sym in all_symbols:
df = client.stock.get_kline(sym, kline_type=KlineType.DAY, count=500)
if df.empty:
print(f" ✗ {sym}: no data returned")
continue
filename = sym.replace(".", "_") + "_daily.csv"
filepath = os.path.join(OUTPUT_DIR, filename)
df.to_csv(filepath, index=False)
print(f" ✓ {sym:14s} {len(df)} bars → {filepath}")Using raw HTTP (requests) (batch mode):
import requests
import pandas as pd
import json
import os
API_KEY = "YOUR_API_KEY"
ENDPOINT = "https://data.infoway.io/stock/v2/batch_kline"
HEADERS = {"Content-Type": "application/json", "apiKey": API_KEY}
OUTPUT_DIR = "candlestick_export"
os.makedirs(OUTPUT_DIR, exist_ok=True)
def fetch_batch(codes: list[str], kline_type: int, count: int) -> dict[str, pd.DataFrame]:
payload = {"codes": ",".join(codes), "klineType": kline_type, "klineNum": count}
resp = requests.post(ENDPOINT, headers=HEADERS, data=json.dumps(payload), timeout=30)
resp.raise_for_status()
result = {}
for item in resp.json().get("data", []):
candles = item.get("respList", [])
if not candles:
continue
df = pd.DataFrame(candles,
columns=["time", "open", "high", "low", "close",
"volume", "amount", "pct_chg", "chg"])
df[["open", "high", "low", "close", "volume", "amount"]] = \
df[["open", "high", "low", "close", "volume", "amount"]].astype(float)
df["datetime"] = pd.to_datetime(df["time"].astype(int), unit="s")
df.sort_values("datetime", inplace=True)
result[item["s"]] = df.reset_index(drop=True)
return result
WATCHLIST = {
"US": ["AAPL.US", "TSLA.US", "NVDA.US", "MSFT.US", "META.US"],
"HK": ["00700.HK", "09988.HK", "03690.HK"],
"CN": ["600519.SH", "300750.SZ", "000858.SZ"],
"JP": ["7203.JP", "6758.JP", "9984.JP"],
"KR": ["005930.KS", "000660.KS"],
}
all_symbols = [sym for group in WATCHLIST.values() for sym in group]
BATCH_SIZE = 10 # symbols per request
saved, missing = [], []
for i in range(0, len(all_symbols), BATCH_SIZE):
batch = all_symbols[i:i + BATCH_SIZE]
data = fetch_batch(batch, kline_type=8, count=500)
for sym, df in data.items():
filepath = os.path.join(OUTPUT_DIR, sym.replace(".", "_") + "_daily.csv")
df[["datetime", "open", "high", "low", "close", "volume", "amount"]].to_csv(filepath, index=False)
print(f" ✓ {sym:14s} {len(df)} bars → {filepath}")
saved.append(sym)
for sym in [s for s in batch if s not in data]:
print(f" ✗ {sym}: no data returned")
missing.append(sym)
print(f"\nDone. Saved: {len(saved)} | Missing: {len(missing)}")Both approaches save CSV files to candlestick_export/, named by symbol: AAPL_US_daily.csv, 00700_HK_daily.csv, and so on. Use the SDK version for cleaner code; use the raw HTTP version when you need fine-grained control over request batching or response parsing.
Example 3: Full History Download with Pagination
Each request returns up to 500 candles. To pull years of minute-level data or complete daily history for a single instrument, walk backwards using the timestamp parameter:
import requests
import pandas as pd
import json
API_KEY = "YOUR_API_KEY"
ENDPOINT = "https://data.infoway.io/stock/v2/batch_kline"
HEADERS = {"Content-Type": "application/json", "apiKey": API_KEY}
def download_full_history(symbol: str, kline_type: int = 8) -> pd.DataFrame:
"""Page backward through all available candles for a single symbol."""
all_candles: list[dict] = []
cursor: int | None = None
while True:
payload = {"codes": symbol, "klineType": kline_type, "klineNum": 500}
if cursor:
payload["timestamp"] = cursor
resp = requests.post(ENDPOINT, headers=HEADERS, data=json.dumps(payload), timeout=30)
resp.raise_for_status()
items = resp.json().get("data", [])
candles = next((it.get("respList", []) for it in items if it["s"] == symbol), [])
if not candles:
break
all_candles.extend(candles)
# respList is newest-first; use the oldest timestamp to fetch the next page
oldest_ts = int(candles[-1]["t"])
if cursor and oldest_ts >= cursor:
break # no earlier data available
cursor = oldest_ts
if not all_candles:
return pd.DataFrame()
df = pd.DataFrame(all_candles,
columns=["time", "open", "high", "low", "close",
"volume", "amount", "pct_chg", "chg"])
df[["open", "high", "low", "close", "volume", "amount"]] = \
df[["open", "high", "low", "close", "volume", "amount"]].astype(float)
df["datetime"] = pd.to_datetime(df["time"].astype(int), unit="s")
df.drop_duplicates("datetime", inplace=True)
df.sort_values("datetime", inplace=True)
return df.reset_index(drop=True)
# Download Apple's complete daily history
df = download_full_history("AAPL.US", kline_type=8)
print(f"AAPL.US: {len(df)} daily bars | {df['datetime'].iloc[0].date()} → {df['datetime'].iloc[-1].date()}")
df.to_csv("AAPL_US_full.csv", index=False)
# Same function works for any other market
df_hk = download_full_history("00700.HK", kline_type=8)
print(f"00700.HK: {len(df_hk)} daily bars | {df_hk['datetime'].iloc[0].date()} → {df_hk['datetime'].iloc[-1].date()}")The same pagination logic works for any timeframe, swap kline_type=1 to pull every available minute bar going back three years.
Candlestick Response Fields
Each candle object in respList contains:
| Field | Description |
|---|---|
t | Candle open time, Unix seconds, newest-first in the response |
o | Open |
h | High |
l | Low |
c | Close |
v | Volume (shares / contracts / units depending on market) |
vw | Trade value in local currency |
pc | Change % vs. previous candle |
pca | Change in points vs. previous candle |
FAQ
How many symbols can I include in a single request?
There is no hard symbol limit per request, but keep batches to 10–20 symbols to avoid large response payloads and timeouts. For 100+ symbols, loop in batches of 10–20, the whole job typically completes in under a minute.
Can I mix markets in the same request?
Yes. A single batch_kline call can include AAPL.US, 00700.HK, 600519.SH, 7203.JP, and 005930.KS in the same codes string.
How far back does minute-level data go?
Three years for all supported markets. Daily, weekly, monthly, and yearly candles have no lookback limit.
Are the timestamps in UTC?
Yes. The t field is Unix seconds (UTC) regardless of the instrument’s home market. Convert to local market time using pytz or Python’s zoneinfo when you need session-aware logic.
Can I get intraday data for markets with a midday break (Japan, A-share)?
Yes. The API returns candles only for actual trading intervals, the lunch-break gap is simply absent from the data, not filled with zeroes. Handle it in your charting layer as a session boundary rather than a data error.
Should I use the Python SDK or raw HTTP requests?
For most projects, the SDK is the better choice , it handles authentication, JSON serialization, and returns pandas DataFrames directly, which cuts out 10–15 lines of boilerplate per call. Raw HTTP is useful when you need control over pagination (like the full-history example above), want zero external dependencies beyond requests, or are integrating into a framework that manages HTTP sessions itself. Install with pip install infoway-sdk.
Is there a WebSocket feed for live candlestick updates?
Yes. Infoway provides a WebSocket interface for real-time trade and depth data. The REST batch_kline endpoint described in this guide is best suited for historical downloads and batch backfills; for live candle construction, subscribe to the trade stream and aggregate on your side. The Python SDK exposes this via from infoway.ws import InfowayWebSocket.