M21 AI 赋能的量化研发、除错、优化与复盘¶
层级:L4 实战专题 | 优先级:★★★ | 预计学时:8 小时 对应规范:研究与模拟盘计划(
docs/post_tutorial_research_backtest_futu_plan.md) 第 5.3、8.1、9.1 节 前置知识:M03 的前视;M13 的 2τ c;M15 的先落库;M16 的时间切分。 状态:reviewed 数据截至:2026-10|最近复核:2026-10-29本章的价格、分红、费率和净值都是教学输入。S4 的百分数沿用实验记录里已经写下的数字,本章不再另算一遍。
学习目标¶
- 把 AI 放在契约、审计和复盘三条离线链上,不放进定时信号和发单热路径。
- 用四项硬契约约束生成的代码:时点、价格、精度与守恒、状态机。
- 用手算看懂十类静默错误,并用截断不变性、五日夹具、费用单调性三道测试卡住它们。
- 把一次方案改动记成消耗 1 次尝试预算,开发集和验证集向前推,留出集不参与调参。
- 把影子账和成本调整账的差额拆成四项加残差,残差按月初净值超过 1 bp 就停新风险。
本章知识拓扑图¶
flowchart TB
H["人类研究员:写合同、批尝试、批新单"] --> C["契约式编码"]
C --> B["静默 Bug 狩猎"]
B --> O["归因驱动的受限优化"]
O --> R["三本账复盘"]
R --> H
C --> GATE["RiskGate"]
B --> GATE
O --> GATE
R --> GATE
LLM["模型"] --> C
LLM --> B
LLM --> O
LLM --> R
LLM -.-> HOT["定时信号与发单热路径"]
HOT --> STOP["不进入"]
人决定合同、尝试预算和是否发单。模型起草代码、对照夹具、阅读亏损窗口和桥接表。RiskGate 仍在发单前。模型不另开一条下单通道。
🟢 入门层:副驾、审计员、复盘分析器¶
1. 模型不拍板¶
计划第 5.3 节写明:Agent/LLM 不进入定时信号或交易执行热路径,不允许通过 Skill 另起一条绕过 RiskGate 的下单通道。开发机不连接 OpenD,不写模拟盘账本。
因此模型的三个席位是:
| 席位 | 它交出来的东西 | 人留下的决定 |
|---|---|---|
| 研发副驾 | 带测试的草稿,四项契约写在提示词里 | 合并与否 |
| 除错审计员 | 失败的截断测试、对不上的夹具 | 哪一处算错 |
| 三本账分析器 | 四项差额和残差的表 | 是否停新风险 |
自动穷举参数不属于这三席。每换一个回看、缓冲或缩放,都是一次新的尝试,记入预算 m。
2. 四阶段闭环¶
截断不变性是时光机防偷看:把未来数据砍掉,从当天重算,已经发生的信号必须和当初那一版相同。 变了,就是用了当时还不存在的信息。五日夹具和三本账残差在后面两节里各有一把尺子。 模型可以提议改结构。每改一次消耗 1 次尝试预算,留出集不参与调参。 开发机不连接 OpenD。契约没写死时点、价格、精度和状态机,草稿不能合并。
- 契约式编码。 先写死
as_of、effective_at、价格口径、Decimal和「先record_approval再发单」。模型按契约填实现。 - 静默 Bug 狩猎。 程序能跑完,数字仍可能是错的。用截断不变性、五日手算和费用单调性三条性质测试。
- 归因驱动的受限优化。 只把已经发生的亏损窗口、换手拖累 2τ c 和未成交率交给模型,请它提出结构性改动。改动消耗 1 次 m。验证向前滚动。最终留出集不给模型调参。
- 三本账复盘。 影子策略账、券商模拟事实账、成本调整账。差额拆开。残差超过 1 bp 就排查,不把残差抹进费用项。
图在 truncation_and_bridge.png。左图是第 1–2 阶段的测试,右图是第 4 阶段的拆解。脚本是 plot_ai_quant_workflow.py。
模型可以交草稿、交失败的测试、交一张差额表。合并、发单、以及要不要停新风险,仍由人决定。开发机不连接 OpenD,不写模拟盘账本。 它最会悄悄做错的,是用了未来才有的数据,或者现金和股数对不上你用纸笔算过的五日。 五日夹具把第 5 日锁在 10484.66。三本账对到残差:教学月月初 100000,1 bp 是 10,过了这条线就停新风险。 每换一个回看或缓冲,都是一次新的尝试,记入预算。自动把参数网格跑完,不属于副驾的三个席位。 进阶层把时点、价格、精度和状态机写成契约,再把十类不抛异常的错误摊开。
三个席位交出来的分别是草稿、失败的测试和差额表。合并、发单、停新风险仍由人决定。RiskGate 留在发单前,模型不另开下单通道。
开发机不连接 OpenD,也不写模拟盘账本。契约没写死时点、价格、精度和状态机时,草稿停在合并之前。
五日夹具的第 5 日是 10484.66。程序能跑完,只说明没有抛异常。对不上这一个数,就停,不改期望值去迁就输出。 教学月月初净值 100000,1 bp 是 10。残差 0.20 是 0.020000 bp,还在线内。残差 12.00 是 1.200000 bp,过线之后停新风险,不把残差抹进费用。
第 5 日的 10484.66 来自现金 5644.66 加上 40 股乘 121。少一股或费用进错栏,这个数就会变。 先把审批写入账本,再发单。写库失败就拒绝新单。模型可以起草这段顺序,不能在开发机上连接 OpenD 去试。
🟡 进阶层:四项任务¶
3. 模块 A:四项硬契约¶
向模型布置编码任务时,下面四项写进提示词,也写进测试。缺一项,草稿不能合并。
① 时点。 信号在会话 t 的收盘形成,索引是 as_of。成交在下一交易日,索引是 effective_at。回测引擎已经做了这一次推移。策略函数内部再 shift(1),信号会晚一个节拍。仓库的手算夹具在 tests/unit/test_signal_contract.py:2024-07-01 收盘形成的第一笔多头,执行时刻是 2024-07-02 13:30 UTC,既不是当天,也不是再延到 07-03。
五日教学夹具里,第 2 日收盘的总收益是 0.100000,因此第 3 日执行权重 0.50。若引擎之外再延迟一次,第 3 日执行的权重变成 0.00。
② 价格。 排名和动量用向前累乘的 total_return_index(src/quant/data/corporate_actions.py)。下单用未复权价,股数经过 whole_lots。第 5 日总收益指数是 1.331000,未复权收盘是 121。目标名义金额除以 121 得到 43.345041 股,整手 10 之后成交 40 股。
③ 精度与守恒。 现金和费用用 Decimal,分位用半偶或费用向上取整,与 src/quant/backtest/money.py 一致。每个会话收盘满足
现金 + 股数 × 未复权价 = 净值
第 5 日:5644.66 + 40 × 121 = 10484.66。
④ 状态机。 LedgerStore.record_approval 先把批准写入账本。RiskGate 看到 unknown_orders > 0 时写入 unknown_orders_open,不再开新风险。教学函数只复述这条顺序,它不是生产闸门:
def may_send(recorded: bool, unknown_orders: int) -> bool:
# 批准没落库,或账上还有 UNKNOWN,都不能开新单。
return recorded and unknown_orders == 0
print("no_record", may_send(False, 0))
print("unknown", may_send(True, 1))
print("clear", may_send(True, 0))
打印是 False、False、True。
4. 模块 B:十类静默错误¶
五日黄金夹具是纸笔算到分的测谎仪。第 5 日现金 5644.66 加 40 股乘 121,净值是 10484.66。 模型改完代码之后,这五个交易日的现金、股数和净值必须仍等于这张表。对不上的那一位,以夹具为准。 程序能跑完只说明没有抛异常,不说明这张表还在。 十类静默错误都会留下一条净值,只是净值的含义变了。夹具对不上就停,不要改期望值去迁就程序。
这十类都不抛异常。回测会给出一条净值,只是净值的含义变了。
| # | 错误写法 | 正确写法 | 本页数字 |
|---|---|---|---|
| 1 | 长表直接 shift / rolling |
先 groupby('symbol') |
错收益 −0.890000,对收益 0.100000 |
| 2 | price > sma 在 sma 为 NaN 时当 False |
暖机期保持缺失,不计入波动 | 样本标准差 0.089443 对 0.115470 |
| 3 | 用全样本分位数做去极值或标准化 | 分位数只用截至 as_of 的样本 |
第 6 日偏差 −0.400000 |
| 4 | 丢掉负动量后再 w / w.sum() |
空位留现金,名额不重新归一 | 1.000000 对 0.333333 |
| 5 | 卖单未成交的回款提前给买单 | 可用现金不含未成交卖出 | 80 股对 50 股 |
| 6 | 总收益指数已经含分红,账本再记一笔现金分红 | 除息现金只付给开盘前持有的股份 | 多记 500.00 |
| 7 | 年化用错期数,或多空两边再减 r_f |
超额只减一次,期数与收益同频率 | 夏普 0.866025 被算成 0.577350 或 0.138564 |
| 8 | 评估 IC 的 shift(-1) 流进持仓权重 |
前向收益只出现在评分,不出现在下单权重 | 当日权重变成 2.000000;泄漏相关 1.000000 |
| 9 | 用二进制浮点累加现金 | Decimal 按分记账 |
1.0000000000000007 对 1.00 |
| 10 | 用训练截止日已经见过结局的文本权重给历史打分 | 分数的信息集冻结在 as_of 之前 |
抄未来收益时相关是 1.000000 |
第 2 行的方向要看清:持仓日收益是 0.10, −0.10, 0.10, −0.10,样本标准差 0.115470。前面塞两天暖机期的 0,标准差变成 0.089443。波动被拉低,夏普会被抬高。
第 7 行的手算:月超额均值 0.01,月标准差 0.04。
0.01/0.04 × √12 = 0.866025
若把年无风险利率 0.04 再除以 12 减一次,得到 0.577350。日超额均值 0.0004、日标准差 0.01 时,√252 给出 0.634980;误用 √12 给出 0.138564。多空组合的两条腿若已经是超额收益,不能再各减一次 r_f。
第 8 行和第 10 行用同一条纪律:shift(-1) 可以构造评分用的未来收益,不能构造今天的权重。信号路径 [1, 2, 3] 若整体前移,第一天的持仓是 2.000000,而这个 2 要到下一天才知道。教学分数 [0.2, −0.1, 0.0] 与未来收益 [0.01, −0.02, 0.04] 的相关是 0.327327。把分数直接抄成未来收益,相关变成 1.000000。0.327327 不是技能估计,只说明这条教学向量没有抄答案。
import math
import numpy as np
def sample_std(values: list[float]) -> float:
return float(np.std(np.array(values, dtype=float), ddof=1))
def pearson(left: list[float], right: list[float]) -> float:
grid = np.corrcoef(np.array(left), np.array(right))
return float(grid[0, 1])
wrong_ret = 11.0 / 100.0 - 1.0
right_ret = 11.0 / 10.0 - 1.0
held = [0.10, -0.10, 0.10, -0.10]
raw_w = [-2.0, -1.0, 0.5]
kept = [max(x, 0.0) for x in raw_w]
wrong_last = kept[-1] / sum(kept)
cash_free, pending_sell, px, wanted = 1000, 1000, 20, 80
wrong_shares = wanted if cash_free + pending_sell >= wanted * px else 0
right_shares = min(wanted, cash_free // px)
signal_path = [1.0, 2.0, 3.0]
month_sr = 0.01 / 0.04 * math.sqrt(12.0)
double_rf = (0.01 - 0.04 / 12.0) / 0.04 * math.sqrt(12.0)
daily_sr = 0.0004 / 0.01 * math.sqrt(252.0)
wrong_scale = 0.0004 / 0.01 * math.sqrt(12.0)
float_sum = sum(0.01 for _ in range(100))
print(f"{wrong_ret:.6f}")
print(f"{right_ret:.6f}")
print(f"{sample_std([0.0, 0.0, *held]):.6f}")
print(f"{sample_std(held):.6f}")
print(f"{wrong_last:.6f}")
print(f"{1 / len(raw_w):.6f}")
print(wrong_shares)
print(right_shares)
print(f"{signal_path[1]:.6f}")
print(f"{pearson([0.01, -0.02, 0.03], [0.01, -0.02, 0.03]):.6f}")
print(f"{pearson([0.2, -0.1, 0.0], [0.01, -0.02, 0.04]):.6f}")
print(f"{month_sr:.6f}")
print(f"{double_rf:.6f}")
print(f"{daily_sr:.6f}")
print(f"{wrong_scale:.6f}")
print(format(float_sum, ".16f"))
三板斧 1:截断不变性¶
价格取教学序列 10, 11, …, 19,共 T = 10。第 t 天的偏差定义为:用全部 10 天算出的信号,减去只用前 t 天算出的信号在同一天的值。
分位定义为「小于等于该价格的个数 / 样本长度」。第 6 天价格是 15。
- 前 6 天里,15 是最大的一个,分位 6/6 = 1.000000。
- 全部 10 天里,不超过 15 的有 6 个,分位 6/10 = 0.600000。
- 偏差 0.600000 − 1.000000 = −0.400000。
十天的分位偏差是
−0.900000, −0.800000, −0.700000, −0.600000, −0.500000, −0.400000, −0.300000, −0.200000, −0.100000, 0.000000
全样本均值偏差是同一套价格减去样本均值。前 6 天均值 12.5,第 6 天信号 15 − 12.5 = 2.500000。十天均值 14.5,信号 15 − 14.5 = 0.500000。偏差 −2.000000。最后一天两种喂法是同一样本,均值偏差回到 0.000000。
合规函数用截至当天的样本做分位。截断样本和全样本在这一天上用的是同一段历史,偏差是 0.000000。十天都是 0.000000。
import numpy as np
PRICES = np.arange(10, 20, dtype=float)
def percentile_rank(sample: np.ndarray) -> np.ndarray:
# 分母是这次喂入的长度。喂入未来,今天的秩就会变。
n = len(sample)
return np.array([(sample <= x).sum() / n for x in sample], dtype=float)
def expanding_rank(prices: np.ndarray) -> np.ndarray:
# 分母停在当天。后面的价格不参与今天的秩。
out = np.empty(len(prices), dtype=float)
for i in range(len(prices)):
out[i] = percentile_rank(prices[: i + 1])[-1]
return out
def mean_deviation(prices: np.ndarray) -> np.ndarray:
return prices - float(prices.mean())
def bias_at(signal_fn, day: int) -> float:
full = signal_fn(PRICES)
short = signal_fn(PRICES[:day])
return float(full[day - 1] - short[-1])
print(f"{percentile_rank(PRICES[:6])[-1]:.6f}")
print(f"{percentile_rank(PRICES)[5]:.6f}")
print(f"{bias_at(percentile_rank, 6):.6f}")
print(f"{mean_deviation(PRICES[:6])[-1]:.6f}")
print(f"{mean_deviation(PRICES)[5]:.6f}")
print(f"{bias_at(mean_deviation, 6):.6f}")
print(f"{bias_at(expanding_rank, 6):.6f}")
左图把分位偏差画成红线,把向前滚动的偏差画成贴在 0 上的蓝线。第 6 天标的是 −0.400000。
三板斧 2:五日黄金夹具¶
初始现金 10000.00。一手 10 股。佣金 10 bp,无最低佣金,无平台费,费用向上取整到分。这组费率标明不是券商价目。规则:
- 总收益指数从 1 向前累乘。除息日的因子用 (收盘 + 每股分红) / 前收。
- 第 1 日没有昨日指数,下一日权重是 0。其后,当日总收益大于 0 则下一日目标权重 0.50,否则 0。
- 分红现金 = 开盘前股数 × 每股分红。成交发生在收盘,用未复权价。
- 目标股数 = 权重 × 成交前净值 / 价格,再向下取整到手。
| 日 | 收盘 | 分红 | 指数 | 当日总收益 | 执行权重 | 原始股数 | 成交后股数 | 费用 | 分红入账 | 现金 | 净值 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 100.00 | 0.00 | 1.000000 | 0.000000 | 0.00 | 0.000000 | 0 | 0.00 | 0.00 | 10000.00 | 10000.00 |
| 2 | 110.00 | 0.00 | 1.100000 | 0.100000 | 0.00 | 0.000000 | 0 | 0.00 | 0.00 | 10000.00 | 10000.00 |
| 3 | 100.00 | 10.00 | 1.100000 | 0.000000 | 0.50 | 50.000000 | 50 | 5.00 | 0.00 | 4995.00 | 9995.00 |
| 4 | 110.00 | 0.00 | 1.210000 | 0.100000 | 0.00 | 0.000000 | 0 | 5.50 | 0.00 | 10489.50 | 10489.50 |
| 5 | 121.00 | 0.00 | 1.331000 | 0.100000 | 0.50 | 43.345041 | 40 | 4.84 | 0.00 | 5644.66 | 10484.66 |
第 3 日开盘前股数是 0,所以每股 10 的分红入账是 0.00。指数在这一天是 (100 + 10) / 110 = 1,相对前一日指数保持 1.100000。若把收盘才买到的 50 股再乘上 10,现金会多出 500.00,变成 5495.00。那 10 元已经在指数里,不能再进账本。
第 4 日卖出 50 × 110 = 5500,费用 5.50,现金 4995.00 + 5500.00 − 5.50 = 10489.50。
第 5 日成交前净值仍是 10489.50。一半是 5244.75,除以 121 是 43.345041 股,取整为 40。名义金额 4840.00,费用 4.84,现金 10489.50 − 4840.00 − 4.84 = 5644.66。
from decimal import ROUND_CEILING, ROUND_DOWN, Decimal
CENT = Decimal("0.01")
def whole_lots(shares: Decimal, lot_size: Decimal) -> Decimal:
# 尾数不够一手就留下现金。向上取整会在没现金时借出碎股。
if shares <= 0:
return Decimal(0)
lots = (shares / lot_size).to_integral_value(rounding=ROUND_DOWN)
return lots * lot_size
def teaching_fee(notional: Decimal) -> Decimal:
if notional == 0:
return Decimal(0)
raw = notional * Decimal("10") / Decimal(10000)
return raw.quantize(CENT, rounding=ROUND_CEILING)
closes = [Decimal(x) for x in ("100", "110", "100", "110", "121")]
dividends = [Decimal(x) for x in ("0", "0", "10", "0", "0")]
tri = [Decimal(1)]
day_return = [Decimal(0)]
next_weight = [Decimal(0)]
for i in range(1, 5):
factor = (closes[i] + dividends[i]) / closes[i - 1]
tri.append(tri[-1] * factor)
ret = tri[i] / tri[i - 1] - 1
day_return.append(ret)
next_weight.append(Decimal("0.50") if ret > 0 else Decimal(0))
cash = Decimal("10000.00")
shares = Decimal(0)
for i, price in enumerate(closes):
# 分红看开盘前的股数。当天的买卖还没发生。
div_cash = shares * dividends[i]
cash += div_cash
weight = Decimal(0) if i == 0 else next_weight[i - 1]
nav_pre = cash + shares * price
raw_shares = (weight * nav_pre / price) if price else Decimal(0)
target = whole_lots(raw_shares, Decimal(10))
delta = target - shares
notional = abs(delta) * price
fee = teaching_fee(notional)
if delta > 0:
cash -= notional + fee
elif delta < 0:
cash += notional - fee
shares = target
nav = cash + shares * price
print(
i + 1,
f"{tri[i]:.6f}",
f"{day_return[i]:.6f}",
f"{weight:.2f}",
f"{raw_shares:.6f}",
f"{shares:.0f}",
f"{fee:.2f}",
f"{div_cash:.2f}",
f"{cash:.2f}",
f"{nav:.2f}",
)
print(f"{Decimal('50') * Decimal('10'):.2f}")
三板斧 3:费用单调,资金守恒¶
固定成交序列:以 100 买入 50 股,再以 110 卖出 50 股,盯市价 110。股数不因费率改变。10 bp 时期末净值 10489.50,与夹具第 4 日收盘相同。费率翻倍后净值 10479.00。差额 −10.50,等于多出来的买入佣金 5.00 和卖出佣金 5.50。
FeeSchedule.scaled 拒绝小于 1 的倍数,避免压力测试把成本越压越低。翻倍之后净值不升高,才算这道性质通过。动态策略若会因为费用变高而少买股,这道单调性不再自动成立,所以测试必须钉死股数。
from decimal import Decimal
from quant.backtest.fees import FeeSchedule, FixedFill, apply_fixed_fills
from quant.core.types import Side
fees = FeeSchedule(
commission_bps=Decimal("10"),
min_commission=Decimal("0"),
platform_fee=Decimal("0"),
label="teaching-not-a-broker-schedule",
)
fills = (
FixedFill(Side.BUY, Decimal("50"), Decimal("100")),
FixedFill(Side.SELL, Decimal("50"), Decimal("110")),
)
nav_1x = apply_fixed_fills(Decimal("10000"), fills, fees, Decimal("110"))
nav_2x = apply_fixed_fills(
Decimal("10000"), fills, fees.scaled(Decimal("2")), Decimal("110")
)
print(f"{nav_1x:.2f}")
print(f"{nav_2x:.2f}")
print(f"{nav_2x - nav_1x:.2f}")
五类高频工程与脏数据,以及三道管道断言¶
十类静默错误查的是金融含义。下面五类是代码和行情对不上时最常卡住的地方。修复契约写在发现之前,不把整文件丢给模型猜。
- 时区。 美东
America/New_York、香港Asia/Hong_Kong和UTC不能混在一根没有时区的时间戳里。合成日线之前先转到UTC。冬夏令时只存在于美东,香港没有。 - 代码。
0700.HK和HK.00700、BRK.B和BRK-B在merge里对不上,内连接变成空表或少行,也不抛异常。连接前先归一成仓库的代码。 - 停牌、退市和缺失价。 零成交或空价格进了 NumPy 的均值和标准差之后,整行标准化会变成
NaN。这一行先剔除或拒绝,再做截面。 - 整手和
Decimal。Decimal(0.1)带进二进制尾数,和Decimal("0.1")不相等,守恒式会在分这一位报错。金额用字符串构造,再按分量化。 - 账本锁和限流。 账本只有一个写入者。多个进程同时写会碰到 SQLite 锁。网关调用走
RateLimiter。开发机不打开 VPS 上的ledger.db。
定位用三道断言,不靠把几百行再贴一次。行数要守恒:内连接之后的行数小于左表,就是丢票。键集合的交集率小于 1,就是代码没对齐。标准化之后有限值的个数为 0,就是 NaN 已经传染。教学左表 3 行,右表只有 HK.00700 和 BRK-B,内连接剩下 1 行,交集率 ⅓ = 0.333333。价格 10, NaN, 12 用 NumPy 做 z 分数,有限值个数是 0。Decimal(0.1) == Decimal("0.1") 打印 False。
# goal9-check
"""三道管道断言:行数、键交集、有限值。离线,不读行情。"""
from decimal import Decimal
import numpy as np
import pandas as pd
left = pd.DataFrame(
{"symbol": ["0700.HK", "HK.00700", "BRK.B"], "x": [1.0, 2.0, 3.0]}
)
right = pd.DataFrame({"symbol": ["HK.00700", "BRK-B"], "y": [20.0, 30.0]})
# 代码没归一就内连接。少掉的行不会抛异常。
merged = left.merge(right, on="symbol", how="inner")
overlap = len(set(left["symbol"]) & set(right["symbol"])) / len(left)
prices = np.array([10.0, np.nan, 12.0])
# NumPy 的均值碰到 NaN,整列标准化跟着变成 NaN。
zscore = (prices - np.mean(prices)) / np.std(prices)
finite = int(np.isfinite(zscore).sum())
same_decimal = Decimal(0.1) == Decimal("0.1")
print(len(left))
print(len(merged))
print(f"{overlap:.6f}")
print(finite)
print(same_decimal)
assert len(merged) < len(left)
assert overlap < 1
assert finite == 0
assert same_decimal is False
5. 模块 C:用归因改结构,不用网格改参数¶
模型可以读三类已经算完的诊断:
- 一段亏损窗口的复利。教学三月简单收益 −4%、−3%、+1%,窗口收益 0.96 × 0.97 × 1.01 − 1 = −0.059488。
- 换手拖累。单边换手 τ、单边成本率 c 时,拖累按 2τ c 记。教学值 τ = 0.40、c = 0.0010,拖累 0.000800。
- 未成交率。计划名义 8000、未成交 2000 时,比率 0.250000。
它被允许提出的改动是结构:调仓缓冲、波动缩放、减少同时持有的名数。它不被允许做的事是:在最终留出集上搜索回看月数,直到夏普好看。
一次这样的结构改动消耗尝试预算 1 次。开发窗口上形成的规则,只在紧接着的验证窗口上看一次。验证失败就停止,不把留出集打开再试。
S4 的记录给出一个已经发生的对照,数字来自 g12 实验记录(research/experiments/g12_s4_xsmom_v1/record.md),本章没有重跑:
| 项目 | 记录中的印出值 |
|---|---|
| 尝试计数 | S4 族 ⅙,全项目 3/18 |
| 拼接区年化,基础成本 | S4 14.84%,B0 16.81% |
| 最大回撤 | S4 45.51%,B0 44.89% |
| 年化单边换手 | S4 2.90,B0 0.14 |
| 费用 | S4 11065.45,B0 697.34 |
| 未成交名义 | S4 23563.00,B0 1823.69 |
| 零成本年化 | S4 15.77%,B0 16.82%,记录写明收益差 −1.04 个百分点 |
| 裁决 | REJECTED(robust_gate_failure) |
按记录里的百分数,15.77 − 14.84 = 0.93 个百分点,这是印出值之差,不是新的回测。若只用记录里的佣金假设 5 bp 做 2τ c:2 × 2.90 × 0.0005 = 0.002900。它不等于那 0.93 个百分点,因为记录里的差距还包含让价、最低佣金和复利。模型若把 0.002900 说成「已经解释了全部成本」,这句不成立。
合同写明这族没有单独留出集。即便如此,也不能让模型在同一段拼接区上再搜一个回看。下一版若要试缓冲或降低换手,必须新的实验编号、新的一次 m,并且先写失效条件。
from decimal import Decimal
loss = Decimal("0.96") * Decimal("0.97") * Decimal("1.01") - 1
drag = 2 * Decimal("0.40") * Decimal("0.0010")
s4_commission_drag = 2 * Decimal("2.90") * Decimal("0.0005")
printed_gap = Decimal("15.77") - Decimal("14.84")
unfilled = Decimal("2000") / Decimal("8000")
print(f"{loss:.6f}")
print(f"{drag:.6f}")
print(f"{s4_commission_drag:.6f}")
print(f"{printed_gap:.2f}")
print(f"{unfilled:.6f}")
从 S4 v1 的 REJECTED 到一份还没跑的缓冲合同¶
上面的表就是 v1。拼接区基础成本下年化 14.84%,同池 B0 是 16.81%。年化单边换手 2.90,费用 11065.45 美元,未成交名义 23563.00 美元。7 项门槛在基础成本和零成本下都失败,裁决是 REJECTED。这些数只引用记录,本章不重跑。
模型若建议把回看从 12 个月改成 9 个月,依据是记录里 9 个月年化 19.59%。同一行的回撤是 38.82%,已经大于预先写下的 30%,该行的结果是失败。这是看过裁决之后再挑参数。结构性的病因是月末硬切排名、没有缓冲区,换手高到 2.90,限价未成交和最低佣金一起上来。
下一版可以预注册成缓冲:持仓留在前 10 名里,排名掉出前 15 名才卖出替换。这还不是一次实验结果。教学里假设这条规则把年化单边换手从 2.90 压到 1.20。成本假设单独写明:单边显性 15 bps,平均让价 10 bps,合计每侧 25 bps,也就是 0.0025。它不是记录里那条 5 bp 佣金,所以不能拿去解释 11065.45,也不能拿去解释 15.77 − 14.84 = 0.93 个百分点。
2 × 2.90 × 0.0025 = 0.014500,即 1.4500%。2 × 1.20 × 0.0025 = 0.006000,即 0.6000%。节省 0.008500,即 0.8500 个百分点,85.00 bps。要做这件事,必须新的实验编号,并消耗 S4 族第 2/6 次尝试预算。1.20 倍是这道算术的输入,不是 v1 记录里印出来的换手。
# goal9-check
"""缓冲假设下的换手拖累。25 bps 是教学每侧成本,不是记录里的 5 bp。"""
from decimal import Decimal
cost = Decimal("0.0015") + Decimal("0.0010")
drag_hard = 2 * Decimal("2.90") * cost
drag_buffer = 2 * Decimal("1.20") * cost
saved = drag_hard - drag_buffer
print(f"{drag_hard * 100:.4f}")
print(f"{drag_buffer * 100:.4f}")
print(f"{saved * 100:.4f}")
print(f"{saved * Decimal(10000):.2f}")
assert f"{saved * 100:.4f}" == "0.8500"
6. 模块 D:三本账拆成四项加残差¶
三本账把影子策略、券商模拟事实和补上明确费率之后的净值分开。对得上的差额进四项,对不上的留在残差。 教学月月初净值 100000,1 bp 是 10。残差超过这一档就停新风险,不把对不平的钱抹进费用。 下面月末的 0.20 是 0.020000 bp,还在线内。12.00 才是 1.200000 bp,过线。 影子账只读、不发单。对不平的残差不要抹进佣金。
三本账的定义在计划 8.1,实现在 src/quant/portfolio/shadow.py。影子账用同一套目标和配置回放,成交规则冻结,并且只读,不发单。
教学月的月初净值是 100000。月末:
101200.00 + (−80.00) + (−150.00) + (−40.00) + (−90.00) + 0.20
= 100840.20
| 项 | 记号 | 金额 | 在仓库桥里的对应 |
|---|---|---|---|
| 影子策略账 | 起点 | 101200.00 | 影子净值 |
| 数据与信号 | Δ_signal |
−80.00 | 信号或数据口径不同 |
| 整手与缓冲 | Δ_lot |
−150.00 | sizing_buffer |
| 执行让价与延迟 | Δ_exec |
−40.00 | execution_price |
| 费用与未成交 | Δ_cost |
−90.00 | fees 与 unfilled |
| 未解释残差 | ε | +0.20 | unexplained |
| 成本调整账 | 终点 | 100840.20 | 事实成交加上明确费率 |
总偏差 100840.20 − 101200.00 = −359.80。按月初净值 100000 折合 −35.980000 bp。
1 bp 对应的金额是 100000 × 0.0001 = 10.00。残差 0.20 是 0.020000 bp,低于 10.00,不触发。若残差是 12.00,则是 1.200000 bp,超过 UNEXPLAINED_BPS_LIMIT = 1。计划要求这种差额留到下一交易日仍未解释时,暂停新增风险。残差不能被改记进费用项来「对平」。
右图从影子账 101200.00 走到成本调整账 100840.20。纵轴从 100700 起画,是为了看见这几步差额,不是说净值从 100700 涨上来。
from decimal import Decimal
shadow = Decimal("101200.00")
signal = Decimal("-80.00")
lot = Decimal("-150.00")
execution = Decimal("-40.00")
cost = Decimal("-90.00")
epsilon = Decimal("0.20")
actual = shadow + signal + lot + execution + cost + epsilon
base = Decimal("100000")
print(f"{actual:.2f}")
print(f"{actual - shadow:.2f}")
print(f"{(actual - shadow) / base * Decimal(10000):.6f}")
print(f"{epsilon / base * Decimal(10000):.6f}")
print(f"{Decimal('12.00') / base * Decimal(10000):.6f}")
print(f"{base * Decimal('0.0001'):.2f}")
上面五个结果数字可以收成一个函数。月初净值 100000.00。影子账期末 101200.00。然后依次减去信号差 80.00、整手与缓冲 150.00、让价 40.00、费用与踏空 90.00,再加上残差 0.20,得到 100840.20。残差相对月初净值是 0.020000 bp。
# goal9-check
"""三本账从月初净值走到成本调整账。金额是教学桥,不是对账单。"""
from decimal import Decimal
def decompose_three_books_bridge() -> tuple[Decimal, ...]:
# 影子期末是研究回放。四项差额从那里往下减,不从月初直接跳到终点。
nav0 = Decimal("100000.00")
shadow = Decimal("101200.00")
delta_signal = Decimal("-80.00")
delta_lot = Decimal("-150.00")
delta_exec = Decimal("-40.00")
delta_cost = Decimal("-90.00")
epsilon = Decimal("0.20")
adjusted = shadow + delta_signal + delta_lot + delta_exec + delta_cost + epsilon
residual_bp = epsilon / nav0 * Decimal(10000)
return (
nav0,
shadow,
delta_signal,
delta_lot,
delta_exec,
delta_cost,
epsilon,
adjusted,
residual_bp,
)
steps = decompose_three_books_bridge()
for value in steps[:-1]:
print(f"{value:.2f}")
print(f"{steps[-1]:.6f}")
assert steps[-2] == Decimal("100840.20")
assert f"{steps[-1]:.6f}" == "0.020000"
🔴 深入层:幻觉、会话预算、脱敏¶
7. 模型报出的数字要能重算¶
模型会把相近的概念写成一个数:把 2 × 2.90 × 0.0005 = 0.002900 说成 S4 的 0.93 个百分点,或把教学价格 10 到 19 说成某只股票的收盘。对抗的办法不是再问一次,而是让独立进程重跑夹具。对不上的那一位,以夹具为准。
实验记录里的百分数只允许原样引用。本章把 15.77 − 14.84 算成 0.93,是因为两位小数相减是确定的;记录写明的零成本收益差 −1.04 个百分点不要改成自己减出来的另一位。
8. 一次会话的预算¶
计划 9.1:每次 Agent 会话默认最多 90 分钟或 30 次修改/测试循环,先到为准;同一失败最多 3 轮修复。计时工具若不能强制停,就记下开始时间,在工具之间检查,不宣称平台已经替你超时。到点就保存证据并退出。续期要人批准。不得为了把测试跑绿而扩大目标,例如改去连开发机上的 OpenD。
30 轮指的是修改和测试,不是聊天句数。第 3 轮仍失败的同一个断言,停止修,把失败命令和退出码留给人。
9. 离开本机的内容要先脱敏¶
不把账户原始数据发给第三方模型。报告用账户别名、脱敏持仓和最小必要字段。交易密码不读取、不粘贴。订单号、原始现金余额和完整持仓列表,若与这道夹具无关,就不进提示词。
可以进提示词的是:契约、教学夹具、已经公开写在实验记录里的汇总百分数、以及你自己手算的桥接表。模拟盘账本仍只在 VPS 上有一个写入者。
模型若把 2 × 2.90 × 0.0005 = 0.002900 说成已经解释了 S4 的费用,仍不成立。第 5 节后半的 1.4500% 和 0.6000% 用的是每侧 25 bps 的教学假设,不是这 0.002900,也不是记录里的 11065.45 美元。
10. 先写不偷看未来的循环,再让模型改成向量化¶
向量化容易把窗口右端写成含当天。先留一份双层循环当黄金参照:外层是时点 t,内层只加 [t − window, t),当天不进合计。窗口里有非有限值,这一天的输出就是缺失,不拿其余样本把均值补上。并列的价格只是相同的加数,不改变均值。
对照用同一条含 NaN 和并列值的序列。两份输出的缺失位置必须相同。把缺失填成 0 之后,np.max(np.abs(out_vec - out_loop)) 要小于 1e-12。教学序列上这个差打印为 0.000000000000。模型交来的矩阵版若把 rolling 的右端放在 t 上,这道断言会失败。失败就停,不改黄金参照去迎合向量化。
# goal9-check
"""黄金循环与向量化必须在同一套过去窗口上重合。"""
import numpy as np
def golden_past_mean(values: np.ndarray, window: int) -> np.ndarray:
# 内层只加 t 之前的 window 根。当天的价格不进信号。
out = np.full(len(values), np.nan)
for t in range(window, len(values)):
total = 0.0
ok = True
for i in range(t - window, t):
if not np.isfinite(values[i]):
ok = False
break
total += float(values[i])
if ok:
out[t] = total / window
return out
def vector_past_mean(values: np.ndarray, window: int) -> np.ndarray:
# 缺失先记 0,再用有限值个数判断窗口是否完整。
clean = np.where(np.isfinite(values), values, 0.0)
finite = np.isfinite(values).astype(float)
csum = np.cumsum(clean)
cfin = np.cumsum(finite)
out = np.full(len(values), np.nan)
for t in range(window, len(values)):
start = t - window
total = csum[t - 1] - (csum[start - 1] if start else 0.0)
count = cfin[t - 1] - (cfin[start - 1] if start else 0.0)
if count == window:
out[t] = total / window
return out
series = np.array([1.0, 1.0, np.nan, 2.0, 2.0, 3.0, 3.0, 4.0])
out_loop = golden_past_mean(series, 3)
out_vec = vector_past_mean(series, 3)
same_missing = np.array_equal(np.isfinite(out_loop), np.isfinite(out_vec))
filled_loop = np.where(np.isfinite(out_loop), out_loop, 0.0)
filled_vec = np.where(np.isfinite(out_vec), out_vec, 0.0)
diff = float(np.max(np.abs(filled_vec - filled_loop)))
print(f"{diff:.12f}")
print(same_missing)
assert same_missing and diff < 1e-12
真实案例¶
这三则写的是仓库里已经落地的测试、实验记录和只读影子账。教学夹具的手算留在上文,这里不再重算一遍。
案例 1:截断断言锁的是整张目标表,不是一个偏差数字¶
信号契约测试(tests/unit/test_signal_contract.py) 里,SmaCrossStrategy(5, 20) 先在全样本上生成目标,再只喂入每个标的的前 250 根 K 线。通过条件是截断结果与全样本的前 250 行逐格相等。另一则把第 300 根之后的收盘乘以 3,要求第 300 根之前的目标不变。同一文件还把第一笔多头钉在 2024-07-01 收盘形成、2024-07-02 13:30 UTC 执行:当天成交是前视,拖到 07-03 是多延迟了一次。
横截面动量测试(tests/unit/test_xs_momentum.py) 用两只教学月线(US.A 为 10, 11, 12, 11, 13, 12, 14,US.B 为 10, 9, 12, 13, 12, 14, 13),把历史切到 2024-05-31,再要求切面两侧的目标表相等。模型若交来「先对整段样本做分位数」,这两则断言会在已发生的日期上失败。它们比较的是整张表,不打印本章第 6 日的分位偏差。
这两则都按标的分字典传入,长表不分组的 shift 不在它们的覆盖里。审计提示词仍要把 groupby('symbol') 写成必查项,不能用 SMA 测试通过来代替。
案例 2:S4 的合同先于数字,否决之后不能再搜同一窗口¶
实验记录(research/experiments/g12_s4_xsmom_v1/record.md) 的顺序本身就是约束。合同在任何 S4 数字出现之前提交于 777e51a。2026-10-06 20:01:05 注册时,清单哈希 C5EA1648…177AB 写入尝试账本,计数为 S4 族 ⅙、全项目 3/18,此时策略还没有在真实快照上跑过。devval 把数据截到 2022-12-30,每个运行的最后一个会话都是这一天。20:07 的 final 因 7 项门槛失败而 REJECTED,2026-10-08 归档。
拼接区基础成本下,记录印出的是 S4 年化 14.84%、回撤 45.51%、单边换手 2.90,B0 年化 16.81%、回撤 44.89%、换手 0.14,费用 11065.45 对 697.34。零成本世界记录写明收益差 −1.04 个百分点。邻域回看 6 的回撤 48.34%、回看 9 的回撤 38.82%,都高于预先写下的 30% 上限,两行的结果都是失败。
模型可以读这张表,并建议下一版用缓冲处理换手。那一版要新的实验编号和新的一次 m。把回看改成已经失败的 6 或 9,再交回同一段拼接区,是在看过裁决之后更换假设。
案例 3:影子账把残差留在分这一位,实验室脚本不发单¶
src/quant/portfolio/shadow.py 回放的是已经批准的目标,成交规则冻结在开盘价模型,全程只读。桥的桶是 sizing_buffer、unfilled、execution_price、fees,另加非调仓日的 position_drift 和账本修正 corrections。未解释项是恒等式剩下的残差:账本把每笔名义金额记到分,桥的价格项用未四舍五入的价格,所以残差只吸收大约每笔成交半分(0.005 乘以成交笔数)。超过这个量级就是报告缺陷,不允许并进费用项抹平。UNEXPLAINED_BPS_LIMIT 是 1。
实验室总说明 写明运行时不发单、不写模拟盘账本。动量实验室 在核对快照 SHA-256 之后才把文件拷进沙箱。JPM 实验室 和 腾讯 ADR 实验室 各自写明不进入正式尝试预算,也不连接模拟盘。模型若建议把实验室净值直接送到模拟账户,越过的是这三份说明,不是某一条教学净值。上文的 0.20 与 12.00 只用来练习 1 bp 的门槛,不是从这些实验室或影子账导出的对账单。
方法论来源¶
| 概念 | 出处 |
|---|---|
模型不进入热路径、不绕过 RiskGate |
计划第 5.3 节 |
| 三本账与四类偏差 | 计划第 8.1 节;src/quant/portfolio/shadow.py |
| 会话 90 分钟 / 30 轮 | 计划第 9.1 节 |
先落库、UNKNOWN 停新单 |
record_approval;RiskGate 的 unknown_orders_open |
| 时点只延迟一次 | tests/unit/test_signal_contract.py |
| 截断后历史目标不变 | 同上,以及 tests/unit/test_xs_momentum.py |
| 总收益指数向前累乘 | total_return_index |
| 单边换手与 2τ c | metrics.py 的 traded / 2;M13 |
| 实施缺口 | Perold (1988),见 M13 卡片 |
| S4 的否决数字 | research/experiments/g12_s4_xsmom_v1/record.md |
| 时间切分与泄漏 | M16 |
| 图 | figures/M21/plot_ai_quant_workflow.py |
💡 思考回顾与巩固¶
核心认知纠偏¶
| 误区 | 反例 | 用法 |
|---|---|---|
| 模型可以在盘中改权重 | 热路径不接收模型输出 | 权重来自已发布的策略代码 |
| 能跑完的回测就是对的 | 第 6 日分位偏差 −0.400000,过程没有异常 | 先看截断偏差 |
| 全样本标准化只影响水平 | 均值偏差 −2.000000 | 同一天的信号变了 |
| 暖机期写成空仓更保守 | 标准差从 0.115470 降到 0.089443 | 缺失不能当成 0 收益 |
| 负动量丢掉后应该满仓剩下的票 | 1.000000 对 0.333333 | 空位是现金 |
| 卖单挂出就算现金已回 | 80 股被放行,正确上限是 50 | 未成交不入可用 |
| 分红再记一笔更完整 | 多记 500.00 | 指数与现金只记一次 |
| 费率越高越能成交出更高净值 | 10489.50 降到 10479.00 | 股数钉死再翻倍 |
| 2τ c 解释了 S4 的全部成本差 | 0.002900 不是 0.93 个百分点 | 两个数分开报 |
| 残差很小就可以并进费用 | 0.20 仍单列 | 超过 10.00 才报警 |
| 实验室赚钱就可以发模拟盘 | 实验室声明不连接模拟盘 | 发单走另一条已批准的链 |
| 同一失败可以一直修到绿 | 第 4 轮仍是同一断言 | 3 轮后停 |
费曼自测¶
- 为什么第 6 日的分位偏差是 −0.400000,而向前滚动的偏差是 0.000000?
- 第 5 日为什么成交 40 股,而不是 43.345041 股?
- 费率翻倍后净值为什么少了 10.50,不是少了一个含糊的比例?
- 残差 0.20 和 12.00,哪一笔超过月初净值的 1 bp?
自测要点
1. 前 6 天的分位是 1.000000,十天样本的分位是 0.600000,差 −0.400000。向前滚动在第 6 日只用前 6 天,两种喂法的输入相同,差是 0.000000。 2. 5244.75/121 = 43.345041,一手 10 股,向下取整得到 40。排名用的是指数 1.331000,下单用的是未复权价 121。 3. 买入多付 5.00,卖出多付 5.50,合计 10.50。股数仍是 50 进、50 出,所以净值从 10489.50 降到 10479.00。 4. 1 bp 是 10.00。0.20 对应 0.020000 bp,不触发。12.00 对应 1.200000 bp,触发排查并按计划停新风险。四维习题¶
A. 概念题¶
- 模型的三个席位分别交出什么,哪一件事仍由人决定?
- 四项硬契约各卡住哪一种静默错误?
- 为什么评估 IC 可以用未来收益,下单权重不可以?
- 影子账、事实账、成本调整账各回答什么问题?
B. 计算题¶
- 价格 10 到 19。写出第 6 日的分位偏差、均值偏差和向前滚动偏差。
- 按五日夹具写出第 3 日到第 5 日的费用、现金和净值。错误分红会多记多少?
- 固定那两笔成交,10 bp 与 20 bp 的期末净值各是多少?
- 写出教学桥的四项、残差、终点净值和按 100000 计算的残差 bp。12.00 是多少 bp?
- 三月收益 −4%、−3%、+1% 的窗口收益是多少?τ = 0.40、c = 0.0010 的拖累是多少?
- 每侧 25 bps,年化单边换手从 2.90 降到 1.20。两档拖累各是多少,省下多少个百分点和多少 bps?
C. 编程实战题¶
- 运行
figures/M21/plot_ai_quant_workflow.py。核对第 6 日分位偏差 −0.400000、均值偏差 −2.000000、向前滚动偏差 0.000000,以及净值 10489.50、10479.00、100840.20。 - 把费用倍数改成 0.5,确认
FeeSchedule.scaled拒绝。不要改成允许降费。 - 用夹具代码把第 3 日的分红误记到成交后的 50 股上,确认现金多出 500.00。改回开盘前股数后再看分红入账 0.00。
- 运行第 4、5、6、10 节里带
# goal9-check的代码。写出内连接行数、键交集率、换手拖累节省和桥的终点。
D. 个人实战思考题¶
- 你想把本周模拟盘的完整持仓粘贴给模型做复盘。哪些字段不该出现在提示词里?
- 同一个断言已经失败 3 轮。第 4 轮你准备让模型改测试期望值。这轮还在 30 轮预算里吗,该不该继续?
- S4 已经 REJECTED。模型建议把回看从 12 改成 9,因为记录里 9 个月的年化是 19.59%。你如何回答?
完整参考答案与解析
**A1.** 副驾交草稿,审计员交失败的性质测试,复盘分析器交四项和残差。人决定是否合并、是否消耗一次 m、是否发单。发单仍经过 `RiskGate`。 **A2.** 时点卡住重复 `shift` 和第 8 类把未来收益写进权重。价格卡住第 6 类重复记分红,以及用指数价格去下单。精度卡住第 9 类浮点累加。状态机卡住未落库批准和 `UNKNOWN` 之后仍发新单。第 1 到第 4 类要靠截断测试和权重断言,契约文本单独不够。 **A3.** IC 的定义就是今天的分数对下一期收益。下一期收益直到下一期收盘才存在,所以它不能成为今天已经发出的订单数量。`shift(-1)` 的结果放在评分表,不放进 `generate_targets`。 **A4.** 影子账回答「冻结规则下这套目标值多少」。事实账回答「模拟券商记了什么」。成本调整账回答「把明确费率补上之后值多少」。三项之间的差用桥来解释,解释不了的留在 ε。 **B5.** 分位偏差 −0.400000。均值偏差 −2.000000。向前滚动偏差 0.000000。 **B6.** 第 3 日费用 5.00、现金 4995.00、净值 9995.00。第 4 日费用 5.50、现金 10489.50、净值 10489.50。第 5 日费用 4.84、现金 5644.66、净值 10484.66。错误分红多记 500.00。 **B7.** 10489.50 与 10479.00。差额 −10.50。 **B8.** −80.00、−150.00、−40.00、−90.00,残差 0.20,终点 100840.20。残差 bp 是 0.020000。12.00 是 1.200000 bp。 **B9.** 窗口收益 −0.059488。拖累 0.000800。 **B10.** 1.4500% 与 0.6000%。节省 0.8500 个百分点,即 85.00 bps。1.20 是缓冲合同的教学输入,不是 v1 印出的换手。 **C11–C13.** 图脚本打印上述数字后退出码为 0。`scaled(Decimal("0.5"))` 抛出 `ValueError`。分红若乘成交后的 50 股,金额是 500.00;乘开盘前的 0 股,金额是 0.00。 **C14.** 内连接剩下 1 行,交集率 0.333333,有限值个数 0。拖累节省 0.8500 个百分点。桥的终点 100840.20,残差 0.020000 bp。过去均值的最大绝对差是 0.000000000000。from decimal import Decimal
from quant.backtest.fees import FeeSchedule
schedule = FeeSchedule(
commission_bps=Decimal("10"),
min_commission=Decimal("0"),
platform_fee=Decimal("0"),
label="teaching-not-a-broker-schedule",
)
try:
schedule.scaled(Decimal("0.5"))
allowed = True
except ValueError:
allowed = False
print(allowed)
print(f"{Decimal('50') * Decimal('10'):.2f}")
print(f"{Decimal('0') * Decimal('10'):.2f}")
📌 章节知识总结¶
一句话核心洞察¶
- 模型是受契约约束的副驾、审计员和复盘分析器。它不进入定时信号,也不绕过
RiskGate。 - 四项契约是时点只延迟一次、排名用总收益指数而下单用未复权价加整手、现金用
Decimal并守恒、先落库再发单且UNKNOWN停新单。 - 第 6 日的未来分位偏差是 −0.400000,未来均值偏差是 −2.000000,向前滚动偏差是 0.000000。
- 五日夹具的期末净值是 10484.66。同一段往返在 10 bp 下是 10489.50,费率翻倍后是 10479.00。
- 结构改动消耗 1 次 m。S4 已用全项目第 3 次并被 REJECTED,不能在同一窗口上再搜。
- 教学桥的终点是 100840.20。残差 0.20 是 0.020000 bp;12.00 是 1.200000 bp,越过 1 bp。
公式与样本¶
| 对象 | 数值 | 前提 |
|---|---|---|
| 第 6 日分位,截断 / 全样本 / 偏差 | 1.000000 / 0.600000 / −0.400000 | 价格 10 到 19 |
| 第 6 日均值信号,截断 / 全样本 / 偏差 | 2.500000 / 0.500000 / −2.000000 | 均值 12.5 与 14.5 |
| 向前滚动偏差 | 0.000000 | 十天皆然 |
| 不分组收益 / 分组收益 | −0.890000 / 0.100000 | 11/100 − 1 与 11/10 − 1 |
| 暖机写成 0 的标准差 | 0.089443 对 0.115470 | 持仓日 ± 0.10 各两次 |
| 重新归一 / 留现金 | 1.000000 / 0.333333 | 三只里一只为正 |
| 未成交卖单放行的股数 | 80 对 50 | 现金 1000,价格 20 |
| 重复分红 | 500.00 | 50 × 10,正确入账 0.00 |
| 月超额夏普 | 0.866025 | 0.01/0.04 × √12 |
再减一次 r_f |
0.577350 | 年利率 0.04 |
| 日频 √252 / 误用 √12 | 0.634980 / 0.138564 | 日超额 0.0004,波动 0.01 |
| 抄未来的相关 / 教学分数的相关 | 1.000000 / 0.327327 | 教学向量,不是股票 IC |
| 百次 0.01 | 1.0000000000000007 对 1.00 |
浮点与 Decimal |
| 五日夹具净值 | 10000.00 到 10484.66 | 见上表 |
| 费率 1 × / 2 × | 10489.50 / 10479.00 | 差额 −10.50 |
| 三月窗口 | −0.059488 | 0.96 × 0.97 × 1.01 − 1 |
| 教学拖累 | 0.000800 | 2 × 0.40 × 0.0010 |
| S4 佣金近似 | 0.002900 | 2 × 2.90 × 0.0005,不等于 0.93 个百分点 |
| 缓冲教学拖累 | 1.4500% → 0.6000% | 每侧 25 bps;换手 2.90 → 1.20。节省 0.8500 个百分点,85.00 bps |
| 印出的成本年化差 | 0.93 个百分点 | 15.77 − 14.84,来自记录的印出值 |
| 桥的终点 | 100840.20 | 四项加 0.20 |
| 残差 / 报警例子 | 0.020000 bp / 1.200000 bp | 月初净值 100000 |
| 1 bp 的金额 | 10.00 | 100000 × 0.0001 |
跨模块¶
| 结论 | 交给 | 位置 |
|---|---|---|
| 前视与重复延迟 | 回测工程 | M03 |
| 多次试验 | 因子陷阱 | M08 |
| 2τ c 与实施缺口 | 成本 | M13 |
先落库、UNKNOWN |
运营 | M15 |
| 时间切分 | 机器学习 | M16 |
| 影子账只读 | 代码 | ../../../src/quant/portfolio/shadow.py |
| 闸门 | 代码 | ../../../src/quant/risk/gate.py |
| 复算 | 图 | ../figures/M21/plot_ai_quant_workflow.py |
| 提示词 | 手册 | learning_prompts.md |
学习指引¶
概念卡片:截断不变性与三本账
- 精读:先把第 6 日的 −0.400000 和 −2.000000 手算完,再看图。图的纵轴从 100700 起,是为了展开瀑布。
- 对照:五日夹具、费用翻倍和桥的金额以脚本打印为准。S4 的百分数以实验记录为准。
- 下一步:用 提示词手册 里的八个模板时,把本章的四项契约整段贴上。循环改成矩阵之前先走模板 8。会话开始先写时间。开发机不连接 OpenD。
自检清单¶
- 能说出模型的三个席位,以及热路径为什么没有模型
- 能把第 6 日分位偏差算成 −0.400000,均值偏差算成 −2.000000,合规偏差算成 0.000000
- 能把五日夹具收到第 5 日净值 10484.66,并说明分红入账是 0.00
- 能把费率翻倍的净值从 10489.50 算到 10479.00
- 能把 2 × 0.40 × 0.0010 算成 0.000800,并把它和 S4 的 0.002900 分开
- 能把每侧 25 bps、换手 2.90 到 1.20 的拖累算成 1.4500% 与 0.6000%,节省 0.8500 个百分点
- 能把桥收到 100840.20,并说明 0.20 是 0.020000 bp、12.00 是 1.200000 bp
- 能拒绝把 S4 的回看改成 9 再交回同一窗口
- 能在第 3 轮失败后停止,而不是改断言
