← กลับไปยังบทความ
March 18, 2026
อ่าน 5 นาที

PnL ตามเวลาที่ใช้งาน: ตัวชี้วัดที่เปลี่ยนการจัดอันดับกลยุทธ์

PnL ตามเวลาที่ใช้งาน: ตัวชี้วัดที่เปลี่ยนการจัดอันดับกลยุทธ์
#algotrading
#backtest
#ตัวชี้วัด
#PnL
#orchestration
#portfolio
#การจัดการความเสี่ยง

คุณมีกลยุทธ์อยู่สองแบบ กลยุทธ์แรก: PnL +300%, 418 การซื้อขาย, ถือโพซิชันไว้ 45% ของเวลา กลยุทธ์ที่สอง: PnL +27%, 38 การซื้อขาย, ถือโพซิชันไว้ 5% ของเวลา อันไหนดีกว่ากัน?

ถ้าคุณเลือกอันแรก — คุณตอบผิด นี่คือเหตุผล

ปัญหาของ PnL แบบดิบ

PnL แบบดิบ — ผลตอบแทนรวมตลอดช่วงการทดสอบย้อนหลัง — ไม่ได้คำนึงถึง สัดส่วนเวลา ที่กลยุทธ์อยู่ในโพซิชัน กลยุทธ์ที่ได้ +300% และมีเวลาซื้อขาย 45% ใช้ทุนของคุณน้อยกว่าครึ่งเวลา อีก 55% ที่เหลือ ทุนนอนอยู่เฉยๆ

กลยุทธ์ที่ได้ +27% และมีเวลาซื้อขาย 5% ใช้ทุนเพียง 5% ของเวลา — แต่อีก 95% ที่เหลือพร้อมใช้กับกลยุทธ์อื่น

ถ้าคุณรันพอร์ตโฟลิโอของกลยุทธ์ผ่าน orchestrator เวลาที่ไม่ได้ใช้ของกลยุทธ์หนึ่งจะถูกกลยุทธ์อื่นเติมเต็ม ตัวชี้วัดหลักจึงไม่ใช่ว่ากลยุทธ์ทำกำไรได้เท่าไรต่อปี แต่เป็น ทำกำไรได้เท่าไรต่อหน่วยเวลาที่ใช้งาน

สูตรผลตอบแทนที่มีประสิทธิภาพ

การเปรียบเทียบการจัดอันดับกลยุทธ์ตาม PnL ต่อวันที่ใช้งาน

การคำนวณพื้นฐาน

PnLdaily=Total PnLActive days\text{PnL}_{daily} = \frac{\text{Total PnL}}{\text{Active days}}

Annualizedraw=PnLdaily×365\text{Annualized}_{raw} = \text{PnL}_{daily} \times 365

Annualizedeffective=Annualizedraw×fill_efficiency\text{Annualized}_{effective} = \text{Annualized}_{raw} \times \text{fill\_efficiency}

โดยที่:

  • 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% ของเวลาที่เหลือสามารถเติมด้วยกลยุทธ์อื่นได้

การขยายแบบเส้นตรงเทียบกับแบบทบต้น

สูตรข้างต้นเป็นแบบเส้นตรง ซึ่งง่ายกว่าและอนุรักษ์นิยมกว่า แบบทบต้นจะคำนึงถึงการนำกำไรมาลงทุนซ้ำ:

Daily return (compound)=(1+Total PnL)1/Active days1\text{Daily return (compound)} = (1 + \text{Total PnL})^{1/\text{Active days}} - 1

Annualizedcompound=(1+Daily return)365×fill_eff1\text{Annualized}_{compound} = (1 + \text{Daily return})^{365 \times \text{fill\_eff}} - 1

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 เพื่อลงโทษตัวอย่างขนาดเล็ก:

CIlower=rˉtα/2,n1×sn\text{CI}_{lower} = \bar{r} - t_{\alpha/2, n-1} \times \frac{s}{\sqrt{n}}

โดยที่ rˉ\bar{r} คือผลตอบแทนเฉลี่ยต่อการซื้อขาย, ss คือส่วนเบี่ยงเบนมาตรฐาน, nn คือจำนวนการซื้อขาย, tα/2,n1t_{\alpha/2, n-1} คือควอนไทล์ของการแจกแจง 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: หาได้จากที่ไหน

ประสิทธิภาพการเติมเต็มและการจัดสรรสล็อตของ orchestrator

พารามิเตอร์ fill_efficiency ตอบคำถามที่ว่า: "สัดส่วนเวลาเท่าไรที่ orchestrator สามารถให้ทุนทำงานได้อย่างต่อเนื่อง?"

ตัวเลือกที่ 1: ค่าคงที่แบบตายตัว

วิธีที่ง่ายที่สุด: fill_efficiency = 0.80 สำหรับทุกกลยุทธ์ สมมติว่า orchestrator ใช้เวลาว่าง 80% กับกลยุทธ์/คู่ซื้อขายอื่น

ข้อดี: เหมือนกันสำหรับทุกกลยุทธ์ เปรียบเทียบได้ง่าย ข้อเสีย: ไม่คำนึงถึงความสัมพันธ์ระหว่างกลยุทธ์

ตัวเลือกที่ 2: การประมาณเชิงวิเคราะห์

ถ้าคุณมี NN คู่ซื้อขาย แต่ละคู่ใช้งาน p%p\% ของเวลา ความน่าจะเป็นที่อย่างน้อยหนึ่งคู่ใช้งานอยู่:

P(1 active)=1(1p)NP(\geq 1\ \text{active}) = 1 - (1 - p)^N

แต่สกุลเงินดิจิทัลมีความสัมพันธ์กันสูง — BTC ดึง ETH, SOL และส่วนที่เหลือตามไปด้วย จำนวนคู่อิสระที่มีประสิทธิภาพ:

Neff=Ncorrelation factorN_{eff} = \frac{N}{\text{correlation factor}}

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 ต่อปีแบบดิบเป็นตัวชี้วัดที่สะดวกแต่หลอกลวง มันไม่คำนึงถึงทรัพยากรที่สำคัญที่สุดของนักซื้อขาย — เวลา ที่ทุนทำงาน

สามข้อสรุปสำคัญ:

  1. คำนวณ PnL ต่อวันที่ใช้งาน กลยุทธ์ที่ได้ +27% ใน 38 วันที่ถือโพซิชัน = +0.72%/วัน กลยุทธ์ที่ได้ +300% ใน 338 วัน = +0.89%/วัน ความต่างไม่ใช่ 11 เท่า แต่เพียง 1.2 เท่า

  2. คำนึงถึง fill_efficiency ในพอร์ตโฟลิโอของคู่ crypto ที่มีความสัมพันธ์กัน fill_efficiency ต่ำกว่าที่เห็น 10 คู่ไม่เท่ากับการกระจายความเสี่ยง 10 เท่า ด้วย correlation_factor = 3 จำนวนคู่ที่มีประสิทธิภาพอยู่ที่เพียง ~3

  3. ลงโทษตัวอย่างขนาดเล็ก 38 การซื้อขายที่มีค่าเฉลี่ย +0.71% ให้ CI ตั้งแต่ +0.14% ถึง +1.28% 418 การซื้อขายที่มี +0.72% ให้ CI ตั้งแต่ +0.62% ถึง +0.82% กลยุทธ์ที่สองน่าเชื่อถือกว่า แม้ค่าเฉลี่ยจะเกือบเท่ากัน

ตัวชี้วัด PnL ต่อเวลาที่ใช้งานไม่ได้แทนที่ PnL@MaxLev — มันเสริมโดยเพิ่มมิติของ ประสิทธิภาพการใช้ทุน สำหรับกลยุทธ์เดียว PnL@ML เพียงพอ สำหรับพอร์ตโฟลิโอของกลยุทธ์ PnL ต่อเวลาที่ใช้งานเป็นสิ่งจำเป็น


อ้างอิง

  1. Lopez de Prado — Advances in Financial Machine Learning: The Sharpe Ratio
  2. Pardo, R. — The Evaluation and Optimization of Trading Strategies
  3. Bailey, D.H. & Lopez de Prado — The Deflated Sharpe Ratio
  4. Kelly, J.L. — A New Interpretation of Information Rate (1956)
  5. Quantopian — Lecture on Strategy Evaluation Metrics
  6. 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\%}
}
ข้อจำกัดความรับผิดชอบ: ข้อมูลที่ให้ไว้ในบทความนี้มีไว้เพื่อการศึกษาและให้ข้อมูลเท่านั้น และไม่ถือเป็นคำแนะนำทางการเงิน การลงทุน หรือการเทรด การเทรดสกุลเงินดิจิทัลมีความเสี่ยงสูงที่จะขาดทุน

ผู้เขียน

Eugen Soloviov
Eugen Soloviov

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.

Newsletter

ก้าวนำหน้าตลาด

สมัครรับจดหมายข่าวของเราเพื่อรับข้อมูลเชิงลึกการเทรดด้วย AI เฉพาะ การวิเคราะห์ตลาด และการอัปเดตแพลตฟอร์ม

เราเคารพความเป็นส่วนตัวของคุณ ยกเลิกการสมัครได้ทุกเมื่อ