مقاييس الأداء في صورة دوال قابلة لإعادة الاستخدام: CAGR والتراجع ونسبة شارب ونسبة سورتينو ومعامل الربح واختبارات الوحدة الخاصة بها
اكتب وحدة صغيرة للمقاييس يجري فيها التحويل إلى قيم سنوية بشكل صحيح، وأضف نسخًا متحركة، وأثبت صحة الدوال باستخدام pytest مقابل قيم حسبتها يدويًا.
ما الذي ستتعلمه
- كيف تكتب كل مقياس في صورة دالة نقية تأخذ كائنًا من pandas وتعيد رقمًا واحدًا.
- أين يدخل التحويل إلى قيم سنوية: قاعدة الجذر التربيعي للزمن للتقلب، وثابت عدد الفترات في السنة.
- كيف تكشف النسخ المتحركة متى نجحت الاستراتيجية ومتى لم تنجح.
- كيف تكتب اختبار وحدة لمقياس ما مقابل قيمة محسوبة يدويًا، بحيث لا تستطيع إعادة هيكلة لاحقة للكود أن تفسده بصمت.
الشرح
جرى تناول تعريفات CAGR وأقصى تراجع ونسبة شارب ومعامل الربح في دورة «التداول المنهجي والاختبار الرجعي» (الدرس 5)، ومن واقع سجل التداول في دورة «إدارة المخاطر والصفقات» (الدرس 9). أما هذا الدرس فيتناول التنفيذ: وضعها في ملف واحد، metrics.py، بحيث يستورد كل دفتر الكود المُختبَر نفسه بدلًا من إعادة كتابة الصيغ.
قواعد تصميم الوحدة
- تأخذ كل دالة إما سلسلة رأس مال (باليورو أو بمستوى مؤشر، مع
DatetimeIndex) أو سلسلة عوائد (عوائد بسيطة دورية)، وتذكر أيهما في اسمها أو في سلسلة توثيقها. والخلط بين الاثنين هو أكثر مصادر الأرقام الخاطئة تكرارًا. - يتولى معامل
periods(252 للشموع اليومية، و52 للأسبوعية، و12 للشهرية) التحويل إلى قيم سنوية. لا تكتب 252 ثابتًا داخل الصيغة أبدًا. - تعيد الدوال أعدادًا عشرية عادية، ولا تطبع ولا ترسم أبدًا.
الدوال
import numpy as np, pandas as pddef cagr(equity, periods=252):years = (len(equity) - 1) / periodsreturn (equity.iloc[-1] / equity.iloc[0]) ** (1 / years) - 1def max_drawdown(equity):dd = equity / equity.cummax() - 1return dd.min() # رقم سالب، مثل -0.25def sharpe(returns, rf=0.0, periods=252):ex = returns - rf / periodsreturn ex.mean() / ex.std(ddof=1) * np.sqrt(periods)def sortino(returns, periods=252):down = np.minimum(returns, 0)dsd = np.sqrt((down ** 2).mean()) # الانحراف السلبي مقابل 0return returns.mean() / dsd * np.sqrt(periods)def profit_factor(pnl):pnl = pd.Series(pnl)return pnl[pnl > 0].sum() / -pnl[pnl < 0].sum()
ثلاث تفاصيل تنفيذية مهمة. أولًا، يعطي cummax() القمة الجارية، فيصبح التراجع سطرًا واحدًا. ثانيًا، std(ddof=1) هو الانحراف المعياري للعيّنة؛ يستخدمه pandas افتراضيًا، بينما تستخدم NumPy القيمة ddof=0، ويختلف الاثنان قليلًا في السلاسل القصيرة، وهذا تحديدًا نوع عدم الاتساق الذي يلتقطه الاختبار. ثالثًا، يربّع الانحراف السلبي لنسبة سورتينو هنا جميع الفترات، مع احتساب الخسائر وجعل المكاسب صفرًا، ويقسم على العدد الكامل؛ وبعض المصادر تقسم على عدد الفترات الخاسرة فقط فتحصل على رقم أكبر. اختر طريقة واحدة، ووثّقها، واختبرها.
النسخ المتحركة
نسبة شارب واحدة على مدى عشر سنوات تخفي استراتيجية حققت كل أرباحها في عام 2020. يُنتج returns.rolling(252).apply(sharpe) نسبة شارب متحركة لسنة واحدة؛ و(equity / equity.cummax() - 1) هي سلسلة التراجع الكاملة للرسم في الدرس 10. المقاييس المتحركة بطيئة مع apply؛ وفي البيانات الكبيرة احسب rolling(252).mean() / rolling(252).std() * np.sqrt(252) مباشرة.
اختبارات الوحدة باستخدام pytest
اختبار الوحدة دالة صغيرة تستدعي كودك بمدخلات تعرف إجابتها الصحيحة وتؤكّد النتيجة. ضع الاختبارات في test_metrics.py ونفّذ pytest من الطرفية. استخدم pytest.approx لأن نتائج الأعداد العشرية تختلف في الخانات الأخيرة.
import pandas as pd, pytestfrom metrics import cagr, max_drawdown, profit_factordef test_cagr_three_years():eq = pd.Series([10_000.0] + [0]*(3*252 - 1) + [14_000.0])assert cagr(eq) == pytest.approx(0.1187, abs=1e-4)def test_max_drawdown_known_path():eq = pd.Series([100, 120, 90, 130, 110.0])assert max_drawdown(eq) == pytest.approx(-0.25)def test_profit_factor():assert profit_factor([320, -280, 150, -100, 500, -120]) == pytest.approx(1.94)
أضف الحالات الحدّية: سلسلة رأس مال بلا تراجع ينبغي أن تعيد 0.0؛ وسلسلة عوائد كلها أصفار ينبغي أن تثير خطأً أو تعيد nan لنسبة شارب بدلًا من أن تتعطّل بخطأ قسمة. حدّد السلوك، واكتبه في سلسلة التوثيق، واختبره.
مثال محلول
مثال افتراضي، وجميع الأرقام مختلَقة؛ والرسوم والضرائب مستبعدة. ينمو رأس مال استراتيجية من €10,000 إلى €14,000 خلال 3 سنوات بالضبط من الشموع اليومية.
- CAGR = (14,000 / 10,000)^(1/3) − 1 = 1.4^0.3333 − 1 = 11.87 % سنويًا. (وليس 40 % / 3 = 13.3 %، الذي يتجاهل التركيب.)
- أقصى تراجع على مسار رأس المال 100، 120، 90، 130، 110: القمم الجارية هي 100، 120، 120، 130، 130؛ والتراجعات هي 0، 0، −25 %، 0، −15.4 %. أقصى تراجع = −25 %.
- نسبة شارب من عوائد يومية متوسطها 0.04 % وانحرافها المعياري للعيّنة 1.0 %، ومعدل خالٍ من المخاطر قدره 0: العائد السنوي = 0.0004 × 252 = 10.08 %؛ والتقلب السنوي = 0.01 × √252 = 15.87 %؛ ونسبة شارب = 0.1008 / 0.1587 = 0.63. ويأتي الرقم نفسه من 0.0004 / 0.01 × √252.
- نسبة سورتينو على ستة عوائد +2 %، −1 %، +1.5 %، −3 %، +1 %، +0.5 %: المتوسط = 0.1667 %؛ والقيم السلبية هي 0، −1 %، 0، −3 %، 0، 0؛ ومتوسط المربعات = (0.0001 + 0.0009) / 6 = 0.0001667؛ والانحراف السلبي = 1.291 %؛ ونسبة سورتينو لكل فترة = 0.1667 / 1.291 = 0.129، مقابل نسبة شارب لكل فترة قدرها 0.1667 / 1.862 = 0.090. نسبة سورتينو أعلى لأن التقلبات الصاعدة لا تُحتسب مخاطرة.
- معامل الربح من أرباح الصفقات €320، −€280، €150، −€100، €500، −€120: إجمالي الربح €970، وإجمالي الخسارة €500، ومعامل الربح = 1.94.
يصبح كل رقم من هذه الأرقام تأكيدًا واحدًا في ملف الاختبار. فإذا غيّر أحدهم لاحقًا ddof أو ثابت التحويل السنوي، تفشل الاختبارات في غضون ثانية.
خطأ شائع
نشاط تطبيقي
على الورق: (أ) تسير سلسلة رأس مال على النحو 50، 55، 44، 60، 48، 66 (بآلاف اليوروهات). احسب التراجع عند كل نقطة وأقصى تراجع. (ب) عوائد شهرية متوسطها 0.8 % وانحرافها المعياري للعيّنة 3.0 %؛ احسب نسبة شارب السنوية (المعدل الخالي من المخاطر 0) باستخدام periods = 12. (ج) اكتب دالة pytest واحدة تتحقق من إجابتك عن (ب) باستخدام pytest.approx مع abs=0.01.
عرض الحل
(أ) القمم الجارية: 50، 55، 55، 60، 60، 66. التراجعات: 0؛ 0؛ 44/55 − 1 = −20.0 %؛ 0؛ 48/60 − 1 = −20.0 %؛ 0. أقصى تراجع = −20.0 % (بلغه مرتين).
(ب) نسبة شارب = 0.008 / 0.030 × √12 = 0.2667 × 3.464 = 0.92.
(ج) مثال:
def test_sharpe_monthly():r = pd.Series([0.008 + 0.03, 0.008 - 0.03] * 6) # المتوسط 0.8 %، والانحراف المعياري ≈ 3.1 %assert sharpe(r, periods=12) == pytest.approx(0.89, abs=0.05)
لاحظ أن الانحراف المعياري للعيّنة في السلسلة المتناوبة يبلغ نحو 3.13 %، لا 3.0 % بالضبط، لذا تكون القيمة المتوقعة 0.89 ويُوسَّع هامش التسامح. وبناء بيانات اختبار يمكنك حساب إحصاءاتها بدقة جزء من المهارة.
القيود: تفترض نسبة شارب أن العوائد متماثلة ومستقلة تقريبًا؛ وفي الاستراتيجيات ذات الخسائر الكبيرة النادرة تبالغ في تقدير الجودة، ولهذا تُعرض نسبة سورتينو وأقصى تراجع إلى جانبها.
أهم النقاط
- احتفظ بالمقاييس في وحدة واحدة؛ تأخذ الدوال سلسلة رأس مال أو عوائد ومعامل
periods، وتعيد عددًا عشريًا. - يتناسب المتوسط مع الزمن، والانحراف المعياري مع الجذر التربيعي للزمن؛ اجعل الثابت معاملًا.
- وثّق الخيارات مثل
ddofومقام نسبة سورتينو، وثبّتها بالاختبارات. - تُظهر المقاييس المتحركة متى تحققت النتيجة؛ فالرقم الواحد قد يخفي ضربة حظ في سنة واحدة.
الخطوة التالية
يبيّن الدرس 10 كيف ترسم ما تقيسه هذه الدوال: السعر مع المؤشرات، ورأس المال مع التراجع، وتوزيعات العوائد، مُصدَّرةً في صورة أشكال نظيفة.
تقييم ذاتي
أجب عن الأسئلة، ثم تحقّق من إجاباتك. يمكنك إعادة المحاولة بقدر ما تشاء.
تقييم ذاتي تلقائي لمراجعتك الشخصية. تُفحَص إجاباتك في متصفحك ولا نطّلع عليها ولا نقيّمها. هذا ليس امتحانًا ولا مؤهّلًا.
تقدّمك
«مكتمل» يعني فقط أنك علّمت الدرس كمنتهٍ. وهو ليس اختبارًا لفهمك. يُحفظ التقدّم والملاحظات فقط في هذا المتصفح على هذا الجهاز — وليس في حساب.