AI Agents · درس

اختبار قرارات الوكيل بأثر رجعي

محاكاة استراتيجيات الوكيل على بيانات تاريخية للتحقق من الأداء.

الدرس 4 من 413 خطوة

اختبار قرارات الوكيل بأثر رجعي درس مجاني في AI Agents على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في AI Agents، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة AI Agents 4 دروس في المجموع.

لماذا نُجري اختبارًا رجعيًا لقرارات الوكيل؟

لا يمكنك معرفة ما إذا كان وكيلك المالي يصدر توصيات جيدة من دون اختباره باستخدام بيانات تاريخية. يعيد الاختبار الرجعي تشغيل ظروف السوق السابقة عبر منطق اتخاذ القرار لدى الوكيل، ويقيس مدى أداء تلك القرارات لو اتُّخذت حينها.

وهو الأداة الأساسية للتحقق من جودة الاستراتيجية قبل نشرها في بيئة التشغيل الفعلية.

فخ انحياز التطلع إلى المستقبل

انحياز التطلع إلى المستقبل هو الخطأ الأكثر شيوعًا وخطورة في الاختبار الرجعي: استخدام بيانات مستقبلية لاتخاذ قرار «سابق». فعلى سبيل المثال، استخدام سعر الإغلاق اليومي لتقرير ما إذا كان ينبغي الشراء عند الافتتاح في اليوم نفسه.

يجب أن يستخدم كل قرار تاريخي البيانات المتاحة فقط في وقت اتخاذ القرار المُحاكى.

import pandas as pd
import yfinance as yf

# WRONG — uses today's close to decide today's trade
def bad_signal(history: pd.DataFrame, date: str) -> str:
    today_close = history.loc[date, 'Close']  # look-ahead!
    if today_close > history['Close'].mean():
        return 'BUY'
    return 'HOLD'

# CORRECT — uses only data available BEFORE the decision date
def good_signal(history: pd.DataFrame, date: str) -> str:
    past_data = history.loc[:date].iloc[:-1]  # exclude today
    if past_data.empty:
        return 'HOLD'
    today_open = history.loc[date, 'Open']    # open price known at market open
    if today_open > past_data['Close'].mean():
        return 'BUY'
    return 'HOLD'

بناء حلقة إعادة التشغيل التاريخية

كرّر العملية عبر كل تاريخي متاح. في كل خطوة، لا يرى الوكيل سوى البيانات المتاحة حتى تلك اللحظة. سجّل القرار، ثم انتقل إلى التاريخ التالي لملاحظة النتيجة.

import yfinance as yf
import pandas as pd

def backtest_simple(ticker: str, start: str, end: str) -> list[dict]:
    hist = yf.Ticker(ticker).history(start=start, end=end)
    results = []

    for i in range(20, len(hist)):  # need 20 days of history for signals
        window   = hist.iloc[:i]           # data available on day i
        today    = hist.iloc[i]
        decision = good_signal(window, str(hist.index[i].date()))

        results.append({
            'date':     str(hist.index[i].date()),
            'decision': decision,
            'open':     float(today['Open']),
            'close':    float(today['Close']),
            'next_day_return': None  # filled in next iteration
        })
    return results

محاكاة منطق الوكيل في كل خطوة

لإجراء اختبار رجعي أكثر واقعية، حاكِ استدعاء أداة الوكيل كاملًا في كل تاريخ — بما في ذلك بناء المطالبة، واستدعاء LLM، وتحليل القرار. يختبر هذا النظام بأكمله، وليس منطق الإشارة فقط.

import openai, json
import yfinance as yf

client = openai.OpenAI(api_key='YOUR_OPENAI_KEY')

def agent_decision_at_date(ticker: str, hist_up_to: pd.DataFrame) -> str:
    recent = hist_up_to.tail(20)
    closes = recent['Close'].tolist()
    returns = [round((closes[i] - closes[i-1]) / closes[i-1], 4)
               for i in range(1, len(closes))]
    prompt = (
        f'Ticker: {ticker}\n'
        f'Last 20 closing prices: {closes}\n'
        f'Daily returns: {returns}\n'
        f'Based on this data, recommend: BUY, SELL, or HOLD.\n'
        f'Return JSON: {{"action": "BUY/SELL/HOLD", "reason": "..."}}'
    )
    resp = client.chat.completions.create(
        model='gpt-4o-mini',
        messages=[{'role': 'user', 'content': prompt}],
        response_format={'type': 'json_object'}
    )
    return json.loads(resp.choices[0].message.content).get('action', 'HOLD')

حساب معدل النجاح

يقيس معدل النجاح النسبة المئوية لقرارات BUY التي أعقبها عائد إيجابي في اليوم التالي. ويعني معدل نجاح يتجاوز 50% أن الوكيل يتنبأ بالاتجاه الصحيح بمعدل يفوق الصدفة.

def compute_hit_rate(results: list[dict]) -> dict:
    buy_decisions = [r for r in results if r['decision'] == 'BUY'
                     and r.get('next_day_return') is not None]

    if not buy_decisions:
        return {'hit_rate': None, 'sample_size': 0}

    correct = sum(1 for r in buy_decisions if r['next_day_return'] > 0)
    return {
        'hit_rate':     round(correct / len(buy_decisions), 4),
        'sample_size':  len(buy_decisions),
        'correct':      correct,
        'interpretation': 'above_chance' if correct / len(buy_decisions) > 0.52 else 'at_or_below_chance'
    }

if __name__ == '__main__':
    results = [
        {'decision': 'BUY', 'next_day_return': 0.012},
        {'decision': 'BUY', 'next_day_return': -0.004},
        {'decision': 'HOLD', 'next_day_return': None},
        {'decision': 'BUY', 'next_day_return': 0.007},
    ]
    print('Hit rate:', compute_hit_rate(results))

حساب العائد مقارنةً بالمؤشر المرجعي

قارن عائد الاستراتيجية بالمؤشر المرجعي للشراء والاحتفاظ. فإذا كان أداء الوكيل أسوأ من مجرد الاحتفاظ بالمؤشر، فلا مبرر لتعقيده.

def compare_to_benchmark(results: list[dict], ticker: str, period: str = '1y') -> dict:
    import yfinance as yf

    # Strategy return: only invested on BUY days
    buy_days = [r for r in results if r['decision'] == 'BUY'
                and r.get('next_day_return') is not None]
    strategy_return = sum(r['next_day_return'] for r in buy_days)

    # Benchmark: buy-and-hold
    hist = yf.Ticker(ticker).history(period=period)['Close']
    bh_return = float((hist.iloc[-1] - hist.iloc[0]) / hist.iloc[0])

    return {
        'strategy_return_pct':  round(strategy_return * 100, 2),
        'benchmark_return_pct': round(bh_return * 100, 2),
        'outperformed':         strategy_return > bh_return,
        'alpha_pct':            round((strategy_return - bh_return) * 100, 2)
    }

تأثير تكاليف المعاملات

من دون احتساب تكاليف المعاملات (العمولات، وفارق السعر، والانزلاق السعري)، قد تبدو الاستراتيجية مربحة، لكنها تخسر المال فعليًا عند التطبيق. وحتى نسبة 0.1% لكل عملية تداول تتراكم بصورة ملحوظة عبر عدد كبير من العمليات.

COMMISSION_PCT = 0.001   # 0.1% per trade (buy + sell)
SLIPPAGE_PCT   = 0.0005  # 0.05% per trade

def returns_after_costs(decisions: list[dict]) -> dict:
    total_return = 0.0
    total_cost   = 0.0

    for d in decisions:
        if d['decision'] == 'BUY' and d.get('next_day_return') is not None:
            gross = d['next_day_return']
            cost  = COMMISSION_PCT + SLIPPAGE_PCT
            net   = gross - cost
            total_return += net
            total_cost   += cost

    return {
        'gross_return_pct': round((total_return + total_cost) * 100, 2),
        'net_return_pct':   round(total_return * 100, 2),
        'total_costs_pct':  round(total_cost * 100, 2),
        'num_trades':       sum(1 for d in decisions if d['decision'] == 'BUY')
    }

if __name__ == '__main__':
    decisions = [
        {'decision': 'BUY', 'next_day_return': 0.012},
        {'decision': 'BUY', 'next_day_return': -0.004},
        {'decision': 'HOLD', 'next_day_return': None},
    ]
    print('Returns after trading costs:', returns_after_costs(decisions))

التحقق بالتقسيم المتتابع

يؤدي التحسين داخل العينة (التدريب على الفترة نفسها التي تختبر فيها) إلى فرط التكيّف. يستخدم التحقق بالتقسيم المتتابع نافذة تدريب متحركة تليها فترة اختبار جديدة — تمامًا كما يحدث عند النشر الفعلي.

def walk_forward_test(ticker: str, total_years: int = 3,
                      train_months: int = 12, test_months: int = 3) -> list[dict]:
    import yfinance as yf
    from dateutil.relativedelta import relativedelta
    from datetime import date

    results = []
    start = date(date.today().year - total_years, 1, 1)
    end   = date.today()

    window_start = start
    while True:
        train_end = window_start + relativedelta(months=train_months)
        test_end  = train_end + relativedelta(months=test_months)
        if test_end > end:
            break

        # In practice: train your signal on window_start->train_end
        # then evaluate on train_end->test_end
        results.append({
            'train_period': f'{window_start} to {train_end}',
            'test_period':  f'{train_end} to {test_end}'
        })
        window_start += relativedelta(months=test_months)

    return results

تقرير الاختبار الرجعي

لخّص نتائج الاختبار الرجعي في تقرير منظم يتضمن: معدل النجاح، وإجمالي العائد مقارنةً بالمؤشر المرجعي، وألفا، ونسبة شارب، وأقصى تراجع، وعدد عمليات التداول، والعائد الصافي بعد احتساب التكاليف.

def backtest_report(ticker: str, results: list[dict]) -> dict:
    hit = compute_hit_rate(results)
    benchmark = compare_to_benchmark(results, ticker)
    costs = returns_after_costs(results)

    return {
        'ticker':                  ticker,
        'total_decisions':         len(results),
        'buy_signals':             costs['num_trades'],
        'hit_rate':                hit.get('hit_rate'),
        'strategy_return_gross':   f'{costs["gross_return_pct"]}%',
        'strategy_return_net':     f'{costs["net_return_pct"]}%',
        'benchmark_return':        f'{benchmark["benchmark_return_pct"]}%',
        'alpha':                   f'{benchmark["alpha_pct"]}%',
        'outperformed_benchmark':  benchmark['outperformed'],
        'transaction_costs':       f'{costs["total_costs_pct"]}%'
    }

تفسير نتائج الاختبار الرجعي

لا تضمن نتائج الاختبار الرجعي الجيدة الأداء المستقبلي، لكن النتائج السيئة تحدد الاستراتيجيات التي ينبغي عدم نشرها. تشمل مؤشرات التحذير الرئيسية: معدل نجاح أقل من 50%، أو ألفا سلبية بعد التكاليف، أو نسبة شارب أقل من 0.5، أو تجاوز أقصى تراجع لحدود تحمّل المخاطر لديك.

def interpret_backtest(report: dict) -> list[str]:
    warnings = []

    hit_rate = report.get('hit_rate')
    if hit_rate and hit_rate < 0.50:
        warnings.append(f'Hit rate {hit_rate:.1%} is below chance — strategy predicts direction poorly')

    alpha_str = report.get('alpha', '0%')
    alpha = float(alpha_str.replace('%', '')) / 100
    if alpha < 0:
        warnings.append(f'Negative alpha {alpha_str}: strategy underperforms buy-and-hold')

    if not warnings:
        return ['Backtest results look acceptable — proceed with paper trading before live deployment']
    return warnings

if __name__ == '__main__':
    report = {'hit_rate': 0.45, 'alpha': '-1.2%'}
    for warning in interpret_backtest(report):
        print('-', warning)

محاكاة مونت كارلو لقياس المتانة

يتأثر الاختبار الرجعي الواحد بالفترة الزمنية المحددة له. وتُعيد محاكاة مونت كارلو ترتيب العوائد التاريخية عشوائيًا لاختبار ما إذا كان الأداء يستمر عبر تسلسلات مختلفة، مما يكشف ما إذا كانت الاستراتيجية متينة أم أنها محض حظ.

import numpy as np

def monte_carlo_backtest(daily_returns: list[float],
                         simulations: int = 1000) -> dict:
    returns_arr = np.array(daily_returns)
    final_returns = []

    for _ in range(simulations):
        shuffled = np.random.choice(returns_arr, size=len(returns_arr), replace=True)
        cumulative = float((1 + shuffled).prod() - 1)
        final_returns.append(cumulative)

    final_returns = sorted(final_returns)
    return {
        'median_return':      round(float(np.median(final_returns)) * 100, 2),
        'percentile_5':       round(float(np.percentile(final_returns, 5)) * 100, 2),
        'percentile_95':      round(float(np.percentile(final_returns, 95)) * 100, 2),
        'prob_positive':      round(sum(1 for r in final_returns if r > 0) / simulations, 3)
    }

ما انحياز التطلع إلى المستقبل في الاختبار الرجعي؟

انحياز التطلع إلى المستقبل هو الطريقة الأكثر شيوعًا للمبالغة عن طريق الخطأ في تقدير أداء الاختبار الرجعي. ويمنع فهم تعريفه الدقيق بناء اختبارات رجعية معيبة.

مراجعة الاختبار الرجعي لقرارات الوكيل

يتطلب الاختبار الرجعي السليم ما يلي: تجنب انحياز التطلع إلى المستقبل (بحيث تستخدم القرارات البيانات السابقة فقط)، ومحاكاة منطق الوكيل الواقعي في كل خطوة تاريخية، وقياس معدل النجاح وألفا مقارنةً بالمؤشر المرجعي، واحتساب تكاليف المعاملات، والتحقق باستخدام تقسيمات متتابعة لاكتشاف فرط التكيّف.

البدء مجانًا

تعلم AI Agents مع معلم ذكاء اصطناعي — مجانًا

اكتب وقم بتشغيل أكوادك الفعلية في المتصفح، واحصل على مساعدة فورية من معلم ذكاء اصطناعي متاح 24/7، واستمر من حيث توقفت على الويب أو في التطبيق.

الدورات
60
الدروس
239

الأسئلة الشائعة

هل درس «اختبار قرارات الوكيل بأثر رجعي» مجاني؟

نعم — نص درس «اختبار قرارات الوكيل بأثر رجعي» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة AI Agents، انتقل إلى CoddyKit PRO. تتضمن دورة AI Agents 4 دروس في المجموع.

ماذا ستتعلم في «اختبار قرارات الوكيل بأثر رجعي»؟

محاكاة استراتيجيات الوكيل على بيانات تاريخية للتحقق من الأداء. تتمرن على AI Agents مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ AI Agents؟

لا تُشترط خبرة سابقة. AI Agents على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.

كم من الوقت يستغرق درس «اختبار قرارات الوكيل بأثر رجعي»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس AI Agents هذا؟

نعم. كل درس في AI Agents يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. تكامل واجهات برمجة تطبيقات بيانات السوق
  2. أدوات الوكيل لتحليل المحافظ الاستثمارية
  3. ضوابط المخاطر والامتثال
  4. اختبار قرارات الوكيل بأثر رجعي
← العودة إلى AI Agents