Computing technical indicators from K-line data
With raw K-lines, indicators are easy to compute yourself - no expensive tools needed. This guide uses the free endpooint for forward-adjusted daily bars and writes MA / MACD / RSI / KDJ / BOLL in pure Python + pandas, no TA-Lib.
1. Get the data first (free endpoint)
`/v1/kline` is a free endpoint (no Key). Returns forward-adjusted daily/weekly/monthly bars:
import requests
import pandas as pd
r = requests.get("https://api.ashareapi.com/v1/kline",
params={"code": "sh600667", "period": "day", "count": 60},
timeout=30)
bars = r.json()["data"] # list[dict]
df = pd.DataFrame(bars)
print(df[["date", "open", "last", "high", "low", "volume"]].head())- Forward-adjusted: adjusted for dividends/splits (no gap on ex-dividend days). To get raw prices, adjust back yourself (not covered here)
- `last` is the close, not the live price — computing indicators off the close is standard
| Field | Meaning | Note |
|---|---|---|
| date | Trade date | Day = that day; week/month = period end date |
| open / last | Open / close | last is the close (not live price) |
| high / low | High / low | |
| volume | Volume | unit: lots (×100 = shares) |
| amount | Turnover value | unit: CNY |
| turnover | Turnover rate | % |
2. MA (simple moving average, the most basic)
MA(N) = mean of last N closes. It is the "smoothing filter" for trend and an input to other indicators.
def ma(close, n):
return close.rolling(n).mean()
df["ma5"] = ma(df["last"], 5)
df["ma10"] = ma(df["last"], 10)
df["ma20"] = ma(df["last"], 20)
df["ma60"] = ma(df["last"], 60)
print(df[["date", "last", "ma5", "ma20"]].tail())- Golden cross: MA5 crosses above MA20 (short turns stronger)
- Death cross: MA5 crosses below MA20 (weaker)
- Bull alignment: MA5 > MA10 > MA20 (uptrend); bear alignment the reverse
3. MACD (trend + momentum, most used)
MACD = difference of fast/slow EMAs (DIF) smoothed again (DEA), standard 12/26/9.
def macd(close, fast=12, slow=26, sig=9):
ema_f = close.ewm(span=fast).mean()
ema_s = close.ewm(span=slow).mean()
dif = ema_f - ema_s # fast - slow
dea = dif.ewm(span=sig).mean() # EMA of DIF
hist = (dif - dea) * 2 # bar (common ×2)
return dif, dea, hist
df["dif"], df["dea"], df["macd"] = macd(df["last"])- Golden cross: DIF crosses above DEA (momentum turns positive); death cross: below
- Bar: red growing = bullish momentum, green growing = bearish
- Divergence: price new high but MACD not (top divergence, warning); price new low but MACD not (bottom divergence)
4. RSI (overbought / oversold)
RSI(N) measures recent up/down strength, used to spot overbought/oversold. Classic 14.
def rsi(close, n=14):
delta = close.diff()
gain = delta.clip(lower=0).rolling(n).mean()
loss = (-delta.clip(upper=0)).rolling(n).mean()
rs = gain / loss
return 100 - 100 / (1 + rs)
df["rsi14"] = rsi(df["last"])- > 70 usually overbought (watch for pullback); < 30 oversold
- Divergence applies too: price new high but RSI not = top divergence
- Not absolute: in strong trends RSI can stay overbought a long time (not necessarily a pullback)
5. KDJ (stochastic, short-term favorite)
KDJ uses high/low/close relative position; sensitive but noisy. Classic 9,3,3.
def kdj(high, low, close, n=9):
low_n = low.rolling(n).min()
high_n = high.rolling(n).max()
rsv = (close - low_n) / (high_n - low_n) * 100
k = rsv.ewm(alpha=1/3).mean()
d = k.ewm(alpha=1/3).mean()
j = 3 * k - 2 * d
return k, d, j
df["k"], df["d"], df["j"] = kdj(df["high"], df["low"], df["last"])- K crosses above D = golden cross (short-term stronger); below = death cross
- > 80 overbought / < 20 oversold, more sensitive than RSI
- J value can exceed 0/100 (extremes), used to detect saturation
6. BOLL (Bollinger Bands, volatility range)
BOLL = mid-band MA20 ± 2×std, describing the "normal range" of price.
def boll(close, n=20, k=2):
mid = close.rolling(n).mean()
std = close.rolling(n).std()
upper = mid + k * std
lower = mid - k * std
return upper, mid, lower
df["boll_up"], df["boll_mid"], df["boll_low"] = boll(df["last"])- Squeeze (band narrows) = volatility about to expand (pre-breakout); expansion = volatility up
- Touch upper band = strong (can ride along in a trend); touch lower band = weak
- Break below lower then reclaim = possible oversold bounce; break above upper = possible acceleration (but in a strong trend price rides the band, not necessarily falling back)
7. Combining the five (more is not better)
- Trend + momentum: MA (direction) + MACD (momentum) — the most common combination
- Overbought/oversold: RSI / KDJ (pick one; both short-term, noisy)
- Volatility range: BOLL (gives MA a "where it should be" band)
- Discipline: indicators are confirmation aids, not standalone buy/sell. Two or more agreeing + your own logic is more reliable (this guide only covers data retrieval and computation, not investment advice)
8. Common misreads (important)
- ① Adjustment basis: the API returns forward-adjusted — computing indicators off it is correct (keeps continuity). But compare against raw price when reconciling with your broker
- ② Parameters are configurable: MACD(12,26,9) / RSI(14) / KDJ(9,3,3) / BOLL(20,2) are "common", not "only". Changing them = a different indicator, possibly different conclusion
- ③ Technical indicators lag: they are all historical averages/smoothing — they reflect the past. In strong trends they can saturate; do not mechanically trade on crosses
FAQ
No. kline is one of the 5 free endpoints (quote / kline / hot / market-overview / changedist), no Key needed.
Yes, the API always returns forward-adjusted (no gap on ex-dividend days). Computing indicators off forward-adjusted is correct (keeps the series continuous). For raw unadjusted prices, adjust back using dividend/split factors.
TA-Lib is powerful but awkward to install (needs compilation). This guide implements 5 common indicators in pure pandas, zero extra dependencies, ideal for teaching and light use. For large backtests, consider TA-Lib for speed.
No. kline currently offers only daily / weekly / monthly — no minute bars. Intraday data is elsewhere (e.g. orderbook live quote), but no minute K-lines.
Last updated: 2026-09-29
Data from live ashareapi /v1/kline responses (measured 2026-09-29: forward-adjusted daily bars with date/open/last/high/low/volume/amount/turnover); indicators implemented with standard algorithms (pandas, no TA-Lib).