返回交易笔记
TR Day 6

vectorbt 第一个完整回测 — SMA 交叉含成本

vectorbt 数据流 / Portfolio.from_signals API / 交易成本建模 / 参数扫描 / 回测严谨性 self-check

2026-05-15
Phase 1: 基础与工具链
vectorbtBacktestSMATransactionCostSlippageParameterSweepOverfitting

日期: 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:选回测框架的真实考量

维度vectorbtbacktraderbtziplineQuantConnect
速度极快(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 流动性极好<$50k1 bp
SPY / QQQ$50k-$500k1-3 bp
SPY / QQQ$1M+5-10 bp(开始扫单)
中等流动性大盘股<$50k3-5 bp
中小盘 / illiquid ETF<$50k10-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 %

看到了什么?

  1. 不加成本 Sharpe 0.41——本身就不到 0.5,距离「能上线」的 1.0+ 差很远。
  2. 加成本 Sharpe 跌到 0.37-0.39——成本吃掉 ~10% 边际,因为这个策略换手率不高(10 年 27 笔,平均 4 个月一笔)。
  3. Buy & Hold 翻倍碾压——SPY 2014-2024 是大牛市,SMA 死叉让你在每一次小回撤都退出,错过了大段上涨。
  4. 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 的格子无效,标 ----)

看到的现象

  1. 大片正值,少数负值:因为 SPY 2014-2024 大牛市,几乎任何「跟趋势」策略都正收益,但绝大多数 Sharpe 都 < 0.5。
  2. 存在「最优区」:通常在 fast=20, slow=100 附近 Sharpe ~0.46。
  3. 相邻参数差异不大:(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 ReturnCAGRSharpeMax DDWin Rate# Trades评价
Buy & Hold SPY244%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 视角:今天学到的迁移性思考

  1. Pipeline > Strategy:能信任地跑 100 组参数 + 输出干净 stats + 知道 6 项 self-check 都 pass,比找到一个 Sharpe=2 的 magic 策略重要 10 倍。前者是工程能力(线性可叠加),后者是 luck(不可复制)。这跟做 PM 一样:建立可复用的需求 → 设计 → 评审 pipeline,比赌一个爆款 feature 重要

  2. 诚实的负预期收益是资产:今天我们写下了「SMA 在 SPY 上 Sharpe 0.39,劣于 Buy & Hold」——这是有信息含量的负结论。如果你的回测引擎只能让你看到正收益的策略,那它一定是错的。敢于让自己的工具产出「这不 work」的结论,是它能用的标志

  3. 换手率是隐形税:27 笔 / 10 年看似低换手,每笔 3 bp 累计 81 bp ≈ 0.8% 总成本。但如果策略是周线(10 年 500 笔)就是 15%,把 alpha 全吃光。做产品也一样:每多一次「让用户操作」的步骤都是 conversion tax——大多数 PM 严重低估这个数字。

  4. 参数扫描 ≠ 调优:vectorbt 让你 5 秒钟扫 100 组——但扫不是调,扫只是体检。看的是「最优区是不是稳健 plateau」「多远会跌成负值」「年度差异多大」。如果你只取最高那个数字写进报告,你在自己骗自己。

  5. Buy & Hold 是大多数策略的隐藏 benchmark:很多策略「看起来赚了」但其实没跑赢拿着不动——Sharpe 可能更低(持仓时间短,错过涨幅),Calmar 可能差不多(DD 减小但收益也减小)。永远要问:vs Buy & Hold 多了什么?少了什么?trade-off 划得来吗?

  6. 回测是 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 字 今日完成度:理论 ✓ / 代码(你自己跑) / 笔记 ✓