رفتن به محتوای اصلی
سرفصل مسیر Quant Developer

مسیر Quant Developer · بخش ۸: پروژه عملی و آمادگی استخدام · درس ۳۱

تحلیل عملکرد: Markout، تفکیک PnL و اختلاف بک‌تست با اجرای واقعی

کالبدشکافی خروجی Backtester: تفکیک PnL، منحنی Markout، آزمودن تصمیم‌های طراحی، جاروب پارامتر و خطر Overfitting، و فاصله بک‌تست تا اجرای واقعی.

سطح: پیشرفته۱۵ دقیقه مطالعه

۱. یک عدد کافی نیست

Backtester درس ۳۰ گفت طراحی درس ۲۹ در یک ساعت بازار مصنوعی 9.12 دلار سود خالص داشته است. این عدد به‌تنهایی تقریباً هیچ‌چیز نمی‌گوید. سؤال‌های واقعی این‌ها هستند:

  1. سود از کجا آمده و کجا از دست رفته؟
  2. چقدر از پر شدن‌ها سمی بوده‌اند؟
  3. تصمیم‌های طراحی (مثلاً Microprice) واقعاً کمک کرده‌اند؟
  4. اگر پارامترها را تنظیم کنیم، بهبود واقعی است یا شانس؟
  5. نتیجه اجرای واقعی چقدر از این عدد فاصله خواهد داشت؟

همه کدهای این درس را در یک فایل یا نوت‌بوک Jupyter کنار backtest.py و mm_quote.py بگذار و تکه‌ها را به ترتیب اجرا کن. هر تکه از متغیرهای تکه قبلی استفاده می‌کند. همه اعداد این درس خروجی همین کدها روی همان داده مصنوعی‌اند.

۲. تفکیک PnL بر اساس نوع پر شدن

در درس ۳۰ هر پر شدن را با یکی از دو برچسب ثبت کردیم: trade (یک سفارش Market به سفارش ما رسید) و cross (قیمت بازار از روی سفارش ما رد شد). اول ببینیم هر گروه چه سهمی از Spread Capture دارد:

import itertools
import numpy as np
import pandas as pd
import backtest as bt
from mm_quote import Params

df, eq, mid_path, res = bt.run()                   # the lesson-30 baseline
sign = np.where(df.side == "buy", 1, -1)
df["edge"] = sign * (df.mid - df.px) * df.qty      # spread capture of each fill, USDT
print(df.groupby("how").agg(fills=("qty", "size"), btc=("qty", "sum"), edge=("edge", "sum")).round(2))
       fills    btc   edge
how                       
cross     84   0.84 -13.24
trade   1248  11.56  60.05
نوعتعدادحجم (BTC)Spread Capture (دلار)به ازای هر BTC
trade1,24811.5660.05 سود5.19 سود
cross840.8413.24 زیان15.76 زیان

پر شدن‌های cross فقط ۶.۳٪ تعداد پر شدن‌ها هستند، ولی ۲۲٪ از Spread Capture گروه دیگر را پاک کرده‌اند. هر کدام به‌طور متوسط ۳ برابر بیشتر از آنچه یک پر شدن معمولی می‌سازد، از بین می‌برد. این‌ها لحظه‌هایی‌اند که قیمت جهش کرده و سفارش ما هنوز سر جایش بوده؛ اولین هدف بهبود، کم کردن همین گروه است (مثلاً با عقب کشیدن سفارش‌ها وقتی σ ناگهان بالا می‌رود).

۳. منحنی Markout

در درس ۸ تعریف کردیم: Markout یعنی سود هر پر شدن، اگر آن را با Mid چند لحظه بعد ارزش‌گذاری کنیم. اگر معامله‌گرهای طرف ما آگاه باشند، Markout با گذشت زمان افت می‌کند:

def markout(h_ms):
    """Average PnL per BTC if every fill were valued at the mid h_ms later."""
    idx = np.minimum(df.t // bt.STEP_MS + h_ms // bt.STEP_MS, len(mid_path) - 1)
    pnl = sign * (mid_path[idx] - df.px) * df.qty
    return pnl.sum() / df.qty.sum()

for h in (0, 1_000, 5_000, 30_000):
    print(f"markout {h / 1000:>4.0f}s: {markout(h):6.2f} USDT/BTC")
markout    0s:   3.77 USDT/BTC
markout    1s:   3.40 USDT/BTC
markout    5s:   2.34 USDT/BTC
markout   30s:   2.70 USDT/BTC

خواندن این منحنی:

  • لحظه صفر (3.77): همان Spread Capture تقسیم بر حجم: 46.81 / 12.405.
  • از صفر تا ۵ ثانیه: افت 1.43 دلار به ازای هر BTC. این هزینه Adverse Selection است: در دنیای ما معامله‌گرهای آگاه حرکت دو ثانیه بعد را می‌دانند و قیمت بعد از معامله با ما، علیه ما حرکت می‌کند.
  • بعد از ۵ ثانیه: منحنی دیگر پایین نمی‌رود. تغییر کوچک ۵ تا ۳۰ ثانیه در حد نویز است (چقدر نویز؟ چند خط پایین‌تر می‌بینیم).

حالا حدس درس ۳۰ را می‌سنجیم. هزینه Adverse Selection در کل حجم:

1.43 × 12.405 ≈ 17.7 USDT

این از کل زیان موجودی (13.00 دلار) هم بیشتر است. پس حدس درس ۳۰ با داده سازگار است: زیان موجودی عمدتاً از معامله با افراد آگاه آمده، نه از روند ۶۸۰ دلاری قیمت. («سازگار است» و نه «ثابت شد»؛ چند خط پایین‌تر می‌بینیم این اعداد چقدر عدم‌قطعیت دارند.) حالا یک مقایسه مهم‌تر: کارمزد به ازای هر BTC حدود 1.99 دلار است (24.70 / 12.405). Markout پنج‌ثانیه‌ای منهای کارمزد:

2.34 − 1.99 = 0.35 USDT/BTC

لبه واقعی این استراتژی، بعد از Adverse Selection و کارمزد، حدود ۳۵ سنت برای هر بیت‌کوین است. هر چیزی که این عدد را جابه‌جا کند، کل نتیجه را جابه‌جا می‌کند.

هر عدد یک خطای استاندارد لازم دارد

این ۳۵ سنت چقدر قابل‌اعتماد است؟ پر شدن‌هایی که به هم نزدیک‌اند، به هم وابسته‌اند (همه در یک حرکت قیمت شریک‌اند)، پس فرمول ساده خطای استاندارد که استقلال را فرض می‌کند، عدم‌قطعیت را کم نشان می‌دهد. به‌جایش از Block Bootstrap استفاده می‌کنیم: دقیقه‌های کامل را با جای‌گذاری دوباره نمونه می‌گیریم و Markout را صدها بار از نو حساب می‌کنیم (درس ۹):

def markout_se(h_ms, n_boot=500):
    """Block bootstrap: resample whole minutes, because nearby fills are correlated."""
    idx = np.minimum(df.t // bt.STEP_MS + h_ms // bt.STEP_MS, len(mid_path) - 1)
    pnl = (sign * (mid_path[idx] - df.px) * df.qty).to_numpy()
    qty, minute = df.qty.to_numpy(), (df.t // 60_000).to_numpy()
    rng = np.random.default_rng(0)
    minutes = np.unique(minute)
    stats = []
    for _ in range(n_boot):
        pick = rng.choice(minutes, len(minutes))       # minutes drawn with replacement
        rows = np.concatenate([np.flatnonzero(minute == m) for m in pick])
        stats.append(pnl[rows].sum() / qty[rows].sum())
    return np.std(stats)

for h in (0, 5_000):
    print(f"markout {h / 1000:>2.0f}s: {markout(h):5.2f} +/- {markout_se(h):4.2f} USDT/BTC")
markout  0s:  3.77 +/- 0.20 USDT/BTC
markout  5s:  2.34 +/- 0.71 USDT/BTC

خطای استاندارد Markout پنج‌ثانیه‌ای 0.71 دلار است؛ دو برابر بزرگ‌تر از کل لبه ۳۵ سنتی. یعنی با یک ساعت داده نمی‌توانیم بگوییم این استراتژی بعد از کارمزد لبه مثبت دارد. برای اینکه لبه ۰.۳۵ دلاری دو خطای استاندارد از صفر فاصله بگیرد، خطا باید به حدود ۰.۱۷ برسد. خطا با جذر حجم داده کم می‌شود، پس به حدود (0.71 / 0.175)² ≈ 16 برابر داده، یعنی حدود ۱۶ ساعت، نیاز داریم؛ آن هم با این فرض که ساعت‌ها شبیه هم‌اند. نقطه دقیق خم شدن منحنی هم با این خطاها از روی یک ساعت داده قابل تشخیص نیست.

۴. آزمودن یک تصمیم طراحی: Microprice یا Mid؟

در درس ۲۹ Microprice را انتخاب کردیم و نوشتیم «این فرض قابل آزمایش است». حالا وقتش است. کلید use_micro را خاموش می‌کنیم و همه‌چیز دیگر را ثابت نگه می‌داریم. به این کار Ablation می‌گویند: یک جزء را برمی‌داری تا سهمش را ببینی.

def decompose(**kw):
    """Run one backtest and split its PnL into the lesson-7 pieces."""
    df, eq, mid_path, res = bt.run(**kw)
    s = np.where(df.side == "buy", 1, -1)
    capture = (s * (df.mid - df.px) * df.qty).sum()
    return pd.Series(dict(fills=len(df), btc=df.qty.sum(), capture=capture,
                          inventory=res["pnl"] + res["fees"] - capture,
                          fees=-res["fees"], net=res["pnl"]))

mid_only = Params(use_micro=False)
print(pd.DataFrame(dict(micro=decompose(), mid=decompose(p=mid_only))).round(2))
seeds = (7, 11, 12, 13, 14, 15)
for name, p in (("micro", Params()), ("mid", mid_only)):
    print(f"{name:>5}:", " ".join(f"{bt.run(p, seed=s)[3]['pnl']:6.1f}" for s in seeds))
             micro      mid
fills      1332.00  1160.00
btc          12.40    10.80
capture      46.81    65.61
inventory   -13.00   -31.81
fees        -24.70   -21.49
net           9.12    12.30
micro:    9.1   12.5   10.2   11.6   -4.1   19.5
  mid:   12.3   15.8   14.7   11.5    9.3   25.1

نتیجه بر خلاف انتظار ماست: Mid ساده در ۵ ساعت از ۶ ساعت بهتر بوده و میانگین سودش 14.8 دلار است، در برابر 9.8 دلار برای Microprice. دو نکته در این جدول هست که ارزش دارد با دقت ببینی:

تفکیک به مرجع وابسته است. با Microprice، Spread Capture کمتر و زیان موجودی هم کمتر است. دلیلش این است که Spread Capture را نسبت به Mid اندازه می‌گیریم. وقتی سفارش‌ها را حول Microprice می‌چینی، از دید Mid «لبه کمتری» می‌گیری، ولی چون Microprice کمی از آینده خبر دارد، موجودی‌ات کمتر ضرر می‌دهد. سهم هر تکه به انتخاب مرجع بستگی دارد؛ فقط سود خالص مستقل از مرجع است.

اطلاعات داشتن با سودمند بودن فرق دارد. در درس ۳۰ دیدیم Microprice واقعاً اطلاعات دارد. ولی فرمول درس ۵ عدم‌تعادل حجم را با وزن کامل وارد قیمت می‌کند: با Spread بیست‌دلاری و عدم‌تعادل شدید، Fair Value تا ۹ دلار از Mid فاصله می‌گیرد، در حالی که سیگنال واقعی در دنیای ما ضعیف است. نتیجه یک تخمین پرنویز است. راه‌حل منطقی، کوچک کردن این اثر است:

fair = mid + β × (microprice − mid)

که β را با یک رگرسیون ساده (درس ۱۰) روی حرکت بعدی Mid تخمین می‌زنی. این را به‌عنوان تمرین برای خودت نگه دار.

۵. جاروب پارامتر و خطر Overfitting

وسوسه بعدی، تنظیم پارامترهاست. یک شبکه ۳ در ۳ از min_half و skew را روی همان یک ساعت اجرا می‌کنیم. ولی این بار هر ترکیب را روی پنج ساعت دیگر هم که در تنظیم نقشی نداشته‌اند (Seed 11 تا 15) می‌سنجیم. اجرای این تکه حدود یک دقیقه طول می‌کشد:

rows = []
for min_half, skew in itertools.product((6.0, 10.0, 14.0), (0.0, 1.0, 2.0)):
    p = Params(min_half=min_half, skew=skew)
    pnl_in = bt.run(p, seed=7)[3]["pnl"]                                 # the hour we tune on
    pnl_out = [bt.run(p, seed=s)[3]["pnl"] for s in (11, 12, 13, 14, 15)]  # five unseen hours
    rows.append((min_half, skew, pnl_in, np.mean(pnl_out), min(pnl_out)))
sweep = pd.DataFrame(rows, columns=["min_half", "skew", "in_sample", "out_mean", "out_worst"])
print(sweep.round(1).sort_values("in_sample", ascending=False).to_string(index=False))
 min_half  skew  in_sample  out_mean  out_worst
     14.0   0.0       23.0       9.8      -18.4
      6.0   0.0       22.8      14.5       -5.1
     10.0   0.0       16.3      22.5        6.1
     10.0   1.0       14.3      12.2        3.5
     10.0   2.0       10.4      11.4        5.5
      6.0   1.0        9.1       9.9       -4.1
     14.0   1.0        8.2       4.6        2.2
      6.0   2.0        6.3       9.2       -0.4
     14.0   2.0        1.3       5.0        1.8

جدول بر اساس نتیجه داخل نمونه (In-Sample) مرتب شده است. حالا ببین چه اتفاقی می‌افتد:

ترکیبداخل نمونهمیانگین خارج از نمونهبدترین ساعت خارج از نمونه
14 و 0 (برنده داخل نمونه)23.09.818.4 زیان
10 و 016.322.56.1 سود
6 و 1 (پیش‌فرض درس ۲۹)9.19.94.1 زیان

برنده داخل نمونه، بدترین ساعت کل جدول را دارد. اگر فقط به یک ساعت نگاه می‌کردیم، min_half=14, skew=0 را انتخاب می‌کردیم و در اولین ساعت بد ۱۸ دلار از دست می‌دادیم؛ تقریباً دو برابر کل سود پایه.

چند درس از همین جدول کوچک:

  • رتبه‌بندی داخل نمونه نویز است. فاصله ۲۳.۰ و ۲۲.۸ بی‌معناست. تفاوت واقعی min_half=6 و min_half=10 هم کمتر از آن است که به نظر می‌رسد: σ تخمینی در این ساعت هیچ‌وقت زیر 6 دلار نمی‌رود (میانه‌اش حدود 8.3 دلار است)، پس کف 6 اصلاً فعال نمی‌شود و فرق واقعی دو تنظیم به‌طور متوسط حدود ۱.۵ دلار در نیم‌اسپرد است.
  • دنبال فلات بگرد، نه قله. ستون min_half=10 تنها جایی است که هر سه مقدار skew در همه ساعت‌های خارج از نمونه سودده بوده‌اند. یک ناحیه پایدار از یک نقطه درخشان قابل‌اعتمادتر است.
  • Skew را با میانگین قضاوت نکن. در این جدول skew=0 میانگین بهتری دارد، چون دنیای مصنوعی ما رژیم خطرناکی ندارد و سقف 0.05 BTC بیشتر ریسک را مهار می‌کند. ولی بدترین ساعت کل جدول هم مال skew=0 است. Skew بیمه است؛ ارزشش در دمِ توزیع دیده می‌شود، نه در میانگین پنج ساعت. اگر بخواهیم انتخاب کنیم، min_half=10, skew=1 را برمی‌داریم و دلیلش را در سند می‌نویسیم.
  • هر آزمایش اضافه، شانس خطا را بالا می‌برد. ما ۹ ترکیب را امتحان کردیم. با ۹۰۰ ترکیب، بهترین نتیجه داخل نمونه تقریباً حتماً شانسی است. هر چه بیشتر جست‌وجو کنی، نتیجه خارج از نمونه باید سخت‌گیرانه‌تر و طولانی‌تر باشد. در عمل از Walk-Forward استفاده می‌شود: روی یک بازه تنظیم کن، روی بازه بعدی بسنج، پنجره را جلو ببر و تکرار کن (درس ۲۰).

۶. حساسیت به فرض‌ها

حالا سه فرض اصلی Backtester را تکان می‌دهیم: تأخیر، کارمزد و مدل صف. نسخه بدبینانه صف فرض می‌کند همیشه یک رقیب زودتر از ما رسیده: دو برابر حجم نمایش‌داده‌شده جلوی ماست، و حتی وقتی قیمت را بهتر می‌کنیم، 0.05 BTC جلوتر از ما نشسته است.

for lat in (0, 50, 100, 200):
    print(f"latency {lat:>3} ms:   net {decompose(latency_ms=lat)['net']:7.2f}")
for fee in (0.0, 0.2, 0.5):
    print(f"maker fee {fee} bp: net {decompose(fee_bps=fee)['net']:7.2f}")

def pessimistic_queue(side, px, book):
    """A competitor always got there first: 0.05 BTC ahead even when we improve the price."""
    bid, ask, bid_sz, ask_sz = book
    if side == "buy":
        return 0.05 if px > bid else 2 * bid_sz
    return 0.05 if px < ask else 2 * ask_sz

optimistic_queue = bt.queue_on_arrival
bt.queue_on_arrival = pessimistic_queue
print(f"pessimistic queue: net {decompose()['net']:7.2f}")
latency   0 ms:   net   18.36
latency  50 ms:   net    9.12
latency 100 ms:   net   -8.33
latency 200 ms:   net  -31.18
maker fee 0.0 bp: net   33.82
maker fee 0.2 bp: net    9.12
maker fee 0.5 bp: net  -27.92
pessimistic queue: net   -3.20
فرضخوش‌بینانهپایهبدبینانه
تأخیر0 ms: سود 18.3650 ms: سود 9.12100 ms: زیان 8.33 ؛ 200 ms: زیان 31.18
کارمزد Maker (فرضی)صفر: سود 33.820.2 bp: سود 9.120.5 bp: زیان 27.92
مدل صفمدل درس ۳۰ (همان پایه)سود 9.12زیان 3.20

هر کدام از این سه فرض به‌تنهایی می‌تواند علامت نتیجه را عوض کند:

  • تأخیر: دو برابر شدن تأخیر از ۵۰ به ۱۰۰ میلی‌ثانیه سود را به زیان تبدیل می‌کند. با ۱۰۰ میلی‌ثانیه، پیام جایگزینی درست هم‌زمان با داده بعدی می‌رسد و سفارش کهنه یک گام کامل در دفتر می‌ماند؛ تعداد پر شدن‌های cross از ۸۴ به ۲۳۸ می‌رسد.
  • کارمزد: تست درس ۳۰ تأیید شد: با کارمزد صفر، سود دقیقاً 9.12 + 24.70 = 33.82 است. کارمزد 0.5 bp، یعنی حدود ۵ دلار برای هر BTC، از کل Markout پنج‌ثانیه‌ای (2.34 دلار به ازای هر BTC) هم بیشتر است.
  • صف: با فرض بدبینانه، تعداد پر شدن‌ها از ۱,۳۳۲ به ۵۸۳ می‌رسد و نتیجه منفی می‌شود.

جمله‌ای که باید در گزارش پروژه بنویسی، «این استراتژی سودده است» نیست. این است: «در این دنیای مصنوعی، این استراتژی فقط با تأخیری جایی بین ۵۰ تا ۱۰۰ میلی‌ثانیه یا کمتر، کارمزد Maker زیر حدود 0.27 bp و جایگاه صف نزدیک به مدل خوش‌بینانه سودده است.» آستانه کارمزد را می‌شود مستقیم حساب کرد، چون کارمزد روی تصمیم‌ها اثری ندارد و سود با آن خطی کم می‌شود: هر 0.1 bp حدود ۱۲.۳۵ دلار، پس سود در 33.82 / 123.5 ≈ 0.27 bp صفر می‌شود. آستانه تأخیر را با جاروب ریزتر می‌شود پیدا کرد.

۷. چرا اجرای واقعی از بک‌تست بدتر است؟

تقریباً هر کسی که یک استراتژی بازارسازی را از Backtest به اجرای واقعی برده، این تجربه را دارد که نتیجه واقعی بدتر بوده است. دلیل‌ها معمولاً همین‌هاست:

منبع شکافدر Backtest مادر اجرای واقعی
جایگاه صف (درس ۱۸)تخمین از حجم نمایش‌داده‌شدهرقبا سریع‌ترند و صف پنهان دارد
تأخیر (درس ۱۹)عدد ثابت 50 msتوزیع با دم سنگین؛ p99 شاید ۱۰ برابر میانه
اثر خود ماهیچ؛ رقبا به ما واکنش نشان نمی‌دهندرقبا قیمت ما را می‌بینند و جلو می‌زنند
دادهتمیز و کاملقطعی، پیام گم‌شده، ترتیب به‌هم‌ریخته (درس ۱۶)
خطاهای عملیاتی (درس ۲۶)وجود نداردReject، Rate Limit، قطع اتصال
کارمزدیک نرخ ثابتپله‌ای بر اساس حجم ماهانه
انتخاب پارامترروی همین داده تنظیم شدهداده تازه، رژیم تازه

نکته مهم: تقریباً همه این ردیف‌ها در یک جهت اثر می‌گذارند. بک‌تستی که هیچ‌کدام را مدل نکرده، به‌طور سیستماتیک خوش‌بین است، نه فقط پرنویز.

۸. شکاف را اندازه بگیر، نه حدس بزن

راه درست مقایسه، مقایسه تکه‌به‌تکه است، نه فقط مقایسه سود نهایی. برای نشان دادن روش، اجرای «بدبینانه» (تأخیر ۱۰۰ و صف بدبینانه) را جای «اجرای واقعی» می‌گذاریم و هر دو را با یک تابع تفکیک می‌کنیم:

bt.queue_on_arrival = optimistic_queue
backtest = decompose()
bt.queue_on_arrival = pessimistic_queue              # a stand-in for "live"
live = decompose(latency_ms=100)
bt.queue_on_arrival = optimistic_queue
print(pd.DataFrame(dict(backtest=backtest, live=live, gap=live - backtest)).round(2))
           backtest    live     gap
fills       1332.00  932.00 -400.00
btc           12.40    8.57   -3.83
capture       46.81    8.22  -38.59
inventory    -13.00   -0.05   12.95
fees         -24.70  -17.07    7.63
net            9.12   -8.89  -18.01

شکاف ۱۸.۰۱ دلاری سود خالص، این‌طور تقسیم می‌شود: Spread Capture ۳۸.۵۹ دلار کم شده، و در مقابل زیان موجودی ۱۲.۹۵ دلار و کارمزد ۷.۶۳ دلار کمتر شده‌اند (چون حجم کمتری معامله شده است). حجم ۳۱٪ کم شده (از 12.40 به 8.57 BTC)، ولی Spread Capture ۸۲٪. یعنی پر شدن‌هایی که از دست رفته‌اند، خوب‌ها بوده‌اند. وقتی تفکیک بر اساس نوع پر شدن را برای این اجرا تکرار کنی (تکه بخش ۲ با همین تنظیمات)، سهم پر شدن‌های cross از ۶.۳٪ به ۲۵٪ (۲۳۴ از ۹۳۲) می‌رسد: صف بدبینانه پر شدن‌های بی‌خطر جلوی صف را می‌گیرد و تأخیر بیشتر پر شدن‌های سمی را زیاد می‌کند.

در اجرای واقعی، همین روش با داده واقعی این شکل را دارد:

  1. بازپخش هم‌دوره: داده بازار ثبت‌شده در همان بازه اجرای واقعی را (با Pipeline درس ۱۶) به Backtester بده، با همان پارامترها. حالا دو اجرای قابل‌مقایسه داری.
  2. تفکیک هر دو با یک تابع: همان decompose، روی پر شدن‌های واقعی و شبیه‌سازی‌شده. این دلیل اصلی است که در درس ۲۹ Mid لحظه پر شدن را لاگ کردیم.
  3. کالیبره کردن نرخ پر شدن: برای سفارش‌های واقعی، نرخ پر شدن را بر اساس جایگاه صف تخمینی دسته‌بندی کن و با پیش‌بینی مدل صف مقایسه کن (درس ۱۸).
  4. مقایسه منحنی Markout: اگر Markout واقعی سریع‌تر از Backtest افت کند، یا تأخیر واقعی بیشتر است یا طرف معامله‌ات آگاه‌تر از مدل.
  5. تأخیر واقعی را به مدل برگردان: به‌جای عدد ثابت، از توزیع اندازه‌گیری‌شده با ابزار درس ۲۸ نمونه بگیر.

هدف این حلقه این نیست که شکاف صفر شود. هدف این است که Backtester آن‌قدر به واقعیت نزدیک شود که تصمیم‌ها (کدام پارامتر بهتر است، کدام تغییر ارزش دارد) در هر دو یکسان باشند.

۹. گزارش پروژه

اگر بخواهی سه درس آخر را در یک صفحه خلاصه کنی، چیزی شبیه این می‌نویسی:

  • طراحی: بازارساز تک‌بازاره با Microprice، نیم‌اسپرد وابسته به نوسان، Skew خطی و سقف موجودی 0.05 BTC.
  • نتیجه پایه: ۹.۱۲ دلار در یک ساعت مصنوعی؛ Spread Capture برابر ۴۶.۸۱، زیان موجودی ۱۳.۰۰، کارمزد ۲۴.۷۰.
  • لبه واقعی: Markout پنج‌ثانیه‌ای منهای کارمزد، حدود ۰.۳۵ دلار به ازای هر BTC، با خطای استاندارد ۰.۷۱؛ یک ساعت داده برای اثبات وجود لبه کافی نیست.
  • یافته‌ها: Microprice با وزن کامل در این دنیا از Mid بدتر است. پر شدن‌های cross فقط ۶.۳٪ تعدادند ولی معادل ۲۲٪ از لبه بقیه پر شدن‌ها را از بین می‌برند. برنده تنظیم داخل نمونه، بدترین ساعت خارج از نمونه را دارد.
  • حساسیت: نتیجه با تأخیر ۱۰۰ میلی‌ثانیه، کارمزد ۰.۵ bp یا صف بدبینانه منفی می‌شود.
  • محدودیت‌ها: داده مصنوعی، صف ساده، بدون اثر روی رقبا، کارمزد ثابت.

این صفحه همان چیزی است که در درس ۳۲ به مصاحبه‌کننده نشان می‌دهی. دقت کن که تقریباً نیمی از آن درباره محدودیت‌ها و شکست‌هاست؛ این ضعف گزارش نیست، نقطه قوتش است.

۱۰. تمرین این درس

گزینه مناسب هر سؤال را انتخاب کن و در پایان نتیجه را ببین.

سؤال ۱: Markout لحظه صفر 3.77 و Markout پنج‌ثانیه‌ای 2.34 دلار به ازای هر BTC است و حجم کل 12.405 BTC. هزینه تقریبی Adverse Selection در پنج ثانیه چقدر است؟
سؤال ۲: ترکیب min_half=14 و skew=0 بهترین نتیجه داخل نمونه را داشت. کدام کار درست است؟
سؤال ۳: با Microprice، Spread Capture برابر 46.81 و با Mid برابر 65.61 است، ولی زیان موجودی با Microprice کمتر است. بهترین تفسیر کدام است؟
سؤال ۴: در مقایسه بک‌تست با اجرای بدبینانه، حجم ۳۱٪ و Spread Capture ۸۲٪ کم شد. این یعنی چه؟

درس بعدی

درس سی‌ودوم آخرین درس این مسیر است: مصاحبه Quant Developer. سؤال‌های فنی رایج درباره ساختار داده دفتر سفارش، تأخیر، هم‌روندی و آمار را با پاسخ‌های کوتاه مرور می‌کنیم، یک سؤال طراحی سیستم را قدم‌به‌قدم حل می‌کنیم، و می‌بینیم چطور همین پروژه سه درس آخر را طوری ارائه کنی که مصاحبه‌کننده بفهمد واقعاً می‌دانی چه ساخته‌ای.

این مطلب صرفاً آموزشی است و توصیه خرید، فروش یا سرمایه‌گذاری نیست.