/~learn/s/97a0ba8f
> 📅 2026-09-21 建立
>
> 本文把「写一个择时策略 → 遗传算法调参 → 入选策略池」这条完整链路固化成可复制的 SOP。
> 之前只有一份口语化的《策略研发sop.txt》(在 adata/aohan/learn/策略研发/),本文是对它的工程化落地版:每一步都对应到仓库里的真实文件、真实函数、真实契约。
>
> 本文回答三个问题:
> 1. 一个新策略从零到能跑,要碰哪几个文件?(第一章 · 链路总览)
> 2. 参数是怎么被遗传算法"看见"并优化的?(第二章 · 参数体系)
> 3. 每一步最容易踩什么坑?(第三章 · 避坑清单)
---
① 写策略类 ──→ ② 暴露参数 ──→ ③ 声明参数边界 ──→ ④ 未来函数自检(写的时候就防)
Strategy.py __init__ params_grid.py
↓
⑤ GA 参数优化 ──→ ⑥ 未来函数终检(必检!) ──→ ⑦ 入选策略池
ParamsOptGeneticDevelop future-function timing_config.py
+ params_deep_seek.py check
| 步骤 | 做什么 | 落点文件 |
| ------ | -------- | ---------- |
| ① | 继承 Strategy,实现 generate_signals() 产出 position | adata/aohan/quant/backtest/strategy/<新策略>.py |
| ② | __init__ 暴露可优化参数 + 同名实例属性 | 同上 |
| ③ | 写参数搜索空间(GA 的基因边界) | backtest/strategy/params_grid/params_grid.py |
| ④ | 编码时就避开未来函数(指标全部用历史数据) | 策略类内部 |
| ⑤ | 配置策略类 + 参数边界 + 目标品种,跑遗传算法 | backtest/deep_learning/params_deep_seek.py |
| ⑥ | 高收益策略必须手工 shift 1 天复检 | 手工 + 可视化 |
| ⑦ | 用最优参数实例注册到策略池 | chart_test/timing_config.py |
> ⚠️ ④ 和 ⑥ 不是同一件事:④ 是写的时候就按"只能用历史数据"的规矩写;⑥ 是跑出来之后再用"手工 shift 一天,看收益是否大幅缩水"做终检。两者都做,缺一不可。
---
基类文件:Strategy.py · class Strategy(L25)
你只需要实现一个方法:
def generate_signals(self):
# 算出 self.df['position']:全仓=1,空仓=0
...
self.generate_trade_signals_based_on_positions(delay_days=self.signal_offset)
基类把职责收得很窄,其余全部由框架代劳。完整流程入口是 init_and_generate_signal:
_preprocess_missing_columns() # 补 open/high/low/volume 缺失列
→ initialize_positions() # 初始化仓位列
→ calculate_daily_return() # 算日收益
→ generate_signals() # ★ 你实现的那一个
position:每天结束时持仓市值占总资产比例,全仓 1、空仓 0。signal:由基类从 position 的变化自动推导 —— generate_trade_signals_based_on_positions 里 signal = position.diff()。buy_timing='open'(默认)表示"当天开盘价成交",即 position[i]=1 代表第 i 天开盘买入。> 🚩 这条直接决定未来函数怎么防:因为 position 是"当天开盘"用的,所以生成 position 的所有指标,只能用到当天开盘之前已知的数据(严格说 ≤ 前一日收盘)。详见 Step 4。
| 方法 | 含仓位管理 | 速度 | 谁在用 |
| ------ | ----------- | ------ | -------- |
| calculate_returns_quick | ❌ 不含 | 快 | GA 优化器、快速验证 |
| calculate_returns | ✅ 含 | 慢 | 最终评估 |
指标统一入口是 evaluate_strategy_metrics(verbose, quick),结果写进 self.last_calculated_results(一个 dict,值多为带 % 的字符串)。
---
没有任何声明式 schema,参数就是子类 __init__ 的显式形参,并且必须存成同名实例属性:
def __init__(self, df=None, rsi_period=32, rsi_buy=45, rsi_sell=60, signal_offset=0):
super().__init__(df)
self.rsi_period = rsi_period # 形参 → 同名属性,缺一不可
self.rsi_buy = rsi_buy
self.rsi_sell = rsi_sell
self.signal_offset = signal_offset
为什么必须是同名属性? 两个地方靠这个约定吃饭:
strategy_class(df, **params) 构造实例,params 的 key 必须能对上形参名。__init__ 的形参名,再从实例上取值拼 ID —— 这是全库唯一的"参数自描述"逻辑,用于给优化结果命名。> 💡 约定速成:形参名 = 属性名 = params_grid 里的 key 名 = GA 基因名。四者一旦不一致,链路就断。
---
文件:params_grid.py,全部是模块级 dict,一个策略一个,命名 XxxStrategy_params。
参数边界支持 4 种形态,由 create_toolbox 统一解释:
| 形态 | 含义 | 示例 |
| ------ | ------ | ------ |
| 单元素 list | 固定值(不参与搜索) | 'k_type': [3] |
| 全 int 二元 tuple | 整数均匀区间 randint | 'rsi_period': (4, 80) |
| 全 float 二元 tuple | 浮点均匀区间 uniform | 'threshold': (0.03, 0.12) |
| 多元素 list | 枚举 choice(字符串参数靠这个) | 'priority': ['buy', 'sell'] |
RSIStrategy_params = {
'rsi_period': (4, 80),
'rsi_buy': (10, 80),
'rsi_sell': (10, 80),
}
> params_grid.py 里还有个 _calculate_total_combinations(),会把 tuple 区间按小数位数推断步长,折算成组合总数,用于"搜索空间过大时提前终止"。
---
核心规矩:生成 position 时,只能用"当天开盘前已知"的数据。
| 错误写法 | 正确写法 |
| ---------- | ---------- |
close.pct_change().rolling(5) | 先 .shift(1) 再 rolling |
df['high'].rolling(5).max() | df['high'].shift(1).rolling(5).max() |
close > sar(当日 sar) | close.shift(1) > sar.shift(1) |
signal_offset 参数,generate_trade_signals_based_on_positions(delay_days=self.signal_offset) 在需要时把信号整体延后。---
文件:ParamsOptGeneticDevelop.py · class ParamsOptGeneticDevelop(基于 deap)
ParamsOptGeneticDevelop(
df_list, # 目标品种的数据列表(每个品种一个 df)
strategy_class, # 策略类
param_bounds, # 来自 params_grid.py 的 dict
weight_rules, # 多目标权重规则(阶段式)
result_dir, codes,
enable_short=False,
)
df_list 里每个品种各跑一次 strategy_class(df, params) → 指标按品种数取平均** → 加权适应度。annual_return / 指数衰减年化 / 凯利 / 平均回撤 / 资金占用。
入口:params_deep_seek.py · ParamsDeepSeek.test_opt_mult_market_params(L99)→ optimize_strategy_for_codes(L353)
strategy_configs = [
{"strategy_class": FourMovingAveragesStrategy, "param_bounds": FourMovingAveragesStrategy_params},
# ... 一个策略一条
]
for config in strategy_configs:
for codes in codes_list: # ← 目标品种
self.optimize_strategy_for_codes(codes=codes, strategy_class=..., param_bounds=..., ...)
阶段式多目标权重(写在 optimize_strategy_for_codes 内):
weight_rules = {'type': 'stage', 'rules': [
(0, 0, (3.0, 12.0, 2.0, -8.0, 0)), # 高年化
(1, 1, (1.0, 4.0, 2.0, -4.0, -1.0)), # 高凯利低回撤
(2, 2, (3.0, 12.0, 1.0, -5.0, -20.0)), # 高收益+超低资金占用
(3, float('inf'),(2.0, 8.0, 1.0, -20.0, -1.0)), # 高收益低回撤
]}
数据准备:fetch_data(code, start, end, k_type=1) → DataProcessor(df).process_all()。
> 🚩 大坑:目标品种是硬编码的 codes_list,而且这个变量在同一个函数里被反复重赋值十余次(L243~L302)。因为是顺序覆盖,文本上最后一次赋值生效。改目标品种就改这最后一处,别改中间的。详见 附录 B。
generations=200, pop_size=500 → 一晚约优化 2 个策略。generations=100, pop_size=100 → 一晚约优化 10 个策略。---
跑出结果后,尤其当收益漂亮得不像话时,做这两件事:
> 原则:不检未来函数,参数优化就是白做。
---
注册表本体:allocation_utils.py · class StrategyAllocationManager
池内容(真正写死的地方):timing_config.py · build_allocation_manager()
allocation_manager = build_allocation_manager()
strategy = RSIStrategy(rsi_period=32, rsi_buy=45, rsi_sell=60)
allocation_manager.add_strategy_to_board(strategy, ['399102', '399300'], weight=2)
add_strategy_to_board(策略实例, code列表, weight=权重)。strategies_by_board 的 key(排除 short- 前缀),被 run_all_baselines() 和 scripts/run_backtest_baseline.py 缺省使用。PICKERS / 中央 registry —— 策略池就是 strategies_by_board 这个 dict。---
□ 1. 新建 adata/aohan/quant/backtest/strategy/<XxxStrategy>.py
class XxxStrategy(Strategy):
def __init__(self, df=None, <param1>=..., signal_offset=0):
super().__init__(df)
self.<param1> = <param1>
self.signal_offset = signal_offset
def generate_signals(self):
... # 只用历史数据,产出 self.df['position']
self.generate_trade_signals_based_on_positions(delay_days=self.signal_offset)
□ 2. 在 params_grid.py 加 XxxStrategy_params = {...}(四形态选其一)
□ 3. 在 params_deep_seek.py:
- import 策略类 与 param
- strategy_configs 里加一条
- 确认 codes_list(目标品种,改最后一处赋值)
□ 4. 先小样本手动跑一次 evaluate_strategy_metrics(quick=True) 看有没有交易
□ 5. 未来函数自检(Step 4)→ 跑 GA → 未来函数终检(Step 6)
□ 6. 用最优参数实例 add_strategy_to_board 注册进 timing_config.py
---
codes_list 的坑
params_deep_seek.py 的 test_opt_mult_market_params 里,codes_list 被反复赋值,最后生效的是文本上最后一次那处。当前源码最后一次生效的是医药四指数:
codes_list = [
["000933.SH"], # 中证医药卫生指数
['399139.SZ'], # 深证医药卫生指数
['000913.SH'], # 沪深300医药卫生指数
['931152.CSI'], # 中证创新药产业指数
]
想换品种,直接改这最后一处,不要改中间的(改了不生效,会让你怀疑人生)。
---
position 是"当天开盘成交",指标必须 shift(1) 或只用历史窗。---