مسیر Quant Developer · بخش ۳: مدلهای ریاضی و Quantitative Finance · درس ۱۰
Regression و Time Series
با رگرسیون خطی بررسی میکنیم آیا عدمتوازن دفتر سفارش حرکت بعدی قیمت را پیشبینی میکند، و وابستگی زمانی، ایستایی و Look-ahead چطور نتیجه را گمراه میکنند.
۱. یک سؤال مشخص
در درس قبل یک متغیر را تنها توصیف کردیم: میانگین و نوسانش. در این درس رابطه دو متغیر را میسنجیم. سؤالمان مشخص است:
آیا عدمتوازن حجم در بالای دفتر سفارش، حرکت بعدی Mid Price را پیشبینی میکند؟
عدمتوازن دفتر سفارش (Order Book Imbalance) را اینطور تعریف میکنیم:
که Vbid و Vask حجم Best Bid و Best Ask هستند. I بین 1 منفی و 1 مثبت است. در دفتر سفارش درس ۱، Best Bid حجم 0.4 و Best Ask حجم 0.3 داشت:
شهودش ساده است: وقتی صف خرید سنگینتر از صف فروش است، احتمال اینکه سطح Ask زودتر مصرف شود و قیمت بالا برود بیشتر است. اگر این رابطه واقعی باشد، هم برای تخمین Fair Value (درس ۵) مهم است و هم برای فهم اینکه کدام Fillها احتمالاً سمّیاند (درس ۸). ولی «شهودش ساده است» دلیل نیست. باید اندازهاش بگیریم.
۲. رگرسیون خطی (OLS)
سادهترین مدل این است:
- β (شیب): به ازای یک واحد افزایش در I، تغییر مورد انتظار Mid در ثانیه بعد چقدر است.
- α (عرض از مبدأ): تغییر مورد انتظار وقتی I صفر است. اگر مدل خوب باشد، باید نزدیک صفر باشد.
- ε (خطا یا Residual): هر چیزی که مدل توضیح نمیدهد.
روش حداقل مربعات معمولی (Ordinary Least Squares یا OLS) α و β را طوری انتخاب میکند که مجموع مجذور خطاها کمینه شود. بعد از برازش، میپرسیم مدل چه سهمی از نوسان ΔMid را توضیح داده است:
R² برابر صفر یعنی مدل هیچ چیزی را توضیح نمیدهد و R² برابر 1 یعنی همه چیز را.
۳. پیادهسازی با numpy
برای آموزش، داده مصنوعی میسازیم که «حقیقت» آن را میدانیم: β واقعی 1.5 دلار است و روی آن نویزی با انحراف معیار 4 دلار سوار شده. عدمتوازن را هم ماندگار میسازیم (هر ثانیه 90% مقدار ثانیه قبل را نگه میدارد)، چون در دفتر سفارش واقعی هم صفها یکشبه عوض نمیشوند.
import numpy as np
rng = np.random.default_rng(11)
n = 5000 # 5000 snapshots, e.g. one per second
# Order-book imbalance in [-1, 1]: persistent, like a real book (AR(1) process)
imb = np.zeros(n)
for i in range(1, n):
imb[i] = 0.9 * imb[i - 1] + rng.normal(0, 0.15)
imb = np.clip(imb, -1, 1)
# Synthetic "truth": next-second mid change = 1.5 USDT per unit of imbalance + noise
dmid = 1.5 * imb + rng.normal(0, 4.0, n)
def ols(x, y):
"""OLS of y on [1, x]. Returns coefficients, standard errors, t-stats, R^2."""
X = np.column_stack([np.ones_like(x), x])
beta, *_ = np.linalg.lstsq(X, y, rcond=None)
resid = y - X @ beta
s2 = resid @ resid / (len(y) - X.shape[1]) # residual variance
se = np.sqrt(np.diag(s2 * np.linalg.inv(X.T @ X)))
r2 = 1 - resid @ resid / ((y - y.mean()) @ (y - y.mean()))
return beta, se, beta / se, r2
beta, se, t, r2 = ols(imb, dmid)
print(f"alpha={beta[0]:+.3f} (t={t[0]:+.1f}) beta={beta[1]:.3f} "
f"(se={se[1]:.3f}, t={t[1]:.1f}) R2={r2:.3f}")
def acf(x, lag):
x = x - x.mean()
return (x[:-lag] @ x[lag:]) / (x @ x)
print("acf imbalance lag1:", round(acf(imb, 1), 2), " acf dmid lag1:", round(acf(dmid, 1), 2))
خروجی:
alpha=+0.077 (t=+1.4) beta=1.582 (se=0.162, t=9.8) R2=0.019
acf imbalance lag1: 0.9 acf dmid lag1: 0.05
تفسیر:
- β تخمینی 1.58 است با خطای استاندارد 0.16. مقدار واقعی 1.5 فقط حدود نیم SE با آن فاصله دارد. با I برابر 0.5، مدل انتظار دارد Mid در ثانیه بعد حدود 0.79 دلار بالا برود.
- α با t برابر 1.4 از صفر قابل تشخیص نیست؛ همان چیزی که انتظار داشتیم.
- R² فقط 1.9% است. یعنی بیش از 98% حرکت ثانیهبهثانیه Mid با این مدل توضیح داده نمیشود.
آیا R² برابر 2% یعنی مدل بیفایده است؟ لزوماً نه. در پیشبینی بازده کوتاهمدت، R² کوچک عادی است؛ سؤال درست این است که پیشبینی در برابر هزینهها چقدر است. 0.79 دلار در برابر نیمSpread دهدلاری درس ۱ کوچک است؛ پس دلیلی برای معامله Taker نمیشود. ولی برای بازارسازی که به هر حال دو طرف قیمت میدهد، میتواند دلیلی باشد که قیمتهایش را کمی به سمت پیشبینی جابهجا کند (همان Skew درس ۶) تا کمتر در طرف اشتباه پر شود.
۴. آزمون فرضیه و p-value به زبان ساده
سؤال آماری این است: آیا β واقعاً صفر نیست، یا ما فقط در نویز الگو دیدهایم؟
- فرض صفر (Null Hypothesis): β برابر صفر است؛ I هیچ اثری ندارد.
- آماره t: چند برابر SE از صفر فاصله داریم.
- p-value: «اگر β واقعاً صفر بود، چقدر احتمال داشت فقط از روی شانس، t را به این بزرگی (یا بزرگتر) ببینیم؟» برای t حدود 2، این احتمال حدود 5% است. برای t برابر 9.8 تقریباً صفر است.
سه سوءبرداشت رایج:
- p-value احتمال درست بودن فرضیه نیست. p-value برابر 3% یعنی «اگر اثری نبود، چنین دادهای نادر بود»، نه «به احتمال 97% اثر واقعی است».
- معنیدار آماری با مهم اقتصادی فرق دارد. با میلیونها ردیف داده Tick، اثرهای خیلی کوچک هم t بزرگ پیدا میکنند. اگر اثر کمتر از کارمزد باشد، معنیدار بودنش کمکی نمیکند.
- آزمونهای متعدد. اگر ۱۰۰ ویژگی بیربط را امتحان کنی، بهطور متوسط ۵ تا در سطح 5% «معنیدار» درمیآیند. هر چه بیشتر بگردی، پیدا کردن الگوی تصادفی آسانتر است.
۵. Time Series: وابستگی زمانی
داده بازار سری زمانی است: ترتیب نقطهها مهم است. یکی از ابزارهای اصلی، خودهمبستگی (Autocorrelation) است: همبستگی یک سری با نسخه تأخیریافته خودش. تابع acf در کد بالا همین را حساب میکند.
در خروجی، خودهمبستگی I با تأخیر یک ثانیه 0.9 است (ماندگار، همانطور که ساختیم) ولی برای ΔMid تقریباً صفر (0.05). یعنی تغییرات قیمت تقریباً حافظه ندارند، ولی ورودی مدل حافظه زیادی دارد.
چرا مهم است؟ فرمول SE که در ols استفاده کردیم فرض میکند خطاها مستقل از هماند. در مثال ما این برقرار است. ولی فرض کن بهجای تغییر یک ثانیه، تغییر ده ثانیه بعد را هر ثانیه پیشبینی کنی. پنجرههای دهثانیهای پشت سر هم ۹ ثانیه مشترک دارند (Overlapping)، خطاهایشان شدیداً به هم وابسته میشوند، و SE معمولی بهطور قابلتوجهی کوچکتر از واقع درمیآید. نتیجه: t بزرگ و اعتماد به نفس کاذب. راهحل استاندارد، استفاده از SE مقاوم به خودهمبستگی (مثل روش Newey–West) یا نمونهبرداری بدون همپوشانی است.
۶. ایستایی و رگرسیون کاذب
یک سری زمانی ایستا (Stationary) است اگر ویژگیهای آماریاش (میانگین، واریانس، خودهمبستگی) در طول زمان ثابت بماند. بازدهها تقریباً ایستا هستند؛ سطح قیمت نیست. قیمت شبیه گام تصادفی (Random Walk) است: هر روز از جایی شروع میکند که دیروز تمام شد، و واریانسش با زمان بزرگتر میشود.
رگرسیون روی دو سری ناایستا یکی از قدیمیترین تلههای آمار است. ادامه همان اسکریپت (با همان تابع ols):
rng = np.random.default_rng(3)
# Two INDEPENDENT random walks: e.g. BTC price and some unrelated index
a = 100_000 + np.cumsum(rng.normal(0, 50, 2000))
b = 5_000 + np.cumsum(rng.normal(0, 10, 2000))
_, _, t_lvl, r2_lvl = ols(b, a) # levels on levels
_, _, t_chg, r2_chg = ols(np.diff(b), np.diff(a)) # changes on changes
print(f"levels : t={t_lvl[1]:6.1f} R2={r2_lvl:.2f}")
print(f"changes: t={t_chg[1]:6.1f} R2={r2_chg:.3f}")
خروجی:
levels : t= -58.9 R2=0.63
changes: t= -0.5 R2=0.000
این دو سری را مستقل ساختیم، ولی رگرسیون سطح روی سطح R² برابر 0.63 و t حدود 59 (با علامت منفی) میدهد. دلیلش این است که هر دو سری روندهای تصادفی طولانی دارند و OLS هر دو روند را «همجهت» یا «خلافجهت» میبیند. به این رگرسیون کاذب (Spurious Regression) میگویند. وقتی همان رابطه را روی تغییرات بسنجی، چیزی باقی نمیماند.
قاعده عملی: روی بازده یا تغییرات رگرسیون بگیر، نه روی سطح قیمت. استثنای مهم وقتی است که دو قیمت دلیل اقتصادی برای کنار هم ماندن دارند (مثلاً قیمت یک دارایی در دو بازار)؛ به این همانباشتگی (Cointegration) میگویند و در درس ۲۳ (Basis و Arbitrage) به آن نزدیک میشویم.
۷. Look-ahead و تلههای دیگر
حتی با آمار درست، چند خطای دادهای میتواند کل نتیجه را بیاعتبار کند:
- Look-ahead Bias: استفاده از اطلاعاتی که در لحظه تصمیم در دسترس نبود. مثالهای رایج: ساختن I از Snapshotی که بعد از تغییر قیمت گرفته شده؛ همترازی اشتباه زمان صرافی با زمان دریافت داده؛ نرمالسازی ویژگی با میانگین و انحراف معیار کل داده؛ انتخاب پارامترها روی همان دادهای که نتیجه را با آن گزارش میکنی. نشانه هشدار: نتیجهای که «زیادی خوب» است.
- تقسیم داده: داده آزمون را بر اساس زمان جدا کن (گذشته برای برازش، آینده برای آزمون)، نه با جابهجایی تصادفی ردیفها. با ردیفهای ماندگار، جابهجایی تصادفی اطلاعات آینده را به داده آموزش نشت میدهد.
- تغییر رابطه در طول زمان: β امروز لزوماً β ماه بعد نیست. رابطهای که در دوره آرام برقرار است ممکن است در دوره پرتلاطم عوض شود؛ همان ناایستایی، این بار در خود رابطه.
این تلهها را در درس ۱۷ (طراحی Backtest واقعگرایانه) با جزئیات بیشتر میبینیم.
۸. تمرین این درس
درس بعدی
در درس یازدهم ابزارهای این بخش را کنار هم میگذاریم و به اولین مدل کلاسیک بازارسازی میرسیم: مدل Avellaneda–Stoikov. میبینیم چطور از حرکت تصادفی قیمت، ریسکگریزی و احتمال پر شدن سفارش، دو فرمول بیرون میآید: Reservation Price که قیمت را بر اساس موجودی جابهجا میکند، و Spread بهینه.
این مطلب صرفاً آموزشی است و توصیه خرید، فروش یا سرمایهگذاری نیست.