PnL ตามเวลาที่ใช้งาน: ตัวชี้วัดที่เปลี่ยนการจัดอันดับกลยุทธ์
คุณมีกลยุทธ์อยู่สองแบบ กลยุทธ์แรก: PnL +300%, 418 การซื้อขาย, ถือโพซิชันไว้ 45% ของเวลา กลยุทธ์ที่สอง: PnL +27%, 38 การซื้อขาย, ถือโพซิชันไว้ 5% ของเวลา อันไหนดีกว่ากัน?
ถ้าคุณเลือกอันแรก — คุณตอบผิด นี่คือเหตุผล
ปัญหาของ PnL แบบดิบ
PnL แบบดิบ — ผลตอบแทนรวมตลอดช่วงการทดสอบย้อนหลัง — ไม่ได้คำนึงถึง สัดส่วนเวลา ที่กลยุทธ์อยู่ในโพซิชัน กลยุทธ์ที่ได้ +300% และมีเวลาซื้อขาย 45% ใช้ทุนของคุณน้อยกว่าครึ่งเวลา อีก 55% ที่เหลือ ทุนนอนอยู่เฉยๆ
กลยุทธ์ที่ได้ +27% และมีเวลาซื้อขาย 5% ใช้ทุนเพียง 5% ของเวลา — แต่อีก 95% ที่เหลือพร้อมใช้กับกลยุทธ์อื่น
ถ้าคุณรันพอร์ตโฟลิโอของกลยุทธ์ผ่าน orchestrator เวลาที่ไม่ได้ใช้ของกลยุทธ์หนึ่งจะถูกกลยุทธ์อื่นเติมเต็ม ตัวชี้วัดหลักจึงไม่ใช่ว่ากลยุทธ์ทำกำไรได้เท่าไรต่อปี แต่เป็น ทำกำไรได้เท่าไรต่อหน่วยเวลาที่ใช้งาน
สูตรผลตอบแทนที่มีประสิทธิภาพ

การคำนวณพื้นฐาน
โดยที่:
- Active days — เวลารวมที่อยู่ในโพซิชัน (หน่วยเป็นวัน)
- fill_efficiency — สัดส่วนเวลาที่ orchestrator สามารถเติมด้วยสัญญาณได้ (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%/d | 259% |
| กลยุทธ์ B | +27% | 5% | 37.5 | 0.72%/d | 210% |
| กลยุทธ์ A | +58% | 15% | 112.5 | 0.51%/d | 150% |
ตาม PnL แบบดิบ: กลยุทธ์ C (300%) >> กลยุทธ์ A (58%) >> กลยุทธ์ B (27%) ตามผลตอบแทนที่มีประสิทธิภาพ: กลยุทธ์ C (259%) > กลยุทธ์ B (210%) > กลยุทธ์ A (150%)
กลยุทธ์ B ที่มี PnL 27% กลับเทียบเคียงได้กับกลยุทธ์ C ที่มี PnL 300% — เพราะมันทำกำไรได้เท่ากันในเวลาที่ใช้งานน้อยกว่า 9 เท่า อีก 95% ของเวลาที่เหลือสามารถเติมด้วยกลยุทธ์อื่นได้
การขยายแบบเส้นตรงเทียบกับแบบทบต้น
สูตรข้างต้นเป็นแบบเส้นตรง ซึ่งง่ายกว่าและอนุรักษ์นิยมกว่า แบบทบต้นจะคำนึงถึงการนำกำไรมาลงทุนซ้ำ:
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% เทียบกับ 231% การจัดอันดับพลิกกลับ
คำแนะนำ: ใช้การขยายแบบเส้นตรงในการจัดอันดับ มันอนุรักษ์นิยมกว่าและมีโอกาสน้อยกว่าที่จะให้รางวัลกับการ overfitting จากจำนวนการซื้อขายที่น้อย
กับดัก: จำนวนการซื้อขายที่น้อย
กลยุทธ์ 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:
"""
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 lower | Conf. factor | คะแนนที่ปรับแล้ว |
|---|---|---|---|---|---|---|
| กลยุทธ์ 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 ที่แคบและ confidence factor สูง กลยุทธ์ B ที่มี 38 การซื้อขายถูกลงโทษ — ผลงานที่ "ยอดเยี่ยม" อาจเป็นผลจากความแปรปรวน
fill_efficiency: หาได้จากที่ไหน

พารามิเตอร์ fill_efficiency ตอบคำถามที่ว่า: "สัดส่วนเวลาเท่าไรที่ orchestrator สามารถให้ทุนทำงานได้อย่างต่อเนื่อง?"
ตัวเลือกที่ 1: ค่าคงที่แบบตายตัว
วิธีที่ง่ายที่สุด: fill_efficiency = 0.80 สำหรับทุกกลยุทธ์ สมมติว่า orchestrator ใช้เวลาว่าง 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)
สำหรับกลยุทธ์ B ที่มีกิจกรรม 5% และ 10 คู่ที่มีความสัมพันธ์กัน 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 ซึ่งป้อนเข้าสูตรคะแนน ยิ่งดึงลดลึก คะแนนยิ่งต่ำ — แบบไม่เป็นเส้นตรง เนื่องจากความไม่สมมาตรของการฟื้นตัว
-
Monte Carlo bootstrap: ช่วงความเชื่อมั่นจาก bootstrap ให้การประมาณ confidence factor ที่แม่นยำกว่าการแจกแจง t คุณสามารถแทนที่ CI จากการแจกแจง t ด้วยเปอร์เซ็นไทล์ที่ 5 จาก bootstrap
-
อัตรา Funding: ต้นทุน funding ถูกหักออกจาก PnL ต่อวันที่ใช้งาน เมื่อ leverage สูงและ PnL/วันต่ำ funding สามารถทำให้คะแนนสุทธิติดลบได้ — กลยุทธ์ไม่ทำกำไรในความเป็นจริงแม้ PnL แบบดิบจะเป็นบวก
ทำไมเรื่องนี้จึงสำคัญสำหรับการ Orchestration
PnL ต่อเวลาที่ใช้งานเป็นตัวชี้วัดหลักสำหรับการจัดอันดับกลยุทธ์ใน orchestrator เมื่อหลายกลยุทธ์แข่งขันกันเพื่อสล็อตเดียวกัน กลยุทธ์ที่มีคะแนนสูงสุด (คำนึงถึงการปรับด้วยความเชื่อมั่น) จะชนะ
ในทางปฏิบัติ สิ่งนี้นำไปสู่การตัดสินใจที่น่าแปลกใจ: กลยุทธ์ที่มี PnL แบบดิบ "ปานกลาง" แต่ถือโพซิชันในระยะสั้นมักได้รับความสำคัญกว่ากลยุทธ์ "เด่น" ที่มี PnL สูงแต่ถือโพซิชันนาน กลยุทธ์แบบแรกใช้ทุนได้มีประสิทธิภาพกว่าในพอร์ตโฟลิโอของกลยุทธ์หลายสิบแบบ
ข้อสรุปสำคัญ: ตัวชี้วัดเดียวที่ scale ได้ คือ PnL ต่อวันที่ใช้งาน PnL แบบดิบไม่ scale: คุณไม่สามารถรันกลยุทธ์เดิมซ้ำสองครั้งได้ แต่คุณสามารถเติมเวลาว่างด้วยกลยุทธ์อื่นได้ — และ PnL ต่อวันที่ใช้งานทำนายได้อย่างแม่นยำว่าคุณจะทำกำไรได้เท่าไรในพอร์ตโฟลิโอ
บทสรุป
PnL ต่อปีแบบดิบเป็นตัวชี้วัดที่สะดวกแต่หลอกลวง มันไม่คำนึงถึงทรัพยากรที่สำคัญที่สุดของนักซื้อขาย — เวลา ที่ทุนทำงาน
สามข้อสรุปสำคัญ:
-
คำนวณ PnL ต่อวันที่ใช้งาน กลยุทธ์ที่ได้ +27% ใน 38 วันที่ถือโพซิชัน = +0.72%/วัน กลยุทธ์ที่ได้ +300% ใน 338 วัน = +0.89%/วัน ความต่างไม่ใช่ 11 เท่า แต่เพียง 1.2 เท่า
-
คำนึงถึง fill_efficiency ในพอร์ตโฟลิโอของคู่ crypto ที่มีความสัมพันธ์กัน 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 ต่อเวลาที่ใช้งานเป็นสิ่งจำเป็น
อ้างอิง
- 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
การอ้างอิง
@article{soloviov2026pnlactivetime,
author = {Soloviov, Eugen},
title = {PnL by Active Time: The Metric That Changes Strategy Rankings},
year = {2026},
url = {https://marketmaker.cc/th/blog/post/pnl-active-time-metric},
version = {0.1.0},
description = {ทำไม PnL ต่อปีแบบดิบจึงเป็นตัวชี้วัดที่ไม่ดีสำหรับการเปรียบเทียบกลยุทธ์ที่มีเวลาซื้อขายต่างกัน วิธีคำนวณผลตอบแทนที่มีประสิทธิภาพ ทำไมจึงต้องใช้ fill\_efficiency และเหตุใดกลยุทธ์ที่มี PnL 27\% จึงอาจเหนือกว่ากลยุทธ์ที่มี 300\%}
}
ผู้เขียน
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.