활성 시간 기준 PnL: 전략 순위를 바꾸는 지표
두 가지 전략이 있습니다. 첫 번째: PnL +300%, 418 거래, 포지션 보유 시간 45%. 두 번째: PnL +27%, 38 거래, 포지션 보유 시간 5%. 어떤 것이 더 좋습니까?
첫 번째를 선택했다면 — 틀렸습니다. 그 이유를 설명합니다.
원시 PnL의 문제
원시 PnL — 전체 백테스트 기간의 총 수익률 — 은 전략이 포지션에 있었던 시간 비율을 고려하지 않습니다. +300%에 거래 시간 45%인 전략은 절반도 안 되는 시간 동안 자본을 사용합니다. 나머지 55%의 시간 동안 자본은 유휴 상태입니다.
+27%에 거래 시간 5%인 전략은 5%의 시간만 자본을 사용합니다 — 하지만 나머지 95%는 다른 전략에 활용 가능합니다.
오케스트레이터를 통해 전략 포트폴리오를 실행하면, 한 전략의 유휴 시간이 다른 전략으로 채워집니다. 이때 핵심 지표는 전략이 1년간 얼마를 벌었는지가 아니라, 활성 시간 단위당 얼마를 버는지입니다.
실효 수익률 공식

기본 계산
여기서:
- Active days — 포지션 보유 총 시간(일 단위)
- fill_efficiency — 오케스트레이터가 시그널로 채울 수 있는 시간 비율(0...1)
def pnl_per_active_time(
total_pnl: float, # total PnL, %
test_period_days: int, # backtest length, days
trading_time_pct: float, # fraction of active time, 0..1
fill_efficiency: float = 0.80, # slot fill efficiency
) -> dict:
"""
Calculate effective return per active time.
"""
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) |
|---|---|---|---|---|---|
| 전략 C | +300% | 45% | 337.5 | 0.89%/일 | 259% |
| 전략 B | +27% | 5% | 37.5 | 0.72%/일 | 210% |
| 전략 A | +58% | 15% | 112.5 | 0.51%/일 | 150% |
원시 PnL: 전략 C (300%) >> 전략 A (58%) >> 전략 B (27%). 실효 수익률: 전략 C (259%) > 전략 B (210%) > 전략 A (150%).
PnL 27%의 전략 B가 PnL 300%의 전략 C와 비슷한 것으로 밝혀집니다 — 9분의 1의 활성 시간으로 같은 금액을 벌기 때문입니다. 나머지 95%의 시간은 다른 전략으로 채울 수 있습니다.
선형 vs 복리 외삽
위 공식은 선형입니다. 더 간단하고 보수적입니다. 복리 변형은 이익 재투자를 고려합니다:
import numpy as np
def compound_annualized(total_pnl_pct, active_days, fill_efficiency=0.80):
"""Compound extrapolation."""
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)
복리 외삽에서 전략 B가 전략 C를 역전합니다: 540% vs 231%. 순위가 뒤집힙니다.
권장: 순위에는 선형 외삽을 사용하세요. 더 보수적이고 적은 거래 수에서의 과적합에 덜 취약합니다.
함정: 적은 거래 수
38 거래에 PnL/일 = 0.72%인 전략 B는 매력적으로 보입니다. 하지만 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:
"""
Strategy ranking with sample size adjustment.
"""
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 = [...] # holding hours for each trade
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 하한 | 신뢰도 계수 | 조정 점수 |
|---|---|---|---|---|---|---|
| 전략 B | 38 | 0.71% | 0.28% | 0.14% | 0.20 | 210% x 0.20 = 42% |
| 전략 C | 418 | 0.72% | 0.05% | 0.62% | 0.86 | 259% x 0.86 = 223% |
| 전략 A | 491 | 0.12% | 0.02% | 0.08% | 0.67 | 150% x 0.67 = 100% |
신뢰도 조정 후, 전략 C가 확실히 선두: 418 거래는 좁은 CI와 높은 신뢰도 계수를 제공합니다. 38 거래의 전략 B는 페널티를 받습니다 — "훌륭한" 성과가 분산의 결과일 수 있습니다.
fill_efficiency: 어디서 얻는가

fill_efficiency 매개변수는 다음 질문에 답합니다: "오케스트레이터가 자본을 가동 상태로 유지할 수 있는 시간 비율은?"
옵션 1: 고정 상수
가장 간단한 접근: 모든 전략에 fill_efficiency = 0.80. 오케스트레이터가 유휴 시간의 80%를 다른 전략/페어로 활용한다고 가정.
장점: 모두에게 동일, 비교 용이. 단점: 전략 간 상관관계 미고려.
옵션 2: 분석적 추정
개의 페어가 있고 각각 의 시간 활성인 경우, 적어도 하나가 활성일 확률:
하지만 암호화폐는 높은 상관관계가 있습니다 — BTC가 ETH, SOL 등을 함께 끌고 갑니다. 독립 페어의 실효 수:
def estimate_fill_efficiency(
trading_time_pct: float,
n_pairs: int,
correlation_factor: float = 3.0, # crypto — high correlation
max_slots: int = 10,
) -> float:
"""
Analytical estimate of fill_efficiency.
Args:
trading_time_pct: fraction of active time for one strategy
n_pairs: number of trading pairs
correlation_factor: correlation coefficient (1=independent, 5=strong)
max_slots: maximum number of simultaneous positions
"""
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개의 상관 페어를 가진 전략 B의 경우, fill_efficiency는 약 16%에 불과합니다. 이는 실효 수익률을 극적으로 줄입니다.
옵션 3: 데이터 시뮬레이션
가장 정확한 접근은 모든 전략을 모든 페어에서 실행하고 실제 슬롯 활용률을 계산하는 것입니다:
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:
"""
Simulate real orchestrator slot utilization.
"""
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:
"""
Final score for strategy ranking.
Accounts for:
- PnL per active day (capital usage efficiency)
- MaxLev (risk-adjusted scaling)
- Confidence adjustment (penalty for small sample)
- Funding costs (realistic costs at leverage)
"""
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 # in %
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를 결정하고, 이것이 점수 공식에 입력됩니다. 드로다운이 깊을수록 점수가 낮아집니다 — 회복 비대칭으로 인해 비선형적으로.
-
몬테카를로 부트스트랩: 부트스트랩의 신뢰구간은 t-분포보다 신뢰도 계수의 더 정확한 추정을 제공합니다. t-분포의 CI를 부트스트랩의 5번째 백분위수로 대체할 수 있습니다.
-
펀딩 비율: 펀딩 비용은 활성일당 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개.
-
적은 표본에 페널티를 부과하라. 평균 +0.71%의 38 거래는 CI +0.14%
+1.28%. +0.72%의 418 거래는 CI +0.62%+0.82%. 평균이 거의 같아도 두 번째 전략이 더 신뢰할 수 있습니다.
활성 시간당 PnL 지표는 PnL@MaxLev를 대체하지 않습니다 — 자본 사용 효율성의 차원을 추가하여 보완합니다. 단일 전략에는 PnL@ML이 충분합니다. 전략 포트폴리오에는 활성 시간당 PnL이 필수적입니다.
참고 문헌
- Lopez de Prado — Advances in Financial Machine Learning: The Sharpe Ratio
- Pardo, R. — The Evaluation and Optimization of Trading Strategies
- Bailey, D.H. & Lopez de Prado — The Deflated Sharpe Ratio
- Kelly, J.L. — A New Interpretation of Information Rate (1956)
- Quantopian — Lecture on Strategy Evaluation Metrics
- Ernest Chan — Algorithmic Trading: Portfolio Management
Citation
@article{soloviov2026pnlactivetime,
author = {Soloviov, Eugen},
title = {PnL by Active Time: The Metric That Changes Strategy Rankings},
year = {2026},
url = {https://marketmaker.cc/ru/blog/post/pnl-active-time-metric},
version = {0.1.0},
description = {Why raw annual PnL is a poor metric for comparing strategies with different trading time. How to calculate effective return, why you need fill\_efficiency, and why a strategy with 27\% PnL can outperform one with 300\%.}
}
MarketMaker.cc Team
퀀트 리서치 및 전략