اپیزود ۷- یادگیری ماشین Walk-Forward در فارکس برای پرهیز از Look‑ahead (Case: EURUSD)
سطح: مبتدی تا نیمهحرفهای
هدف: یاد میگیریم ارزیابی مدل را «زمانمحور و واقعگرایانه» انجام دهیم تا به دام Look‑ahead (نگاه به آینده) و خوشبینی کاذب نیفتیم.
مقدمه یادگیری ماشین Walk-Forward در فارکس
در اپیزود ۶ اولین مدل کدنویسی را ساختیم (درخت تصمیم) و روی دادهٔ «train/valid/test» ارزیابی کردیم. اما یک مشکل مهم وجود دارد: بازار تغییر میکند. اگر فقط یکبار مدل را آموزش دهیم و یکبار تست کنیم، ممکن است خوشبینانه قضاوت کنیم.
Walk-Forward در فارکس دقیقاً برای حل این مشکل است:
- پنجرهٔ آموزش را جلو میبریم،
- روی بازهٔ بعدی پیشبینی میکنیم،
- نتایج را ذخیره،
- بعد دوباره یک پله جلو میرویم… تا کل دوره پوشش داده شود.
نتیجهٔ نهایی یک بردار پیشبینی زنده است که هر نقطهاش با اطلاعاتِ فقط «تا دیروز» ساخته شده—مثل دنیای واقعی.لینک مربوط به محتوای پایه سری مقالات یادگیری ماشین در فارکس.
۱- Look‑ahead چیست؟
Look‑ahead یعنی از اطلاعات آینده—حتی ناخواسته—برای ساخت ویژگی، تنظیم مدل یا انتخاب پارامتر استفاده کنیم. مثالهای رایج:
- محاسبهٔ اندیکاتور با میانگین دوطرفه یا استفاده از «close آینده» برای ساخت یک ویژگی.
- اسکیلینگ/نرمالسازی روی کل داده، نه فقط train.
- تقسیم تصادفی دادهٔ زمانی.
اثر: دقتِ ظاهراً عالی روی کاغذ، اما در عمل ضعیف. Walk‑Forward این ریسک را کم میکند.
۲- دو الگوی رایج Walk-Forward در فارکس
Expanding Window پنجرهٔ توسعهیاب: از ابتدای تاریخ تا t آموزش میدهیم و روی بازهٔ بعدی (مثلاً یک ماه بعدی) پیشبینی میکنیم؛ سپس t جلو میرود و پنجره بزرگتر میشود.
Rolling Window پنجرهٔ لغزنده: فقط آخرین N روز/میله را برای آموزش نگه میداریم (مثلاً ۵۰۰ کندل اخیر) تا مدل همیشه «بهروز» بماند.
برای شروع، Expanding سادهتر و باثباتتر است. بعداً میتوانید Rolling را امتحان کنید.
۳- طرح آزمایش ما (Case: EURUSD)
- داده: date, open, high, low, close, volume (D1)
- ویژگیها: sma20, rsi14, atr14, return_1d (مثل اپیزود ۶)
- هدف: target_up = 1 if close[t+1] > close[t] else 0
- مدل پایه: DecisionTreeClassifier با تنظیمات محافظهکارانه (max_depth, min_samples_leaf)
- گام Walk-Forward در فارکس: پیشبینی به صورت یکروز جلوتر؛ ارزیابی تجمیعی روی کل دورهٔ آزمایش
۴- شبهکد ساده (Expanding) Walk-Forward در فارکس
- داده را بر حسب تاریخ مرتب کنید و ویژگیها را بسازید.
- یک نقطهٔ شروع برای آموزش تعیین کنید (مثلاً ۷۰% اولِ داده—یا حداقل ۲ سال اول).
- برای هر گام t از نقطهٔ شروع تا انتها:
-
- آموزش روی [start … t]
- پیشبینی برای t+1
- ذخیرهٔ پیشبینی و برچسب واقعی
- در پایان، معیارها را روی تمامی پیشبینیهای انباشته محاسبه کنید.
۵- کدنویسی گامبهگام Walk-Forward در فارکس
اگر از Anaconda استفاده میکنید: محیط forex-ml اپیزود ۶ کافی است. در VS Code هم همان پکیجها را دارید.
۱ ۲ ۳ ۴ ۵ ۶ ۷ |
import pandas as pd import numpy as np from sklearn.tree import DecisionTreeClassifier from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix |
۱- خواندن داده و ساخت ویژگیها (مثل اپیزود ۶)
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۱۰ ۱۱ ۱۲ ۱۳ ۱۴ ۱۵ ۱۶ ۱۷ ۱۸ ۱۹ ۲۰ ۲۱ ۲۲ ۲۳ ۲۴ ۲۵ ۲۶ ۲۷ ۲۸ ۲۹ ۳۰ ۳۱ ۳۲ ۳۳ ۳۴ ۳۵ ۳۶ ۳۷ ۳۸ ۳۹ ۴۰ ۴۱ ۴۲ ۴۳ ۴۴ ۴۵ ۴۶ |
df = pd.read_csv("EURUSD_D1.csv", parse_dates=["date"]).sort_values("date").reset_index(drop=True) def sma(s, n): return s.rolling(n).mean() def rsi(s, n=۱۴): d = s.diff() up = d.clip(lower=۰); dn = (-d).clip(lower=۰) rs = up.rolling(n).mean() / (dn.rolling(n).mean() + 1e-۹) return ۱۰۰ - ۱۰۰/(۱+rs) def atr(df, n=۱۴): hi, lo, cl = df["high"], df["low"], df["close"] tr = pd.concat([(hi-lo), (hi-cl.shift(۱)).abs(), (lo-cl.shift(۱)).abs()], axis=۱).max(axis=۱) return tr.rolling(n).mean() # Features + target df["sma20"] = sma(df["close"], ۲۰) df["rsi14"] = rsi(df["close"], ۱۴) df["atr14"] = atr(df, ۱۴) df["return_1d"] = df["close"].pct_change() df["target_up"] = (df["close"].shift(-۱) > df["close"]).astype(int) # پاکسازی df = df.dropna().reset_index(drop=True) features = ["sma20","rsi14","atr14","return_1d"] |
۲- نقطهٔ شروع Walk-Forward در فارکس
۱ |
start_index = int(len(df) * ۰.۷) # ۷۰% اول برای شروع؛ قابل تغییر |
۳- حلقهٔ (Expanding) Walk-Forward در فارکس
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۱۰ ۱۱ ۱۲ ۱۳ ۱۴ ۱۵ ۱۶ ۱۷ ۱۸ ۱۹ ۲۰ ۲۱ ۲۲ ۲۳ ۲۴ ۲۵ ۲۶ ۲۷ ۲۸ ۲۹ ۳۰ ۳۱ ۳۲ ۳۳ ۳۴ ۳۵ ۳۶ ۳۷ ۳۸ ۳۹ ۴۰ ۴۱ ۴۲ ۴۳ ۴۴ ۴۵ ۴۶ ۴۷ ۴۸ ۴۹ ۵۰ ۵۱ ۵۲ ۵۳ ۵۴ ۵۵ ۵۶ ۵۷ ۵۸ ۵۹ ۶۰ ۶۱ ۶۲ ۶۳ ۶۴ |
probas, preds, trues, dates = [], [], [], [] for t in range(start_index, len(df)-۱): train = df.loc[:t, features] y_tr = df.loc[:t, "target_up"] X_one = df.loc[[t+۱], features] # نمونهٔ روز بعد برای پیشبینی model = DecisionTreeClassifier(max_depth=۵, min_samples_leaf=۲۰, random_state=۴۲) model.fit(train, y_tr) p = float(model.predict_proba(X_one)[:,۱]) yhat = int(p >= ۰.۵) # آستانه ۰.۵ (فعلاً) probas.append(p) preds.append(yhat) trues.append(int(df.loc[t+۱, "target_up"])) dates.append(df.loc[t+۱, "date"]) # تاریخ نقطهٔ پیشبینی walk_df = pd.DataFrame({"date": dates, "proba_up": probas, "pred": preds, "true": trues}) # ۴) ارزیابی نهایی روی همهٔ پیشبینیها acc = accuracy_score(walk_df["true"], walk_df["pred"]) prec = precision_score(walk_df["true"], walk_df["pred"], zero_division=۰) rec = recall_score(walk_df["true"], walk_df["pred"], zero_division=۰) f1 = f1_score(walk_df["true"], walk_df["pred"], zero_division=۰) cm = confusion_matrix(walk_df["true"], walk_df["pred"]) print("Walk-Forward Metrics:\n", { "Accuracy": round(acc,۳), "Precision": round(prec,۳), "Recall": round(rec,۳), "F1": round(f1,۳) }) print("Confusion Matrix:\n", cm) |
نکات مهم کد بالا
- در هر گام، مدل فقط تا همان روز آموزش میبیند و سپس برای روز بعد پیشبینی میکند.
- خروجیها در walk_df جمع میشود؛ این خروجی «واقعیترین» تصویر از عملکرد مدل است.
۶- آستانهٔ دوطرفه + منطقهٔ خنثی به سبک اپیزود ۶
همان ایده را اینجا هم داریم؛ با احتمالات proba_up سیگنال بسازید:
۱ ۲ ۳ ۴ ۵ ۶ ۷ |
th_buy, th_sell = ۰.۵۵, ۰.۴۵ walk_df["signal"] = np.where(walk_df["proba_up"] >= th_buy, ۱, np.where(walk_df["proba_up"] <= th_sell, -۱, ۰)) walk_df["signal"].value_counts() |
بعداً در اپیزودهای بعدی (بکتست) این سیگنال را با هزینههای معاملاتی میسنجیم.
۷- نسخهٔ Rolling Window اختیاری
برای «فراموش کردن قدیمیها» و تمرکز روی رژیم فعلی بازار:
window = 500 فقط ۵۰۰ کندل آخر را برای آموزش نگه میداریم
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۱۰ ۱۱ ۱۲ ۱۳ ۱۴ ۱۵ ۱۶ ۱۷ ۱۸ ۱۹ ۲۰ ۲۱ ۲۲ ۲۳ ۲۴ ۲۵ ۲۶ |
probas_r, preds_r, trues_r = [], [], [] for t in range(start_index, len(df)-۱): left = max(۰, t - window + ۱) train = df.loc[left:t, features] y_tr = df.loc[left:t, "target_up"] X_one = df.loc[[t+۱], features] model = DecisionTreeClassifier(max_depth=۵, min_samples_leaf=۲۰, random_state=۴۲) model.fit(train, y_tr) p = float(model.predict_proba(X_one)[:,۱]) probas_r.append(p) preds_r.append(int(p>=۰.۵)) trues_r.append(int(df.loc[t+۱, "target_up"])) |
مقایسه: Expanding معمولاً پایدارتر است؛ Rolling واکنشپذیرتر به تغییر رژیم.
۸- خطاهای رایج و دامها
- اسکیل/نرمالسازی روی کل داده: اگر بعداً مدل دیگری (مثل لجستیک) استفاده کردید، فقط روی train هر گام fit کنید.
- تنظیمهای اغراقآمیز: max_depth زیاد → قوانین شکننده.
- گامهای بسیار کوتاه یا بسیار بلند: اگر گام ارزیابی خیلی کوچک باشد، نویز زیاد میشود؛ اگر خیلی بزرگ باشد، تعداد نقاط ارزیابی کم میشود.
- جاانداختن ذخیرهٔ نتایج هر گام: یادتان باشد پیشبینیهای هر گام را نگه دارید تا آمار نهایی معنادار باشد.
۹- تمرینهای کوچک
- Expanding و Rolling را با هم مقایسه کنید (Accuracy/Precision/Recall/F1).
- آستانهها را ۰.۶۰/۰.۴۰ بگذارید و تعداد سیگنالها را بسنجید.
- max_depth را بین ۳ و ۷ تغییر دهید؛ پایداری کدام بهتر است؟
- یک ویژگی سادهٔ دیگر اضافه کنید (مثلاً sma50) و اثرش را روی نتایج Walk‑Forward ببینید.
منابع پیشنهادی
- Scikit‑learn: Time Series Split — برای ایدههای تقسیم زمانی
- ISL — مباحث ارزیابی مدلها
- Babypips / Investopedia — مفاهیم مقدماتی فارکس و اندیکاتورها
- Depth Market Pro — اپیزودهای ۶ و قبلتر برای ساخت ویژگیها