系列名:《从零搭建你的 A 股量化系统》| 第 3 篇 / 共 100+ 篇
标签:#量化投资 #Python #pandas #matplotlib #A股 #散户 #零基础 #实战

上篇《半小时装好环境》里,我们把 Python + conda + Git 这套地基打好、自检通过了。但说实话——你那时候大概率还是"照着敲、能跑、但不知道为啥"。

量化 90% 的日常工作,不是在写算法,而是在"摆弄数据"——把乱七八糟的行情读进来、洗干净、算指标、画出来看。而干这件事,几乎全靠一个库:pandas

这一篇,我带你补齐"看得懂、改得动"代码所需的 Python 底子,重点拿下 pandas 这个量化最核心的库,最后亲手用 pandas 把双均线策略跑成一个能看绩效的回测雏形——不仅画出收益曲线,还算出年化收益、最大回撤、交易次数。

⏱️ 时间预期:跟着做约 40~60 分钟(前半段偏"看懂",后半段偏"动手")。代码都不长,关键是边读边在编辑器里敲一遍。


一、这篇我们要搞定什么

先给你一张"本篇地图",免得看着看着迷路:

模块 你会学到 产出
Python 速成 只学"为 pandas 铺路"的那点语法 一段看得懂的 Python
pandas 核心 Series、DataFrame、读 CSV、列运算 能把一份行情表玩出花
可视化 折线图、双子图、标注买卖点 一张看得见的图
实战 用 pandas 跑通双均线 + 算出绩效 src/s1_003_dual_ma.py + 收益曲线图

整篇的代码会串成下面这条流水线,记住它,后面每一步都对得上:

生成教学数据

存成 CSV

pd.read_csv 读入

rolling 算均线

金叉持仓/死叉空仓

shift 防前视偏差

算收益与绩效

matplotlib 出图

📌 接上环境篇:本篇跑出来的脚本,就落在第 2 篇建好的项目树 src/ 里——它是你量化系统 src/strategies/第一个策略文件的雏形。"建系统即写博客"的同频推进,从这里正式开始。


二、Python 速成:只学"为 pandas 铺路"的那点

很多人被"学编程"吓退,以为要先啃完一本砖头书。其实做量化,你只需要 Python 的"骨架级"知识。而且——别把它们当成孤立语法,它们每一个都是为后面写策略函数准备的

知识点 量化里干嘛用 马上会用到
列表 list 装一批股票代码 ["600519","000858"]
字典 dict 装策略参数 {"fast":10,"slow":30}
函数 def 把策略逻辑打包复用 def ma_cross(prices): ...
列表推导式 一行批量处理 批量生成标签、过滤
if / 布尔 条件分支 金叉才买入

来看一段"为 pandas 铺路"的例子——它看着是纯 Python,其实已经在召唤 pandas 了:

# 量化里最常用的一小撮 Python(重点看注释)
stocks = ["600519", "000858", "300750"]        # 列表:装一批股票代码
params = {"fast": 10, "slow": 30}              # 字典:装策略参数
fast, slow = params["fast"], params["slow"]     # 从字典里取参数

def ma_cross(prices, f, s):
    """快线在慢线上方返回 1(持仓),否则 0 —— 这就是双均线的灵魂"""
    ma_f = prices.rolling(f).mean()             # 短期均线(prices 是 pandas 的 Series)
    ma_s = prices.rolling(s).mean()             # 长期均线
    return (ma_f > ma_s).astype(int)

# 列表推导式:一行批量生成 "股票 + 参数" 的标签
labels = [f"{code}{params['fast']}/{params['slow']} 双均线" for code in stocks]
print(labels)
# 输出:['600519 用 10/30 双均线', '000858 用 10/30 双均线', '300750 用 10/30 双均线']

💡看到不认识的语法,先问"它想表达什么逻辑",而不是"它为什么这么写"。逻辑通了,语法查一下就懂。量化代码里几乎不出现高阶语法(元类、装饰器、异步),放心。

现在你发现没?ma_cross 这个函数,把"快线在慢线上方就持仓"的双均线逻辑,用最清楚的方式写了出来。这正是量化策略函数的标准形态——输入行情,输出 0/1 信号,后面要做的一切都建立在它之上。


三、pandas:量化最该先学的库

如果说 Python 是发动机,那 pandas 就是量化这台车的底盘——它把"表格数据"变成了一种超级好操作的形态。A 股行情、财务报表、资金流,本质都是一张张表,而 pandas 就是为"表"而生的。

3.1 两个核心概念:Series 和 DataFrame

概念 类比 Excel 说明
Series 一列 带索引的一维数据,比如"收盘价这一列"
DataFrame 一整张表 多列组合,比如"日线行情表"

记住一句话:DataFrame = 很多个 Series 并排站在一起。

3.2 第一步:把数据读进来(读 CSV)

真实工作里,数据往往是从文件或接口拿到的。我们先造一份"教学用"日线数据存成 CSV,再用 pandas 读回来——这正好演示"数据 → 处理"的完整链路。

import pandas as pd
import numpy as np

# 造一份 250 个交易日的模拟日线(教学用,非真实行情)
np.random.seed(7)
n = 250
dates = pd.date_range("2023-01-02", periods=n, freq="B")     # B = 工作日
close = 100 + np.cumsum(np.random.randn(n)) * 1.2
close = np.maximum(close, 20.0)                             # 防止出现负价格
df = pd.DataFrame({
    "date":   dates,
    "open":   np.round(close + np.random.randn(n) * 0.3, 2),
    "high":   np.round(close + np.abs(np.random.randn(n)) * 0.8, 2),
    "low":    np.round(close - np.abs(np.random.randn(n)) * 0.8, 2),
    "close":  np.round(close, 2),
    "volume": np.random.randint(1_000_000, 5_000_000, n).astype(float),
})
df.to_csv("data/sample_prices.csv", index=False)           # 存成 CSV

# 用 pandas 读回来
price = pd.read_csv("data/sample_prices.csv", parse_dates=["date"])
print(price.head(3))     # 看前 3 行
print("行数:", len(price), " 列:", list(price.columns))

几个立刻要记牢的点:

  • pd.read_csv("路径") 是你会用最频繁的函数,没有之一;
  • parse_dates=["date"] 让"日期"列被当成真正的日期(而不是字符串),后面按时间排序、画图才不会乱;
  • price.head(3) 快速预览前几行,调试数据时天天用。

3.3 索引与切片:精准"夹"出你要的数据

closes = price["close"]                       # 取某一列(返回 Series)
sub    = price[["date", "close", "volume"]]  # 取多列
first_10 = price.iloc[:10]                    # 按位置取前 10 行
up_days  = price[price["close"] > price["open"]]   # 按条件过滤:当天收涨的所有行
march    = price[price["date"] >= "2023-03-01"]    # 按日期范围切(date 是日期类型才能这么比)

⚠️ 新手雷区price["close"]price.close 都能取列,但后者在列名带空格/中文时容易翻车,一律用中括号 [] 最稳

3.4 列运算 & 移动窗口:指标就是这么算出来的

量化里绝大部分指标,本质都是"对一列数做运算"。pandas 让你整列一起算,不用写 for 循环

price["ret"]   = price["close"].pct_change().fillna(0)  # 每日收益率
price["range"] = price["high"] - price["low"]           # 当日振幅
price["ma10"]  = price["close"].rolling(10).mean()      # 最近 10 天平均价 = MA10
price["ma30"]  = price["close"].rolling(30).mean()      # 最近 30 天平均价 = MA30
price["signal_yesterday"] = price["signal"].shift(1)    # 整列上下挪一天

rolling(window).mean() 就是"滑动窗口求平均",对应均线;shift(1) 是把数据整体往后挪一天——这个技巧后面实战会保命,先有个印象

3.5 合并与分组(先混个脸熟)

等你后面要处理"多支股票"或"行情+财务"时,会用到这两个:

merged = pd.merge(price, fundamentals, on="code", how="left")  # 横向拼两表(类似 VLOOKUP)
grouped = price.groupby("code")["ret"].mean()                  # 按某列分组聚合

这一篇先用不上,但先记住它们存在,后面遇到"要把几张表合起来"时直接翻回来用。


四、matplotlib:让数据"看得见"

代码跑出一堆数字,人脑是读不动的。画图,是量化的"第二语言"。你只要会三招:

  1. 折线图 plt.plot(x, y):画价格、画均线;
  2. 双子图 plt.subplots(2, 1, sharex=True):上面价格、下面收益,共享 X 轴;
  3. 打标记 plt.scatter(..., marker="^"):用红色箭头标"买入"、绿色标"卖出",信号一眼看清。

具体怎么写,直接看下一节的完整脚本——那里才是它真正落地的地方。


五、实战:把双均线从"玩具"升级成回测雏形

下面这段是本篇的完整可运行脚本——它把前面所有知识点串成一条线:造数据 → 存 CSV → 读回 → 算均线 → 出信号 → 算收益 → 算绩效 → 画图。存成 src/s1_003_dual_ma.py,在 (ashare) 环境里 python src/s1_003_dual_ma.py 就能跑。

⚠️ 关于"真实"二字,先把话说清楚:脚本里的行情是 np.random 模拟生成的,本篇目标是把 pandas 工作流和回测框架跑顺,不是给你真实收益预测。真数据下一篇(S1-004)用 AkShare 抓取。到时你只需把"造数据"那 6 行,换成下面这一行(示意):

import akshare as ak
df = ak.stock_zh_a_hist(symbol="600519", period="daily", adjust="qfq")  # 真实日线

其余回测代码一行都不用改——这就是先把框架练熟的价值。

# src/s1_003_dual_ma.py
# 教学示例:用 pandas 跑通双均线策略并算出绩效(数据为模拟,非真实行情,不构成投资建议)
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib  
matplotlib.rcParams["font.sans-serif"] = ["SimHei"]   # Windows 自带黑体  
matplotlib.rcParams["axes.unicode_minus"] = False      # 防止负号变成方块
# ---------- 1) 造一份教学用日线数据,存成 CSV ----------
np.random.seed(7)
n = 250
dates = pd.date_range("2023-01-02", periods=n, freq="B")
close = 100 + np.cumsum(np.random.randn(n)) * 1.2
close = np.maximum(close, 20.0)
df = pd.DataFrame({
    "date":   dates,
    "open":   np.round(close + np.random.randn(n) * 0.3, 2),
    "high":   np.round(close + np.abs(np.random.randn(n)) * 0.8, 2),
    "low":    np.round(close - np.abs(np.random.randn(n)) * 0.8, 2),
    "close":  np.round(close, 2),
    "volume": np.random.randint(1_000_000, 5_000_000, n).astype(float),
})
df.to_csv("data/sample_prices.csv", index=False)

# ---------- 2) 用 pandas 读回来 ----------
price = pd.read_csv("data/sample_prices.csv", parse_dates=["date"])
print(price.head(3))
print("行数:", len(price), " 列:", list(price.columns))

# ---------- 3) 双均线策略(全程向量化,无 for 循环) ----------
price = price.sort_values("date").reset_index(drop=True)
price["ma10"] = price["close"].rolling(10).mean()     # 短期均线
price["ma30"] = price["close"].rolling(30).mean()     # 长期均线
price["signal"]   = np.where(price["ma10"] > price["ma30"], 1, 0)  # 金叉持仓/死叉空仓
price["position"] = price["signal"].shift(1).fillna(0)             # 用"昨天"信号决定"今天"持仓

# ---------- 4) 收益与绩效(这才是"回测"该算的东西) ----------
price["ret"]       = price["close"].pct_change().fillna(0)
price["buyhold"]   = (1 + price["ret"]).cumprod() - 1            # 买入持有累计收益
price["strat_ret"] = price["ret"] * price["position"]            # 策略每日收益
price["strat"]     = (1 + price["strat_ret"]).cumprod() - 1      # 策略累计收益

# —— 绩效评价(只用 pandas/numpy,零基础也能看懂)——
years   = n / 252
annual  = (1 + price["strat"].iloc[-1]) ** (1 / years) - 1       # 年化收益
cum     = (1 + price["strat_ret"]).cumprod()
max_dd  = ((cum - cum.cummax()) / cum.cummax()).min()            # 最大回撤(负数)
trades  = price["signal"].diff().abs().sum() / 2                 # 交易次数(一金一死=一次往返)

print(f"买入持有累计收益 : {price['buyhold'].iloc[-1]*100:6.1f}%")
print(f"双均线策略累计收益: {price['strat'].iloc[-1]*100:6.1f}%")
print(f"策略年化收益      : {annual*100:6.1f}%")
print(f"策略最大回撤      : {max_dd*100:6.1f}%")
print(f"策略交易次数      : {int(trades)} 次")

# ---------- 5) 画图 ----------
cross_up = price["signal"].diff() == 1     # 金叉当天
cross_dn = price["signal"].diff() == -1    # 死叉当天

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(11, 7), sharex=True)
ax1.plot(price["date"], price["close"], label="收盘价", color="#1f77b4", lw=1)
ax1.plot(price["date"], price["ma10"], label="MA10", color="#ff7f0e", lw=1)
ax1.plot(price["date"], price["ma30"], label="MA30", color="#2ca02c", lw=1)
ax1.scatter(price["date"][cross_up], price["close"][cross_up],
            marker="^", color="#d62728", s=90, zorder=5, label="买入(金叉)")
ax1.scatter(price["date"][cross_dn], price["close"][cross_dn],
            marker="v", color="#2ca02c", s=90, zorder=5, label="卖出(死叉)")
ax1.set_title("双均线策略信号(教学示例,非真实数据)")
ax1.legend()

ax2.plot(price["date"], price["buyhold"], label="买入持有", color="gray", lw=1)
ax2.plot(price["date"], price["strat"], label="双均线策略", color="#e4572e", lw=1.5)
ax2.set_title("累计收益对比")
ax2.legend()

plt.tight_layout()
plt.savefig("data/dual_ma_result.png", dpi=120)
print("图表已保存为 data/dual_ma_result.png ✅")
# plt.show()   # 想看弹窗就把这行取消注释

跑完你会在终端看到几行绩效数字(你的数字会因随机种子/库版本略有不同,这是正常的),项目里多一张 data/dual_ma_result.png

  • 上图:价格 + 两条均线 + 红色"买入"箭头(金叉)、绿色"卖出"箭头(死叉);
  • 下图:灰色"买入持有"曲线 vs 橙红"双均线策略"曲线,谁强谁弱一目了然;
  • 终端:累计收益、年化、最大回撤、交易次数——这就是一个回测该报的四项基本功
    在这里插入图片描述

🎯 关键认知:这段脚本是一个完整的、可复用的分析流程:数据从文件进、指标在表里算、信号和绩效都落到了 DataFrame 的每一行。你以后换任何策略、换任何数据,这套骨架都不用变,只改中间那几行。


六、新手最常卡的 6 个坑

现象 原因 解决办法
ModuleNotFoundError: No module named 'pandas' 忘了激活 ashare 环境 conda activate ashare,看到前缀再跑
read_csv 读出来中文乱码 文件编码不是 UTF-8 encoding="utf-8-sig"encoding="gbk"
改了 DataFrame 报 SettingWithCopyWarning 用了"链式索引" df[mask]["col"]= 改用 df.loc[mask, "col"] = ... 或先 .copy()
几千行用 for 循环算,慢得像蜗牛 没用向量化 rolling / shift / 列运算 整列一起算
策略收益"好得不真实" 前视偏差:用当天信号算当天收益 信号必须 shift(1),用"昨天信号"决定"今天持仓"
图片保存是空白 / 不弹窗 后端或调用顺序问题 plt.savefig()plt.show();无界面环境用 Agg 后端

⚠️ 第六个坑里的前视偏差是量化新手最容易犯的致命错误,没有之一。铁律:回测时,今天能不能持仓,只能由"昨天及之前"的信息决定。 shift(1) 就是这道防火墙。

⚠️ 另一个坑(本篇专属):脚本里的收益、回撤是用模拟数据算的,只能用来验证"流程跑通了",绝不能当成某只股票的真实表现或未来预测。真数据 + 真结论,留给 S1-004。


七、本篇小结 & 下篇预告

小结:这一篇我们补齐了量化最实用的 Python 骨架(只学为 pandas 铺路的那点),拿下了 pandas 三板斧——读 CSV、做列运算与滑动窗口、按条件过滤,再用 matplotlib 把结果画成图。最后落地的那段双均线脚本,已经是一个正规回测的迷你版:数据进表 → 算指标 → 出信号 → 算收益 → 算绩效(年化/回撤/次数)→ 画图验证。

你现在手里有了三样东西:能跑的环境(第 2 篇)+ 看得懂的 Python/pandas(本篇)+ 一个能出绩效图的策略雏形(src/s1_003_dual_ma.py)。离"真金白银的分析",只差最后一块拼图——真实数据

下一篇预告:《从零搭建你的 A 股量化系统(四):拿到真金白银的数据——用 AkShare 抓取 A 股真实行情》。下篇我会带你用国内免费库 AkShare,把本篇脚本里"模拟数据"换成真实的 A 股日 K 线,让你的双均线策略第一次跑在真数据上,并算出它在真实历史上的表现。准备好你的编辑器,我们下篇见。


免责声明:本文所有内容仅用于量化投资技术教学与知识分享。文中的行情数据为模拟生成,示例代码仅用于演示 pandas 工作流与回测框架,不构成任何投资建议或个股推荐,也不代表任何真实收益。投资有风险,入市需谨慎;实际交易前请务必用自己的真实数据充分回测,并充分了解相关风险。

更多推荐