شمعة واحدة في كل مرة
يتصرف الاختبار الرجعي المدفوع بالأحداث مثل نظام حي: يتقدم الزمن شمعة شمعة، وفي كل شمعة لا يرى البرنامج إلا الماضي، ويقرر ويرسل أمرًا ويحدّث حسابه. وهو أبطأ من الاختبار المتجه، لكنه يحمل حالة (نقد وأسهم) ويستطيع نمذجة الأوامر والتنفيذ، ويمكن لشيفرة الاستراتيجية نفسها أن تعمل لاحقًا على بث حي. والحلقة هي الجوهر: for على الشموع والحساب ينتقل من دورة إلى التالية.
def crossover_signal(history, fast, slow):
if len(history) < slow:
return 0 # not enough history yet
fast_mean = sum(history[-fast:]) / fast
slow_mean = sum(history[-slow:]) / slow
return 1 if fast_mean > slow_mean else 0
prices = [100, 101, 103, 102, 105, 107, 104, 103, 99, 98]
print(crossover_signal(prices[:3], 2, 4)) # 0: only 3 prices
print(crossover_signal(prices[:6], 2, 4)) # 1: the 2-bar mean is above the 4-bar mean
print(crossover_signal(prices[:9], 2, 4)) # 0: the trend has turnedالأوامر تغيّر الحساب
يحوّل الأمر النقد إلى أسهم أو العكس. ومع الأسهم الصحيحة يترك شراء int(cash // price) بعض النقد، وقيمة الحساب دائمًا cash + shares * price. وإبقاء منطق الأوامر في دالة صغيرة execute_order يقصّر الحلقة ويتيح إضافة الرسوم والانزلاق لاحقًا في موضع واحد. وبيع كل شيء هو ببساطة cash + shares * price وصفر أسهم.
def execute_order(cash, shares, price, target):
if target == 1:
bought = int(cash // price) # whole shares only
return cash - bought * price, shares + bought
return cash + shares * price, 0
cash, shares = execute_order(1000.0, 0, 103.0, 1)
print(cash, shares, cash + shares * 103.0) # 73.0 9 1000.0
cash, shares = execute_order(cash, shares, 99.0, 0)
print(cash, shares) # 964.0 0الحلقة وما يحق لها معرفته
في الشمعة t يجوز للاستراتيجية استخدام prices[:t] التي تنتهي عند الإغلاق السابق، وتتداول بسعر prices[t]. وقطع prices[:t] هو ما يُبقي المستقبل بعيدًا: فالسعر التالي ببساطة ليس في القائمة. ومن هنا اختبار جيد لمحرك الأحداث: تشغيله على أول k شمعة فقط يجب أن يعطي أول k نتيجة من التشغيل الكامل. وإن اختلفت فقد تلصصت الاستراتيجية.
def crossover_signal(history, fast, slow):
if len(history) < slow:
return 0
return 1 if sum(history[-fast:]) / fast > sum(history[-slow:]) / slow else 0
def execute_order(cash, shares, price, target):
if target == 1:
bought = int(cash // price)
return cash - bought * price, shares + bought
return cash + shares * price, 0
def run_backtest(prices, fast, slow, cash):
shares = 0
values = []
for t, price in enumerate(prices):
if t > 0:
target = crossover_signal(prices[:t], fast, slow) # only what was known at the last close
cash, shares = execute_order(cash, shares, price, target)
values.append(cash + shares * price)
return values
prices = [100, 101, 103, 102, 105, 107, 104, 103, 99, 98]
full = run_backtest(prices, 2, 4, 1000.0)
print([round(v, 2) for v in full])
print(run_backtest(prices[:6], 2, 4, 1000.0) == full[:6]) # True: the future never leaks# The benchmark: buy once at the start and hold
import random
def buy_and_hold(prices, cash):
shares = int(cash // prices[0])
left = cash - shares * prices[0]
return [left + shares * price for price in prices]
random.seed(3)
prices = [100.0]
for _ in range(99):
prices.append(round(prices[-1] * (1 + random.gauss(0.0004, 0.012)), 2))
held = buy_and_hold(prices, 10000.0)
print(round(held[0], 2), round(held[-1], 2))- يمر الاختبار المدفوع بالأحداث على الشموع بحالة (نقد وأسهم)؛ وفي الشمعة
tلا يستخدم إلاprices[:t]. - تنقل الأوامر بين النقد والأسهم الصحيحة: قيمة الحساب
cash + shares * price؛ أبقِ منطق الأوامر في دالة واحدة. - اختبر النظر إلى المستقبل: تشغيل أول
kشمعة يجب أن يساوي أولkنتيجة من التشغيل الكامل؛ وقارن بالشراء والاحتفاظ.
التالي: انحياز النظر إلى المستقبل، الخطأ الصامت الذي يجعل الاختبار الرجعي أجمل من أن يكون حقيقيًا.
المختبر: ما الذي يُقيَّم
- قارن متوسطين باستخدام
crossover_signal25 نقطة - نفّذ الأمر باستخدام
execute_order25 نقطة - اعبر الشموع شمعة شمعة باستخدام
run_backtest25 نقطة - حدّد المقياس المرجعي باستخدام
buy_and_hold25 نقطة