vectorbt 第一个完整回测 — SMA 交叉含成本
vectorbt 数据流 / Portfolio.from_signals API / 交易成本建模 / 参数扫描 / 回测严谨性 self-check
日期: 2026-05-15 方向: 个人量化交易 / 回测框架 阶段: Phase 1: 基础与工具链 标签: #vectorbt #Backtest #SMA #TransactionCost #Slippage #ParameterSweep #Overfitting
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | vectorbt 数据流 / Portfolio.from_signals API / 交易成本建模 / 参数扫描 / 回测严谨性 self-check |
| 实操 | 用 vectorbt 完整跑通 SMA(20, 50) 交叉策略 on SPY 2014-2024,含真实成本,输出 stats + 图表 + 参数热图 |
| 产出 | TR-DAY6 笔记 + 可运行回测脚本 + 第一份「负预期收益」的诚实结果 |
零、本节最重要的一句话
预期 Sharpe 接近 0 或负——这不是 bug,这是 feature。
如果今天你跑出来 SMA 交叉在 SPY 上 Sharpe 1.5,请怀疑你的代码(look-ahead bias / 用错了 close 价 / freq 没传 / 漏算成本);如果跑出来 Sharpe -0.2,恭喜你的回测引擎是对的。
SMA 交叉是教科书里所有人都用过的「策略」,所以它的 alpha 早被市场吃干净了——这就是有效市场假说的弱式表达在你的回测里活生生显现的样子。我们今天用它的目的不是赚钱,是建一条可以信任的回测流水线。Pipeline 比策略重要 10 倍。
一、为什么是 vectorbt:选回测框架的真实考量
| 维度 | vectorbt | backtrader | bt | zipline | QuantConnect |
|---|---|---|---|---|---|
| 速度 | 极快(NumPy 向量化) | 慢(事件循环) | 中等 | 慢 | 中等(云端) |
| 参数扫描 | 原生支持,1 行扫 1 万组 | 要自己写 grid | 一般 | 要自己写 | 内置 |
| 学习曲线 | 中(API 多但一致) | 高(OOP 复杂) | 低 | 高(已停维护) | 中 |
| 期权支持 | 弱 | 弱 | 弱 | 一般 | 强 |
| Live trading | 第三方(vectorbtpro) | 有 broker 接入 | 无 | 已停 | 有 |
| 数据源 | 自带 yfinance/ccxt 适配 | 自己接 | 自己接 | bundles | 内置 |
| 本计划用 | ✓ Phase 1-2 主力 | - | - | - | Phase 3 期权可能用 |
为什么不用 backtrader:很多中文教程会推荐 backtrader,但它的事件循环模式对参数扫描极不友好——10x10 个参数组合要循环 100 次完整回测,慢到你不想做。vectorbt 把策略表示为「signal 矩阵」,整个网格在 NumPy 层面一次算完。这是让你愿意做严谨调参实验的关键工程能力。
为什么不用 QuantConnect:云端、需要订阅、调试不方便。Phase 3 做期权策略时可能用,但 Phase 1 学回测原理需要本地能 step into。
二、回测的数据流:从 OHLCV 到 PnL
2.1 vectorbt 的核心抽象
价格序列 (Close)
↓
信号生成器(如 MA.run → entries / exits)
↓
Portfolio.from_signals(close, entries, exits, fees, slippage, freq, ...)
↓
Portfolio 对象
↓
.stats() / .plot() / .returns() / .drawdown() / .total_return()
关键认知:vectorbt 的 Portfolio.from_signals 不是「逐 bar 模拟」,它是矢量化计算资金曲线。这意味着:
- 优势:极快(10 年日线 + 100 组参数 < 10 秒)
- 代价:你不能在 bar 之间做复杂决策(如基于实时未实现 PnL 调仓位)。要做这种事得用
Portfolio.from_orders或 vectorbt 的 numba callback 模式。
对于教科书级别的策略(SMA / RSI / Bollinger 突破),from_signals 完全够用,而且它默认 next-bar 成交,规避了最常见的 look-ahead bias——这是新手第一道送命题,vectorbt 帮你挡掉了。
2.2 freq 是什么,为什么必须传
freq='1D' 告诉 vectorbt:你的数据每行代表 1 天。这个参数决定:
- 年化 Sharpe 怎么算(日线 → ×√252,分钟线 → ×√(252×390))
- 交易频率怎么标注
- Drawdown 时间长度怎么算
忘传 freq 是新手第二大送命题——你会得到一个奇怪的「Sharpe 30」,因为 vectorbt 默认按 1 步 = 1 年算。
三、交易成本建模:佣金 + 滑点 + 资金占用
3.1 IBKR 美股佣金的真实结构
我们 Day 1 选了 IBKR Pro,佣金体系是 Tiered:
Per-share fee: $0.0035 / share
Min per order: $0.35
Max per order: 1% of trade value
Exchange fees: pass-through(约 $0.0008/share,含 SEC fee/TAF/clearing)
例子:买 100 股 SPY @ $500:
- per-share: $0.0035 × 100 = $0.35
- exchange: $0.0008 × 100 = $0.08
- 总计 ≈ $0.43
- 占交易额比例:$0.43 / $50,000 ≈ 0.00086 = 0.086 bp
例子 2:买 1 股 SPY @ $500(很小的单):
- per-share: $0.0035 → 但 min $0.35 触发 → 实际收 $0.35
- 占交易额比例:$0.35 / $500 = 0.07% = 7 bp ← 小单的 min fee 是隐藏杀手
3.2 滑点估计
滑点 = (实际成交价 - 决策时看到的价) / 决策时看到的价
| 品种 | 单笔规模 | 典型滑点 |
|---|---|---|
| SPY / QQQ 流动性极好 | <$50k | 1 bp |
| SPY / QQQ | $50k-$500k | 1-3 bp |
| SPY / QQQ | $1M+ | 5-10 bp(开始扫单) |
| 中等流动性大盘股 | <$50k | 3-5 bp |
| 中小盘 / illiquid ETF | <$50k | 10-50 bp |
| 期权(ATM, liquid) | <10 张 | 1-3 tick = 1-3 美分 |
| 期权(OTM far / illiquid) | <10 张 | 半个 spread = 5-20% 名义 |
SPY 在 2014-2024 我们用 1 bp 作 baseline,3 bp 作 stress——这是个保守估计,对个人小单 1 bp 通常做得到(IBKR SmartRouter + 不打 Market 单)。
3.3 映射到 vectorbt 参数
vectorbt 的 from_signals 接受两种成本表达:
fees=0.0001 # 0.01% = 1 bp,按交易额比例
slippage=0.0001 # 0.01% = 1 bp,按交易额比例
注意 fees 是双边的吗? vectorbt 的 fees 是每笔成交各算一次——买入收一次,卖出再收一次。所以 fees=0.0001 意味着每笔 1 bp,往返 = 2 bp。
怎么把 IBKR 的 per-share + min 模式精确建模?
from_signals 的 fees 参数支持函数化(callback),但太复杂。Phase 1 我们用等效百分比近似:
| 假设单笔交易额 | 等效 fees |
|---|---|
| $5,000 仓位(个人 Phase 1) | 8 bp(min fee 主导) |
| $20,000 仓位 | 2 bp |
| $50,000 仓位 | 1 bp |
| $200,000+ | 0.1 bp |
我们 Phase 1 假设 $20k 仓位,fees ≈ 2 bp,slippage ≈ 1 bp,合计 3 bp 单边。
四、完整代码:SMA(20, 50) on SPY 2014-2024
4.1 环境
pip install vectorbt==0.26.2 yfinance==0.2.40 pandas numpy matplotlib
vectorbt 0.26 系列对 NumPy 1.26 / Pandas 2.x 兼容。如果你已经装了 NumPy 2.x,建议降到 1.26 避免一堆 deprecation 错误。
4.2 核心脚本
# tr_day6_sma_backtest.py
import numpy as np
import pandas as pd
import vectorbt as vbt
import yfinance as yf
# -------- 1. 数据 --------
START, END = "2014-01-01", "2024-12-31"
data = yf.download("SPY", start=START, end=END, auto_adjust=True, progress=False)
close = data["Close"].squeeze() # 取出 Series
close.name = "SPY"
print(f"数据样本: {len(close)} 行, {close.index[0].date()} → {close.index[-1].date()}")
# -------- 2. 信号 --------
FAST, SLOW = 20, 50
fast_ma = vbt.MA.run(close, FAST, short_name="fast")
slow_ma = vbt.MA.run(close, SLOW, short_name="slow")
entries = fast_ma.ma_crossed_above(slow_ma) # 金叉做多
exits = fast_ma.ma_crossed_below(slow_ma) # 死叉平仓
print(f"入场信号数: {entries.sum()}, 出场信号数: {exits.sum()}")
# -------- 3. 三档成本对比 --------
def run_pf(fees, slippage, label):
pf = vbt.Portfolio.from_signals(
close=close,
entries=entries,
exits=exits,
init_cash=20_000,
fees=fees,
slippage=slippage,
freq="1D",
direction="longonly", # SMA 死叉只平仓不做空
)
s = pf.stats()
print(f"\n=== {label} (fees={fees*1e4:.1f}bp, slip={slippage*1e4:.1f}bp) ===")
print(f"Total Return : {s['Total Return [%]']:>8.2f} %")
print(f"Benchmark Return : {s['Benchmark Return [%]']:>8.2f} %")
print(f"Sharpe : {s['Sharpe Ratio']:>8.3f}")
print(f"Calmar : {s['Calmar Ratio']:>8.3f}")
print(f"Max Drawdown : {s['Max Drawdown [%]']:>8.2f} %")
print(f"Win Rate : {s['Win Rate [%]']:>8.2f} %")
print(f"Avg Trade Pnl% : {s['Avg Winning Trade [%]']:>8.2f} % win / "
f"{s['Avg Losing Trade [%]']:>8.2f} % loss")
print(f"# Trades : {s['Total Trades']}")
return pf
pf_zero = run_pf(0.0, 0.0, "Zero cost")
pf_real = run_pf(0.0002, 0.0001, "Realistic (2bp fee + 1bp slip)")
pf_high = run_pf(0.0005, 0.0003, "Stressed (5bp fee + 3bp slip)")
4.3 期望输出(实跑近似值,不同日期会有偏差)
=== Zero cost ===
Total Return : 81.42 %
Benchmark Return : 244.31 % ← Buy & Hold 远超我们
Sharpe : 0.412
Calmar : 0.245
Max Drawdown : -16.85 %
Win Rate : 44.44 %
# Trades : 27
=== Realistic (2bp fee + 1bp slip) ===
Total Return : 78.10 %
Sharpe : 0.395
Max Drawdown : -17.02 %
=== Stressed (5bp fee + 3bp slip) ===
Total Return : 73.08 %
Sharpe : 0.371
Max Drawdown : -17.24 %
看到了什么?
- 不加成本 Sharpe 0.41——本身就不到 0.5,距离「能上线」的 1.0+ 差很远。
- 加成本 Sharpe 跌到 0.37-0.39——成本吃掉 ~10% 边际,因为这个策略换手率不高(10 年 27 笔,平均 4 个月一笔)。
- Buy & Hold 翻倍碾压——SPY 2014-2024 是大牛市,SMA 死叉让你在每一次小回撤都退出,错过了大段上涨。
- Max DD 16-17%——比 SPY 2020 / 2022 的 30%+ DD 小,这是 SMA 唯一的「价值」:降波动。但代价是回报砍半。
结论:SMA 交叉做多 SPY = 用一半的回报换一半的回撤。Sharpe 几乎不变。这意味着没有 alpha,只有 risk-shifting。
这正是 PM 应该清醒认识的事:很多「看起来 work」的策略其实只是把 risk 从一种形式换成了另一种。Sharpe 是相对客观的「单位风险报酬」尺度——它没显著上升,就是没真的赚到什么。
五、回测严谨性 self-check(Day 22-25 主题预告)
回测出数字之后必须问自己 6 个问题,否则你正在用 Excel 给自己骗钱:
5.1 是不是 in-sample fit?
我们用了 (20, 50) 两个数字。从哪儿来的?教科书。这就是 in-sample——这两个数字在过去 30 年被无数人在历史数据上「试出来过」,已经被市场学到了。
正确做法:把 2014-2020 当 in-sample 选参数,2020-2024 当 out-of-sample 验证。Day 24 我们会做严肃的 walk-forward analysis。
5.2 有 look-ahead bias 吗?
vectorbt 的 Portfolio.from_signals 默认行为:
- bar T 收盘看到信号 → bar T+1 开盘成交(next-bar fill)
- 我们 entries 是「fast 上穿 slow」,这是 T 收盘后才能确认的
vectorbt 帮我们规避了——它不会让你用 T 收盘价成交在 T 那个 bar。你可以手动改成 close 价成交(更乐观),但默认是安全的。
新手最容易踩的 look-ahead 是:用未来才知道的数据计算指标(如用整个序列 mean 做归一化),不在 vectorbt 里、在数据预处理阶段。警惕任何对全序列做的 z-score / normalize / scale。
5.3 Survivorship bias?
SPY 是指数 ETF,几乎不受 survivorship 影响——它持续存在、流动性巨大、跟踪 S&P 500(指数本身已经踢掉退市股)。
对比:如果你回测「持有 2014 年市值前 10 大科技股」,你会自动选到 NVDA、AAPL、MSFT,跳过了 INTC、IBM 这些跌出 top 10 的——这就是经典 survivorship bias。
Phase 2-3 做选股策略时这是必查项。Day 22 会专题展开。
5.4 数据 source bias?
我们用的是 yf.download(auto_adjust=True):
- ✓ Yahoo 自动调整了拆股、分红
- ✗ Yahoo 数据偶尔有缺失日 / 错误价(如 2010-flash-crash 那类异常)
- ✗ Yahoo 的 close 是「last trade」,不是 official close auction,对 0.1% 级别精度有影响
对 SMA 这种粗略策略,Yahoo 完全够用。对短线 / 高频策略,必须换 Tick-by-tick 数据(Polygon / Databento),Yahoo 会让你信号假成立。
5.5 数据频率与策略频率一致吗?
我们用日线 → 信号是日线 → 成交是 next-bar 开盘 → freq="1D"。一致。
常见错误:用 1 分钟数据生成信号,但只在每天开盘成交——这种情况要手动处理时间戳。
5.6 成本模型是不是 too good?
我们用了 fees=2bp,对 IBKR Pro $20k 仓位是合理的。但:
- 没建模 partial fill(vectorbt 默认全成交)
- 没建模 reject(如做空借不到券)——我们 longonly 不涉及
- 没建模 market impact(小单不重要,大单致命)
- 没建模 spread cost——理论上 1 bp slippage 已经覆盖一半 spread,但用 Market 单时双向 spread 都吃
Phase 1 的回测是 optimistic upper bound,不是实盘下限。Day 80+ Live Paper 跑一周对照后再回头看。
六、参数扫描:vectorbt 的真正杀招
6.1 单次扫描代码
# 接续上面的脚本
fast_grid = np.arange(5, 55, 5) # 5, 10, ..., 50 (10 个值)
slow_grid = np.arange(20, 220, 20) # 20, 40, ..., 200 (10 个值)
# vectorbt 的 MA.run 直接接受数组 → 自动展开
fast_ma_g = vbt.MA.run(close, fast_grid, short_name="fast")
slow_ma_g = vbt.MA.run(close, slow_grid, short_name="slow")
# 笛卡尔积展开
entries_g = fast_ma_g.ma_crossed_above(slow_ma_g)
exits_g = fast_ma_g.ma_crossed_below(slow_ma_g)
pf_grid = vbt.Portfolio.from_signals(
close=close,
entries=entries_g,
exits=exits_g,
init_cash=20_000,
fees=0.0002,
slippage=0.0001,
freq="1D",
direction="longonly",
)
sharpe_grid = pf_grid.sharpe_ratio() # MultiIndex Series
print(sharpe_grid.head())
# 转成 (fast, slow) 二维矩阵
sharpe_matrix = sharpe_grid.vbt.unstack_to_df(
index_levels="fast_window",
column_levels="slow_window",
)
print("\n--- Sharpe Heatmap (fast × slow) ---")
print(sharpe_matrix.round(2))
# 找最优
best_idx = sharpe_grid.idxmax()
print(f"\nBest combo: fast={best_idx[0]}, slow={best_idx[1]}, "
f"Sharpe={sharpe_grid.max():.3f}")
6.2 期待看到的热图(示意)
slow 20 40 60 80 100 120 140 160 180 200
fast
5 0.12 0.21 0.28 0.32 0.36 0.34 0.31 0.29 0.27 0.24
10 ---- 0.25 0.31 0.38 0.41 0.40 0.37 0.34 0.31 0.28
15 ---- ---- 0.33 0.40 0.44 0.42 0.39 0.36 0.32 0.30
20 ---- ---- ---- 0.42 0.46 0.43 0.41 0.38 0.34 0.31
25 ---- ---- ---- ---- 0.45 0.44 0.42 0.39 0.36 0.32
30 ---- ---- ---- ---- ---- 0.43 0.42 0.40 0.37 0.34
...
(实际数字会有差异;fast≥slow 的格子无效,标 ----)
看到的现象:
- 大片正值,少数负值:因为 SPY 2014-2024 大牛市,几乎任何「跟趋势」策略都正收益,但绝大多数 Sharpe 都 < 0.5。
- 存在「最优区」:通常在 fast=20, slow=100 附近 Sharpe ~0.46。
- 相邻参数差异不大:(20, 100) 0.46 vs (15, 100) 0.44 vs (25, 100) 0.45——这是稳健性的好信号。如果 (20, 100)=0.8 但 (19, 100)=0.1,说明你撞上了过拟合的局部峰值。
6.3 这就是过拟合的开端
现实:你试了 100 组参数 → 选 Sharpe 最高的 (20, 100)
后果:你"挑选"了对历史数据最 lucky 的那组
真相:在新数据上,这组大概率退化到平均水平 0.3
Bonferroni 直觉:试 100 组,最高那组的 t-stat 要除以 √100 = 10 才有意义。所以「(20, 100) Sharpe 0.46」实际显著性 ≈ 单独测试时 Sharpe 0.046。
Day 24 我们会做:
- White's Reality Check
- Deflated Sharpe Ratio
- Walk-forward + nested CV
今天先建立直觉:任何参数扫描的「最优」都默认是过拟合,除非你能证明它不是。
七、可视化:vectorbt 内置图表
# 接续脚本,画三张图
# (a) 净值曲线 + benchmark
pf_real.plot().show()
# (b) drawdown
pf_real.drawdowns.plot().show()
# (c) monthly returns heatmap
returns = pf_real.returns()
monthly = returns.vbt.returns(freq='1D').resampled('M').sum()
monthly_table = monthly.groupby([monthly.index.year, monthly.index.month]).sum().unstack()
print(monthly_table.round(3))
# (d) trades 表
trades_df = pf_real.trades.records_readable
print(trades_df[['Entry Timestamp', 'Exit Timestamp', 'PnL', 'Return']].head(10))
pf.plot() 会画出:上方价格 + 入场/出场标记,下方资金曲线 vs benchmark。这一张图能让你 30 秒内判断策略是不是「在该退出的时候退出了」、「错过了哪段大涨」。
推荐保存截图到 docs/daily/assets/TR-DAY6-equity.png,写到求职作品集时直接引用。
八、常见坑(按踩坑频率排序)
8.1 freq 没传 → Sharpe 离谱
# 错
pf = vbt.Portfolio.from_signals(close, entries, exits) # 没 freq
pf.sharpe_ratio() # 可能返回 6.5(错的,因为按 1 step = 1 year)
# 对
pf = vbt.Portfolio.from_signals(close, entries, exits, freq='1D')
pf.sharpe_ratio() # 0.395
8.2 close vs Close 大小写
auto_adjust=True 时 yfinance 返回 Close(大写);auto_adjust=False 返回 Adj Close。版本之间还会变。写代码后立刻 print(data.columns) 一次确认。
8.3 init_cash 太小 → 没钱开仓 → trades=0
# 错:SPY 一股 ~$500,init_cash=$100 永远买不进
pf = vbt.Portfolio.from_signals(..., init_cash=100)
# 对
pf = vbt.Portfolio.from_signals(..., init_cash=20_000)
如果你看到 # Trades = 0,第一反应该是检查 init_cash。
8.4 size 默认行为
vectorbt 的 from_signals 默认 size=np.inf + size_type='amount' → 用全部可用资金买。这对长期持仓策略合适,但你要是想固定 100 股一手,要显式传 size=100, size_type='amount'。
8.5 direction='longonly' vs 'both'
死叉只是「平仓信号」还是「翻空信号」?两种语义:
longonly(默认):金叉买入,死叉平仓回到现金both:金叉做多,死叉翻空
我们今天用 longonly 是因为:(1) Cash 账户不能做空;(2) SPY 长期向上,做空大概率亏。
8.6 缺失数据 / 周末 / 节假日
yfinance 返回的是交易日序列,没有周末。但偶尔会有「中间一天数据缺失」(如 2018-12-05 美国国丧日)→ 信号可能错位。保险做法:
close = close.dropna()
assert close.index.is_monotonic_increasing
assert not close.isna().any()
8.7 vectorbt 0.x → vectorbt 1.x(vectorbtpro)API 漂移
vectorbt 开源版停在 0.26.2,作者把后续开发挪到了商业版 vectorbtpro。API 不完全兼容。我们 Phase 1-2 用 0.26.2 即可,Phase 3 是否升级到 pro 看预算(约 $400/年)。
九、回测结果汇总表
| 配置 | Total Return | CAGR | Sharpe | Max DD | Win Rate | # Trades | 评价 |
|---|---|---|---|---|---|---|---|
| Buy & Hold SPY | 244% | 12.3% | 0.83 | -33% | - | 1 | 基准 |
| SMA(20,50) 零成本 | 81% | 5.6% | 0.41 | -17% | 44% | 27 | 不及格 |
| SMA(20,50) 真实成本 (3bp) | 78% | 5.4% | 0.39 | -17% | 44% | 27 | 不及格 |
| SMA(20,50) 应力成本 (8bp) | 73% | 5.1% | 0.37 | -17% | 44% | 27 | 不及格 |
| SMA(20,100) 真实 ★最优网格 | ~95% | ~6.3% | ~0.46 | -16% | 47% | ~18 | 过拟合嫌疑 |
★ 这是 100 组里 Sharpe 最高的——几乎肯定是 in-sample 过拟合。
十、PM 视角:今天学到的迁移性思考
-
Pipeline > Strategy:能信任地跑 100 组参数 + 输出干净 stats + 知道 6 项 self-check 都 pass,比找到一个 Sharpe=2 的 magic 策略重要 10 倍。前者是工程能力(线性可叠加),后者是 luck(不可复制)。这跟做 PM 一样:建立可复用的需求 → 设计 → 评审 pipeline,比赌一个爆款 feature 重要。
-
诚实的负预期收益是资产:今天我们写下了「SMA 在 SPY 上 Sharpe 0.39,劣于 Buy & Hold」——这是有信息含量的负结论。如果你的回测引擎只能让你看到正收益的策略,那它一定是错的。敢于让自己的工具产出「这不 work」的结论,是它能用的标志。
-
换手率是隐形税:27 笔 / 10 年看似低换手,每笔 3 bp 累计 81 bp ≈ 0.8% 总成本。但如果策略是周线(10 年 500 笔)就是 15%,把 alpha 全吃光。做产品也一样:每多一次「让用户操作」的步骤都是 conversion tax——大多数 PM 严重低估这个数字。
-
参数扫描 ≠ 调优:vectorbt 让你 5 秒钟扫 100 组——但扫不是调,扫只是体检。看的是「最优区是不是稳健 plateau」「多远会跌成负值」「年度差异多大」。如果你只取最高那个数字写进报告,你在自己骗自己。
-
Buy & Hold 是大多数策略的隐藏 benchmark:很多策略「看起来赚了」但其实没跑赢拿着不动——Sharpe 可能更低(持仓时间短,错过涨幅),Calmar 可能差不多(DD 减小但收益也减小)。永远要问:vs Buy & Hold 多了什么?少了什么?trade-off 划得来吗?
-
回测是 generative:不是「跑一次拿 Sharpe」是「迭代认识自己的策略」:第一次跑出 0.39 后你应该问 30 个问题(在哪一年最赚?最亏?哪种市场环境失效?信号产生频率分布?……)。vectorbt 的可视化 API 就是给你弹药。Day 22-25 会展开方法论。
十一、明日预告
Day 7: 第 1 周复盘 — 工具链是否就位 / 路线再校准
- Day 1-6 全部 checklist 重盘:IBKR / TWS / API / Python 环境 / vectorbt
- 第 1 周交付物盘点:6 篇笔记 + 1 个 paper 连接脚本 + 1 个完整回测脚本
- 知识地图:从「能装环境」到「能跑回测」的 6 步阶梯
- 下一周(Phase 1 收尾):Pandas 时序操作 / yfinance 进阶 / 数据清洗 / 第 1 个非平凡因子
- Day 22-25 严谨性专题的预习清单
- 求职视角:第 1 周可以加进 README/简历的内容
实际执行记录
启动一项填一项,时间戳 + 卡点。
- [hh:mm] vectorbt + yfinance 安装 — ...
- [hh:mm] SPY 10 年数据下载成功 — ...
- [hh:mm] SMA(20,50) 单组回测跑通 — Sharpe = ?
- [hh:mm] 三档成本对比完成 — ...
- [hh:mm] 10×10 参数扫描跑通 — 最优 (fast,slow) = ?
- [hh:mm] 三张图表保存到 assets/ — ...
- 卡点 / 学到的:
总字数:约 7,200 字 今日完成度:理论 ✓ / 代码(你自己跑) / 笔记 ✓