σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft Academyالمسارات
Ctrl K
AO
+1.2k متعلّم
تسجيل الدخولانضم ↗
المسارات
AO
+1.2k متعلّم
تسجيل الدخول
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
مسار Python›الوحدة 10 · هندسة Python عالية الأداء›الموضوع 10.3·~50 دمتقدم

الاختبار باستخدام pytest

الموضع في المسارالوحدة 10 من 11 · الموضوع 3 من 13
10.3 الاختبار باستخدام pytest
—درجة المختبر— / 100إجمالي XP—التتابع—

لماذا نختبر وكيف يبدو الاختبار

في التداول لا يكون الخطأ استثناءً على الشاشة بل رقمًا خاطئًا يبدو معقولًا: إشارة معكوسة في الأرباح والخسائر، أو رسومًا مطبقة مرتين، أو إشارة تستعمل بصمت سعر الغد. والاختبارات برامج صغيرة تفحص شيفرتك مقابل إجابات تعرفها فيُلتقط الخطأ على جهازك لا في السوق. وتجد pytest الدوال المسماة test_* وتشغّل كلًّا منها وتبلغ عن التي يفشل فيها assert. ولا يحتاج الاختبار إطارًا ليُقرأ: جهّز مدخلًا واستدعِ الدالة وassert لما تتوقعه.

# A test is a function with an assert; pytest runs every test_* function it finds
def net_pnl(entry, exit_price, quantity, fee):
    return (exit_price - entry) * quantity - fee

def test_net_pnl_long_winner():
    assert net_pnl(100.0, 102.0, 50, 1.0) == 99.0

def test_net_pnl_short_loser():
    assert net_pnl(100.0, 102.0, -50, 1.0) == -101.0

for test in (test_net_pnl_long_winner, test_net_pnl_short_loser):
    test()                                           # pytest does this for you and reports failures
    print(test.__name__, "passed")

الأعداد العشرية والأخطاء والحالات الكثيرة

الأعداد العشرية تقريبية، لذا فإن 0.1 + 0.2 == 0.3 خطأ. قارن الأرقام بتسامح: تقبل pytest.approx(expected) فرقًا نسبيًا صغيرًا (افتراضيًا جزء من مليون، مع حد أدنى مطلق ضئيل 1e-12) ويمكن إضافة تسامح مطلق للقيم القريبة من الصفر حيث لا يفيد النسبي. وللتحقق من رفض المدخل السيئ تنجح pytest.raises(ValueError) فقط إذا وقع ذلك الاستثناء وتفشل إن لم يقع شيء (أو وقع غيره). ولتشغيل اختبار واحد على حالات كثيرة يقدّم @pytest.mark.parametrize جدولًا من المدخلات والقيم المتوقعة ويبلّغ عن كل صف على حدة ويجعل إضافة حالة تغييرًا من سطر واحد.

# Floats need a tolerance: approx accepts a tiny relative difference, == does not
import pytest

print(0.1 + 0.2 == 0.3)                              # False
print(0.1 + 0.2 == pytest.approx(0.3))               # True
print(1e-10 == pytest.approx(0.0))                   # False  near zero a relative tolerance cannot help
print(1e-10 == pytest.approx(0.0, abs=1e-9))         # True   a larger absolute tolerance does
# raises: the test passes only if the expected exception happens
import pytest

def leverage(gross, equity):
    if equity <= 0:
        raise ValueError("equity must be positive")
    return gross / equity

with pytest.raises(ValueError) as info:
    leverage(1000.0, 0.0)
print("raised:", info.value)                         # raised: equity must be positive

try:
    with pytest.raises(ValueError):
        leverage(1000.0, 500.0)                      # no exception: pytest.raises itself fails
except BaseException as outcome:
    print(type(outcome).__name__)                    # Failed
# parametrize in miniature: one function, a table of cases, each row judged on its own
def fee(notional, rate=0.001, minimum=1.0):
    return max(notional * rate, minimum)

cases = [
    ((100_000.0,), 100.0),
    ((500.0,), 1.0),                                 # the minimum fee applies
    ((2_000.0, 0.002), 4.0),
    ((0.0,), 1.0),
]
for index, (args, expected) in enumerate(cases):
    got = fee(*args)
    print(index, "ok" if abs(got - expected) <= 1e-9 * abs(expected) else f"FAIL got {got}")

الـ Fixtures والبذور وما الذي نختبره

الـ fixture إعداد مشترك تطلبه الاختبارات بالاسم: سلسلة أسعار صغيرة أو مولّد عشوائي ببذرة أو ملف مؤقت. ومع البيانات العشوائية ثبّت البذرة (أو اطبعها) ليمكن إعادة الفشل: فاختبار يفشل مرة كل مئة تشغيل ولا يمكن إعادته أسوأ من لا شيء. اختبر السلوك الذي يهمك لا التفاصيل الداخلية: حالات معروفة يدويًا، وحالات حدّية (مدخل فارغ، صفر، عنصر واحد)، وثوابت يجب أن تصح دائمًا (الاحتمال بين 0 و1، والشراء ثم البيع بالسعر نفسه يخسر الرسوم بالضبط)، واختبار انحدار لكل خطأ تصلحه. وحين تستطيع صياغة قاعدة تصح لكل مدخل، فإن البحث عن مثال مضاد بين مدخلات عشوائية كثيرة اختبار رخيص وقوي.

# A seed makes a random test replayable; a property is searched for a counterexample
import random

def find_counterexample(prop, inputs):
    for value in inputs:
        if not prop(value):
            return value
    return None

rng = random.Random(42)                              # fixed seed: the same inputs on every run
returns = [round(rng.uniform(-0.05, 0.05), 4) for _ in range(1000)]

def compound_is_positive_growth(r):
    return (1 + r) > 0                               # a daily return above -100% keeps the value positive

print(find_counterexample(compound_is_positive_growth, returns))      # None  the rule holds for all 1000
print(find_counterexample(lambda r: r > -0.049, returns))             # a return that breaks a wrong rule
اختبار لا يمكن أن يفشل لا يثبت شيئًااجعل كل اختبار يفشل أولًا بإفساد الشيفرة عمدًا لتعرف أنه قادر على التقاط الخطأ المقصود. ولا تنسخ معادلة الدالة نفسها إلى الاختبار: فإن اشتركا في الخطأ اتفقا ونجح الاختبار. احسب القيمة المتوقعة باستقلال أو يدويًا. وأبقِ الاختبارات سريعة وخالية من الشبكة والساعة لتشغّلها فعلًا قبل كل تغيير.
  • يجهّز الاختبار المدخل ويستدعي الدالة وassert للنتيجة؛ وتشغّل pytest كل دالة test_* وتبلغ عن الإخفاقات.
  • قارن الأعداد العشرية بـ pytest.approx (وأضف تسامحًا مطلقًا قرب الصفر) وتوقّع الأخطاء بـ pytest.raises واستعمل parametrize لجداول الحالات.
  • ثبّت البذور العشوائية واختبر الحالات الحدّية والثوابت والأخطاء المصلَحة وابحث عن أمثلة مضادة واجعل كل اختبار يفشل مرة قبل أن تثق به.

التالي: تحليل أداء شيفرة بايثون، قياس أين يذهب الوقت قبل محاولة تسريعها.

المختبر: ما الذي يُقيَّم

  • قارن الأعداد العشرية بتسامح في is_close25 نقطة
  • شغّل جدول حالات باستخدام run_cases25 نقطة
  • توقّع خطأً باستخدام raises25 نقطة
  • ابحث عن حالة فاشلة باستخدام find_counterexample25 نقطة
المستكشف · lab-119-1
▾ lab-119-1
…
bash · داخل المتصفح
learner@quantcraft:~/lab-119-1$
لم يتم تحميل Python بعدPythonLn 1, Col 1Spaces: 4UTF-8

هل تريد إعادة ضبط هذا الموضوع؟

سيعود كل ما في lab-119-1 إلى الملفات الابتدائية وستُمسح الطرفية. ستفقد حلّك الحالي ولا يمكن التراجع عن ذلك.

سيُعاد ضبطه
  • main.py ← الشيفرة الابتدائية
  • سجل الطرفية ومخرجات الاختبارات
  • التغييرات غير المُرسلة
سيُحتفظ به
  • أفضل درجة في المختبر
  • نقاط XP والتتابع
  • تقدّم المحاضرة والملاحظات

سجّل الدخول لتشغيل الكود الخاص بك.

قراءة كل المحاضرات مجانية. أنشئ حسابًا أو سجّل الدخول لتشغيل الكود وتنفيذ الاختبارات وتسليم هذا المختبر.

  • بيئة تطوير داخل المتصفح مع طرفية bash — لا حاجة لتثبيت أي شيء
  • تصحيح فوري ونقاط خبرة ومكانك في المسار
إنشاء حسابتسجيل الدخول
أو
المتابعة عبر GitHub