| name | excel-arbitrage-conclusion |
| description | 把 skill `excel-template-contract-fill` 整理出的「合约数据(整理后)/{上期所,大商所,郑商所,广期所}.xlsx」合并成一份跨月套利结论表,输出到项目根目录「套利结论.xlsx」。格式对齐模板 template/预期最后结果.xlsx(sheet=结论,10 列 = 序号 / 交易品 / 利润率 / 交易品 / 价格 / 操作 / 交易品 / 价格 / 操作 / 总保证金)。本 skill 给的是"怎么挑合约对、怎么算利润率、怎么打风险标签"的语义规则,不是一段固定脚本 —— 任何 AI 按规则都能写出等价实现。触发词:生成套利结论、套利结论表、整理套利机会、/build-arbitrage-conclusion。 |
excel-arbitrage-conclusion
这个 skill 为什么不只是一个脚本
套利结论表看起来就是"按利润率排个序",但有很多需要显式描述的决策:
- 每个品种挑哪两个合约作为"主 + 次"套利对?
- 商品名称在四家交易所之间怎么统一口径(
鲜苹果期货 → 苹果、聚乙烯 → 塑料)?
- 哪些品种要贴
(风险) 标签?
- 总保证金 / 利润率 / 多空方向分别怎么算?
如果只说"运行 xxx.py",换一个 AI 或者模板改一列,输出就不一致了。所以本 skill 的核心是 语义规则:规则定死,实现可以换。
tools/build_arbitrage_table.py 只是当前的 参考实现,对应今天这一份模板快照。规则变了就修脚本,或者让别的 AI 按本 SKILL 重写一份等价的。
上下游
原始数据/ ← 用户准备(四大交易所下载)
└─ 期货数据/ + 合约信息/
↓ skill: excel-template-contract-fill
合约数据(整理后)/ ← 本 skill 的输入
├─ 上期所.xlsx
├─ 大商所.xlsx
├─ 郑商所.xlsx
└─ 广期所.xlsx
↓ skill: excel-arbitrage-conclusion (本 skill)
套利结论.xlsx ← 本 skill 的输出(项目根目录)
前置条件:合约数据(整理后)/ 下四个 xlsx 必须都存在、都没有空值。如果不存在,先去跑 skill excel-template-contract-fill。
🛑 读入阶段的硬失败检查(必须做)
开始挑对之前,必须先对每个交易所的 数据 sheet 做一次行数健康检查。只要任一家行数 < 10,立刻 raise,让上游去修 step 1,不要"照着这份半残的数据凑一份结论表"然后悄悄少几十个品种。
MIN_ROWS_PER_EXCHANGE = {
"上期所": 100,
"大商所": 100,
"郑商所": 100,
"广期所": 20,
}
for name, min_rows in MIN_ROWS_PER_EXCHANGE.items():
rows = count_data_rows(f"合约数据(整理后)/{name}.xlsx")
if rows < min_rows:
raise RuntimeError(
f"[硬失败] {name}.xlsx 只有 {rows} 行数据(期望 ≥ {min_rows})。"
f"这意味着 step 1 `excel-template-contract-fill` 在 {name} 解析失败了。"
f"请先去修 step 1(参见该 SKILL 的 §上期所行情解析伪代码 / §硬失败检查),"
f"不要在半残的数据上强行生成套利结论表。"
)
这条检查是 2026-04-11 补的 —— 当时 GPT-5.2 跑 step 1 时把整个上期所的 ~300 条合约静默丢光,step 2 没有任何检查就照着剩下的数据跑,最后套利结论表里整个上期所消失(只有 51 个品种,期望 82),用户直到看最终产物时才发现。如果 step 2 这里早早 raise,问题就会在第一时间指向 step 1 而不是被埋两层。
最终要产出什么
一份 Excel 文件,路径:项目根目录 套利结论.xlsx
- sheet 名必须是
结论
- 固定 10 列表头(顺序、重复都不能改):
序号 | 交易品 | 利润率 | 交易品 | 价格 | 操作 | 交易品 | 价格 | 操作 | 总保证金
注意 交易品 出现 3 次、价格 和 操作 各出现 2 次,这是模板固定格式,不要自作聪明改成 品种/合约1/合约2。
- 一行一个品种,每行 10 个单元格 全部必须有值(
总保证金=0 合法,但 None / 空串禁止)
- 按
利润率 降序排列
- 风险品种在
交易品 列名后贴 (风险) 后缀
工作流程
- 读 合约数据(整理后)/ 下四个 xlsx 的
数据 sheet
- 归并 按
商品名称 分组(先做 §商品名称规范化)
- 挑对 每个品种从所有合约里挑两个作为套利对(§挑对规则)
- 计算 价差 / 总保证金 / 利润率 / 多空方向(§公式)
- 打标签 按 §风险清单 贴
(风险) 后缀
- 排序 按利润率降序
- 写文件 复制模板样式(
template/预期最后结果.xlsx)写到项目根
- 自检 按 §自检规则 校验一遍
挑对规则
对每个品种的所有合约:
- 过滤有效合约:
价格 > 0 且 最低保证金率 > 0 且 最低保证金 > 0
- 找主力合约:按持仓量降序排序,取第一个作为主力合约
- 找临月合约:在剩余有效合约中,筛选交割月 晚于 主力合约的,按交割月升序排序,取第一个(即最近的下一个月份合约)
- 主力 + 临月组成套利对
(c1, c2),其中 c1 = 主力合约,c2 = 临月合约
- 找不到临月合约(主力已是最远月)或有效合约不足 2 个 → 该品种写不出套利对,必须记录(例如写进日志或
未成对品种.csv),不能静默丢弃
为什么是"主力 + 临月"而不是"持仓量前二":套利只做主力合约与其最近的下一个月份合约之间的价差。临月不一定是自然月相邻 —— 比如主力是06月,如果没有07月合约,最近的是08月,则做06和08;以此类推,始终找主力之后最近的可用月份。
为什么用持仓量确定主力:持仓量反映的是资金沉淀,更稳定;成交量会被日内博弈放大。主力的定义在业内用的就是持仓量排序。
如果某品种出现 持仓量全为 0,按持仓量排序退化成按出现顺序取第一个为主力,再按交割月找临月。
公式
记套利对的两条腿为 (p1, m1, r1)、(p2, m2, r2),其中 p 是收盘价,m 是该合约最低保证金(整理后的 xlsx 里已经给出),r 是保证金率。
降低预期(最终写入 xlsx 的利润率)
最终写入 套利结论.xlsx 的利润率 = raw_profit_rate / 2。这一步叫"降低预期",目的是给结论加一层保守修正后再呈现给交易员。
🔒 实现硬性要求 —— 必须用 Excel 公式,不能在 Python 层先除好:
ws.cell(row=r, column=3).value = f"={repr(float(raw_profit_rate))}/2"
ws.cell(row=r, column=3).value = raw_profit_rate / 2.0
Why: 用户要求这一步在最终 xlsx 里肉眼可见 —— 打开结论表选中利润率单元格,编辑栏必须显示 =0.3236.../2 这样的公式,而不是一个已经被烘焙好的数字。理由:
- 便于交易员肉眼复核原始利润率和修正系数是否正确
- 便于事后只调修正系数(比如从
/2 改成 /1.5)而不用重跑整条流水线
- 避免"降低预期"这一步变成隐形规则 —— 把它显式留在单元格里,下一个人一眼就知道有这个修正
排序不变:按利润率降序排列时,仍然用 raw_profit_rate(或等价地,用除 2 之后的值)作为 key —— 因为 /2 是单调变换,两种方式排出来的顺序完全一致。
以后要改修正系数:改脚本里公式字符串的分母(/2 → /1.5 等),并同步更新本章节的文字描述;不要在 Python 层引入新的中间变量去先算好。
多空方向
| 情况 | 合约1操作 | 合约2操作 |
|---|
p1 < p2 | 多 | 空 |
p1 > p2 | 空 | 多 |
p1 == p2 | 多 | 多 |
等价的情况下两边都 多 是对齐模板的写法(见 template/预期最后结果.xlsx 末尾 沪锌 / 棉花 / 尿素 / 碳酸锂 那几行)。意义不大但格式要一致。
商品名称规范化
四大交易所的"商品名称"叫法不统一,整理后文件里原样保留了各自的叫法。本 skill 要把它们统一成业内常用简称,才能和模板对齐。
参考映射表(参考实现里的 NAME_CANON 字典,发现新品种或交易所改名时同步更新):
上期所
| 整理后文件里的名 | 规范化后 |
|---|
| 铜 / 铝 / 锌 / 铅 / 镍 / 锡 | 沪铜 / 沪铝 / 沪锌 / 沪铅 / 沪镍 / 沪锡 |
| 黄金 / 白银 | 沪金 / 沪银 |
| 石油沥青 | 沥青 |
| 天然橡胶 | 橡胶 |
| 热轧卷板 | 热卷 |
| 丁二烯橡胶 | 合成橡胶 |
| 低硫燃料油 | 低硫燃油 |
| 燃料油 | 燃油 |
| 胶版印刷纸 | 双胶纸 |
| SCFIS欧线 | 欧线集运 |
| 铜(BC) | 国际铜 |
大商所
| 整理后文件里的名 | 规范化后 |
|---|
| 液化石油气 | LPG |
| 聚乙烯 | 塑料 |
| 聚氯乙烯 | PVC |
| 聚乙烯月均价 / 聚氯乙烯月均价 | 塑料月均价 / PVC月均价 |
其他(豆一 / 豆二 / 玉米 / 豆粕 / 豆油 / 棕榈油 / 鸡蛋 / 生猪 / 焦炭 / 焦煤 / 铁矿石 / 苯乙烯 / 乙二醇 / 纯苯 / 纤维板 / 胶合板 / 粳米 / 玉米淀粉 / 聚丙烯 / 原木)直接保留原名。
郑商所(统一去掉"期货"后缀 + 使用简称)
| 整理后文件里的名 | 规范化后 |
|---|
| 鲜苹果期货 | 苹果 |
| 棉花期货 | 棉花 |
| 干制红枣期货 | 红枣 |
| 棉纱期货 | 棉纱 |
| 平板玻璃期货 | 玻璃 |
| 粳稻谷期货 | 粳稻 |
| 甲醇期货 | 甲醇 |
| 菜籽油期货 | 菜油 |
| 涤纶短纤期货 | 短纤 |
| 花生仁期货 | 花生 |
| 丙烯期货 | 丙烯 |
| 普通小麦期货 | 普麦 |
| 瓶级聚酯切片期货 | 瓶片 |
| 对二甲苯期货 | PX |
| 早籼稻期货 | 早稻 |
| 菜籽粕期货 | 菜粕 |
| 油菜籽期货 | 菜籽 |
| 纯碱期货 | 纯碱 |
| 硅铁期货 | 硅铁 |
| 烧碱期货 | 烧碱 |
| 锰硅期货 | 锰硅 |
| 白砂糖期货 | 白糖 |
| 精对苯二甲酸期货 | PTA |
| 尿素期货 | 尿素 |
| 优质强筋小麦期货 | 强麦 |
| 动力煤期货 | 动力煤 |
广期所(直接保留:碳酸锂 / 工业硅 / 多晶硅 / 钯 / 铂)
兜底规则
不在表里的名字(通常是新挂牌品种):
- 先 WebSearch 查业内常用简称,关键词:
"<品种全名> 期货 简称" / "<品种全名> 行情简称"。需要两个独立来源互相印证(交易所官网、财经门户、专业论坛),不要只看一处。
- 示例:
瓶级聚酯切片期货 → 瓶片;液化石油气 → LPG
- 自动回写映射(两个位置都要改):
tools/build_arbitrage_table.py 的 NAME_CANON 字典
- 本 SKILL.md 上面按交易所分组的映射表
- 查不到权威简称时的兜底(这时候才走老规则):
- 末尾是
期货 → 去掉 期货 两个字
- 否则原样保留
- 不要在没有 WebSearch 依据的情况下脑补简称(比如不能看到"瓶级聚酯切片期货"就擅自缩成"瓶片",必须查到业内确实这么叫才能写)
- 新品种默认不贴风险标签。RISK_NAMES 是业务决策,查到新品种后只在日志里提一句"建议人工评估是否入风险清单",不要自己往 RISK_NAMES 里塞
风险清单
按 CLAUDE.md 列出的,贴 (风险) 后缀(key 是规范化后的商品名):
| 品种 | 原因 |
|---|
| 欧线集运 | 波动过大 |
| LPG | 流动性 / 基本面 |
| 棕榈油 | 非主力成交困难 |
| 粳米 | 交易不活跃 |
| 烧碱 | 品种特性 |
| 原油 | 消息影响过大 |
| 低硫燃油 | 原油衍生品 |
| 纤维板 | 流通性风险 |
不要自作主张扩充这个清单,要加先改 CLAUDE.md。
注:新品种无 3 次以上正常回归 属于动态判断,本 skill 不做自动识别,需要人工维护。
样式要求
必须从 template/预期最后结果.xlsx 复制样式,至少包括:
- sheet 名
结论
- 列宽(A=6.75, B=15.75, C=10.375, F=10.375, G=10.375, I=10.375, J=10.375,其余继承)
- 表头字体:微软雅黑 10pt 加粗 + 水平居中 + 细边框
- 数据字体:微软雅黑 10pt + 水平居中 + 细边框
- 利润率列(C 列)建议
0.00% 格式(可选)
实现建议:直接 load_workbook(template) → 抓 row 1 / row 2 的样式 → 清空旧数据 → 写新数据时 copy() 样式并赋值。这样列宽、合并单元格等 sheet 级属性全部保留,不用自己重建。
自检规则
跑完之后必须检查:
- 无空值:数据区 82(或当日有效品种数)× 10 = 820 个单元格里,每一个都有值
- 脚本层面:遇到
None / 空串直接 raise,不要写进文件再"希望没人发现"
- 行数合理:写入的品种数 ≈ 四家交易所合约信息里的总品种数,显著偏少就是挑对失败,追查 §挑对规则
- 利润率降序:
profit[i] >= profit[i+1] 恒成立
- 抽查总保证金:随机抽 3 行,用
m1 + m2(直接从整理后 xlsx 里拿两个合约的最低保证金相加)对比,必须完全相等
- 抽查方向:
- 找一行
p1 < p2,验 操作1=多, 操作2=空
- 找一行
p1 > p2,验 操作1=空, 操作2=多
- 风险标签:数一下带
(风险) 的行数,应当 ≤ 8 且全部属于 §风险清单
- 样式:打开文件肉眼检查表头加粗、列宽正确
禁止事项
- ❌ 输出不要放到
合约数据(整理后)/,最终结论必须在 项目根目录
- ❌ 不要改表头顺序 / 改 sheet 名 / 合并单元格
- ❌ 不要在数据 sheet 里留空单元格
- ❌ 不要硬编码交易单位字典 —— 用
m / (p × r) 反推,和整理后数据保持自洽
- ❌ 不要静默跳过"有效合约不足 2 个"的品种,要写到日志告诉用户
- ❌ 不要扩充风险清单,除非 CLAUDE.md 先更新
- ❌ 不要把"月均价"系列和普通品种混在一起(它们是独立的
商品名称,按本 skill 的规则会天然分开)
- ❌ 严禁把"降低预期"这一步在 Python 层先算好再写数字。利润率单元格必须是 Excel 公式字符串
=<raw>/2,打开结论表在编辑栏能看到这个除法。见 §公式 §降低预期。一次性烘焙成数字会让交易员再也看不到原始利润率和修正系数,也无法在不重跑流水线的前提下调系数。
参考实现
tools/build_arbitrage_table.py 是当前的参考实现:
python tools/build_arbitrage_table.py
依赖 openpyxl。它会:
- 读
合约数据(整理后)/ 下四个 xlsx
- 按本 SKILL 的规则生成套利结论
- 写到项目根
套利结论.xlsx(样式从 template/预期最后结果.xlsx 继承)
- 在 stdout 打印丢弃品种、前 10 行预览供人工核对
它不是唯一路径:
- 脚本跑挂了 → 修脚本 > 让 AI 自己凑数据
- 换 AI(Kimi / GLM ...)→ 照本 SKILL 重写一份等价的,输出应当语义等价