← Kembali ke artikel
March 18, 2026
Bacaan 5 minit

PnL mengikut Masa Aktif: Metrik yang Mengubah Kedudukan Strategi

PnL mengikut Masa Aktif: Metrik yang Mengubah Kedudukan Strategi
#algotrading
#backtest
#metrik
#PnL
#orchestration
#portfolio
#pengurusan risiko

Anda mempunyai dua strategi. Yang pertama: PnL +300%, 418 dagangan, kedudukan terbuka 45% daripada masa. Yang kedua: PnL +27%, 38 dagangan, kedudukan terbuka 5% daripada masa. Yang mana lebih baik?

Jika anda memilih yang pertama — anda menjawab dengan salah. Inilah sebabnya.

Masalah dengan PnL Mentah

PnL mentah — jumlah pulangan sepanjang keseluruhan tempoh backtest — tidak mengambil kira pecahan masa yang strategi berada dalam kedudukan. Strategi dengan +300% dan 45% masa dagangan menggunakan modal anda kurang daripada separuh masa. 55% masa yang tersisa, modal terbiar.

Strategi dengan +27% dan 5% masa dagangan menggunakan modal hanya 5% daripada masa — tetapi 95% yang tersisa tersedia untuk strategi lain.

Jika anda menjalankan portfolio strategi melalui orchestrator, masa terbiar satu strategi diisi oleh yang lain. Metrik utama kemudiannya bukan berapa banyak yang strategi perolehi sepanjang setahun, tetapi berapa banyak yang diperolehnya bagi setiap unit masa aktif.

Formula Pulangan Efektif

Perbandingan kedudukan strategi PnL per hari aktif

Pengiraan Asas

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}

di mana:

  • Active days — jumlah masa dalam kedudukan (dalam hari)
  • fill_efficiency — pecahan masa yang boleh diisi orchestrator dengan isyarat (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,
    }

Mengira Semula Strategi Sebenar

Tempoh: 750 hari (25 bulan), fill_efficiency = 0.80:

Strategi PnL Masa dagangan Hari aktif PnL/hari Tahunan (x0.8)
Strategi C +300% 45% 337.5 0.89%/h 259%
Strategi B +27% 5% 37.5 0.72%/h 210%
Strategi A +58% 15% 112.5 0.51%/h 150%

Mengikut PnL mentah: Strategi C (300%) >> Strategi A (58%) >> Strategi B (27%). Mengikut pulangan efektif: Strategi C (259%) > Strategi B (210%) > Strategi A (150%).

Strategi B dengan PnL 27% ternyata setanding dengan Strategi C dengan PnL 300% — kerana ia memperolehi wang yang sama dalam masa aktif 9 kali lebih sedikit. 95% masa yang tersisa boleh diisi dengan strategi lain.

Ekstrapolasi Linear vs Kompaun

Formula di atas adalah linear. Ia lebih mudah dan lebih konservatif. Varian kompaun mengambil kira pelaburan semula keuntungan:

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)

Dengan ekstrapolasi kompaun, Strategi B mengatasi Strategi C: 540% berbanding 231%. Kedudukan terbalik.

Cadangan: gunakan ekstrapolasi linear untuk pemeringkatan. Ia lebih konservatif dan kurang cenderung memberi ganjaran kepada overfitting pada bilangan dagangan yang kecil.

Perangkap: Bilangan Dagangan yang Kecil

Strategi B dengan 38 dagangan dan PnL/hari = 0.72% kelihatan menarik. Tetapi 38 dagangan adalah sampel yang lemah secara statistik. PnL/hari yang tinggi boleh jadi hasil daripada kebetulan yang bertuah.

Pemarkahan yang disesuaikan dengan keyakinan

Kami menggunakan taburan-t untuk menghukum sampel kecil:

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

di mana rˉ\bar{r} ialah min pulangan per dagangan, ss ialah sisihan piawai, nn ialah bilangan dagangan, tα/2,n1t_{\alpha/2, n-1} ialah kuantil taburan-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,
    }

Kesan penyesuaian keyakinan

Strategi Dagangan Min pulangan SE CI bawah Faktor keyakinan Skor disesuaikan
Strategi B 38 0.71% 0.28% 0.14% 0.20 210% x 0.20 = 42%
Strategi C 418 0.72% 0.05% 0.62% 0.86 259% x 0.86 = 223%
Strategi A 491 0.12% 0.02% 0.08% 0.67 150% x 0.67 = 100%

Selepas penyesuaian keyakinan, Strategi C memimpin dengan yakin: 418 dagangan memberikan CI yang sempit dan faktor keyakinan yang tinggi. Strategi B dengan 38 dagangan dihukum — prestasi "cemerlang"-nya mungkin hasil daripada varians.

fill_efficiency: Cara Mendapatkannya

Kecekapan isian dan peruntukan slot orchestrator

Parameter fill_efficiency menjawab soalan: "Berapa pecahan masa boleh orchestrator mengekalkan modal bekerja?"

Pilihan 1: Pemalar tetap

Pendekatan paling mudah: fill_efficiency = 0.80 untuk semua strategi. Mengandaikan orchestrator menggunakan 80% masa terbiar dengan strategi/pasangan lain.

Kelebihan: sama untuk semua, mudah dibandingkan. Kelemahan: tidak mengambil kira korelasi antara strategi.

Pilihan 2: Anggaran analitik

Jika anda mempunyai NN pasangan, setiap satu aktif p%p\% daripada masa, kebarangkalian bahawa sekurang-kurangnya satu aktif:

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

Tetapi mata wang kripto sangat berkorelasi — BTC menarik ETH, SOL, dan yang lain bersamanya. Bilangan pasangan bebas yang berkesan:

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)

Untuk Strategi B dengan aktiviti 5% dan 10 pasangan berkorelasi, fill_efficiency hanya ~16%. Ini mengurangkan pulangan efektif secara drastik.

Pilihan 3: Simulasi daripada data

Pendekatan paling tepat adalah menjalankan semua strategi pada semua pasangan dan mengira penggunaan slot sebenar:

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

Formula Pemeringkatan Akhir

Menggabungkan semua komponen:

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

Hubungan dengan Metrik Lain dalam Siri Ini

Metrik ini tidak menggantikan tetapi melengkapi alat daripada artikel-artikel sebelumnya:

  • Asimetri Kerugian-Keuntungan: drawdown maksimum menentukan MaxLev, yang dimasukkan ke dalam formula skor. Semakin dalam drawdown, semakin rendah skor — secara bukan linear, disebabkan asimetri pemulihan.

  • Bootstrap Monte Carlo: selang keyakinan daripada bootstrap memberikan anggaran faktor keyakinan yang lebih tepat daripada taburan-t. Anda boleh menggantikan CI daripada taburan-t dengan persentil ke-5 daripada bootstrap.

  • Kadar pendanaan: kos pendanaan ditolak daripada PnL per hari aktif. Dengan leverage tinggi dan PnL/hari yang rendah, pendanaan boleh menjadikan skor bersih negatif — strategi tidak menguntungkan pada hakikatnya walaupun PnL mentah adalah positif.

Mengapa Ini Penting untuk Orchestration

PnL per masa aktif adalah metrik utama untuk memeringkat strategi dalam orchestrator. Apabila beberapa strategi bersaing untuk slot yang sama, yang mempunyai skor tertinggi (dengan mengambil kira penyesuaian keyakinan) menang.

Dalam amalan, ini membawa kepada keputusan yang mengejutkan: strategi dengan PnL mentah yang "sederhana" tetapi masa singkat dalam kedudukan sering mendapat keutamaan berbanding strategi "mencolok" dengan PnL tinggi tetapi kedudukan panjang. Yang pertama menggunakan modal dengan lebih cekap dalam portfolio yang mengandungi berpuluh-puluh strategi.

Pandangan utama: satu-satunya metrik yang berskala adalah PnL per hari aktif. PnL mentah tidak berskala: anda tidak boleh menjalankan strategi yang sama dua kali. Tetapi anda boleh mengisi masa terbiar dengan strategi lain — dan PnL per hari aktif meramalkan dengan tepat berapa banyak yang akan anda perolehi dalam portfolio.

Kesimpulan

PnL tahunan mentah adalah metrik yang mudah tetapi mengelirukan. Ia tidak mengambil kira sumber terpenting pedagang — masa semasa modal sedang bekerja.

Tiga pengajaran:

  1. Kira PnL per hari aktif. Strategi dengan +27% selama 38 hari dalam kedudukan = +0.72%/hari. Strategi dengan +300% selama 338 hari = +0.89%/hari. Perbezaannya bukan 11x, tetapi 1.2x.

  2. Ambil kira fill_efficiency. Dalam portfolio pasangan kripto yang berkorelasi, fill_efficiency lebih rendah daripada yang kelihatan. 10 pasangan tidak bermaksud kepelbagaian 10x. Dengan correlation_factor = 3, bilangan pasangan efektif hanya ~3.

  3. Hukum sampel kecil. 38 dagangan dengan min +0.71% memberikan CI dari +0.14% hingga +1.28%. 418 dagangan dengan +0.72% memberikan CI dari +0.62% hingga +0.82%. Strategi kedua lebih boleh dipercayai, walaupun min hampir sama.

Metrik PnL per masa aktif tidak menggantikan PnL@MaxLev — ia melengkapinya dengan menambah dimensi kecekapan penggunaan modal. Untuk strategi tunggal, PnL@ML adalah mencukupi. Untuk portfolio strategi, PnL per masa aktif adalah penting.


Rujukan

  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

Petikan

@article{soloviov2026pnlactivetime,
  author = {Soloviov, Eugen},
  title = {PnL by Active Time: The Metric That Changes Strategy Rankings},
  year = {2026},
  url = {https://marketmaker.cc/ms/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\%.}
}
Penafian: Maklumat yang disediakan dalam artikel ini adalah untuk tujuan pendidikan dan maklumat sahaja dan bukan merupakan nasihat kewangan, pelaburan, atau dagangan. Dagangan mata wang kripto melibatkan risiko kerugian yang ketara.

Pengarang

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

Kekal Mendahului Pasaran

Langgan surat berita kami untuk pandangan dagangan AI eksklusif, analisis pasaran, dan kemas kini platform.

Kami menghormati privasi anda. Berhenti melanggan pada bila-bila masa.