Migrating from AkShare to ashareapi
AkShare wins on breadth; we win on not having to maintain it yourself. If you already use AkShare and are tired of "upstream redesign → broken function", migration is three changes: code format, call style, field names. Below is the mapping and runnable code — and the parts that do not migrate.
First, be clear about why you are migrating
AkShare is a free, MIT-licensed Python library whose data coverage is much wider than ours. The usual reason to migrate is not "more data" but these three:
- You do not want to maintain it: AkShare scrapes public pages, so an upstream redesign breaks a function until you upgrade or patch the code
- You want stability and caching: every one of our endpoints has multiple sources (auto-failover), caching and health checks
- You need it for AI Agents or outside Python: we speak HTTP and MCP, so any language works without installing 1,300+ dependencies
If your budget is zero, you are already in Python and you are doing exploratory research (not production jobs), AkShare is a great choice — this page is for people who want to maintain a little less, not to talk anyone out of it. The two also work fine together.
Step 1: Convert the code format from bare digits to prefixed
This is the easiest trap in the migration. AkShare takes a bare six-digit `symbol` (`"600667"`); ours is prefixed (`sh600667`). Every endpoint takes this format.
# AkShare # ashareapi
# "600667" (Shanghai A) → sh600667
# "000001" (Shenzhen A) → sz000001
# "830799" (Beijing) → bj830799
# "00700" (Hong Kong) → hk00700
# "AAPL" (US) → usAAPL
# Infer the market from the leading digits (A-share rules) — good enough, not exhaustive
def to_code(symbol: str) -> str:
"""AkShare bare code -> ashareapi prefixed code"""
if not symbol.isdigit():
return "us" + symbol.upper() # non-numeric -> US
if symbol.startswith(("60", "68")): # Shanghai main / STAR
return "sh" + symbol
if symbol.startswith(("00", "30")): # Shenzhen main / ChiNext
return "sz" + symbol
if symbol.startswith(("83", "87", "43")): # Beijing
return "bj" + symbol
return "sh" + symbol # fallback (always verify)- Safest route: call `/v1/search?q=
` first — it returns the correct prefixed code and disambiguates automatically - The prefix also removes ambiguity like "is 000001 Ping An Bank or the SSE index"
Step 2: Swap the call style (function → HTTP request)
AkShare is `import akshare as ak` then call a function and get a DataFrame; ours is one GET request returning a uniform JSON envelope. Neither is hard, but the shape differs.
# ── Before (AkShare) ─────────────────────────
# import akshare as ak
# df = ak.stock_zh_a_hist(symbol="600667", period="daily",
# start_date="20260101", end_date="20260925",
# adjust="")
# print(df[["日期", "收盘", "成交量"]])
# ── After (ashareapi) ────────────────────────
import requests
BASE = "https://api.ashareapi.com/v1"
H = {} # free endpoints need no key; add {"Authorization": "Bearer ct-your-key"} for paid
def get(path, **params):
r = requests.get(f"{BASE}/{path}", params=params, headers=H, timeout=30)
if r.status_code == 429: # rate limited: see section 4
raise RuntimeError("rate limited")
r.raise_for_status()
body = r.json()
if not body.get("ok"):
raise RuntimeError(f"upstream failed (not counted): {body}")
return body["data"]
# Historical K-line (free) — equivalent to stock_zh_a_hist
# (no date-range args: take the last N bars, filter locally)
bars = get("kline", code="sh600667", period="day", count=30)
for r in bars:
print(r["date"], r["last"], r["volume"]) # 收盘 -> last
# Realtime quote (free) — the single-name version of stock_zh_a_spot_em
q = get("quote", code="sh600667")
print(q[0]["last"], q[0]["turnover"]) # last price / turnover- No `adjust` (price-adjustment) parameter — the basis is fixed at forward-adjusted; ⚠️ do not re-adjust (double adjustment); unadjusted / back-adjusted series are not provided
- No `start_date` / `end_date` — use `count` for the last N bars and filter locally (this also avoids "no data in range" being mistaken for an API error)
- The return is a `list[dict]` (numeric fields are strings), not a DataFrame; convert with `import pandas as pd; pd.DataFrame(bars)`
Step 3: Rename the fields
Only a few common field names differ; the rest are the same. Note our numeric fields are strings (`"19.41"`, not `19.41`) — cast before comparing or computing.
| Meaning | AkShare | ashareapi | Note |
|---|---|---|---|
| Date | 日期 | date | Ours includes dashes (2026-09-24) |
| Close / last | 收盘 | last | Different name, same meaning |
| Open | 开盘 | open | Same |
| High / low | 最高 / 最低 | high / low | Same |
| Volume (lots) | 成交量 | volume | Both in lots |
| Turnover (CNY) | 成交额 | amount | CNY |
| Turnover rate (%) | 换手率 | turnover | Renamed from `exchange` on 2026-09-26 (the old name read like "exchange" the venue) · same as `snapshot` |
| Code | 代码 / symbol | code | Prefixed: sh600667 |
Mapping table (AkShare function → our endpoint)
The left column lists common AkShare functions from its official docs; the right column is the equivalent (or nearest) endpoint. Where there is no equivalent, we do not force one — see the notes.
| AkShare | ashareapi | Note |
|---|---|---|
| stock_zh_a_spot_em (whole-market snapshot) | GET /v1/quote (single) · /v1/hot (ranking) | We do not bulk-export a whole-market snapshot; only per-code quotes |
| stock_zh_a_hist (history) | GET /v1/kline | period=day|week|month, count for last N bars (no date range) |
| stock_zh_a_hist_min_em (minute) | ❌ | We do not do minute-level data (an explicit boundary) |
| stock_individual_info_em | GET /v1/profile · /v1/snapshot | profile = listing date / business / industry; snapshot = valuation / market cap / shares / limit price |
| stock_bid_ask_em (order book) | GET /v1/orderbook | Buy 1–5 / sell 1–5 prices and sizes |
| stock_financial_abstract | GET /v1/finance | Three statements, multi-period (num controls periods); ROE / margin included |
| stock_profit_sheet_by_report_em | GET /v1/finance | All three statements in one call |
| stock_individual_fund_flow | GET /v1/fund | One endpoint covers everything: money flow + top-trader boards + block trades + margin |
| stock_market_fund_flow (market-wide flow) | ❌ | No market-wide intraday flow; see /v1/changedist (breadth, free) |
| stock_lhb_detail_em | GET /v1/lhb | type=institution / hotmoney / activeseat |
| stock_margin_detail_sse / _szse | GET /v1/margin-trade | Balance / buy / repay / short; code supports batches |
| stock_dzjy_mrmx (block trades) | GET /v1/block-trade | Price / premium / volume / both sides |
| stock_dividend_cninfo | GET /v1/dividend | years controls the window (default 3) |
| stock_gdfx_free_top_10_em | GET /v1/shareholder | Top holders + holder count + institutional holdings |
| stock_zh_a_gdhs (holder count) | GET /v1/shareholder | Holder count (ownership concentration) |
| bond_zh_cov | GET /v1/bond | Terms-focused: conversion price / call trigger / double-low / premium; no bond daily bars |
| fund_etf_spot_em | GET /v1/etf | Quote / size / premium-discount / flows (code like sh510300) |
| stock_new_gh_detail_em (IPOs) | GET /v1/ipo | Issue / subscription / allotment / listing calendar (days) |
| stock_board_industry_name_em | GET /v1/sector · /v1/sector-valuation | Sector ranking + valuation (incl. historical percentile) |
| stock_board_concept_name_em | GET /v1/sector · /v1/industry-chain | sector includes concept boards; industry-chain gives supply-chain position and relevance |
| stock_zh_index_daily | ❌ | /v1/market-overview (breadth / style rotation / valuation percentile) is not an index K-line |
| stock_news_em / filings / full reports | ❌ | No news or full filings; research reports are digest only (/v1/dehydrated) |
| Stock lists / whole-market export | GET /v1/search (by keyword) | No bulk list export |
Step 4: Rate limits and error handling
AkShare has no rate limit (it runs locally); over HTTP you must handle 429 and upstream failures. That is exactly the trade-off — and the point — of the stability you get.
import time, requests
BASE = "https://api.ashareapi.com/v1"
H = {}
def get_with_retry(path, tries=3, **params):
for i in range(tries):
r = requests.get(f"{BASE}/{path}", params=params, headers=H, timeout=30)
if r.status_code == 429: # rate limited
time.sleep(2 ** i) # back off (do not hammer)
continue
r.raise_for_status()
body = r.json()
if not body.get("ok"): # upstream failed -> not counted, retryable
time.sleep(1)
continue
return body["data"]
raise RuntimeError(f"{path}: failed after {tries} tries")- Free endpoints: anonymous normal quota is 5/min; solve one PoW challenge (`GET /v1/challenge`) to reach 60/min
- Note: rapid consecutive calls are temporarily tightened (we measured "anonymous limit 2/min" after six back-to-back calls) — it recovers after back-off, it is not a ban
- Paid keys are tiered (trial 30 · standard 120 · pro 300 · unlimited 600 per minute)
- Failed upstream calls are not counted (`ok:false`), so retrying is safe; only 429 needs a back-off
What moves, what does not (stated up front)
- Moves: realtime quotes · daily / weekly / monthly K-line · three statements · money flow · top-trader boards · margin · block trades · dividends · shareholders · convertible-bond terms · ETFs · IPOs · sectors · supply chain
- Does not move: minute bars · news / filings / full reports · index daily bars · whole-market list export · unadjusted / back-adjusted price series (our K-line is fixed to forward-adjusted)
- Stance: for those, keep using AkShare — it is stronger there, and using both side by side is the pragmatic combination
Common mistakes
- Passing `"600667"` → 400 / empty; must be `sh600667` (or confirm via `/v1/search`)
- Treating strings as numbers → `"19.41" * 2` is not 38.82; cast with `float(...)` first
- Looking for `adjust` / `start_date` / `end_date` → the adjustment basis is fixed (forward-adjusted) and there are no range queries; use `count` and filter locally
- Turnover rate missing → the field is `turnover` (`quote` / `kline` / `snapshot` — same name and value; it was `exchange` before 2026-09-26)
- "Bad data" that is really undisclosed → financials / holders / margin update on disclosure cadence; before the disclosure date there is simply no data (not a failure)
FAQ
Not for more data (AkShare covers more than we do) but for maintenance: AkShare scrapes public pages, so you patch it when upstream changes, whereas we run multi-source failover, caching and health checks. If your budget is zero and you accept the upkeep, AkShare is a great choice — do not migrate just to migrate.
Yes, and it is common. Typical split: niche or very broad data (news, filings, minute bars, indices) from AkShare, and core data (realtime quotes, money flow, top-trader boards, sector valuation) from us, where we carry the stability and maintenance cost.
No. Every endpoint requires the prefixed form (`sh600667`). The laziest correct way is to call `/v1/search?q=Ping+An+Bank` first — it returns the right code and disambiguates same-name / same-number cases.
The HTTP endpoints return a `list[dict]` (values are mostly strings). One line converts it: `import pandas as pd; df = pd.DataFrame(bars)`. The official Python SDK (`pip install ashareapi`) returns DataFrames directly with the `[pandas]` extra.
Start with the endpoint list (32 endpoints by category) or search a keyword via `/v1/search`. For what is genuinely absent (minute bars, news / filings, index daily bars, unadjusted / back-adjusted series, whole-market lists) keep using AkShare — that is our boundary, and we say so here so you do not migrate for nothing.
Last updated: 2026-09-25
AkShare function names are taken from its official docs (akshare.akfamily.xyz); our endpoint count and tiers come from the official endpoint list endpoints.json (32 = 7 free + 25 paid, generated 2026-09-24); fields and shapes come from live responses (quote / kline measured 2026-09-25). Both sides may change — their official pages govern.