MarketMaker.cc Team
量化研究与策略
MarketMaker.cc Team
量化研究与策略
你有两个策略。第一个:PnL +300%,418笔交易,持仓时间占45%。第二个:PnL +27%,38笔交易,持仓时间占5%。哪个更好?
如果你选了第一个——你答错了。原因如下。
原始PnL——整个回测期间的总收益率——没有考虑策略在仓时间占比。一个PnL +300%、交易时间45%的策略,使用你的资金不到一半时间。剩余55%的时间,资金处于闲置状态。
一个PnL +27%、交易时间5%的策略仅使用资金5%的时间——但剩余95%的时间可以用于其他策略。
如果你通过编排器运行策略组合,一个策略的空闲时间会被其他策略填充。那么关键指标就不是策略一年赚了多少,而是每单位活跃时间赚了多少。

其中:
def pnl_per_active_time(
total_pnl: float, # 总PnL,%
test_period_days: int, # 回测长度,天数
trading_time_pct: float, # 活跃时间占比,0..1
fill_efficiency: float = 0.80, # 槽位填充效率
) -> dict:
"""
按活跃时间计算有效收益率。
"""
active_days = test_period_days * trading_time_pct
pnl_per_day = total_pnl / active_days
annualized_raw = pnl_per_day * 365
annualized_effective = annualized_raw * fill_efficiency
return {
"active_days": active_days,
"pnl_per_day": pnl_per_day,
"annualized_raw": annualized_raw,
"annualized_effective": annualized_effective,
}
周期:750天(25个月),fill_efficiency = 0.80:
| 策略 | PnL | 交易时间 | 活跃天数 | PnL/天 | 年化收益 (x0.8) |
|---|---|---|---|---|---|
| Strategy C | +300% | 45% | 337.5 | 0.89%/天 | 259% |
| Strategy B | +27% | 5% | 37.5 | 0.72%/天 | 210% |
| Strategy A | +58% | 15% | 112.5 | 0.51%/天 | 150% |
按原始PnL排名:Strategy C (300%) >> Strategy A (58%) >> Strategy B (27%)。 按有效收益率排名:Strategy C (259%) > Strategy B (210%) > Strategy A (150%)。
PnL仅27%的Strategy B竟然与PnL 300%的Strategy C相当——因为它在少9倍的活跃时间内赚取了同样的收益。剩余95%的时间可以用其他策略填充。
上述公式是线性的。它更简单也更保守。复合变体考虑了利润再投资:
import numpy as np
def compound_annualized(total_pnl_pct, active_days, fill_efficiency=0.80):
"""复合外推。"""
daily_return = (1 + total_pnl_pct / 100) ** (1 / active_days) - 1
annualized = (1 + daily_return) ** (365 * fill_efficiency) - 1
return annualized * 100
b_compound = compound_annualized(27, 37.5)
c_compound = compound_annualized(300, 337.5)
在复合外推下,Strategy B 超越了 Strategy C:540% vs 231%。排名完全反转。
**建议:**使用线性外推进行排名。它更保守,不易奖励少量交易中的过拟合。
Strategy B有38笔交易,PnL/天 = 0.72%,看起来很有吸引力。但38笔交易在统计上是一个薄弱的样本。高PnL/天可能只是运气好的结果。
我们使用t分布对小样本进行惩罚:
其中 是每笔交易的平均收益率, 是标准差, 是交易数量, 是t分布的分位数。
import scipy.stats as st
import numpy as np
def confidence_adjusted_score(
trade_returns: list,
test_period_days: int,
fill_efficiency: float = 0.80,
min_trades: int = 30,
confidence: float = 0.95,
) -> dict:
"""
基于样本量调整的策略排名。
"""
n = len(trade_returns)
if n < min_trades:
return {"score": 0, "reason": f"Too few trades ({n} < {min_trades})"}
returns = np.array(trade_returns)
mean_ret = np.mean(returns)
se = np.std(returns, ddof=1) / np.sqrt(n)
alpha = 1 - confidence
t_crit = st.t.ppf(1 - alpha / 2, df=n - 1)
ci_lower = mean_ret - t_crit * se
if mean_ret <= 0:
confidence_factor = 0
else:
confidence_factor = max(0, ci_lower / mean_ret)
total_pnl = np.sum(returns)
hold_times = [...] # 每笔交易的持仓小时数
active_days = sum(hold_times) / 24
pnl_per_day = total_pnl / active_days if active_days > 0 else 0
annualized = pnl_per_day * 365 * fill_efficiency
score = annualized * max_leverage * confidence_factor
return {
"score": score,
"annualized": annualized,
"confidence_factor": confidence_factor,
"ci_lower": ci_lower,
"n_trades": n,
}
| 策略 | 交易数 | 平均收益 | SE | CI下界 | 置信因子 | 调整后得分 |
|---|---|---|---|---|---|---|
| Strategy B | 38 | 0.71% | 0.28% | 0.14% | 0.20 | 210% x 0.20 = 42% |
| Strategy C | 418 | 0.72% | 0.05% | 0.62% | 0.86 | 259% x 0.86 = 223% |
| Strategy A | 491 | 0.12% | 0.02% | 0.08% | 0.67 | 150% x 0.67 = 100% |
经过置信度调整后,Strategy C稳居领先:418笔交易提供了窄置信区间和高置信因子。Strategy B只有38笔交易受到惩罚——其"亮眼"表现可能只是方差的结果。

fill_efficiency参数回答的问题是:"编排器能让资金工作多大比例的时间?"
最简单的方法:所有策略统一使用fill_efficiency = 0.80。假设编排器利用80%的空闲时间运行其他策略/交易对。
**优点:**对所有策略一视同仁,便于比较。 **缺点:**未考虑策略间的相关性。
如果你有 个交易对,每个活跃 的时间,至少一个活跃的概率为:
但加密货币高度相关——BTC带动ETH、SOL和其他币种。有效独立交易对数量:
def estimate_fill_efficiency(
trading_time_pct: float,
n_pairs: int,
correlation_factor: float = 3.0, # 加密货币——高相关性
max_slots: int = 10,
) -> float:
"""
fill_efficiency的解析估算。
Args:
trading_time_pct: 单个策略的活跃时间占比
n_pairs: 交易对数量
correlation_factor: 相关系数(1=独立,5=强相关)
max_slots: 最大同时持仓数
"""
effective_n = n_pairs / correlation_factor
p_at_least_one = 1 - (1 - trading_time_pct) ** effective_n
expected_active = effective_n * trading_time_pct
utilization = min(expected_active, max_slots) / max_slots
return min(p_at_least_one, utilization)
eff_b = estimate_fill_efficiency(0.05, 10, 3.0)
eff_c = estimate_fill_efficiency(0.45, 10, 3.0)
对于活跃度仅5%、拥有10个相关交易对的Strategy B,fill_efficiency仅约16%。这极大地降低了有效收益率。
最精确的方法是在所有交易对上运行所有策略,并计算实际槽位利用率:
def simulate_fill_efficiency(
all_signals: dict, # {(strategy, pair): [(entry_time, exit_time), ...]}
max_slots: int = 10,
test_period_minutes: int = 750 * 24 * 60,
) -> float:
"""
模拟编排器的实际槽位利用率。
"""
timeline = np.zeros(test_period_minutes)
for signals in all_signals.values():
for entry_min, exit_min in signals:
timeline[entry_min:exit_min] += 1
capped = np.minimum(timeline, max_slots)
fill_efficiency = np.mean(capped) / max_slots
return fill_efficiency
整合所有组件:
def strategy_score(
trades: list,
test_period_days: int,
fill_efficiency: float = 0.80,
min_trades: int = 30,
funding_rate: float = 0.0001,
) -> float:
"""
策略排名的最终得分。
考虑因素:
- 每活跃天PnL(资金使用效率)
- MaxLev(风险调整后的杠杆倍数)
- 置信度调整(小样本惩罚)
- 资金费率成本(杠杆下的实际成本)
"""
n = len(trades)
if n < min_trades:
return 0
returns = np.array([t.pnl_pct for t in trades])
hold_hours = np.array([t.hold_hours for t in trades])
total_pnl = np.sum(returns)
active_days = np.sum(hold_hours) / 24
pnl_per_day = total_pnl / active_days
equity = np.cumprod(1 + returns / 100)
peak = np.maximum.accumulate(equity)
max_dd = ((equity - peak) / peak).min()
max_lev = max(1, int(50 / abs(max_dd * 100)))
funding_daily = funding_rate * 3 * max_lev * 100 # 单位:%
net_pnl_per_day = pnl_per_day - funding_daily
annualized = net_pnl_per_day * 365 * fill_efficiency
se = np.std(returns, ddof=1) / np.sqrt(n)
mean_ret = np.mean(returns)
if mean_ret <= 0:
return 0
t_crit = st.t.ppf(0.975, df=n - 1)
ci_lower = mean_ret - t_crit * se
conf_factor = max(0, ci_lower / mean_ret)
score = annualized * max_lev * conf_factor
return score
这个指标不是替代,而是补充前文中的工具:
**亏损与利润的不对称性:**最大回撤决定了MaxLev,而MaxLev是评分公式的组成部分。回撤越深,得分越低——由于恢复的不对称性,这是非线性的。
**蒙特卡洛自助法:**自助法的置信区间比t分布提供了更准确的置信因子估计。你可以用自助法的第5百分位数替代t分布的CI。
**资金费率:**资金费率成本从每活跃天PnL中扣除。在高杠杆和低PnL/天的情况下,资金费率可能使净得分为负——尽管原始PnL为正,策略实际上是亏损的。
每活跃时间PnL是编排器中策略排名的核心指标。当多个策略竞争同一个槽位时——经过置信度调整后得分最高的策略获胜。
在实践中,这导致了出人意料的决策:原始PnL"不起眼"但持仓时间短的策略,往往比PnL高但持仓时间长的"明星"策略获得更高优先级。前者在数十个策略组成的投资组合中更高效地使用资金。
关键洞察:唯一可以扩展的指标是每活跃天PnL。原始PnL无法扩展:你不能把同一个策略运行两次。但你可以用其他策略填充空闲时间——而每活跃天PnL能准确预测你在投资组合中能赚多少。
年度原始PnL是一个方便但具有误导性的指标。它没有考虑交易者最重要的资源——资金工作的时间。
三个要点:
**计算每活跃天PnL。**持仓38天获得+27%的策略 = +0.72%/天。持仓338天获得+300%的策略 = +0.89%/天。差距不是11倍,而是1.2倍。
**考虑fill_efficiency。**在相关加密货币交易对组成的投资组合中,fill_efficiency比看起来要低。10个交易对不等于10倍分散化。当correlation_factor = 3时,有效交易对数仅约3个。
**惩罚小样本。**38笔交易、均值+0.71%的CI为+0.14%到+1.28%。418笔交易、均值+0.72%的CI为+0.62%到+0.82%。第二个策略更可靠,尽管均值几乎相同。
每活跃时间PnL指标不是替代PnL@MaxLev——它补充了后者,增加了资金使用效率这个维度。对于单个策略,PnL@ML就够了。对于策略组合,每活跃时间PnL是必不可少的。
@article{soloviov2026pnlactivetime, author = {Soloviov, Eugen}, title = {按活跃时间计算PnL:改变策略排名的指标}, year = {2026}, url = {https://marketmaker.cc/ru/blog/post/pnl-active-time-metric}, version = {0.1.0}, description = {为什么年度原始PnL不适合比较不同交易时间的策略。如何计算有效收益率,为什么需要fill\_efficiency,以及为什么27\% PnL的策略可能优于300\%的策略。} }