← العودة إلى قائمة المقالات
March 18, 2026
5 دقائق للقراءة

الربح والخسارة حسب الوقت النشط: المقياس الذي يغيّر ترتيب الاستراتيجيات

الربح والخسارة حسب الوقت النشط: المقياس الذي يغيّر ترتيب الاستراتيجيات
#algotrading
#backtest
#metrics
#PnL
#orchestration
#portfolio
#risk management

لديك استراتيجيتان. الأولى: 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 — نسبة الوقت التي يستطيع المُنظِّم ملأها بالإشارات (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%).

الاستراتيجية B بنسبة PnL 27% تبين أنها مماثلة للاستراتيجية C بنسبة PnL 300% — لأنها تكسب نفس المال في تُسع الوقت النشط. الـ 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%. الترتيب ينعكس.

التوصية: استخدم الاستقراء الخطي للترتيب. أكثر تحفظاً وأقل عرضة لمكافأة التكيف المفرط على عدد قليل من الصفقات.

الفخ: عدد صفقات قليل

الاستراتيجية 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 عامل الثقة النتيجة المعدّلة
الاستراتيجية 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 ضيق وعامل ثقة مرتفع. الاستراتيجية B بـ 38 صفقة تُعاقَب — أداؤها "الرائع" قد يكون نتيجة التباين.

fill_efficiency: من أين نحصل عليه

Fill efficiency وتخصيص فتحات المُنظِّم

معامل fill_efficiency يجيب على السؤال: "ما نسبة الوقت التي يستطيع المُنظِّم إبقاء رأس المال فيها عاملاً؟"

الخيار 1: ثابت محدد

أبسط نهج: fill_efficiency = 0.80 لجميع الاستراتيجيات. يفترض أن المُنظِّم يستغل 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، الذي يدخل في معادلة النتيجة. كلما كان التراجع أعمق، كلما انخفضت النتيجة — بشكل غير خطي، بسبب عدم تماثل التعافي.

  • محاكاة مونتي كارلو بوتستراب: فترات الثقة من البوتستراب توفر تقديراً أكثر دقة لعامل الثقة من توزيع t. يمكنك استبدال CI من توزيع t بالمئوية الخامسة من البوتستراب.

  • معدلات التمويل: تكاليف التمويل تُطرح من PnL لكل يوم نشط. مع رافعة مالية عالية وPnL/يوم منخفض، يمكن للتمويل أن يجعل النتيجة الصافية سلبية — الاستراتيجية غير مربحة فعلياً رغم PnL خام إيجابي.

لماذا هذا مهم للتنظيم

PnL لكل وقت نشط هو المقياس الرئيسي لترتيب الاستراتيجيات في المُنظِّم. عندما تتنافس عدة استراتيجيات على نفس الفتحة، تفوز الاستراتيجية ذات أعلى نتيجة (مع مراعاة تعديل الثقة).

عملياً، هذا يؤدي إلى قرارات مفاجئة: استراتيجيات بـ PnL خام "متواضع" لكن وقت قصير في المركز غالباً ما تحصل على أولوية على استراتيجيات "لامعة" بـ PnL مرتفع لكن مراكز طويلة. الأولى تستخدم رأس المال بكفاءة أكبر في محفظة من عشرات الاستراتيجيات.

الرؤية الرئيسية: المقياس الوحيد القابل للتوسع هو PnL لكل يوم نشط. PnL الخام لا يتوسع: لا يمكنك تشغيل نفس الاستراتيجية مرتين. لكن يمكنك ملء وقت الخمول باستراتيجيات أخرى — وPnL لكل يوم نشط يتنبأ بدقة بكم ستكسب في محفظة.

الخاتمة

PnL السنوي الخام مقياس مريح لكنه مخادع. لا يأخذ في الاعتبار أهم مورد للمتداول — الوقت الذي يعمل فيه رأس المال.

ثلاث نقاط رئيسية:

  1. احسب PnL لكل يوم نشط. استراتيجية بنسبة +27% خلال 38 يوم في المركز = +0.72%/يوم. استراتيجية بنسبة +300% خلال 338 يوم = +0.89%/يوم. الفرق ليس 11 ضعفاً بل 1.2 ضعف.

  2. خذ fill_efficiency في الاعتبار. في محفظة أزواج عملات مشفرة مترابطة، 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

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\%.}
}
blog.disclaimer

MarketMaker.cc Team

البحوث والاستراتيجيات الكمية

ناقش في تلغرام
Newsletter

ابقَ متقدماً على السوق

اشترك في نشرتنا الإخبارية للحصول على رؤى حصرية حول تداول الذكاء الاصطناعي وتحليلات السوق وتحديثات المنصة.

نحترم خصوصيتك. يمكنك إلغاء الاشتراك في أي وقت.