目标配置:固定/动态目标、方向约束与分批部署
重要声明:策引是个人开发的投资策略分析工具和教育平台,通过模拟组合展示交易规则、仓位变化与历史表现。平台不提供投资建议,所有策略分析仅供研究参考。真实账户中的决策与风险由用户自行判断和承担。
普通的原语策略回答"何时买、何时卖";轮动策略回答"这一期在候选里选谁"。还有一种更直接的组合方式:你心里已经想好了一个资产清单和各自的目标比例,只想再给它加一个可选的"开/关"条件。
这就是 TargetAllocation(目标配置)。它先从最简单的固定比例开始:
- 纵向(普通策略):"QQQ 上穿 200 日线就买 QQQ。"
- 横向(轮动 / 横截面):"在我这 10 只里,每月持有 RSI 最强的 3 只。"
- 目标配置(TargetAllocation):"SPY 60% + TLT 40%,每季度调回这个比例;SPY 跌破 200 日线时把 SPY 归零。"
在这个基础之上,TargetAllocation 还支持三种进阶能力,本文按顺序展开:
- 静态目标——写好就固定的权重(60/40)。
- 动态目标——用信号输出作为每个 session 的目标敞口。
- 方向约束——只允许加仓或只允许减仓的非对称再平衡。
- 分批部署——有上限、有节奏地逐步建仓,而不是一步到位。
1. 静态目标:固定 60/40
TargetAllocation 由四部分组成:
| 环节 | 作用 | 举例 |
|---|---|---|
| 固定资产 | 你要持有哪些资产 | SPY、TLT |
| 目标权重 | 每个资产的最终目标比例 | SPY 0.60、TLT 0.40 |
| 可选开关 | 每个资产各自的启用条件(布尔信号) | SPY 是否站上 200 日线 |
| 再平衡节奏 | 多久把组合调回目标比例 | 每季度 / 每月 / 不自动再平衡 |
关键一点:weight 就是最终的、可直接执行的目标权重,不会再乘以其它 exposure 系数。SPY 0.60 + TLT 0.40 之和是 1.0,代表满仓;如果你写 SPY 0.50 + TLT 0.30(之和 0.80),剩余 0.20 就是现金,这是明确的一部分组合,不是"算错"。
因此 TargetAllocation 必须搭配 RebalancingCapitalStrategy,且其 gross_exposure 必须是 1.0——目标权重本身就是唯一的敞口来源,不能再叠加一个杠杆/敞口系数。
普通 60/40,季度再平衡
最经典的股债组合,没有开关,每个季度把漂移后的比例调回 60/40:
{
"strategy_definition": {
"trade_strategy": {
"indicators": [],
"signals": [],
"outputs": {}
},
"portfolio_construction": {
"type": "TargetAllocation",
"allocations": [
{ "symbol": "SPY", "weight": 0.60 },
{ "symbol": "TLT", "weight": 0.40 }
],
"rebalance": {
"frequency": "quarterly"
}
},
"capital_strategy": {
"name": "RebalancingCapitalStrategy",
"params": {
"initial_capital": 100000,
"gross_exposure": 1.0
}
}
}
}
各自独立的开关
SPY 和 TLT 各自用自己资产的趋势作为开关,互不影响:
{
"strategy_definition": {
"market_indicators": {
"indicators": [
{ "code": "SPY" },
{ "code": "TLT" }
],
"transformers": [
{ "name": "spy_price", "type": "IdentityTransformer", "params": { "indicator": "SPY", "field": "Close" } },
{ "name": "tlt_price", "type": "IdentityTransformer", "params": { "indicator": "TLT", "field": "Close" } },
{ "name": "spy_ma200", "type": "MovingAverageTransformer", "params": { "indicator": "SPY", "window": 200 } },
{ "name": "tlt_ma200", "type": "MovingAverageTransformer", "params": { "indicator": "TLT", "window": 200 } }
]
},
"trade_strategy": {
"indicators": [],
"signals": [
{ "id": "spy_gate", "type": "GreaterThan", "inputs": [{ "market": "SPY", "transformer": "spy_price" }, { "market": "SPY", "transformer": "spy_ma200" }] },
{ "id": "tlt_gate", "type": "GreaterThan", "inputs": [{ "market": "TLT", "transformer": "tlt_price" }, { "market": "TLT", "transformer": "tlt_ma200" }] }
],
"outputs": {}
},
"portfolio_construction": {
"type": "TargetAllocation",
"allocations": [
{ "symbol": "SPY", "weight": 0.60, "enabled_by": "spy_gate" },
{ "symbol": "TLT", "weight": 0.40, "enabled_by": "tlt_gate" }
],
"rebalance": {
"frequency": "quarterly"
}
},
"capital_strategy": {
"name": "RebalancingCapitalStrategy",
"params": { "initial_capital": 100000, "gross_exposure": 1.0 }
}
}
}
开关为 true 时,该资产按目标权重配置;为 false 时,该资产的目标精确归 0,剩余部分进现金。
多资产共享一个外部开关
开关信号可以引用 market_indicators 里的外部观察标的——它不必出现在组合的 traded symbols[] 里,也不占用交易标的数量上限,它只是"看"的信号来源:
{
"strategy_definition": {
"market_indicators": {
"indicators": [
{ "code": "QQQ" }
],
"transformers": [
{ "name": "qqq_price", "type": "IdentityTransformer", "params": { "indicator": "QQQ", "field": "Close" } },
{ "name": "qqq_ma200", "type": "MovingAverageTransformer", "params": { "indicator": "QQQ", "window": 200 } }
]
},
"trade_strategy": {
"indicators": [],
"signals": [
{ "id": "qqq_gate", "type": "GreaterThan", "inputs": [{ "market": "QQQ", "transformer": "qqq_price" }, { "market": "QQQ", "transformer": "qqq_ma200" }] }
],
"outputs": {}
},
"portfolio_construction": {
"type": "TargetAllocation",
"allocations": [
{ "symbol": "QQQM", "weight": 0.30, "enabled_by": "qqq_gate" },
{ "symbol": "TQQQ", "weight": 0.20, "enabled_by": "qqq_gate" }
],
"rebalance": {
"frequency": "monthly"
}
},
"capital_strategy": {
"name": "RebalancingCapitalStrategy",
"params": { "initial_capital": 100000, "gross_exposure": 1.0 }
}
}
}
这里 QQQM、TQQQ 是 traded 资产,QQQ 只是观察源。多个资产共用同一个 QQQ 趋势开关,是 TargetAllocation 的一等公民能力,不是某个策略的特例。
2. 动态目标敞口
固定权重只回答"目标是多少",动态目标回答"目标随信号怎么变"。
每个 allocation 恰好提供 weight 或 target_signal 之一:
weight:作者写死的固定目标比例。target_signal:引用trade_strategy.signals[]里一个权重输出(weight-output)信号的 id(例如ConditionalWeight、LinearScaleWeight),该信号每个 session 的输出就是这个资产的动态目标比例,范围 [0, 1]。
target_signal 必须引用权重输出信号,不存在任意"数字 → 权重"的隐式转换。当前引擎的一等权重输出原语主要是 ConditionalWeight 和 LinearScaleWeight。Add / Subtract / Multiply / Divide 等普通数值原语的数值结果不能直接用作 target_signal——生产校验中 ConditionalWeight + ConditionalWeight → Add → 数值 → target_signal 正是因此被正确拒绝。多档目标仍然可以表达:先做数值组合,再经过一个真实的权重输出原语(例如 LinearScaleWeight)映射成权重输出。当前没有 PiecewiseWeight 这样的分档权重原语,文档也不把多档敞口宣称为"不支持";如果真实研究持续暴露多档目标的高频需求,再考虑把分档权重做成一等原语,而不是在配置里近似。
动态目标的取值规则很直接:
- 目标变了,就是一次可执行的调整。
- 目标连续重复,就不再产生新指令——继续持有、自然漂移(hold-and-drift)。
- 没有经济死区:不存在"变化太小就忽略"的阈值,只有浮点误差级别的容差(1e-8)。
- 剩余部分依然是现金,组合总敞口不超过 1。
weight 与 target_signal 可以混用:一个静态资产 + 一个动态资产,同一次整体调仓一起执行。
动态目标示例:0.3–1.0 的连续动态敞口
下面用 LinearScaleWeight 把 SPY 的收盘价线性映射成目标敞口。它是连续映射(不是离散三档):weight = min_weight + (indicator − min_indicator) / (max_indicator − min_indicator) × (max_weight − min_weight)。按这套参数,价格 20 → 目标 0.30,价格 30 → 目标 0.65,价格 40 → 目标 1.00,中间任何价位都有对应的目标值。信号是确定性的,你可以在信号输出里逐 session 核对它产出的目标值:
{
"strategy_definition": {
"trade_strategy": {
"indicators": [],
"signals": [
{
"id": "spy_dynamic_weight",
"type": "LinearScaleWeight",
"inputs": [{ "column": "Close" }],
"params": {
"min_indicator": 20,
"max_indicator": 40,
"min_weight": 0.3,
"max_weight": 1.0
}
}
],
"outputs": {}
},
"portfolio_construction": {
"type": "TargetAllocation",
"allocations": [
{ "symbol": "SPY", "target_signal": "spy_dynamic_weight" }
],
"rebalance": {
"frequency": "none"
}
},
"capital_strategy": {
"name": "RebalancingCapitalStrategy",
"params": {
"initial_capital": 100000,
"gross_exposure": 1.0
}
}
}
}
rebalance.frequency: "none" 时,目标变化本身就是唯一的调仓触发来源——目标没变就继续持有,不会每天重算一遍。
3. 理想目标 ≠ 实际执行 ≠ 实际持仓
动态目标和方向约束放在一起时,最容易混淆三个不同的量:
| 概念 | 含义 |
|---|---|
| 理想目标(Ideal Target) | 规则写出来的目标比例,是策略本身的"答案"。 |
| 执行请求(Capital Request) | 这次实际要执行的动作,可能因为方向约束而被"按住"。 |
| 实际持仓(realized holding) | 组合里真正持有的比例。 |
三者可以各不相同。看一个例子:
- 理想目标 = 40%
- 当前实际持仓 = 31%
- 方向约束 =
decrease_only(只减不加)
结果:不买。理想目标保持 40% 不变,也不会被改写成 31%——理想目标始终是规则的输出,只是这一次的方向约束不允许朝它加仓。
区分这三者的意义:规则告诉你"应该是什么",执行层告诉你"这次做不做",持仓页告诉你"现在是什么"。三者对不上不一定是 bug,可能正是方向约束在工作。
4. 方向约束:非对称再平衡
每个 allocation 可以声明 direction,控制朝理想目标调整时的方向:
direction | 行为 |
|---|---|
both(默认) | 双向调整:高了减、低了加,回到理想目标。 |
increase_only | 只在理想目标 高于 当前持仓时加仓;低了不卖。 |
decrease_only | 只在理想目标 低于 当前持仓时减仓;低了不买。 |
方向约束作用于明确的再平衡事件上(开关翻转、动态目标变化、或 monthly/quarterly 到期),不是每时每刻的连续漂移修正。
举例:季度再平衡 + decrease_only:
- 当前持仓高于理想目标 → 在季度再平衡时减到目标。
- 当前持仓低于理想目标 → 什么都不做,理想目标保持不变。
注意一个直接后果:decrease_only 的资产从 0 仓位冷启动时,永远不会开仓(理想目标不会小于 0)——这是该方向语义下预期的行为,不是缺陷。
decrease_only 示例
SPY 的季度目标 40%,但只允许朝下调整:
{
"strategy_definition": {
"trade_strategy": {
"indicators": [],
"signals": [],
"outputs": {}
},
"portfolio_construction": {
"type": "TargetAllocation",
"allocations": [
{ "symbol": "SPY", "weight": 0.40, "direction": "decrease_only" }
],
"rebalance": {
"frequency": "quarterly"
}
},
"capital_strategy": {
"name": "RebalancingCapitalStrategy",
"params": {
"initial_capital": 100000,
"gross_exposure": 1.0
}
}
}
}
5. 有界分批部署(staged_deployment)
默认情况下,一次可执行的调仓会直接朝理想目标调整。staged_deployment 提供另一种节奏:以静态 weight 作为部署目标上限(Capital Request 上限),按回撤深度分批、有界地朝该上限部署,而不是一步到位。
它只支持最窄的一版契约:
- 必须搭配静态
weight:不能与target_signal同时使用。weight是部署目标上限——分批策略自己不会因为 staged 触发请求一个高于该上限的新目标。 - 必须显式
direction: "decrease_only":这是唯一能与分批触发安全组合的基础方向——非分批触发的可执行 session 上,它只会在当前仓位高于上限时下修,绝不会把仓位直接拉到上限。 - 每个字段都必填,没有隐藏默认值:
| 字段 | 含义 |
|---|---|
levels | 严格递增、去重的回撤幅度序列(相对锚点下跌 15% / 25% / 35% 就写 [0.15, 0.25, 0.35]) |
unit | 每新触发一个 level 部署的单位(占组合总 NAV 的比例,如 0.025 = 2.5 个百分点),不能超过该资产自身的 weight 部署目标上限 |
anchor_lookback_sessions | 计算滚动高点锚点的回看 session 数;决策窗口开始前的历史数据只用于锚点初始化,不会提前消费部署次数 |
gate | 引用一个布尔信号的 id,作为分批部署自身的武装/重置开关 |
reset_on_new_high | 布尔值:true 时,出现真正超过锚点的新高就结束当前周期、开启新周期(之前的部署阶梯作废重来);false 时锚点在周期内永久冻结,只有 gate 关闭才重置 |
周期内的行为规则:
- 一个周期内每个 level 只触发一次部署。
- 反弹不会撤销已触发的部署;再次回到同一 level 不会重复部署。
- 一次 gap 跌穿多个新 level 时,本次一并计入。
- gate 关闭会重置周期(清空已用 level、解冻锚点),但不改变该资产的理想目标——gate 与
enabled_by是两个独立概念(enabled_by翻转会把理想目标清零)。 - 实际的有效 gate = 分批 gate AND
enabled_by权限:enabled_by = false除了把理想目标归零,也会使分批周期失效并重置(已用 level 清空、锚点解冻);重新启用后从全新的周期开始,不会继承旧的已用 level。 - 首次触发会冻结锚点。
- 每次执行收到的都是绝对目标值,从来不是"再加 X"的相对指令。
- 部分执行失败后的重试,继续朝同一个绝对目标推进。
- 分批部署只在还有容量时增加仓位;若当前持仓已经达到/超过部署目标上限,新 level 只会被记录,本次执行请求保持当前仓位——分批策略不会主动卖出。
- 持仓因漂移超过上限时,由既定的再平衡事件或
decrease_only方向约束负责减回来,分批部署不承担这个职责。
weight 不是实际持仓的连续硬顶:它约束的是"分批部署会请求到什么目标",而不是"实际持仓永远不许超过 20%"。持仓会因价格漂移超过部署目标上限——例如当前已持有 22%,而 weight = 0.20——此时 staged 触发不会把仓位卖回 20%:分批部署是有界的、只增不减的部署机制,不是持续强制执行的仓位上限/风控限额。要不要减回 20% 取决于既定的再平衡事件、方向性 Capital Request 或其他合法的可执行事件;在 rebalance: "none" 的 hold-and-drift 下,实际持仓可以长期漂移在部署目标上限之上。这正呼应第 3 节:理想目标 ≠ 执行请求 ≠ 实际持仓。
最小分批部署示例
SPY 部署目标上限 20%(decrease_only),从滚动高点每回撤 15% 部署 2.5 个百分点、回撤 25% 再部署 2.5 个百分点,站上新高后重来。gate 用真实可执行的图形状:先用 SMA indicator 算出 200 日均线,再用 LessThan 判断收盘价是否在均线下方(引用 ref,而不是把 "SMA200" 当成一个裸数据列名):
{
"strategy_definition": {
"trade_strategy": {
"indicators": [
{
"id": "sma200",
"type": "SMA",
"params": { "period": 200, "column": "Close" }
}
],
"signals": [
{
"id": "spy_gate",
"type": "LessThan",
"inputs": [
{ "column": "Close" },
{ "ref": "sma200" }
]
}
],
"outputs": {}
},
"portfolio_construction": {
"type": "TargetAllocation",
"allocations": [
{
"symbol": "SPY",
"weight": 0.20,
"direction": "decrease_only",
"staged_deployment": {
"levels": [0.15, 0.25],
"unit": 0.025,
"anchor_lookback_sessions": 20,
"gate": "spy_gate",
"reset_on_new_high": true
}
}
],
"rebalance": {
"frequency": "none"
}
},
"capital_strategy": {
"name": "RebalancingCapitalStrategy",
"params": {
"initial_capital": 100000,
"gross_exposure": 1.0
}
}
}
}
这里刻意让 gate 在弱势期保持武装(收盘价低于 200 日均线),与"回撤时分批部署"的经济意图一致。要分清两层:
- gate 只是分批周期的许可开关,不是回撤触发条件本身。真正的触发由锚点 +
levels决定。 - 如果 gate 与回撤条件在经济上冲突(例如回撤越深时 gate 反而关闭,如"收盘价必须在均线上方"),配置完全合法也可能让分批部署永远不触发。
生产 smoke 印证了这一点:用 Close > SMA200 作 gate 的 run 从未触发(深度回撤时收盘价往往已在均线下方,gate 先关掉了);改用能保持武装的 gate 后,实际完成了多次 staged 事件(level crossing / once-per-level / reset / +unit 部署)。
精确语义
| 语义 | 规则 |
|---|---|
weight | 最终的、可直接执行的目标权重,不再乘以 gross_exposure;静态 allocation 的 weight 之和必须 ≤ 1.0 |
target_signal | 引用真实存在的权重输出信号 id;与 weight 恰好二选一 |
| 动态目标变化 | 可执行的调整;重复目标 = 无新指令、hold-and-drift;无经济死区(只有 1e-8 浮点容差) |
| 剩余权重 | 自动作为现金(residual cash),组合总敞口 ≤ 1 |
gross_exposure | TargetAllocation 必须为 1.0,否则校验拒绝 |
enabled_by 缺省 | 该资产始终启用 |
enabled_by = false | 该资产目标精确为 0 |
enabled_by 的布尔来源 | 必须解析为布尔信号,可引用 market_indicators 外部观察标的 |
| 外部观察标的 | 只作信号来源,不必是 traded 标的,不占 traded 数量上限 |
direction | both(默认)/ increase_only / decrease_only,作用于明确的再平衡事件,不连续漂移修正 |
staged_deployment | 仅静态 weight + 显式 decrease_only;五个字段全部必填 |
allocation.weight(用于 staged_deployment) | 部署目标上限(Capital Request 上限),不是实际持仓的连续硬顶;weight 是 allocation 的 sibling 字段,不在 staged_deployment 对象内;持仓可因漂移高于它,staged 触发不会卖回上限 |
rebalance.frequency | 必填:none / monthly / quarterly,无隐藏默认值 |
| 开关翻转 / 动态目标变化 | 立即重算整份目标向量(所有启用资产回到各自目标,禁用资产归 0) |
none | 只在目标变化(开关翻转 / 动态目标 / 分批触发)时整体再平衡,其余时间 hold-and-drift |
monthly / quarterly | 在目标变化之外,额外按周期整体再平衡 |
| 重复资产 / 负权重 / 静态权重和大于 1 | 校验拒绝 |
和轮动(Cross-sectional)的区别
两者都用"完整目标权重向量"执行,但选择权重的方式完全不同:
| TargetAllocation | Cross-sectional | |
|---|---|---|
| 资产清单 | 作者固定写明 | 候选池固定,但每一期由评估器选择/排名 |
| 权重来源 | 作者写死(weight)或引用动态信号(target_signal) | 评估器归一化后产出完整向量(如等权前 N) |
| 开关 | 每个资产可选的布尔 enabled_by | 资格条件决定谁参与排名 |
| 定位 | "我想长期 60/40(或随信号调整目标),加个趋势开关" | "我想每期自动选最强的几只" |
两者不互相替代。想长期固定/动态目标比例用 TargetAllocation;想定期在候选里择优用 Cross-sectional。
常见问题
为什么不用 trade_strategy.outputs 写买卖信号?
TargetAllocation 的目标权重完全由 portfolio_construction 产生,trade_strategy.outputs 必须留空({})。trade_strategy 仍然必填,用来承载 enabled_by / target_signal / gate 引用的信号(没有信号时可以是空的 indicators / signals 数组)。写成 outputs.buy_signal 反而会被校验拒绝。
我可以自己写 ADD / REDUCE 这样的执行指令吗?
不能,也没有这个必要。执行层收到的一直是绝对目标权重,不存在"加 X"或"减 X"的相对指令。想表达"逐步加仓",用第 5 节的分批部署;想表达"只减不加",用 direction。自定义的状态机(例如"连续 N 天上涨才启动")不在当前契约内,不要用这些字段去近似实现——validate_strategy_config 和 Engine 运行时是最终的活契约,近似出来的配置过不了校验就是过不了。
理想目标变了,为什么持仓没动?
先看方向约束:decrease_only 在理想目标高于持仓时不会买。再看再平衡节奏:none 时只有目标变化才触发。理想目标、执行请求、实际持仓是三个不同的量,对不上时先分别核对。
怎么确认分批部署真的发生了?
summary.total_trades 是交易生命周期口径(更接近"平仓结算"的次数),不等于目标调整、订单或执行次数,不要用它反推 staged 触发了几次。核对分批部署要看组合执行证据:portfolio.db 的 capital_request_records(每次 staged 触发都是一条绝对目标的 Capital Request,可核对 level crossing、once-per-level、reset、+unit 部署)以及 signals.db / 相关的 Lab evidence。没有下载 SQLite 工件时,先从 get_lab_run 的 evidence 里找 action 事件记录。
TargetAllocation 能用来合成多个策略的净值吗?
不能。把多个策略/组合按比例合成净值属于衍生分析,不是一次 Strategy DSL 运行,用 多策略分析 的边界,不要用 TargetAllocation 的 allocation 去"组合策略 NAV"。