从数据获取到分析可视化,一套完整的 A 股量化研究流程
前言
作为一个程序员,我有个习惯:能用代码解决的,绝不用直觉。
炒股也一样。与其听"老师"荐股、看各种技术分析文章,不如自己把全市场的数据跑一遍,用统计说话。
本文记录了我用 Python 扫描全市场 389 只股票 K 线数据的完整过程——包括数据获取、特征提取、对比分析和可视化。文中提供可运行的代码,你可以直接复现。
1. 研究设计
目标:找出涨停股和大跌股在 K 线特征上的系统性差异
样本:
- 涨停组:当日涨幅 ≥ 9.8%,共 69 只
- 大跌组:当日跌幅 ≥ 3%,共 320 只
- 对比维度:均线位置、近期涨幅、振幅、涨停基因、昨日涨跌
工具:Python 3.9 + requests + pandas
2. 数据获取
我用的是东方财富的公开接口,免费且无需注册:
import requests
import pandas as pd
from datetime import datetime, timedelta
def get_kline(stock_code, days=60):
"""获取个股日K线数据"""
url = f"http://push2his.eastmoney.com/api/qt/stock/kline/get"
params = {
"secid": stock_code, # 如 "1.600519"(上海)或 "0.000001"(深圳)
"fields1": "f1,f2,f3,f4,f5,f6",
"fields2": "f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61",
"klt": "101", # 日线
"fqt": "1", # 前复权
"end": datetime.now().strftime("%Y%m%d"),
"lmt": str(days)
}
resp = requests.get(url, params=params, timeout=10)
data = resp.json()
klines = data.get("data", {}).get("klines", [])
df = pd.DataFrame([k.split(",") for k in klines],
columns=["日期","开盘","收盘","最高","最低","成交量",
"成交额","振幅","涨跌幅","涨跌额","换手率"])
for col in ["开盘","收盘","最高","最低","涨跌幅","振幅","换手率"]:
df[col] = df[col].astype(float)
return df
提示:东方财富接口响应约 200ms,批量扫描建议加并发和缓存。实测稳定率约 95%,建议做三级降级(腾讯 → 东财 → 新浪)。
3. 特征提取
对每只股票提取以下 K 线特征:
def extract_features(df):
"""从K线数据中提取关键特征"""
if len(df) < 20:
return None
latest = df.iloc[-1]
close = latest["收盘"]
# 均线计算
ma5 = df["收盘"].rolling(5).mean().iloc[-1]
ma10 = df["收盘"].rolling(10).mean().iloc[-1]
ma20 = df["收盘"].rolling(20).mean().iloc[-1]
# 均线偏离度
ma5_bias = (close - ma5) / ma5 * 100
ma20_bias = (close - ma20) / ma20 * 100
# 近期涨幅
ret_5d = (close - df.iloc[-6]["收盘"]) / df.iloc[-6]["收盘"] * 100 if len(df) >= 6 else 0
ret_10d = (close - df.iloc[-11]["收盘"]) / df.iloc[-11]["收盘"] * 100 if len(df) >= 11 else 0
# 振幅
avg_amp_3d = df["振幅"].tail(3).mean()
# 昨日涨跌
yesterday_ret = df.iloc[-2]["涨跌幅"] if len(df) >= 2 else 0
# 涨停基因(近10日内有无涨停)
limit_up_count = (df["涨跌幅"].tail(10) >= 9.8).sum()
# MA10角度(判断趋势方向)
ma10_today = ma10
ma10_3d_ago = df["收盘"].rolling(10).mean().iloc[-4] if len(df) >= 13 else ma10
ma10_angle = (ma10_today - ma10_3d_ago) / ma10_3d_ago * 100
# 均线多头排列
bullish_align = 1 if ma5 > ma10 > ma20 else 0
# 大阳线(单日涨幅>5%)
has_big_candle = 1 if (df["涨跌幅"].tail(5) >= 5).any() else 0
return {
"ma5_bias": round(ma5_bias, 2),
"ma20_bias": round(ma20_bias, 2),
"ret_5d": round(ret_5d, 2),
"ret_10d": round(ret_10d, 2),
"avg_amp_3d": round(avg_amp_3d, 2),
"yesterday_ret": round(yesterday_ret, 2),
"limit_up_genes": int(limit_up_count),
"ma10_angle": round(ma10_angle, 2),
"bullish_align": bullish_align,
"has_big_candle": has_big_candle,
"above_ma5": 1 if close >= ma5 else 0,
"above_ma20": 1 if close >= ma20 else 0,
}
4. 对比分析
把涨停组和大跌组的特征放在一起对比:
def compare_groups(limit_up_features, big_drop_features):
"""对比涨停组和大跌组的特征差异"""
import numpy as np
results = {}
for key in limit_up_features[0].keys():
limit_vals = [f[key] for f in limit_up_features]
drop_vals = [f[key] for f in big_drop_features]
results[key] = {
"涨停组均值": round(np.mean(limit_vals), 2),
"大跌组均值": round(np.mean(drop_vals), 2),
"差异": round(np.mean(limit_vals) - np.mean(drop_vals), 2)
}
comparison = pd.DataFrame(results).T
comparison = comparison.sort_values("差异")
return comparison
运行结果:
| 因子 | 涨停组 | 大跌组 | 差异 |
|---|---|---|---|
| MA5上方占比 | 27.5% | 89.5% | -62% ⭐⭐⭐⭐⭐ |
| MA5偏离度 | -0.8% | +4.3% | -5.1% |
| 近5日涨幅 | +5.3% | +11.8% | -6.5% |
| 近3日振幅 | 5.3% | 8.1% | -2.8% |
| 近10日涨幅 | +7.3% | +17.0% | -9.7% |
| MA20偏离度 | +6.1% | +18.3% | -12.2% |
5. 关键发现
发现1:大涨股在MA5下方(最反直觉)
涨停组:72.5% 在 MA5 下方
大跌组:89.5% 在 MA5 上方
涨停不是"一直在涨",而是"回调到位后发力"。
发现2:近期涨幅越大越危险
近5日涨幅超过10%的票,在大跌组中占比远超涨停组。5-8%是最佳区间。
发现3:昨天跌了反而可能涨停
涨停组:52% 昨日下跌
大跌组:37% 昨日下跌
回调洗盘 → 盘中拉升,这是A股的经典模式。
发现4:涨停基因基本没用
82.5%的涨停股没有涨停基因。追"昨日涨停"实际上是追高开,而高开容易被砸。
发现5:振幅适中更安全
近3日振幅超过7%的票,筹码极不稳定,无论涨跌风险都大。
6. 信号强度可视化
import matplotlib.pyplot as plt
import numpy as np
factors = ["MA5上方", "大阳线", "MA20上方", "MA10角度", "均线多头", "昨日下跌", "涨停基因"]
limit_up = [27.5, 52.5, 65.0, 65.0, 52.5, 52.5, 17.5]
big_drop = [89.5, 89.5, 100.0, 89.5, 73.7, 36.8, 31.6]
x = np.arange(len(factors))
width = 0.35
fig, ax = plt.subplots(figsize=(12, 6))
bars1 = ax.bar(x - width/2, limit_up, width, label='涨停组', color='#e74c3c')
bars2 = ax.bar(x + width/2, big_drop, width, label='大跌组', color='#2ecc71')
ax.set_ylabel('占比 (%)')
ax.set_title('涨停组 vs 大跌组:关键因子对比')
ax.set_xticks(x)
ax.set_xticklabels(factors, rotation=15)
ax.legend()
plt.tight_layout()
plt.savefig("factor_comparison.png", dpi=150)
7. 完整项目结构
如果你也想复现这个分析,这是我实际的系统架构:
a-stock/
├── data_source.py # 数据获取层(三级降级)
├── screen_v14.py # 选股引擎(多策略并行)
├── notify_v14.py # 飞书消息推送
├── scheduler.py # 定时调度
└── theme_scan.py # 题材分析
数据源优先级:finshare(自动切换5个源)→ 东方财富 → 新浪,实测全市场扫描耗时约5秒。
8. 局限性
- 单日数据,样本量有限(69只涨停/320只大跌)
- 未考虑行业、市值、流动性的影响
- 需要积累20-30个交易日数据做统计验证
- 因子间相关性未做正交化处理
总结
程序员做量化最大的优势不是数学能力,而是能把想法变成可运行、可验证的代码。
与其在各种论坛里看别人的经验贴,不如自己把数据跑一遍。全市场扫描的代码不超过100行,但得出的结论可能比看100篇文章都更有价值。
如果你也在做类似的事,或者对代码有改进建议,欢迎评论区交流。
相关阅读:
免责声明:本文为个人量化实验记录,所有数据和分析不构成投资建议。