سوگیری نگاه به آینده در بکتست چیست
سوگیری نگاه به آینده نشت اطلاعات آینده به بکتست است. این مطلب سه مکانیسم اصلی آن شامل تصمیمات هممیله، سوگیری بقا و ارقام تجدیدنظر شده را بررسی کرده و روشهای تشخیص آن را توضیح میدهد.
سوگیری نگاه به آینده (Look-ahead bias) به استفاده از اطلاعاتی در یک بکتست گفته میشود که هیچکس در لحظه انجام معامله شبیهسازیشده نمیتوانست در اختیار داشته باشد. منحنی سرمایه بالا میرود، آمارها عالی به نظر میرسند، و هیچکدام از اینها در تماس با یک سفارش زنده دوام نمیآورند. این صفحه این سوگیری را تعریف میکند، سه مکانیسمی که آن را تولید میکنند را بررسی میکند، و هر یک را روی دادههای واقعی بازار اندازهگیری میکند.
سوگیری نگاه به آینده چیست
یک بکتست ادعایی درباره یک تصمیم است: در این برچسب زمانی، با این اطلاعات، قانون این کار را میکرد. این ادعا تنها زمانی معتبر است که هر ورودی که قانون میخواند، به شکل نهایی خود، قبل از آن برچسب زمانی وجود داشته باشد. سوگیری نگاه به آینده هرگونه نقض آن شرط است، و بیصدا است. هیچ چیز در یک صفحه گسترده یا یک اسکریپت وقتی که یک ردیف مقداری را برمیگرداند که برچسب زمانی آن دیرتر از تصمیمی است که به آن تغذیه میکند، شکایت نمیکند.
سه نوع نقض، بیشتر مواردی را که در عمل دیده میشود پوشش میدهند. تصمیمی که یک میله قیمتی را میخواند که در همان محدوده آن معامله میکند. یک جهان (universe) که از نامهایی که امروز هنوز فهرست شدهاند جمعآوری شده است. یک فیلد داده که پس از تاریخی که روی خود دارد، بازبینی شده است.
مکانیسم یک: تصمیمگیری روی میلهای که در آن معامله میکنید
با قانونی شروع کنید که بیضرر به نظر میرسد. خرید در زمان قدرت، یعنی در جلساتی که صندوق بالاتر از قیمت بازگشایی خود بسته میشود. اگر کدنویسی بیدقت باشد، تست در زمان بازگشایی آن جلسه وارد و در زمان بسته شدن آن جلسه خارج میشود، در حالی که شرایطی که روی آن غربال میکند، قیمت بسته شدن همان میله است. هیچکس در زنگ آغاز معاملات، قیمت بسته شدن را نمیداند.
پنل زیر هر دو نسخه را روی SPY، ردیاب S&P 500، در ده سال تقویمی اجرا میکند. ستون اول نسخه غیرممکن است. ستون دوم شرایط یکسان را با یک جلسه تاخیر اجرا میکند، که اولین زمانی است که یک حساب سرمایهگذاریشده میتوانست روی آن عمل کند.
کد دقیق SQL پشت هر عدد
WITH bars AS (
SELECT date,
toFloat64(open) AS o,
toFloat64(close) AS c,
toFloat64(close) > toFloat64(open) AS up_day
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= toDate('2015-12-01')
AND date <= toDate('2025-12-31')
),
lagged AS (
SELECT date, o, c, up_day,
any(up_day) OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS prior_up
FROM bars
)
SELECT toYear(date) AS year,
countIf(up_day) AS signal_days,
round(100 * avgIf(c / o - 1, up_day), 3) AS same_bar_avg_pct,
round(100 * avgIf(c / o - 1, prior_up), 3) AS lagged_avg_pct,
round(100 * (avgIf(c / o - 1, up_day) - avgIf(c / o - 1, prior_up)), 3) AS gap_pp
FROM lagged
WHERE date >= toDate('2016-01-01')
GROUP BY year
ORDER BY yearجدایی ظریف نیست. در 2016 نسخه همان-میله به طور متوسط 0.447% در هر جلسه واجد شرایط درآمد در حالی که نسخه تاخیری به طور متوسط -0.008% درآمد، که اختلافی معادل 0.455 واحد درصد در روز است. در 2025 این دو عدد 0.636% و -0.03% را نشان میدهند. با تقریباً 140 جلسه واجد شرایط در یک سال، اختلاف نیم واحد درصد در هر جلسه به منحنی سرمایهای تبدیل میشود که هیچ حساب زندهای تا به حال چاپ نکرده است.
کل مزیت، تعریفی است. انتخاب جلساتی که بالاتر از قیمت بازگشایی خود بسته شدند و سپس اندازهگیری بازدهی از بازگشایی تا بسته شدن در همان جلسات، کمیتی را اندازهگیری میکند که تضمین شده مثبت است. قانون فیلتر خود را کشف کرد.
انواع مختلف این سوگیری همیشه در دادههای روزانه پنهان میشوند: یک میانگین متحرک که شامل قیمت بسته شدن امروز است و برای معامله امروز استفاده میشود، یک سیگنال محاسبهشده روی قیمتهایی که قبلاً برای یک تقسیم سهام (split) که هنوز اتفاق نیفتاده بود تعدیل شدهاند، یک توقف درونروزی (intraday stop) که در یک حداکثر جلسهای قرار داده شده که هنوز چاپ نشده بود. شکل رایج، یک برچسب زمانی تصمیم است که زودتر از یکی از ورودیهای خودش قرار دارد.
مکانیسم دو: جهانی برگرفته از بازماندگان امروز
مکانیسم دوم هرگز به قانون دست نمیزند. در لیست نامهایی قرار دارد که قانون مجاز به دیدن آنهاست.
یک لیست تیکر از یک ارائهدهنده داده فعلی بردارید، آن را ده سال به عقب اجرا کنید، و نمونه به طور ضمنی هر شرکتی را که در طول مسیر ورشکست شد، خریداری شد یا از فهرست خارج شد، حذف میکند. هرگز بازندگان به قانون پیشنهاد نشدند. پنل اندازه این حذف را اندازهگیری میکند: هر نام فهرستشده در ایالات متحده که حداقل 200 جلسه بالای 1 دلار در یک سال تقویمی معامله کرد، سهم آن گروه که هنوز در جولای 2026 قیمت چاپ میکنند، و میانه بازده سال تقویمی کل گروه در کنار میانه بازده فقط بازماندگان.
کد دقیق SQL پشت هر عدد
WITH recent AS (
SELECT DISTINCT ticker
FROM global_markets.stocks_daily_aggs
WHERE date >= toDate('2026-06-15')
AND date <= toDate('2026-07-28')
),
per_name AS (
SELECT toYear(date) AS year,
ticker,
argMin(toFloat64(close), date) AS first_close,
argMax(toFloat64(close), date) AS last_close,
count() AS sessions
FROM global_markets.stocks_daily_aggs
WHERE date >= toDate('2015-01-01')
AND date <= toDate('2024-12-31')
AND close > 1
GROUP BY year, ticker
HAVING sessions >= 200
)
SELECT year,
uniqExact(ticker) AS names_trading,
round(100 * uniqExactIf(ticker, ticker IN (SELECT ticker FROM recent)) / uniqExact(ticker), 1) AS still_listed_pct,
round(100 * quantileDeterministic(0.5)(last_close / first_close - 1, cityHash64(ticker)), 2) AS median_return_all_pct,
round(100 * quantileDeterministicIf(0.5)(last_close / first_close - 1, cityHash64(ticker), ticker IN (SELECT ticker FROM recent)), 2) AS median_return_survivors_pct,
round(100 * (quantileDeterministicIf(0.5)(last_close / first_close - 1, cityHash64(ticker), ticker IN (SELECT ticker FROM recent))
- quantileDeterministic(0.5)(last_close / first_close - 1, cityHash64(ticker))), 2) AS survivor_gap_pp
FROM per_name
GROUP BY year
ORDER BY yearاز 7063 نامی که در 2015 از مانع عبور کردند، 52.5% هنوز در جولای 2026 در حال معامله بودند. برای گروه 2024 این رقم 87.5% است، که عمدتاً به زمان سپری شده مربوط میشود: یک نام از سال 2024 دو سال فرصت داشته تا ناپدید شود و یک نام از سال 2015 یازده سال.
ستونهای بازده خود سوگیری را حمل میکنند. در 2017 میانه بازده نامها 7.51% بود در حالی که میانه بازده بازماندگان 11.32% بود، شکافی به اندازه 3.81 واحد درصد که رایگان داده شده است. این شکاف در هر یک از 10 سال نشاندادهشده مثبت است. یک استراتژی که روی بازماندگان آزمایش میشود، قبل از انجام یک تصمیم، آن را به ارث میبرد، و قوانینی که برای ضعف غربال میکنند، بیش از میانه آن را به ارث میبرند، زیرا نامهایی که ناپدید شدند به طور نامتناسبی در آن سطل قرار دارند.
راهحل، یک جهان نقطه-در-زمان (point-in-time universe) است: لیست نامها همانطور که در هر تاریخ تعادل مجدد (rebalance date) بود، شامل حذفیات، با یک موقعیت حذفشده که در آخرین چاپ آن بسته شده است به جای حذف از رکورد.
مکانیسم سه: دادهای که پس از برچسب تاریخ آن بازبینی شد
قیمتها یک بار مهر میخورند. دادههای بنیادی اینطور نیستند. یک رقم فصلی تاریخ پایان فصل را حمل میکند، توسط یک پرونده بعدی اصلاح میشود، و یک پایگاه داده که فقط نسخه فعلی را ذخیره میکند، با خوشحالی عدد اصلاحشده را در برابر تاریخ اصلی ارائه میدهد. قانونی که روی آن عدد غربال میکند، در حال خواندن تصحیحی است که ماهها طول کشید تا برسد.
همین الگو در لیستهای عضویت شاخص که به اعضای فعلی بازسازی شدهاند، تاریخچه تخمین تحلیلگران که با اجماع نهایی بازنویسی شده است، و تعدیلهای اقدامات شرکتی که به صورت معکوس در کل یک سری قیمت اعمال شدهاند، جریان دارد. در هر مورد، رکورد ذخیرهشده یک عکس فوری زمان حال است که یک تاریخ زمان گذشته بر تن دارد.
دو روش آن را مهار میکنند. از منبعی استفاده کنید که نسخهها (vintages) را نگه میدارد، یعنی مقدار همانطور که در هر تاریخ شناخته شده بود به جای مقداری که اکنون شناخته شده است. در جایی که نسخهها در دسترس نیستند، یک تاخیر گزارشدهی به اندازه کافی سخاوتمندانه برای پوشش تاخیر واقعی اعمال کنید، و طول آن تاخیر را به عنوان یک پارامتر ارزش آزمایش کردن در نظر بگیرید، نه یک جزئیات ارزش حدس زدن.
منحنی وقتی که آینده نشت میکند چه شکلی است
سوگیری نگاه به آینده یک امضا دارد: عملکردی که بسیار بالاتر از آنچه بازار زیربنایی ارائه داد قرار دارد. یک نقطه مرجع مفید، سقف پسنگری (hindsight ceiling) است، رشدی که یک پیشبینی کامل یک روز جلوتر روی یک ابزار واحد تولید میکرد.
کد دقیق SQL پشت هر عدد
WITH bars AS (
SELECT date,
toFloat64(close) AS c,
any(toFloat64(close)) OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS prev_c
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= toDate('2015-12-01')
AND date <= toDate('2025-12-31')
),
rets AS (
SELECT toYear(date) AS year,
c / prev_c - 1 AS r
FROM bars
WHERE date >= toDate('2016-01-01')
AND prev_c > 0
)
SELECT year,
count() AS sessions,
round(100 * (exp(sum(log(1 + r))) - 1), 1) AS buy_and_hold_pct,
round(100 * (exp(sumIf(log(1 + r), r < 0.02)) - 1), 1) AS without_big_up_days_pct,
round(exp(sum(log(1 + abs(r)))), 1) AS hindsight_ceiling_multiple
FROM rets
GROUP BY year
ORDER BY yearسقف عظیم است. در 2020، پیشآگاهی کامل یک روزه روی SPY سرمایه را 27.7 برابر در 253 جلسه ضرب کرد، در مقابل 16.2% برای صرفاً نگه داشتن صندوق. در آرامترین سال از ده سال، 2017، همان پیشبینی کامل 2.2 برابر سرمایه بازگرداند.
ستون میانی نشان میدهد که این سقف چقدر متمرکز است. جلساتی که بیش از 2٪ سود داشتند را حذف کنید، و 2020 از 16.2% به -44.9% تبدیل میشود. نشتی که ارزش تنها یک مشت از آن جلسات در سال را داشته باشد، یک بکتست را بسیار دور میبرد. منحنی با افتهای کم عمق، نرخ موفقیت بالا، و بهترین روزهایش که روی خشونتترین جلسات بازار متمرکز شدهاند، مستحق حسابرسی است.
کالیبراسیون (Calibration) مهمتر از هر آستانه واحدی است. قانونی که ادعای 40٪ بازدهی صاف در سال به مدت یک دهه دارد، ادعای بخش بزرگی از سقف پسنگری را دارد، و سوال صادقانه این است که کدام ورودی به آن گفت چه چیزی قرار است اتفاق بیفتد.
چگونه یک بکتست را برای سوگیری نگاه به آینده آزمایش کنیم
- همه چیز را یک میله عقب بیندازید. هر سیگنال را یک دوره کامل کامل به جلو ببرید و دوباره اجرا کنید. لبههای واقعی کمی کاهش مییابند. نشتها فرو میریزند، اغلب به صفر.
- آینده را بر هم بزنید. دادههای بعد از هر برچسب زمانی تصمیم را بردارید، آن را با نویز یا یک بازآرایی (reshuffle) جایگزین کنید، و منطق تصمیم را دوباره اجرا کنید. هر تصمیم باید یکسان بیرون بیاید. هر تصمیمی که تغییر کند، چیزی را خوانده که نمیتوانست بداند.
- با برچسبهای زمانی به عنوان داده رفتار کنید. برای هر ورودی، لحظهای که یک مقدار در دسترس شد را در کنار لحظهای که توصیف میکند ذخیره کنید. یک join که هرگز آن دو ستون را مقایسه نمیکند، نمیتواند قانون را اجرا کند.
- جهان را به تاریخ آن بازسازی کنید. نامهای حذفشده و خریداریشده را شامل شوید، آنها را در آخرین چاپشان ببندید، و همان قانون را روی هر دو جهان اجرا کنید تا سهم بقایافتگی (survivorship contribution) را اندازهگیری کنید.
- در برابر سقف اندازهگیری کنید. بازده استراتژی را در کنار یک رقم خرید و نگهداری و یک رقم پیشآگاهی کامل برای همان پنجره قرار دهید. فاصله از سقف، زمینهای است که نسبت شارپ (Sharpe ratio) به تنهایی نمیدهد.
اندازهگذاری موقعیت (Position sizing) در پایین دست همه اینها قرار دارد. یک نرخ برد متورم، یک اندازه شرط بندی متورم را تغذیه میکند، که همان جایی است که اندازهگذاری موقعیت با معیار کلی یک خطای حسابداری را به یک افت سرمایه تبدیل میکند. همان انضباط قبل از حرکت سرمایه واقعی اعمال میشود: معامله آزمایشی (paper trading) قبل از پول واقعی مکانیک سفارش را آزمایش میکند، و پشتههای خودکار خطر یکسان را با سرعت ماشین حمل میکنند، که در سیستمهای معاملاتی هوش مصنوعی چند عامله پوشش داده شده است. اثراتی که از آزمایش دقیق نقطه-در-زمان جان سالم به در میبرند، مانند ناهنجاری نوسان کم، دقیقاً به همین دلیل جالب هستند.
پرسشهای متداول
سوگیری نگاه به آینده در بکتستینگ چیست؟
استفاده از دادهها در یک معامله شبیهسازیشده است که در زمانی که معامله قرار بود انجام شود در دسترس نبود. اشکال رایج عبارتند از تصمیمگیری روی یک میله قیمتی که معامله در همان محدوده آن اجرا میشود، غربال کردن جهانی از شرکتهایی که امروز هنوز فهرست شدهاند، و خواندن یک رقم مالی که پس از تاریخی که به آن الصاق شده بود بازبینی شد.
چگونه میتوان سوگیری نگاه به آینده را تشخیص داد؟
هر سیگنال را یک دوره کامل کامل به جلو ببرید و دوباره اجرا کنید: یک نشت معمولاً فرو میریزد در حالی که یک لبه واقعی به آرامی کاهش مییابد. یک آزمایش قویتر تمام دادههای بعد از هر برچسب زمانی تصمیم را با نویز جایگزین میکند و تأیید میکند که هر تصمیم بدون تغییر بیرون میآید.
آیا سوگیری بقایافتگی (Survivorship bias) با سوگیری نگاه به آینده یکی است؟
آنها متمایز هستند اما با هم سفر میکنند. سوگیری بقایافتگی نمونهای است که از نامهایی ساخته شده که دوام آوردند، و دانستن اینکه کدام نامها دوام آوردند خود اطلاعاتی از آینده است، بنابراین یک جهان فقط-بازمانده یک نشت نگاه به آینده است که در دادهها قرار دارد نه در قانون.
چرا بکتستهای دارای سوگیری نگاه به آینده اینقدر خوب به نظر میرسند؟
نشت نتایج را انتخاب میکند نه اینکه آنها را پیشبینی کند. در پنل بالا، غربال کردن جلسات بر اساس قیمت بسته شدن خودشان، میانگین سود جلسهای حدود 0.455 واحد درصد بالاتر از نسخه تاخیری در 2016 تولید کرد، مزیتی که تعریفی است نه اکتسابی.
داده نقطه-در-زمان (Point-in-time data) چیست؟
داده نقطه-در-زمان هر مقدار را همانطور که در هر تاریخ تاریخی شناخته شده بود ذخیره میکند، شامل رقم اصلی و هر بازبینی بعدی، در کنار جهان اوراق بهادار همانطور که در آن روز بود. این مرجعی است که یک بکتست برای نشان دادن اینکه ورودیهایش قبل از تصمیماتش وجود داشتند به آن نیاز دارد.
هر رقم بالا یک پرس و جو (query) ذخیرهشده و نسخهبندیشده روی میلههای روزانه واقعی است. SQL هر پنل را باز کنید، یا آزمایش تاخیر را روی قانون خود در ترمینال Strasmore اجرا کنید.