/~learn/s/9c191a0c
> 日期:2026-07-15(v3 根据 review 反馈修订:术语修正+执行摘要) > 状态:方案设计(待你确认以下8个决策点后即可开始实施)
---
| # | 决策项 | 默认值 | 你的选择 |
| --- | -------- | -------- | --------- |
| 1 | 最低评分阈值(低于此分数宁可持现金不递补) | 0.35(sigmoid后分数) | |
| 2 | 单只ETF仓位上限 | 15%(代理锚定10%) | |
| 3 | 总仓位目标(保留多少现金) | 95%(5%现金缓冲) | |
| 4 | 同质化相关阈值(同子类别内超过此值只留评分最高的) | 0.90(不同子类别不互斥,QQQ/SPY可共存) | |
| 5 | 置信度计算窗口(ETF↔anchor收益率相关的天数) | 60日(约一个季度) | |
| 6 | 代理锚定ETF单只上限(未建策略池的市场用代理锚定) | 10% | |
| 7 | 置信度现在只展示不参与权重缩放? | 是(先观察分布再决定) | |
| 8 | 周中溢价飙升是否触发换仓? | 否(统一周五再平衡,初版保持简单) |
| 场景 | 现状资金利用率 | 新方案资金利用率 |
| ------ | ------------- | --------------- |
| A股熊市、海外分化(当前) | ~20-40% | ~80-90% |
| A股熊市+QDII高溢价 | ~15-25% | ~60-80%(溢价品种自动跳过递补) |
| A股震荡、海外强势 | ~50-60% | ~90-95% |
| 全球熊市 | ~15-30% | ~30-50%(择时全面冰点时仍持现金) |
---
你的择时策略一直都在跑、在乘权重,并不是"悬空未用"。经过对全项目代码的逐行梳理,问题的真正根源是:择时系数乘完之后仓位缩小了,但缩小后腾出来的资金没有"递补机制"去填补,导致现金闲置。
具体来说,当前系统存在四个结构性缺陷:
exposure_cap(沪深300独家决定)还在额外一刀切
虽然每个ETF在 show_marker.py 推送阶段已经乘以了各自锚定指数的 timing_coef,但在 selection/engine.py 的 _compute_portfolio_weights() 中,还额外存在一个由沪深300单基准决定的全局 exposure_cap(0.15~1.0),在 timing 乘法之前就先把所有ETF等比例压缩了。
# 现行 engine.py 逻辑:先全局砍一刀
top_rows['缩减后权重'] = top_rows['建议权重'] * exposure_cap # 沪深300决定
# 然后 show_marker.py 再逐只乘 timing_coef(各锚定的择时策略仓位)
actual_pos = adj_weight * timing_coef * trend_ratio * prem_coef
这意味着:即使美股择时策略说仓位 100%,如果沪深300判定BEAR(cap=0.3),美股ETF也只能拿到 10% × 0.3 × 1.0 = 3% 的权重,而不是应有的 10%。这个全局 cap 是多余的、与分市场择时重复且矛盾的。
当前流程是:
关键断点:第4步乘法之后,某些ETF(尤其是A股锚定的)权重被压到非常小(甚至被prem_coef压到0),但系统不会回头去候选池里找第11、12、13名递补。结果:
简单说:选谁(Top10)是一锤子买卖,后面不管timing/溢价怎么压,都不再扩充候选,空位就是现金。
engine.py 和 show_marker.py 中的锚定映射:
日股ETF、印度ETF、欧洲ETF、原油ETF都没有独立的锚定指数,它们共用QQQ/GLD的择时信号,无法做到"日股可投就加仓日股、日股择时差就跳过"的精细控制。
现有代码中 prem_coef 已经实现了溢价加权逻辑(show_marker.py:L2646):溢价太高的QDII仓位会被压到接近0(如溢价>7%时仓位≈0)。但这同样没有触发递补——某只ETF因为溢价被砍掉的仓位,不会自动分给下一个候选。
---
周五收盘截面引擎 (selection/engine.py):
① 5因子打分 → sigmoid锐化 → 全池排名
② 沪深300单基准判定 regime → 全局 exposure_cap(0.15~1.0)
③ 取Top10 → 等权或评分加权 → 建议权重
④ 所有建议权重 × exposure_cap(一刀砍)
⑤ 区域上限再平衡(A股≤40%×cap、港股≤30%×cap 等)
⑥ 溢出权重在当前Top10内其他区域按评分分配(不扩充Top10)
⑦ 查timing_positions写入展示列
⑧ 输出缩减后权重组合 → 现金 = 1 - sum(缩减后权重)
周五推送阶段 (chart_test/show_marker.py):
⑨ 对每只ETF取锚定指数的择时策略仓位 → timing_coef
⑩ 计算趋势强度比 trend_ratio = ETF趋势/锚定趋势 → 乘上去
⑪ 计算QDII溢价系数 prem_coef → 乘上去
⑫ actual_pos = adj_weight × timing_coef × trend_ratio × prem_coef
⑬ 输出"建议仓位"文字 → 但不改变Top10组合构成
⑭ 现金 = 1 - sum(actual_pos) ← 这一步才是真正大量现金的来源!
周一开盘:
⑮ 按推送的建议仓位买入/调仓,剩余现金空置
核心一句话:固定Top10选出后,timing/趋势/溢价三重系数把部分ETF压到很小仓位,但系统不会向后递补候选,资金空置。
---
把"截面选基 → 择时缩仓 → 趋势比调整 → 溢价过滤"从现在的两段割裂流程(引擎选Top10 + show_marker后处理乘系数),合并为一个一体化的递补分配流程:
---
算法伪代码:
1. 对全池所有ETF计算5因子综合评分,排序得到 ranked_list
2. 预先计算每只ETF的:
- anchor(锚定指数)
- region(资产区域)
- timing_coef = timing_positions[anchor].today / 100 (择时策略仓位,已存在)
- trend_ratio = clamp((1+etf_trend)/(1+anchor_trend), 0.3, 2.5) (已存在)
- prem_coef = 溢价系数计算(已存在,溢价>7%→0,>3%→0.5 等)
- confidence = corr(ETF近60日收益率, anchor近60日收益率) 【新增:锚定置信度】
- final_score = factor_score (5因子综合评分)
3. 初始化:selected = [], region_used = {各区域:0}, cash_remaining = 1.0
4. 目标总仓位 = 0.95(5%现金缓冲)
5. candidate_ptr = 0
6. WHILE cash_remaining > 0.02 AND candidate_ptr < len(ranked_list):
etf = ranked_list[candidate_ptr]
region = etf.region
anchor = etf.anchor
// ---- 五层过滤 ----
a) 区域硬上限:
region_cap = ASSET_REGION_CAPS[region] // A股40%、港股30% 等
if region_used[region] >= region_cap:
candidate_ptr += 1; continue
b) 择时仓位过滤:
timing_cap = etf.timing_coef // 0~1 来自择时策略
if timing_cap < 0.05: // 择时策略说空仓/极低仓位 → 跳过
candidate_ptr += 1; continue
c) 溢价过滤:
if etf.prem_coef < 0.05: // 溢价过高(如>7%)→ 仓位归零,跳过
candidate_ptr += 1; continue
d) 同质化相关性过滤:
// 不是"高相关就互斥",而是"同质化品种才互斥"
// 判定标准:同一子类别(sub_category)内 + 相关系数 > 0.90 → 只留评分最高的
// 不同子类别/不同区域/不同资产类型之间,即使相关也保留
// 例:QQQ(纳指)和SPY(标普) → 不同子类别(科技vs大盘),两者夏普都高,可共存
// 例:创业板50和创业板100 → 同子类别同指数,相关>0.95,剔除重复的
if is_homogeneous_duplicate(etf, selected):
candidate_ptr += 1; continue
e) 最低评分阈值:
if etf.final_score < MIN_SCORE_THRESHOLD: // 建议0.35
break // 后面的评分更低,不用继续了
// ---- 决定该ETF的实际仓位 ----
base_weight = calc_score_weight(etf.final_score, selected, ranked_list) // 评分加权
adjusted_weight = base_weight * timing_cap * etf.trend_ratio * etf.prem_coef
available_for_region = region_cap - region_used[region]
available_from_cash = cash_remaining - 0.05
single_cap = SINGLE_POSITION_CAP // 15%
actual_weight = min(adjusted_weight, available_for_region,
available_from_cash, single_cap)
actual_weight = max(0, actual_weight)
if actual_weight > 0.01:
selected.append({
'code', 'name', 'weight', 'factor_score',
'timing_coef', 'trend_ratio', 'prem_coef',
'confidence', 'region', 'anchor',
})
region_used[region] += actual_weight
cash_remaining -= actual_weight
candidate_ptr += 1
7. 输出 selected(动态8~15只)+ cash_remaining
不需要新建任何择时模型。直接使用现有的择时信号链路:
backtest/strategy/ 下30+策略 → StrategyAllocationManager加权融合
→ market_data_processer.calculate_average_holding_ratio() → df['position']
→ MarketAnalysis.load_mult_market_strategy() 取末三行
→ timing_positions[anchor] = {today, yesterday, predicted}
这套链路对主要锚定指数(沪深300、创业板指、创业综指、HSI恒生、QQQ纳指、GLD黄金、红利、REITs)已经回测并在运行。
废弃全局 exposure_cap:去掉 SimpleRegimeDetector 用沪深300单基准决定的全局cap,改为完全依赖每个锚定指数的择时策略输出 timing_coef。这才是真正的"分市场独立择时"。
旧逻辑的问题:用相关系数>0.65全局替换,会把QQQ/SPY这种"相关但不同质、都是高夏普优质资产"的组合错误剔除。
新逻辑:同质化判定(homogeneous duplicate),只有同时满足以下条件才判定为重复:
以下情况不剔除,即使相关较高:
溢价系数参考表(复用现有逻辑):
| 溢价率 | prem_coef | 含义 |
| -------- | ----------- | ------ |
| < -1%(折价) | 1.1~1.3 | 折价时略加仓 |
| -1% ~ 1.5% | 1.0 | 正常区间 |
| 1.5% ~ 3% | 0.8 | 轻微溢价,略减 |
| 3% ~ 5% | 0.5 | 中等溢价,减半 |
| 5% ~ 7% | 0.2 | 高溢价,大幅减 |
| > 7% | 0.0 | 溢价过高,归零→递补 |
计算方式:confidence = corr(ETF近60日收益率, anchor近60日收益率)(皮尔逊相关系数,值域[0,1])
用途(先展示、后决策):
- 方案A:effective_timing = timing_coef × confidence + 1.0 × (1 - confidence) → 低置信度时timing信号向1.0收缩("不确定就不干预")
- 方案B:低置信度ETF递补优先级稍降
- 方案C:仅作风险提示
原则:宁可少配、不可乱配。绝不使用未经验证的策略。
过渡期:没有独立择时策略池的市场(日股、欧洲、印度、原油等),用最接近的已有锚定做代理锚定,标注 ⚠️,单只上限10%,待策略池回测完成后替换。
择时策略池待办清单(Backlog):
| 优先级 | 锚定指数 | 代表品种 | 数据情况 | 状态 |
| -------- | --------- | --------- | --------- | ------ |
| ✅ 已有 | 沪深300 (399300) | A股大盘 | >10年 | 策略池在运行 |
| ✅ 已有 | 创业板指 (399006) | A股成长 | >10年 | 策略池在运行 |
| ✅ 已有 | 创业综指 (399102) | A股小盘 | >10年 | 策略池在运行 |
| ✅ 已有 | 恒生指数 (HSI) | 港股大盘 | >10年 | 策略池在运行 |
| ✅ 已有 | 纳斯达克 (QQQ) | 美股科技 | >10年 | 策略池在运行(暂代非美跨境) |
| ✅ 已有 | 黄金 (GLD/518880) | 贵金属 | >10年 | 策略池在运行 |
| ✅ 已有 | 红利 (510880) | 红利策略 | >10年 | 策略池在运行 |
| ✅ 已有 | REITs (508068) | REITs | 中等 | 策略池在运行 |
| P0 | 日经225 | 日股 | >10年 | 待回测(暂用QQQ代理) |
| P0 | 欧洲Stoxx50/DAX | 欧洲 | >10年 | 待回测(暂用QQQ代理) |
| P1 | 标普500 (SPY) | 美股大盘 | >10年 | 待回测(目前共用QQQ) |
| P1 | 印度Nifty | 印度 | 中等 | 待回测(暂用QQQ代理) |
| P1 | 原油 (USO/162411) | 原油 | >10年 | 待回测(暂用GLD代理) |
| P2 | 恒生科技 (HSTECH) | 港股科技 | ~2年(短) | 先用HSI代理,标注低置信 |
| P2 | 东南亚/越南 | 东南亚 | 短 | 如有品种先用QQQ代理 |
| P2 | 美元债/美债 | 防御 | >10年 | 待评估 |
| P3 | 豆粕/有色等商品 | 细分商品 | 不等 | 扩充品类时同步构建 |
策略池准入标准:至少3年日线数据、策略组合夏普>1、最大回撤<20%、评估期望为正。
---
---
etf_pool.py:ETFInfo新增 anchor、anchor_proxy 字段selection/allocator/dynamic_allocator.py:核心递补算法engine.py:废弃全局exposure_cap,调用allocator,新增输出列
---
新增DB列:confidence、timing_coef、trend_ratio、prem_coef、anchor_proxy、cash_weight
前端新增展示:
---
| 风险 | 对策 |
| ------ | ------ |
| QDII高溢价→递补到流动性差的ETF | 增加日均成交额/规模过滤 |
| 汇率风险(满仓海外) | 区域硬上限(美股≤40%等)天然分散 |
| 代理锚定不准 | 单只10%上限+⚠️标记+尽快回测替换 |
| 换手增加 | 周频调仓+周中30%日换手限制 |
| 递补到低质量ETF | 最低评分阈值兜底 |