整理 course 目录:原课程归入同名文件夹,新增学习资料/动手练习分类
This commit is contained in:
@@ -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 文档 + 交易所规则)
|
||||
@@ -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) 这策略赢过买入持有了吗?为什么?
|
||||
@@ -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 分散)
|
||||
@@ -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)
|
||||
@@ -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`
|
||||
|
||||
## 学习闭环
|
||||
跑通 → 改参数 → 想为什么 → 写结论 → 告诉小五批改/加题
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user