因子投资-实验07-加入成本和组合约束

QUANT RESEARCH NOTE LAB 07

把股票信号转成受约束多头权重:单股≤35%,两个行业相对各 50% 基准偏离≤10%,单边换手≤30%。随后计算佣金、卖出印花税与滑点,断言净收益不高于毛收益且所有约束在 1e-8 内满足。

类型动手实验
模块研究实验
阅读约 6 分钟
材料9 节 · 1 段代码

问题

把股票信号转成受约束多头权重:单股≤35%,两个行业相对各 50% 基准偏离≤10%,单边换手≤30%。随后计算佣金、卖出印花税与滑点,断言净收益不高于毛收益且所有约束在 1e-8 内满足。

输入数据

6 只股票,前三只属于 X 行业,后三只属于 Y;当前与基准均等权。信号从 1 降到 0,未来收益仅用于事后计算,不进入优化。

成本参数:佣金 3bp(买卖双边)、卖出印花税 5bp、滑点 10bp(买卖双边)。这是教学参数,不代表任一历史日期的真实费率。

逐步实现

优化目标最大化信号暴露;用辅助变量表示每只股票的绝对交易量,再交给 HiGHS 线性规划。行业约束写成上下两个不等式,换手使用当前漂移后权重与目标权重之差。

交易分为 buy=max(Δw,0)、sell=max(-Δw,0)。印花税只加在 sell,其他线性成本加在两边。

完整代码

PYTHON 可复现实验
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import numpy as np
from scipy.optimize import linprog

signal = np.array([1.0, 0.8, 0.5, 0.2, 0.1, 0.0])
industry_x = np.array([True, True, True, False, False, False])
previous = np.repeat(1 / 6, 6)
benchmark = np.repeat(1 / 6, 6)
future_return = np.array([0.03, 0.01, -0.01, 0.02, 0.00, -0.02])

stock_cap = 0.35
industry_limit = 0.10
turnover_limit = 0.30

# 决策向量为 [w_1...w_6, u_1...u_6],u_i >= |w_i-previous_i|。
n = len(signal)
c = np.r_[-signal, np.zeros(n)]
a_ub = []
b_ub = []
for i in range(n):
row_positive = np.zeros(2 * n)
row_positive[i] = 1
row_positive[n + i] = -1
a_ub.append(row_positive)
b_ub.append(previous[i])

row_negative = np.zeros(2 * n)
row_negative[i] = -1
row_negative[n + i] = -1
a_ub.append(row_negative)
b_ub.append(-previous[i])

turnover_row = np.r_[np.zeros(n), 0.5 * np.ones(n)]
a_ub.append(turnover_row)
b_ub.append(turnover_limit)

industry_row = np.r_[industry_x.astype(float), np.zeros(n)]
a_ub.extend([industry_row, -industry_row])
b_ub.extend([0.5 + industry_limit, -(0.5 - industry_limit)])

a_eq = np.r_[np.ones(n), np.zeros(n)].reshape(1, -1)
result = linprog(
c,
A_ub=np.asarray(a_ub),
b_ub=np.asarray(b_ub),
A_eq=a_eq,
b_eq=np.array([1.0]),
bounds=[(0.0, stock_cap)] * n + [(0.0, None)] * n,
method="highs",
)
weights = result.x[:n]

trade = weights - previous
buy = np.clip(trade, 0, None).sum()
sell = np.clip(-trade, 0, None).sum()
one_way_turnover = 0.5 * np.abs(trade).sum()

commission = 0.0003
stamp_tax = 0.0005
slippage = 0.0010
cost = (
commission * (buy + sell)
+ stamp_tax * sell
+ slippage * (buy + sell)
)
gross = weights @ future_return
net = gross - cost

tol = 1e-8
industry_x_weight = weights[industry_x].sum()
assert result.success
assert np.isclose(weights.sum(), 1.0, atol=tol)
assert np.all(weights >= -tol)
assert np.all(weights <= stock_cap + tol)
assert 0.5 - industry_limit - tol <= industry_x_weight
assert industry_x_weight <= 0.5 + industry_limit + tol
assert one_way_turnover <= turnover_limit + tol
assert net <= gross + tol

print(weights.round(6))
print(gross, cost, net, one_way_turnover, industry_x_weight)

预期输出

高信号股票获得更高权重,但单股不超过 35%,X 行业不超过 60%,换手不超过 30%。成本为非负,净收益≤毛收益。具体最优权重最后几位可能随优化器版本略变,结构断言应稳定。

断言

资金和为 1、权重在 [0,0.35]、行业 X 在 [0.4,0.6]、单边换手≤0.3、净收益≤毛收益,容差均为 1e-8。真实项目还需对每个行业、风格、不可交易股票、整手和现金逐项复核,不能只相信优化器 success。

结果解释

约束改变的是可行集合。最高信号不能无限集中;行业中性避免把行业押注误称选股;换手预算迫使组合保留部分旧持仓。成本是由实际交易而非目标权重水平产生。

未来收益只在优化完成后用于评价。若把它放入目标函数,代码会变成完美前视优化;真实 μ 只能来自形成日可得模型。

常见故障

  • 使用调仓前目标而非收益漂移后权重算换手。
  • 印花税同时加买卖两边。
  • 行业约束只写上限没有下限。
  • 优化 success 后不复核残差。
  • 不可交易股票仍自由调权。
  • 成本单位把 bp 写成百分数。
  • 用未来收益当预期收益。

练习

  1. 将单股上限改为 20%,观察可行性。
  2. 加入第三行业和相对基准约束。
  3. 用漂移后权重计算换手。
  4. 加平方根冲击和资金规模。
  5. 让一只股票跌停不可卖,冻结或只允许增持/减持方向。

因子投资-实验07-加入成本和组合约束
https://www.polarishaw.top/2026/07/17/因子投资-实验07-加入成本和组合约束/
作者
Polaris
发布于
2026年7月17日
许可协议