Strasmore Research
Deep Dives · Matt ConnorBy Matt Connor ·

سوگیری بقا در داده‌های سهام چیست؟

سوگیری بقا با حذف نمادهای منحل شده یا خریداری شده از مجموعه‌ داده‌ها، نتایج آزمون‌های بازگشتی را مخدوش می‌کند. با نحوه شناسایی این شکاف آماری و تحلیل دقیق بازار آشنا شوید.

سوگیری بقا (Survivorship bias) در داده‌های سهام، شکافی است که هنگام استفاده از فایلی که تنها شامل نمادهای (tickers) فعالِ امروز است، ایجاد می‌شود. هر شرکتی که از فهرست بورس حذف، خریداری، تغییر نام یا منحل شده باشد، به‌طور خاموش از این فهرست غایب است. در نتیجه، آزمون بازگشتی (backtest) که روی چنین مجموعه‌ای اجرا شود، به‌جای اندازه‌گیری بازار در زمان وقوع، تنها نمونه‌ای فیلترشده از تاریخ را ارزیابی می‌کند. در ظاهر، فایل هیچ ایرادی ندارد؛ هر سطر دارای قیمت واقعی است، اما مشکل در سطرهایی است که هیچ‌کس آن‌ها را ثبت نکرده است.

سوگیری بقا در داده‌های سهام چیست؟

نحوه گردآوری یک مجموعه داده «تمام سهام ایالات متحده» را در نظر بگیرید. در این فرایند، فهرستی از نمادها از یک ارائه‌دهنده درخواست می‌شود و سپس تاریخچه قیمت برای هر نام موجود در آن فهرست استخراج می‌گردد. این فهرست ذاتاً «به‌روز» است. این فهرست به پرسش «چه چیزی اکنون معامله می‌شود» پاسخ می‌دهد و هر نمادی که پیش از زمان درخواست، معامله‌اش متوقف شده باشد، به همراه کل تاریخچه‌اش از پاسخ حذف می‌شود.

این اصطلاح از پژوهش آبراهام والد در سال 1943 درباره هواپیماهای بمب‌افکن بازگشته از جنگ گرفته شده است: زره باید در نقاطی نصب می‌شد که هواپیماهای بازگشته در آنجا هیچ سوراخی نداشتند، زیرا هواپیماهایی که در آن نقاط هدف قرار گرفته بودند، هرگز بازنگشتند تا مورد بررسی قرار گیرند. پایگاه داده سهامی که از فهرست‌های امروز ساخته شده، همین وضعیت را دارد. نمونه‌ای که می‌بینید، نمونه‌ای است که «به خانه بازگشته» و عضویت در آن، دقیقاً توسط همان نتیجه‌ای تعیین می‌شود که آزمون بازگشتی قصد اندازه‌گیری آن را دارد.

هر سال چه تعداد نماد از بازار خارج می‌شوند؟

با بررسی ابعاد این شکاف شروع می‌کنیم. جدول زیر، تمام نمادهای موجود در نوار قیمت روزانه سهام را بر اساس سال تقویمی آخرین قیمت ثبت‌شده‌شان گروه‌بندی کرده و تنها نمادهایی را نگه داشته است که پیش از ژانویه 2026 از بازار خارج شده‌اند.

پرس‌وجوتعداد نمادهای حذف‌شده از فهرست معاملاتی به تفکیک سال
کد دقیق SQL پشت هر عدد
SELECT
    toString(toYear(last_bar))  AS year,
    count()                     AS gone_dark_count,
    round(avg(bars_on_tape))    AS avg_bars_on_tape
FROM
(
    SELECT
        ticker,
        max(date) AS last_bar,
        count()   AS bars_on_tape
    FROM global_markets.stocks_daily_aggs
    WHERE date >= '2015-01-01'
      AND ticker NOT IN ('SPCX')
    GROUP BY ticker
    HAVING max(date) >= toDate('2016-01-01')
       AND max(date) <  toDate('2026-01-01')
)
GROUP BY year
ORDER BY year
Run this yourself

در 2025، 1283 نماد آخرین قیمت روزانه خود را ثبت کرده و دیگر بازنگشتند. این‌ها پوسته‌های یک‌هفته‌ای نیستند: میانگین طول عمر این گروه از سال 2015 به بعد، 999 روز معاملاتی بوده است. در طول 10 سالِ موجود در جدول، این تعداد نوسان دارد و هیچ سالی نزدیک به صفر نیست. نمادها به دلایل عادی بازار را ترک می‌کنند: تکمیل یک فرایند خرید، انحلال یک صندوق، ادغام یک نماد در نمادی دیگر، یا عدم رعایت استانداردهای فهرست‌بندی.

مشاهده سوگیری در یک اسکریپت ده خطی

درک محاسبات ریاضی این موضوع از توصیف آن آسان‌تر است. اسکریپت زیر دو فایل کوچک CSV می‌نویسد: یکی شامل تنها نمادهای باقی‌مانده (بازماندگان) و دیگری همان مجموعه به همراه نام‌هایی که معامله‌شان متوقف شده است. سپس میانگین هر فایل را محاسبه کرده و تفاوت را چاپ می‌کند. این اسکریپت تنها از کتابخانه استاندارد پایتون استفاده می‌کند و نیازی به نصب هیچ ابزاری ندارد.

python3 <<'PY'
import csv, statistics

survivors = [("AAA", 0.42), ("BBB", 0.18), ("CCC", 0.63), ("DDD", 0.07)]
delisted  = [("EEE", -1.00), ("FFF", -0.55), ("GGG", 0.31)]

def write_csv(path, rows):
    with open(path, "w", newline="") as f:
        w = csv.writer(f)
        w.writerow(["ticker", "total_return"])
        w.writerows(rows)

def mean_return(path):
    with open(path, newline="") as f:
        return statistics.mean(float(r["total_return"]) for r in csv.DictReader(f))

write_csv("survivors_only.csv", survivors)
write_csv("full_universe.csv", survivors + delisted)

a = mean_return("survivors_only.csv")
b = mean_return("full_universe.csv")
print(f"survivors only: {a:+.1%} across {len(survivors)} names")
print(f"full universe: {b:+.1%} across {len(survivors) + len(delisted)} names")
print(f"survivorship bias: {a - b:+.1%}")
PY

هفت رقم بازدهی در این اسکریپت فرضی هستند و داده‌های واقعی بازار نیستند؛ آن‌ها صرفاً برای نمایان کردن مکانیسم سوگیری به کار رفته‌اند. میانگین بازماندگان 32.5٪+ و میانگین کل مجموعه 0.9٪+ است و تفاوت 31.6 واحدی، همان سوگیری است. هیچ‌چیز در فایل «فقط بازماندگان» غلط نیست؛ هر چهار نام واقعاً همان بازدهی را داشته‌اند، اما فایل صرفاً سه نامی را که میانگین را پایین می‌کشیدند، نادیده گرفته است.

همان اندازه‌گیری روی گروه‌های واقعی

اکنون همین محاسبات را روی داده‌های واقعی اجرا می‌کنیم. جدول زیر یک مجموعه اولیه را در ژانویه هر سال از 2016 تا 2022 تشکیل می‌دهد، نمادهایی را که ماه را با قیمت بالای 5 دلار و میانگین حجم روزانه بالای 250 هزار سهم آغاز کرده‌اند نگه می‌دارد و سپس هر یک از آن‌ها را تا آخرین قیمت بسته شدن دنبال می‌کند. نام‌هایی که معامله‌شان متوقف شده، با آخرین قیمت ثبت‌شده‌شان در محاسبات باقی می‌مانند و حذف نمی‌شوند. ستون «بازماندگان» تنها میانگین نمادهایی را نشان می‌دهد که هنوز در حال معامله هستند. ستون «کل مجموعه» میانگین تمام کسانی را نشان می‌دهد که در ابتدا حضور داشتند.

پرس‌وجومقایسه میانگین بازدهی شرکت‌های باقی‌مانده با میانگین کل گروه، به تفکیک سال آغازین
کد دقیق SQL پشت هر عدد
WITH
    entry AS
    (
        SELECT
            ticker,
            toYear(date)                   AS cohort_start,
            argMin(toFloat64(close), date) AS entry_close
        FROM global_markets.stocks_daily_aggs
        WHERE toMonth(date) = 1
          AND date BETWEEN '2016-01-01' AND '2022-01-31'
          AND ticker NOT IN ('SPCX')
        GROUP BY ticker, cohort_start
        HAVING argMin(toFloat64(close), date) >= 5
           AND avg(volume) >= 250000
    ),
    outcome AS
    (
        SELECT
            ticker,
            argMax(toFloat64(close), date) AS final_close,
            max(date)                      AS last_bar
        FROM global_markets.stocks_daily_aggs
        WHERE date >= '2016-01-01'
        GROUP BY ticker
    )
SELECT
    toString(e.cohort_start)                                                             AS cohort_year,
    count()                                                                              AS cohort_size,
    countIf(o.last_bar < today() - 45)                                                   AS gone_count,
    round(100 * avgIf(o.final_close / e.entry_close - 1, o.last_bar >= today() - 45), 1) AS survivors_only_pct,
    round(100 * avg(o.final_close / e.entry_close - 1), 1)                               AS full_universe_pct,
    round(100 * (avgIf(o.final_close / e.entry_close - 1, o.last_bar >= today() - 45)
                 - avg(o.final_close / e.entry_close - 1)), 1)                           AS gap_pct
FROM entry AS e
INNER JOIN outcome AS o ON o.ticker = e.ticker
GROUP BY e.cohort_start
HAVING countIf(o.last_bar >= today() - 45) > 0
ORDER BY e.cohort_start
Run this yourself

برای گروه 2016، 968 از 2728 نام تا زمان تهیه این صفحه از بازار خارج شده بودند. میانگین‌گیری تنها از بازماندگان، عدد 212% را می‌دهد. میانگین‌گیری از کل گروه، عدد 149.3% را می‌دهد. ستون «شکاف» فاصله بین این دو عدد را نشان می‌دهد: 62.7 واحد برای آن گروه و 9.1 واحد برای گروه 2022 که زمان کمتری برای از دست دادن اعضا داشته است. این‌ها بازدهی‌های کل در بازه‌های زمانی متفاوت هستند، بنابراین به‌جای مقایسه سطوح در ردیف‌های مختلف، ستون شکاف را به‌صورت عمودی بخوانید. همین مسئله «مجموعه» در زیر نحوه اندازه‌گیری بازدهی ماهانه و در زیر هر میانگینِ با وزن برابر نهفته است.

نام‌های گمشده کجا رفتند؟

حذف از فهرست بورس لزوماً به معنای نابودی کامل سرمایه نیست. جدول زیر یک مجموعه اولیه در ژانویه 2019 را در نظر گرفته و آن را بر اساس سرنوشت بعدی‌شان مرتب می‌کند.

پرس‌وجووضعیت نمادهای موجود در ژانویه ۲۰۱۹، دسته‌بندی‌شده بر اساس عملکرد
کد دقیق SQL پشت هر عدد
WITH
    entry AS
    (
        SELECT
            ticker,
            argMin(toFloat64(close), date) AS entry_close
        FROM global_markets.stocks_daily_aggs
        WHERE date BETWEEN '2019-01-01' AND '2019-01-31'
          AND ticker NOT IN ('SPCX')
        GROUP BY ticker
        HAVING argMin(toFloat64(close), date) >= 5
           AND avg(volume) >= 250000
    ),
    outcome AS
    (
        SELECT
            ticker,
            argMax(toFloat64(close), date) AS final_close,
            max(date)                      AS last_bar
        FROM global_markets.stocks_daily_aggs
        WHERE date >= '2019-01-01'
        GROUP BY ticker
    )
SELECT
    if(o.last_bar >= today() - 45,
       'Still trading',
       concat('Last bar in ', toString(toYear(o.last_bar))))            AS outcome_label,
    count()                                                             AS cohort_size,
    round(100 * avg(o.final_close / e.entry_close - 1), 1)              AS avg_return_pct,
    round(100 * countIf(o.final_close > e.entry_close) / count(), 1)    AS share_above_entry_pct
FROM entry AS e
INNER JOIN outcome AS o ON o.ticker = e.ticker
GROUP BY outcome_label
ORDER BY outcome_label
Run this yourself

گروه Still trading شامل 1954 نام است که میانگین بازدهی 139.8% را از قیمت ورودی ژانویه 2019 داشته‌اند و 72.1% از آن‌ها بالاتر از قیمت شروع خود هستند. ردیف‌های 9 را بخوانید؛ خروج‌ها متفاوت به نظر می‌رسند. شرکتی که با قیمت بالاتر (Premium) خریداری شده، ممکن است با یک قیمت نهایی قوی از فهرست خارج شود، در حالی که شرکتی که به‌تدریج به سمت اخطار حذف پیش می‌رود، با قیمت ضعیفی خارج می‌شود. هر دو در فایل «فقط بازماندگان» غایب هستند. آن‌ها را بازگردانید تا میانگین تغییر کند.

سوگیری بقا در برابر سوگیری نگاه به آینده

این دو اغلب با هم اشتباه گرفته می‌شوند، اما آزمون بازگشتی را در نقاط متفاوتی تخریب می‌کنند. سوگیری نگاه به آینده یک مشکل زمان‌بندی است: آزمون از حقیقتی استفاده می‌کند که در آن لحظه کسی از آن آگاه نبوده است، مانند رقم سود تعدیل‌شده یا اصلاحیه‌ای که ماه‌ها بعد اعمال شده است. سوگیری بقا یک مشکل عضویت است: فهرستی که آزمون با آن معامله می‌کند، فهرستی است که «امروز» وجود دارد، نه فهرستی که «آن زمان» وجود داشته است. یک آزمون بازگشتی می‌تواند از نظر زمان‌بندی بی‌نقص باشد اما همچنان با مجموعه‌ای معامله کند که با نگاه به گذشته (hindsight) ساخته شده است. تعدیل قیمت‌ها نیز تله مشابهی دارد، زیرا تاریخچه قیمت تعدیل‌شده برای تقسیم سهام، سطح هر میله قیمت پیش از تاریخ تقسیم را بازنویسی می‌کند.

چگونه سوگیری بقا را در آزمون بازگشتی اصلاح کنیم؟

  1. مجموعه را برای هر تاریخ بازسازی کنید. عضویت «نقطه زمانی» (Point in time) به این معناست که فهرست برای روزی که آزمون در آن قرار دارد، از روی سوابق موجود در همان روز بازسازی شود. در این صورت، آزمون شامل نام‌هایی است که در آن زمان فعال بوده‌اند، حتی آن‌هایی که بعداً ناپدید شده‌اند.
  2. یک «مرجع امنیت» (Security master) داشته باشید. مرجع امنیت، شناسه‌ها را در طول زمان نگاشت می‌کند: کدام نماد در چه تاریخی به کدام شرکت اشاره داشته و پس از ادغام یا تغییر نام، چه چیزی جایگزین آن شده است. فایل‌های قیمت بر اساس نمادها کلیدگذاری می‌شوند، اما شرکت‌ها چنین نیستند.
  3. نام‌های حذف‌شده را با آخرین قیمت واقعی‌شان ببندید. وقتی نامی از بازار خارج می‌شود، موقعیت معاملاتی باید با آخرین قیمت ثبت‌شده بسته شود، نه اینکه از میانگین حذف گردد. حذف کردن، همان چیزی است که شکاف را در جداول بالا ایجاد می‌کند. یک آزمون بازگشتی قابل بازتولید، این قاعده را در کدِ کنار مجموعه داده تثبیت می‌کند.
  4. مستندات ارائه‌دهنده داده را درباره نمادهای مرده بخوانید. فروشندگانی که این موضوع را جدی می‌گیرند، آن را مکتوب می‌کنند. Massive نمادهای حذف‌شده را از طریق اندپوینت tickers با یک پرچم فعال (active flag) نمایش می‌دهد و تغییرات نماد، حذف‌ها، ادغام‌ها و تملک‌ها را به‌عنوان سوابق در یک اندپوینت جداگانه برای رویدادهای نماد (ticker events) نگهداری می‌کند.
«ما داده‌ها را تغییر نمی‌دهیم و تغییرات نماد را در یک سری مجموع واحد ادغام نمی‌کنیم.»
پایگاه دانش Massive، «Massive چگونه تغییرات نماد و تملک‌ها را مدیریت می‌کند؟»، مطالعه‌شده در اوت 2026.

اگر مستندات ارائه‌دهنده داده هیچ اشاره‌ای به نمادهای حذف‌شده نمی‌کند، تا زمانی که بررسی نکرده‌اید، فرض کنید دانلود شما فقط شامل بازماندگان است. پوشش داده‌ها بین منابع بسیار متفاوت است: یادداشت‌های ما را درباره APIهای رایگان داده‌های بازار سهام ببینید.

تله بازیافت نمادها

نمادها پس از پایان یک دوره فهرست‌بندی، دوباره به چرخه بازمی‌گردند. جدول زیر نمادهایی را می‌شمارد که حداقل 200 میله روزانه ثبت کرده، حداقل 250 روز تقویمی ساکت بوده‌اند و سپس از سال 2022 به بعد به‌عنوان یک نماد کاملاً جدید ظاهر شده‌اند.

پرس‌وجونمادهایی که پس از یک دوره توقف طولانی، با ناشر جدید عرضه شدند
کد دقیق SQL پشت هر عدد
SELECT
    toString(toYear(relisted_on)) AS year,
    count()                       AS relisting_count,
    round(avg(dark_stretch))      AS avg_dark_stretch,
    max(dark_stretch)             AS longest_dark_stretch
FROM
(
    SELECT
        ticker,
        any(relist_date)                             AS relisted_on,
        dateDiff('day', max(date), any(relist_date)) AS dark_stretch
    FROM
    (
        SELECT
            d.ticker      AS ticker,
            d.date        AS date,
            i.relist_date AS relist_date
        FROM global_markets.stocks_daily_aggs AS d
        INNER JOIN
        (
            SELECT
                ticker,
                min(toDate(listing_date)) AS relist_date
            FROM global_markets.stocks_ipos
            WHERE toDate(listing_date) BETWEEN '2022-01-01' AND today()
              AND ticker NOT IN ('SPCX')
            GROUP BY ticker
        ) AS i ON i.ticker = d.ticker
        WHERE d.date < i.relist_date
    )
    GROUP BY ticker
    HAVING dateDiff('day', max(date), any(relist_date)) >= 250
       AND count() >= 200
)
GROUP BY year
ORDER BY year
Run this yourself

در 2022، 16 نماد به این شکل بازگشتند، پس از میانگین سکوت 3247 روز تقویمی. طولانی‌ترین دوره سکوت در آن ردیف 5734 روز بوده است. فایل قیمتی که فقط بر اساس نماد کلیدگذاری شده، تاریخچه یک شرکت را به شرکت دیگری می‌چسباند و این اتصال در تمام طول مسیر تمیز به نظر می‌رسد. این موردی است که یک «مرجع امنیت» آن را تشخیص می‌دهد، اما یک ستون نمادِ خالی قادر به شناسایی آن نیست.

یادداشت‌های داده و تعاریف

یک نماد زمانی «هنوز در حال معامله» محسوب می‌شود که آخرین میله روزانه آن در محدوده 45 روز تقویمی از روز تولید این صفحه باشد. این بازه، تأخیر یک تا دو روزه در ابتدای نوار قیمت و نمادهای کم‌حجم که برخی جلسات را معامله نمی‌کنند، پوشش می‌دهد.

بازدهی‌ها، بازدهی کل قیمت از قیمت بسته شدن در ماه ورود تا آخرین قیمت بسته شدن موجود برای آن نماد هستند، بدون سود سهام و بدون سالانه کردن. گروه‌های قدیمی‌تر زمان بیشتری برای ترکیب سود داشته‌اند، بنابراین سطوح در ردیف‌های مختلف قابل مقایسه نیستند، اما شکاف بین دو میانگین قابل مقایسه است.

مجموعه‌های ورودی، نمادهایی را با قیمت 5 دلار یا بیشتر و میانگین حجم روزانه 250 هزار سهم یا بیشتر در طول ماه ورود نگه می‌دارند. نوار قیمت روزانه شامل صندوق‌ها، واحدها و حق تقدم‌ها در کنار شرکت‌های عملیاتی است و هیچ‌چیز در اینجا آن‌ها را جدا نمی‌کند، بنابراین انحلال یک صندوق در کنار حذف یک شرکت محاسبه می‌شود. نمادهایی که در فیدهای فروشندگان دارای تضاد هویتی شناخته‌شده هستند، در SQL از تمام جداول حذف شده‌اند، نه اینکه بعداً فیلتر شوند.

پرسش‌های متداول

سوگیری بقا در آزمون بازگشتی سهام چیست؟

این خطایی است که هنگام ساخت مجموعه قابل معامله از نمادهایی که «امروز» وجود دارند، باقی می‌ماند. شرکت‌هایی که حذف یا خریداری شده‌اند، هرگز وارد آزمون نمی‌شوند، بنابراین استراتژی فقط با نام‌هایی تست می‌شود که تا زمان حال دوام آورده‌اند.

چگونه بفهمم مجموعه داده من سوگیری بقا دارد؟

مجموعه‌ای را که فایل شما ادعا می‌کند برای چند سال پیش پوشش می‌دهد بردارید و آخرین میله قیمت هر نماد را بررسی کنید. اگر هر نام در فهرست سال 2016 هنوز امروز در حال ثبت قیمت است، فهرست از روی نمادهای فعلی ساخته شده و نام‌های مرده هرگز در آن نبوده‌اند.

آیا سوگیری بقا همیشه باعث می‌شود آزمون بازگشتی بهتر به نظر برسد؟

خیر. جهت آن بستگی به نحوه خروج نام‌ها از بازار دارد. یک تملک (Acquisition) ممکن است با قیمت بالاتر بسته شود و حذف آن نام‌ها، عملکرد گروه را کمتر از واقع نشان دهد، در حالی که محو شدن تدریجی به سمت اخطار حذف، نتیجه معکوس دارد. جداول بالا برآیند هر دو را روی گروه‌های واقعی اندازه‌گیری می‌کنند.

«مجموعه نقطه زمانی» (Point in time universe) چیست؟

این فهرستی از اعضا است که برای هر تاریخ در آزمون بازسازی شده و تنها از سوابقی استفاده می‌کند که در آن تاریخ وجود داشته‌اند. این فهرست شامل نام‌هایی است که در آن زمان قابل معامله بوده‌اند و بعداً ناپدید شده‌اند، و نام‌هایی را که هنوز فهرست نشده بودند، حذف می‌کند.

آیا سوگیری بقا همان سوگیری نگاه به آینده است؟

خیر. سوگیری نگاه به آینده مربوط به زمان‌بندی است: آزمون از اطلاعاتی استفاده می‌کند که در آن زمان کسی به آن دسترسی نداشته است. سوگیری بقا مربوط به عضویت است: آزمون با فهرستی معامله می‌کند که با نگاه به گذشته ساخته شده است. یک آزمون بازگشتی می‌تواند در یکی تمیز و در دیگری خراب باشد.


هر جدول در اینجا شامل کد SQL است که آن را تولید کرده است. ماه ورودی را تغییر دهید یا فیلتر نقدینگی را شل کنید، سپس آن را در ترمینال Strasmore اجرا کنید تا ببینید چه مقدار از این شکاف با قوانین مجموعه خودتان باقی می‌ماند.