PnL theo Thời Gian Hoạt Động: Chỉ Số Thay Đổi Thứ Hạng Chiến Lược
Bạn có hai chiến lược. Chiến lược thứ nhất: PnL +300%, 418 giao dịch, vị thế mở 45% thời gian. Chiến lược thứ hai: PnL +27%, 38 giao dịch, vị thế mở 5% thời gian. Chiến lược nào tốt hơn?
Nếu bạn chọn chiến lược đầu tiên — bạn đã trả lời sai. Đây là lý do tại sao.
Vấn Đề với PnL Thô
PnL thô — tổng lợi nhuận trong toàn bộ khoảng thời gian backtest — không tính đến tỷ lệ thời gian mà chiến lược giữ vị thế. Một chiến lược với +300% và 45% thời gian giao dịch sử dụng vốn của bạn ít hơn một nửa thời gian. 55% thời gian còn lại, vốn nằm không hoạt động.
Một chiến lược với +27% và 5% thời gian giao dịch chỉ sử dụng vốn 5% thời gian — nhưng 95% còn lại có sẵn cho các chiến lược khác.
Nếu bạn chạy một danh mục chiến lược qua orchestrator, thời gian nhàn rỗi của một chiến lược được lấp đầy bởi các chiến lược khác. Khi đó, chỉ số quan trọng không phải là chiến lược kiếm được bao nhiêu trong một năm, mà là kiếm được bao nhiêu trên mỗi đơn vị thời gian hoạt động.
Công Thức Lợi Nhuận Hiệu Quả

Tính Toán Cơ Bản
trong đó:
- Active days — tổng thời gian giữ vị thế (tính bằng ngày)
- fill_efficiency — tỷ lệ thời gian mà orchestrator có thể lấp đầy bằng tín hiệu (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,
}
Tính Lại Các Chiến Lược Thực Tế
Khoảng thời gian: 750 ngày (25 tháng), fill_efficiency = 0.80:
| Chiến lược | PnL | Thời gian giao dịch | Ngày hoạt động | PnL/ngày | Hàng năm (x0.8) |
|---|---|---|---|---|---|
| Chiến lược C | +300% | 45% | 337.5 | 0.89%/d | 259% |
| Chiến lược B | +27% | 5% | 37.5 | 0.72%/d | 210% |
| Chiến lược A | +58% | 15% | 112.5 | 0.51%/d | 150% |
Theo PnL thô: Chiến lược C (300%) >> Chiến lược A (58%) >> Chiến lược B (27%). Theo lợi nhuận hiệu quả: Chiến lược C (259%) > Chiến lược B (210%) > Chiến lược A (150%).
Chiến lược B với PnL 27% hóa ra có thể so sánh với Chiến lược C với PnL 300% — vì nó kiếm được số tiền tương đương trong thời gian hoạt động ít hơn 9 lần. 95% thời gian còn lại có thể được lấp đầy bằng các chiến lược khác.
Ngoại Suy Tuyến Tính vs Lãi Kép
Công thức trên là tuyến tính. Nó đơn giản hơn và thận trọng hơn. Biến thể lãi kép tính đến việc tái đầu tư lợi nhuận:
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)
Với ngoại suy lãi kép, Chiến lược B vượt qua Chiến lược C: 540% so với 231%. Thứ hạng bị đảo ngược.
Khuyến nghị: sử dụng ngoại suy tuyến tính để xếp hạng. Nó thận trọng hơn và ít có xu hướng thưởng cho overfitting trên số lượng giao dịch nhỏ.
Cạm Bẫy: Số Lượng Giao Dịch Nhỏ
Chiến lược B với 38 giao dịch và PnL/ngày = 0.72% trông hấp dẫn. Nhưng 38 giao dịch là một mẫu thống kê yếu. PnL/ngày cao có thể là kết quả của sự trùng hợp may mắn.
Điểm số được điều chỉnh theo độ tin cậy
Chúng ta sử dụng phân phối t để phạt các mẫu nhỏ:
trong đó là lợi nhuận trung bình trên mỗi giao dịch, là độ lệch chuẩn, là số lượng giao dịch, là phân vị phân phối 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,
}
Tác động của điều chỉnh độ tin cậy
| Chiến lược | Giao dịch | Lợi nhuận trung bình | SE | CI lower | Hệ số tin cậy | Điểm đã điều chỉnh |
|---|---|---|---|---|---|---|
| Chiến lược B | 38 | 0.71% | 0.28% | 0.14% | 0.20 | 210% x 0.20 = 42% |
| Chiến lược C | 418 | 0.72% | 0.05% | 0.62% | 0.86 | 259% x 0.86 = 223% |
| Chiến lược A | 491 | 0.12% | 0.02% | 0.08% | 0.67 | 150% x 0.67 = 100% |
Sau khi điều chỉnh độ tin cậy, Chiến lược C dẫn đầu một cách tự tin: 418 giao dịch cho khoảng tin cậy hẹp và hệ số tin cậy cao. Chiến lược B với 38 giao dịch bị phạt — hiệu suất "xuất sắc" của nó có thể là kết quả của phương sai.
fill_efficiency: Lấy Từ Đâu

Tham số fill_efficiency trả lời câu hỏi: "Orchestrator có thể giữ vốn hoạt động trong bao nhiêu phần trăm thời gian?"
Tùy chọn 1: Hằng số cố định
Cách tiếp cận đơn giản nhất: fill_efficiency = 0.80 cho tất cả các chiến lược. Giả định rằng orchestrator sử dụng 80% thời gian nhàn rỗi với các chiến lược/cặp khác.
Ưu điểm: giống nhau cho tất cả, dễ so sánh. Nhược điểm: không tính đến mối tương quan giữa các chiến lược.
Tùy chọn 2: Ước tính phân tích
Nếu bạn có cặp, mỗi cặp hoạt động thời gian, xác suất để ít nhất một cặp hoạt động:
Nhưng tiền mã hóa có tương quan cao — BTC kéo theo ETH, SOL, và phần còn lại. Số lượng cặp độc lập hiệu quả:
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)
Với Chiến lược B có 5% hoạt động và 10 cặp tương quan, fill_efficiency chỉ khoảng ~16%. Điều này làm giảm đáng kể lợi nhuận hiệu quả.
Tùy chọn 3: Mô phỏng từ dữ liệu
Cách tiếp cận chính xác nhất là chạy tất cả các chiến lược trên tất cả các cặp và tính toán mức sử dụng slot thực tế:
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
Công Thức Xếp Hạng Cuối Cùng
Kết hợp tất cả các thành phần:
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
Mối Liên Hệ với Các Chỉ Số Khác trong Bộ Bài
Chỉ số này không thay thế mà bổ sung cho các công cụ từ các bài viết trước:
-
Bất đối xứng Lỗ-Lợi nhuận: mức drawdown tối đa xác định MaxLev, được đưa vào công thức điểm số. Drawdown càng sâu, điểm số càng thấp — theo cách phi tuyến, do bất đối xứng phục hồi.
-
Bootstrap Monte Carlo: các khoảng tin cậy từ bootstrap cung cấp ước tính chính xác hơn về hệ số tin cậy so với phân phối t. Bạn có thể thay thế CI từ phân phối t bằng phân vị thứ 5 từ bootstrap.
-
Tỷ lệ funding: chi phí funding được trừ từ PnL trên mỗi ngày hoạt động. Với đòn bẩy cao và PnL/ngày thấp, funding có thể làm cho điểm số ròng trở nên âm — chiến lược không có lợi nhuận trong thực tế mặc dù PnL thô dương.
Tại Sao Điều Này Quan Trọng Đối Với Orchestration
PnL theo thời gian hoạt động là chỉ số chính để xếp hạng các chiến lược trong orchestrator. Khi nhiều chiến lược cạnh tranh cho cùng một slot, chiến lược có điểm số cao nhất (tính đến điều chỉnh độ tin cậy) sẽ thắng.
Trong thực tế, điều này dẫn đến các quyết định đáng ngạc nhiên: các chiến lược với PnL thô "khiêm tốn" nhưng thời gian giữ vị thế ngắn thường được ưu tiên hơn các chiến lược "bắt mắt" với PnL cao nhưng vị thế dài. Chiến lược trước sử dụng vốn hiệu quả hơn trong danh mục hàng chục chiến lược.
Hiểu biết quan trọng: chỉ số duy nhất có thể mở rộng quy mô là PnL trên mỗi ngày hoạt động. PnL thô không thể mở rộng: bạn không thể chạy cùng một chiến lược hai lần. Nhưng bạn có thể lấp đầy thời gian nhàn rỗi bằng các chiến lược khác — và PnL trên mỗi ngày hoạt động dự đoán chính xác bạn sẽ kiếm được bao nhiêu trong danh mục.
Kết Luận
PnL hàng năm thô là chỉ số thuận tiện nhưng gây hiểu lầm. Nó không tính đến nguồn lực quan trọng nhất của nhà giao dịch — thời gian mà vốn đang hoạt động.
Ba điểm chính:
-
Tính PnL trên mỗi ngày hoạt động. Chiến lược với +27% trong 38 ngày giữ vị thế = +0.72%/ngày. Chiến lược với +300% trong 338 ngày = +0.89%/ngày. Sự khác biệt không phải là 11 lần, mà là 1.2 lần.
-
Tính đến fill_efficiency. Trong danh mục các cặp crypto tương quan, fill_efficiency thấp hơn so với vẻ ngoài. 10 cặp không bằng đa dạng hóa 10 lần. Với correlation_factor = 3, số lượng cặp hiệu quả chỉ là ~3.
-
Phạt các mẫu nhỏ. 38 giao dịch với trung bình +0.71% cho CI từ +0.14% đến +1.28%. 418 giao dịch với +0.72% cho CI từ +0.62% đến +0.82%. Chiến lược thứ hai đáng tin cậy hơn, mặc dù các giá trị trung bình gần như giống hệt nhau.
Chỉ số PnL theo thời gian hoạt động không thay thế PnL@MaxLev — nó bổ sung cho nó bằng cách thêm chiều hiệu quả sử dụng vốn. Đối với một chiến lược đơn lẻ, PnL@ML là đủ. Đối với danh mục chiến lược, PnL theo thời gian hoạt động là thiết yếu.
Tài Liệu Tham Khảo
- 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
Trích Dẫn
@article{soloviov2026pnlactivetime,
author = {Soloviov, Eugen},
title = {PnL by Active Time: The Metric That Changes Strategy Rankings},
year = {2026},
url = {https://marketmaker.cc/vi/blog/post/pnl-active-time-metric},
version = {0.1.0},
description = {Tại sao PnL hàng năm thô là chỉ số kém để so sánh các chiến lược với thời gian giao dịch khác nhau. Cách tính lợi nhuận hiệu quả, tại sao bạn cần fill\_efficiency, và tại sao chiến lược với PnL 27\% có thể vượt trội hơn chiến lược với 300\%.}
}
Tác Giả
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.