Pair Tradability Score(PTS)最小可用版本
很好,这一步问得非常对。
因为 MVP 的关键不是“少”,而是“少而不偏”。
下面我会直接给你一个可以今天就落地的 Pair Tradability Score(PTS)最小可用版本,它满足:
- ✅ 覆盖你提出的 5 个维度的“信息核心”
- ✅ 不依赖 ACF / GARCH / KPSS(先别上)
- ✅ 参数少、解释性强
- ✅ 能直接接到你现在的
zscore_analysis流程
一、MVP 设计原则(为什么要这样压缩)
我们先明确一件事:
MVP 的目标不是“评估全面”,而是“防止犯致命错误”
在配对交易里,真正会一票否决一个 pair 的只有三类问题:
- 赚不到钱(波动 < 成本)
- 钱回不来(回归太慢)
- 关系在漂(不稳定)
所以 MVP 必须强约束这三点,其余的(ACF 细节、异方差形态)先降级。
二、MVP 的 5 → 3 → 1 压缩逻辑
原始 5 维度
| 原维度 | 是否保留 | MVP 处理方式 |
|---|---|---|
| 平稳性 | ✅ | 二值 gate(不过直接 reject) |
| 波动幅度 | ✅ | 连续得分 |
| 回归速度 | ✅ | 连续得分(最重要) |
| 自相关结构 | ❌ | 暂不显式建模 |
| 稳定性 | ✅ | 轻量 proxy |
MVP 实际只算 4 个量
用 4 个 scalar → 1 个 PTS
三、Pair Tradability Score(MVP)定义
1️⃣ 必要门槛(Gate,不计分)
ADF p < 0.05
不通过:
→ PTS = 0,直接 reject
2️⃣ 四个核心指标(全部 ∈ [0, 1])
🟦 (A) 波动 / 成本充分性得分 S_vol
直觉:
能不能覆盖手续费,而且不是偶尔一下
vol_cost_ratio = spread_std / round_trip_cost
映射到分数(soft clip):
S_vol = clip( (vol_cost_ratio - 2) / (8 - 2), 0, 1 )
解释:
| ratio | S_vol |
|---|---|
| ≤2 | 0 |
| 5 | 0.5 |
| ≥8 | 1 |
🟦 (B) 回归速度得分 S_mr(最重要)
用你已经实现的 AR(1) 半衰期(小时)。
设定一个 理想区间:
T_min < half_life < T_max
但 不是硬 cut,而是钟形得分:
S_mr = exp( - |log(half_life / T_opt)| )
其中:
| timeframe | T_opt |
|---|---|
| 5m | 2h |
| 1h | 12h |
| 4h | 48h |
性质:
- 太快(噪声)→ 低分
- 太慢(资金占用)→ 低分
- 只奖励“刚刚好”
🟦 (C) 稳定性 proxy 得分 S_stab
MVP 不做完整 rolling beta,只用 分段方差稳定性:
var_ratio = max(std_1, std_2) / min(std_1, std_2)
映射为:
S_stab = exp( - (var_ratio - 1) )
| var_ratio | S_stab |
|---|---|
| 1.0 | 1.00 |
| 1.5 | 0.61 |
| 2.0 | 0.37 |
| ≥3 | ~0 |
🟦 (D) 有效偏离频率得分 S_freq
防止“95% 时间贴着 0 抖动”的假波动:
freq = P(|spread| > 1.5 * spread_std)
S_freq = clip( freq / 0.15 , 0, 1 )
3️⃣ 最终 PTS 聚合(不是平均!)
这是关键。
❌ 不要:
( S_vol + S_mr + S_stab + S_freq ) / 4
✅ 推荐:加权几何平均
PTS = (
S_vol ** 0.25 *
S_mr ** 0.40 *
S_stab** 0.20 *
S_freq** 0.15
)
原因:
- 任一维度接近 0 → PTS 快速塌陷
- 回归速度权重最大(你工程直觉是对的)
4️⃣ 决策规则(MVP)
PTS ≥ 0.6 → 高质量可交易
0.4–0.6 → 观察 / 降权
< 0.4 → reject
四、MVP 版 compute_pair_tradability_score
def compute_pts_mvp(spread, half_life_hours, timeframe, fee_rate):
# ---------- A. 波动 / 成本 ----------
spread_std = spread.std()
round_trip_cost = fee_rate * 2
vol_cost_ratio = spread_std / round_trip_cost
S_vol = np.clip((vol_cost_ratio - 2) / 6, 0, 1)
# ---------- B. 回归速度 ----------
T_opt = {'5m': 2, '1h': 12, '4h': 48}.get(timeframe, 12)
S_mr = np.exp(-abs(np.log(half_life_hours / T_opt)))
# ---------- C. 稳定性 ----------
mid = len(spread) // 2
std1 = spread.iloc[:mid].std()
std2 = spread.iloc[mid:].std()
var_ratio = max(std1, std2) / max(min(std1, std2), 1e-8)
S_stab = np.exp(-(var_ratio - 1))
# ---------- D. 有效偏离频率 ----------
freq = (spread.abs() > 1.5 * spread_std).mean()
S_freq = np.clip(freq / 0.15, 0, 1)
# ---------- PTS ----------
pts = (
S_vol ** 0.25 *
S_mr ** 0.40 *
S_stab** 0.20 *
S_freq** 0.15
)
return {
'PTS': pts,
'components': {
'S_vol': S_vol,
'S_mr': S_mr,
'S_stab': S_stab,
'S_freq': S_freq,
'vol_cost_ratio': vol_cost_ratio,
'half_life_hours': half_life_hours,
'var_ratio': var_ratio,
'freq': freq
}
}
五、为什么这个 MVP 很适合你现在用
✅ 不破坏你现有结构
- 接在
cointegration_result['spread']后 - 只多一次 AR(1)
✅ 能直接排序 + 筛选
- 用 PTS 排序所有 pair
- 选 Top-K 或设阈值
✅ 为 Phase 2 留足接口
以后你可以:
- 把 ACF 得分并入
S_mr - 把 GARCH 并入
S_stab - 把 rolling beta 稳定性替换 var_ratio