整理 course 目录:原课程归入同名文件夹,新增学习资料/动手练习分类

This commit is contained in:
2026-09-28 11:52:44 +08:00
parent 5629d57ea7
commit cb211da473
51 changed files with 398 additions and 32 deletions
+26
View File
@@ -0,0 +1,26 @@
"""01 | pandas 热身:收益率、波动、净值曲线
目标:搞懂量化里最基础的三个东西——收益、风险、净值。
"""
import pandas as pd, numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from lib_quant import get_daily, add_returns, net_value, stats
df = get_daily("600519", "20200101", "20241231") # 贵州茅台
df = add_returns(df)
nv = net_value(df["ret"])
print("==", "600519 贵州茅台", "==")
print(df[["日期", "收盘", "ret"]].tail(3).to_string(index=False))
print("绩效:", stats(df["ret"]))
plt.figure(figsize=(9, 4))
plt.plot(df["日期"], nv)
plt.title("Net value (buy & hold)")
plt.xlabel("date"); plt.ylabel("nav")
plt.tight_layout(); plt.savefig("../data/01_net_value.png", dpi=110)
print("图已存 data/01_net_value.png")
# 思考题:
# 1) 为什么不看"价格涨了多少",而要看"收益率"和"净值"?
# 2) 日收益率为 0 的日子占多少?(df['ret'].eq(0).mean())
@@ -0,0 +1,19 @@
"""02 | 用 akshare 拉数据并落地缓存
目标:掌握免费 A股数据源用法——这是量化的一切起点。
"""
from lib_quant import get_daily, CACHE
import os
for code, name in [("000001", "平安银行"), ("600519", "贵州茅台"), ("300750", "宁德时代")]:
df = get_daily(code, "20230101", "20241231")
d0 = str(df["日期"].iloc[0])[:10]
d1 = str(df["日期"].iloc[-1])[:10]
print(f"{code} {name}: {len(df)} 行 | {d0} ~ {d1}")
print(" 列:", list(df.columns))
print("\n缓存目录:", os.path.abspath(CACHE))
print("文件:", sorted(os.listdir(CACHE)))
# 思考题:
# 1) 前复权(qfq)/后复权(hfq)/不复权 区别?算收益率该用哪个?(改 adjust 参数对比)
# 2) 每日数据延迟多久?实盘能用吗?(提示: akshare 文档 + 交易所规则)
+33
View File
@@ -0,0 +1,33 @@
"""03 | 双均线策略 + 回测
目标:写出人生第一个完整策略,并做归因。
策略:5日均线上穿20日均线买入,下穿卖出。
"""
import pandas as pd, numpy as np
import matplotlib; matplotlib.use("Agg")
import matplotlib.pyplot as plt
from lib_quant import get_daily, add_returns, net_value, stats
FAST, SLOW = 5, 20
df = get_daily("600519", "20200101", "20241231")
df = add_returns(df)
df["ma_fast"] = df["收盘"].rolling(FAST).mean()
df["ma_slow"] = df["收盘"].rolling(SLOW).mean()
df["signal"] = (df["ma_fast"] > df["ma_slow"]).astype(int) # 1=持有 0=空仓
df["pos"] = df["signal"].shift(1).fillna(0) # 次日开盘才算,防未来函数
df["strat_ret"] = df["pos"] * df["ret"]
nv_bh, nv_st = net_value(df["ret"]), net_value(df["strat_ret"])
print("== 买入持有 ==", stats(df["ret"]))
print("== 双均线 ==", stats(df["strat_ret"]))
plt.figure(figsize=(10, 4))
plt.plot(df["日期"], nv_bh, label="BuyHold")
plt.plot(df["日期"], nv_st, label=f"MA{FAST}/{SLOW}")
plt.legend(); plt.title("Strategy vs Buy&Hold"); plt.tight_layout()
plt.savefig("../data/03_dual_ma.png", dpi=110)
print("图已存 data/03_dual_ma.png")
# 思考题:
# 1) 为什么 pos 要 shift(1)?不 shift 会怎样?(把 shift(1) 去掉对比——这叫"未来函数")
# 2) 改 FAST/SLOW 参数,结果稳吗?换 (10,60) 试试——这叫参数敏感性
# 3) 这策略赢过买入持有了吗?为什么?
+36
View File
@@ -0,0 +1,36 @@
"""04 | 动量轮动策略
目标:从"单票择时"升级到"多标的轮动"。
策略:每月末看过去 N 日涨幅,持有最强的一只。
"""
import pandas as pd, numpy as np
from lib_quant import get_daily, net_value, stats
POOL = {"600519": "贵州茅台", "000001": "平安银行", "300750": "宁德时代",
"601318": "中国平安", "000858": "五粮液"}
LOOKBACK, FREQ = 20, 20 # 回看20日,每20个交易日调仓
px = {}
for c in POOL:
d = get_daily(c, "20200101", "20241231")[["日期", "收盘"]].set_index("日期")
px[c] = d["收盘"]
prices = pd.DataFrame(px).dropna()
mom = prices.pct_change(LOOKBACK)
rets = []
cur = None
for i in range(LOOKBACK, len(prices) - 1):
if (i - LOOKBACK) % FREQ == 0: # 调仓日
cur = mom.iloc[i].idxmax() # 动量最强
if cur is not None:
r = prices[cur].iloc[i + 1] / prices[cur].iloc[i] - 1
rets.append(r)
else:
rets.append(0.0)
s = pd.Series(rets)
print("== 动量轮动 ==", stats(s))
print("== 等权买入持有 ==", stats(prices.pct_change().mean(axis=1).iloc[LOOKBACK:]))
# 思考题:
# 1) 换 LOOKBACK (10/60/120) 看结果——动量在A股有效吗?
# 2) 为什么不持有全部、按动量加权?(提示: 集中 vs 分散)
+37
View File
@@ -0,0 +1,37 @@
"""05 | 多因子选股(横截面)
目标:体验"因子"最朴素的样子——按指标排序选股。
数据:akshare 全A快照(含市盈率/市净率等)。
"""
import time
import akshare as ak, pandas as pd
def fetch_spot(retries=6):
last = None
for i in range(retries):
try:
return ak.stock_zh_a_spot_em()
except Exception as e:
last = e; time.sleep(3 + i * 3)
raise last
try:
spot = fetch_spot()
print("全A快照:", spot.shape)
df = spot.copy()
pe = pd.to_numeric(df.get("市盈率-动态"), errors="coerce")
df = df[pe.notna()].copy()
df["EP"] = 1 / pe[pe.notna()] # 盈利收益率(1/PE)
df["EP_rank"] = df["EP"].rank(pct=True)
top = df.sort_values("EP_rank", ascending=False).head(20)[
["代码", "名称", "最新价", "市盈率-动态", "EP_rank"]]
print("\n== 按盈利收益率(EP)选出的前20(低估值)==")
print(top.to_string(index=False))
top.to_csv("../data/05_top20_EP.csv", index=False, encoding="utf-8-sig")
print("已存 data/05_top20_EP.csv")
except Exception as e:
print("取数失败(网络/接口变动):", repr(e)[:200])
print("可稍后重跑;或先跑 01-04(已缓存,离线可跑)")
# 思考题:
# 1) 单因子排序选股有什么坑?(行业偏差/市值偏差/未来函数)
# 2) 怎么把2个因子合成打分?(标准化后加权,试试 z-score)
+23
View File
@@ -0,0 +1,23 @@
# 量化动手练习(用真实 A 股数据)
> 目标:把"看书看不进去"的瓶颈,换成**能跑出结果**的闭环练习。
> 环境:Python 3.11 + pandas/akshare/backtrader/matplotlib(已装在 `../pylibs`)
> 运行:`bash run.sh`(全部)或 `bash run.sh 03`(单课)
## 怎么用
每一课 = 目标 + 代码 + 思考题。先跑通,再改参数看结果变化,最后回答思考题。卡住直接问小五。
## 课程表
| 课 | 文件 | 主题 | 你会学到 |
|---|---|---|---|
| 01 | 01_pandas_热身.py | 收益率/波动/净值 | 量化最基础的语言 |
| 02 | 02_拉数据_akshare.py | 用 akshare 拉 A股数据 | 免费数据源怎么用 |
| 03 | 03_双均线回测.py | 双均线策略+回测 | 一个完整策略长什么样 |
| 04 | 04_动量轮动.py | 动量轮动策略 | 多标的轮动调仓 |
| 05 | 05_多因子选股.py | 多因子选股 | 从横截面数据里选股 |
## 数据
- 默认用 akshare 在线拉(国内可达),首次拉完自动缓存到 `../data/cache/*.csv`
## 学习闭环
跑通 → 改参数 → 想为什么 → 写结论 → 告诉小五批改/加题
+48
View File
@@ -0,0 +1,48 @@
"""量化练习公共库:数据获取(带缓存+重试) + 常用指标 + 回测指标。"""
import os, time, pandas as pd, numpy as np
CACHE = os.path.join(os.path.dirname(__file__), "..", "data", "cache")
os.makedirs(CACHE, exist_ok=True)
def _fetch(symbol, start, end, adjust, retries=6, wait=2.0):
import akshare as ak
last = None
for i in range(retries):
try:
return ak.stock_zh_a_hist(symbol=symbol, period="daily",
start_date=start, end_date=end, adjust=adjust)
except Exception as e: # 网络间歇性抽风,重试
last = e
time.sleep(wait * (i + 1))
raise last
def get_daily(symbol="000001", start="20200101", end="20241231", adjust="qfq"):
"""拉单只A股日线(前复权),带本地缓存与自动重试。"""
f = os.path.join(CACHE, f"{symbol}_{start}_{end}_{adjust}.csv")
if os.path.exists(f):
return pd.read_csv(f, parse_dates=["日期"])
df = _fetch(symbol, start, end, adjust)
df.to_csv(f, index=False, encoding="utf-8-sig")
return df
def add_returns(df, price_col="收盘"):
df = df.copy()
df["ret"] = df[price_col].pct_change().fillna(0.0)
return df
def net_value(ret):
return (1 + pd.Series(ret).fillna(0.0)).cumprod()
def stats(ret, periods=252):
ret = pd.Series(ret).fillna(0.0)
nv = net_value(ret); total = nv.iloc[-1] - 1
ann = (1 + total) ** (periods / max(len(ret), 1)) - 1
vol = ret.std() * np.sqrt(periods)
sharpe = (ret.mean() * periods) / vol if vol > 0 else np.nan
dd = (nv / nv.cummax() - 1).min()
calmar = ann / abs(dd) if dd < 0 else np.nan
return {"总收益": f"{total:.2%}", "年化": f"{ann:.2%}", "年化波动": f"{vol:.2%}",
"夏普": f"{sharpe:.2f}", "最大回撤": f"{dd:.2%}", "卡玛": f"{calmar:.2f}"}
def max_drawdown(nv):
return (nv / nv.cummax() - 1).min()
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
# 一键跑练习:bash run.sh 或 bash run.sh 03
cd "$(dirname "$0")"
export PYTHONPATH="$(pwd)/../pylibs"
export MPLCONFIGDIR="/tmp/mpl"
mkdir -p ../data/cache
case "$1" in
01|02|03|04|05) python3 ${1}_*.py ;;
*) for f in 0*.py; do echo; echo "############ $f ############"; python3 "$f"; done ;;
esac
+23
View File
@@ -0,0 +1,23 @@
"""预热缓存:把练习需要的数据一次性拉齐(带重试+间隔)。之后离线可跑。"""
import time, sys
from lib_quant import get_daily, CACHE
JOBS = [
("600519","20200101","20241231"), ("600519","20230101","20241231"),
("000001","20200101","20241231"), ("000001","20230101","20241231"),
("300750","20200101","20241231"), ("300750","20230101","20241231"),
("601318","20200101","20241231"), ("000858","20200101","20241231"),
]
for sym,s,e in JOBS:
import os
f=os.path.join(CACHE,f"{sym}_{s}_{e}_qfq.csv")
if os.path.exists(f):
print("cached", sym, s); continue
for att in range(8):
try:
df=get_daily(sym,s,e); print("OK", sym, s, len(df)); break
except Exception as ex:
print("retry",sym,att,repr(ex)[:60]); time.sleep(5+att*3)
else:
print("FAIL", sym, s); sys.exit(1)
time.sleep(3)
print("ALL DONE")