مسیر Quant Developer · بخش ۸: پروژه عملی و آمادگی استخدام · درس ۳۲
مصاحبه Quant Developer
سؤالهای رایج مصاحبه Quant Developer با پاسخ کوتاه: دفتر سفارش، تأخیر، همروندی، آمار، طراحی سیستم و سناریوهای بازار، و ارائه پروژه دوره بهعنوان نمونهکار.
۱. مصاحبه چه چیزی را میسنجد؟
نقش Quant Developer بین پژوهش کمی و مهندسی نرمافزار قرار دارد: کسی که ایده یک پژوهشگر را به سیستمی تبدیل میکند که در بازار واقعی، با تأخیر کم و بدون خطای پرهزینه، کار کند. برای همین مصاحبهاش ترکیبی از چند نوع سؤال است. اسم و ترتیب مرحلهها در هر تیم فرق دارد، ولی محتوا معمولاً اینهاست:
| بخش | چه چیزی سنجیده میشود | درسهای مرتبط |
|---|---|---|
| کدنویسی و ساختار داده | پیادهسازی تمیز و درست، تحلیل پیچیدگی | ۳، ۱۳، ۱۶ |
| سیستم و تأخیر | فهم شبکه، حافظه، همروندی | ۱۹، ۲۵، ۲۶ |
| احتمال و آمار | شهود عددی، خطای استاندارد، آزمون فرض | ۹، ۱۰، ۲۰ |
| طراحی سیستم | تقسیم مسئله، Trade-off، حالتهای خرابی | ۱۶، ۲۵ تا ۲۸ |
| دانش بازار | Spread، موجودی، Adverse Selection | ۱ تا ۸، ۲۱ تا ۲۴ |
| پروژه | عمق فهم کاری که خودت کردهای | ۲۹ تا ۳۱ |
یک نکته کلی قبل از سؤالها: در تقریباً همه بخشها، روند فکر کردن از جواب نهایی مهمتر است. بلند فکر کن، فرضهایت را بگو و قبل از کد زدن سؤال روشنکننده بپرس. پاسخهای نمونه زیر عمداً کوتاهاند؛ هدف نشان دادن اسکلت جواب است، نه حفظ کردنش.
۲. ساختار داده دفتر سفارش
سؤال: «یک دفتر سفارش طراحی کن که اضافه کردن سفارش، لغو سفارش با شناسه، و گرفتن بهترین قیمت را پشتیبانی کند. پیچیدگی هر عمل چقدر است؟»
پاسخ کوتاه: سه ساختار لازم است. سطحهای قیمت در یک ساختار مرتب (برای پیدا کردن بهترین قیمت)، سفارشهای هر سطح در یک صف FIFO (برای اولویت زمان، درس ۳)، و یک جدول Hash از شناسه سفارش به جای آن (برای لغو بدون جستوجو). یک نسخه ساده برای سمت خرید:
import heapq
from collections import OrderedDict
class BidSide:
"""Buy side of a book: add O(log n), cancel O(1), best() amortized O(log n)."""
def __init__(self):
self.levels = {} # price -> OrderedDict(order_id -> qty), FIFO within a level
self.heap = [] # -price, so the max price sits on top
self.where = {} # order_id -> price, for O(1) cancel
def add(self, oid, price, qty):
if price not in self.levels:
self.levels[price] = OrderedDict()
heapq.heappush(self.heap, -price)
self.levels[price][oid] = qty
self.where[oid] = price
def cancel(self, oid):
price = self.where.pop(oid)
level = self.levels[price]
del level[oid]
if not level:
del self.levels[price] # its heap entry is removed lazily in best()
def best(self):
while self.heap and -self.heap[0] not in self.levels:
heapq.heappop(self.heap) # drop prices whose level is already empty
return -self.heap[0] if self.heap else None
b = BidSide()
b.add("A", 99_990, 0.1); b.add("B", 99_990, 0.2); b.add("C", 99_980, 0.7)
b.cancel("A"); print(b.best(), list(b.levels[99_990]))
b.cancel("B"); print(b.best())
99990 ['B']
99980
نکتههایی که مصاحبهکننده دنبالشان است:
- حذف تنبل (Lazy Deletion): Heap حذف از وسط را ارزان پشتیبانی نمیکند؛ پس سطح خالی را فقط از دیکشنری برمیداریم و وقتی به بالای Heap رسید، دورش میاندازیم.
OrderedDictبهجایdeque: هم ترتیب ورود را نگه میدارد و هم حذف از وسط صف را در O(1) انجام میدهد. در درس ۳ گفتیم لغو سفارش از وسط یکdequeساختار جداگانهای لازم دارد؛ این همان ساختار است.- سؤال بعدی تقریباً حتمی: «اگر خیلی سریعتر لازم باشد؟» جواب: چون قیمتها روی Tick گسستهاند و نزدیک بهترین قیمت جمعاند، یک آرایه بر حسب Tick حول قیمت فعلی، دسترسی O(1) و حافظه پیوسته (Cache-friendly) میدهد. در زبانهای سطح پایین، صف هر سطح را با یک لیست پیوندی دوطرفه درونسفارشی (Intrusive) میسازند تا هیچ تخصیص حافظهای در مسیر داغ نباشد.
- قیمت به عدد صحیح: اگر قیمت را اعشاری ذخیره کنی، احتمالاً همینجا سؤال بعدی را میشنوی (درس ۳).
۳. تأخیر و سیستم
چند سؤال رایج با اسکلت پاسخ:
«میانگین تأخیر سیستم ما ۱ میلیثانیه است. خوب است؟» میانگین برای تأخیر معیار بدی است، چون توزیع دم سنگین دارد. باید صدکها را دید: p50، p99 و p99.9. بازارساز دقیقاً در لحظههای پرتلاطم ضربه میخورد، و همان لحظههایی است که صفها پر و تأخیر در دم توزیع است. در درس ۳۱ دیدیم تفاوت ۵۰ و ۱۰۰ میلیثانیه تأخیر میتواند علامت PnL را عوض کند.
«تأخیر از کجا میآید و چطور اندازهاش میگیری؟» شبکه، سریالسازی و پارس پیام، صفهای داخلی، توقفهای Garbage Collector، تغییر Context سیستمعامل، و خود منطق استراتژی. اندازهگیری: در هر مرحله (رسیدن داده، تصمیم، ارسال، تأیید) یک Timestamp با ساعت یکنوا (Monotonic Clock) ثبت کن و فاصلهها را بهصورت هیستوگرام در سیستم مانیتورینگ ببین (درس ۲۸). معیار کلیدی، Tick-to-Trade است: از رسیدن داده تا خروج سفارش.
«Python برای این کار کند نیست؟» بستگی دارد مسیر داغ کجاست. برای تصمیمهایی در مقیاس دهها میلیثانیه، Python با asyncio (درس ۲۵) کافی است. برای مسیرهایی که بودجهشان چند میکروثانیه است، بخش داغ را به زبان سطح پایین منتقل میکنند و Python برای پژوهش، Backtest و کنترل میماند. جواب خوب این است که بگویی کجا را اندازه میگیری قبل از اینکه چیزی را بازنویسی کنی.
۴. همروندی
«asyncio یا Thread؟» برای کار I/O-محور مثل نگه داشتن چند WebSocket و ارسال سفارش، asyncio سادهتر و قابلپیشبینیتر است: یک Thread، بدون قفل، و ترتیب اجرای روشن. Threadها در Python به خاطر GIL برای کار محاسباتی موازی واقعی نمیسازند و خطر Race Condition را اضافه میکنند. کار محاسباتی سنگین را میشود به Process جدا فرستاد.
«سفارشی را لغو کردهای و قبل از رسیدن تأیید لغو، پیام پر شدن همان سفارش میرسد. چه میکنی؟» این سؤال محبوب است چون Race Condition واقعی است. پاسخ: سفارش یک ماشین حالت دارد (درس ۲۶) و حالت «در انتظار لغو» یکی از حالتهای معتبر است. پر شدن در این حالت کاملاً قانونی است و باید موجودی را بهروز کند. لغو بعداً یا با «سفارش پیدا نشد» رد میشود یا فقط باقیمانده را لغو میکند. اشتباه رایج: فرض کردن اینکه بعد از ارسال لغو، سفارش دیگر وجود ندارد. در Backtester درس ۳۰ هم سفارش قدیمی تا رسیدن پیام جایگزینی قابل پر شدن بود.
«از کجا میفهمی پیامی از داده بازار گم شده؟» از شماره ترتیب (Sequence Number) پیامها. اگر شکافی دیدی، دفتر محلی دیگر قابل اعتماد نیست: باید سفارشها را عقب بکشی، یک Snapshot تازه بگیری و دفتر را از نو بسازی (درس ۱۶).
۵. احتمال و آمار
«احتمال پر شدن Bid و Ask تو هر کدام ۰.۶ و مستقل از هم است. احتمال اینکه بعد از یک دوره موجودی داشته باشی چقدر است؟» موجودی یعنی دقیقاً یکی پر شده باشد:
دنبالهاش معمولاً این است: «در واقعیت مستقلاند؟» نه؛ وقتی قیمت حرکت میکند، احتمال پر شدن یک طرف بالا و طرف دیگر پایین میرود. همبستگی منفی یعنی احتمال موجودیدار شدن بیشتر از ۰.۴۸ است. این یعنی Adverse Selection درس ۸ از زاویه احتمال.
«۴۰٪ سفارشهای Market آگاهاند و بعد از معامله با آنها بهطور متوسط ۱۰ دلار به ازای هر BTC ضرر میکنی. کارمزد هر طرف ۲ دلار است. حداقل نیماسپرد چقدر باشد؟» با نیماسپرد s، ارزش انتظاری هر پر شدن به ازای هر BTC:
این همان منطق کف min_half در درس ۲۹ است. سؤالهای این شکلی را باید بتوانی بدون کاغذ حل کنی.
«یک استراتژی در سه ماه Sharpe سالانه ۲ داشته. چقدر مطمئنی که لبه دارد؟» خطای استاندارد Sharpe سالانه تقریباً یک تقسیم بر جذر طول دوره به سال است:
یعنی Sharpe ۲ فقط یک خطای استاندارد از صفر فاصله دارد. اگر بتوانی سریع با شبیهسازی نشانش بدهی، بهتر است:
import numpy as np
rng = np.random.default_rng(0)
days, trials = 63, 20_000 # three months of daily returns
r = rng.normal(0.0, 0.01, (trials, days)) # a strategy with zero true edge
sharpe = r.mean(axis=1) / r.std(axis=1, ddof=1) * np.sqrt(252)
print(f"std of annualized Sharpe: {sharpe.std():.2f}")
print(f"share of zero-edge strategies with Sharpe above 2: {(sharpe > 2).mean():.1%}")
std of annualized Sharpe: 2.03
share of zero-edge strategies with Sharpe above 2: 16.0%
از هر شش استراتژی بدون هیچ لبهای، تقریباً یکی در سه ماه Sharpe بالای ۲ نشان میدهد. حالا اگر ۲۰ استراتژی را امتحان کرده باشی و بهترین را گزارش کنی، تقریباً حتماً یک «Sharpe بالای ۲» داری. این همان خطر Overfitting درس ۳۱ است، با زبان آمار. (این فرمول تقریبی بازدههای مستقل و نرمال را فرض میکند؛ دم سنگین و خودهمبستگی خطا را بزرگتر میکنند.)
۶. طراحی سیستم: یک Market Maker از صفر
سؤال: «یک سیستم بازارسازی برای یک جفتارز طراحی کن.» این سؤال باز است و مصاحبهکننده میخواهد ببیند چطور مسئله را میشکنی. یک ترتیب امن:
قدم ۱: سؤال روشنکننده بپرس. چند بازار و چند صرافی؟ بودجه تأخیر چقدر است، میلیثانیه یا میکروثانیه؟ چند پیام در ثانیه؟ اگر سیستم بیفتد، بدترین اتفاق چیست؟ جواب همین سؤالها انتخاب زبان و معماری را تعیین میکند.
قدم ۲: اجزا و جریان داده را بکش.
- Feed Handler اتصال به داده بازار، شماره ترتیب، بازسازی دفتر
- Strategy Fair Value، Spread، Skew؛ تابع خالص
- Risk Gate سقف موجودی و زیان، داده کهنه، محدوده قیمت
- Order Manager ماشین حالت سفارش، شناسه یکتا، تطبیق با صرافی
- Gateway ارسال پیام، Rate Limit، اتصال دوباره
در کنار این زنجیره: Position Keeper (موجودی و PnL از روی پر شدنها)، Kill Switch مستقل از استراتژی (درس ۲۷)، و لاگ و متریک برای هر مرحله (درس ۲۸).
قدم ۳: حالت و بازیابی را توضیح بده. این همان جایی است که نامزدهای قوی از بقیه جدا میشوند. اگر فرایند وسط کار بیفتد و دوباره بالا بیاید، از کجا میداند چه سفارشهایی باز است و موجودیاش چقدر است؟ پاسخ: هرگز به حافظه خودش اعتماد نمیکند؛ قبل از هر سفارش تازه، سفارشهای باز و موجودی را از صرافی میخواند و با دفتر خودش تطبیق (Reconcile) میدهد.
قدم ۴: حالتهای خرابی را بشمار. داده قطع شد؛ صرافی پاسخ نمیدهد؛ لغو رد شد؛ موجودی محلی با صرافی نمیخواند؛ قیمت ۵٪ جهش کرد. برای هر کدام بگو سیستم چه میکند. پیشفرض امن تقریباً همیشه یکی است: سفارشها را عقب بکش و انسان را خبر کن.
قدم ۵: آزمون و استقرار. همان کد استراتژی روی داده ثبتشده بازپخش میشود (درس ۳۰)، بعد با حجم خیلی کوچک در بازار واقعی اجرا میشود و نتیجهاش تکهبهتکه با Backtest مقایسه میشود (درس ۳۱).
اگر سؤال «یک Market Data Pipeline طراحی کن» بود، همین اسکلت با تأکید متفاوت جواب میدهد: دریافت و شماره ترتیب، نرمالسازی قالب صرافیهای مختلف، ذخیره خام (برای بازپخش) جدا از ذخیره پردازششده، و بازسازی دفتر از Snapshot بهاضافه پیامهای تغییر (درسهای ۱۵ و ۱۶).
۷. سؤالهای سناریوی بازار
این سؤالها جواب یکتا ندارند؛ مصاحبهکننده میخواهد ببیند اولویتهایت درست است.
- «قیمت در یک ثانیه ۱٪ بالا رفت و تو موجودی فروش داری.» اول جلوی بدتر شدن را بگیر: سفارشهای سمتی که موجودی را بیشتر میکند عقب بکش، Spread را باز کن، Skew خودش قیمتها را بالا میبرد. اگر موجودی از سقف گذشته، با Hedge یا سفارش تهاجمی کمش کن (درسهای ۲۱ و ۲۴). بعد بپرس چرا سیستم زودتر نفهمید.
- «نرخ پر شدن دو برابر شد ولی PnL افت کرد.» علامت کلاسیک Adverse Selection. Markout را بر حسب زمان، اندازه سفارش و نوع پر شدن تفکیک کن (درسهای ۸ و ۳۱). احتمالاً قیمتهایت از بازار عقب افتادهاند یا یک معاملهگر آگاه پیدایت کرده است.
- «داده بازار دو ثانیه عقب است ولی اتصال قطع نشده.» این بدتر از قطع کامل است، چون هیچ خطایی نمیدهد. به همین دلیل کنترل «داده کهنه» در درس ۲۹ سن داده را میسنجد، نه وجود اتصال را.
- «Funding Rate قرارداد دائمی که با آن Hedge میکنی ناگهان بالا رفت.» هزینه نگه داشتن Hedge بالا رفته است؛ یا موجودی Spot را سریعتر کم کن یا هزینه را در Skew وارد کن (درس ۲۲).
- «همه سیستمها سبزند ولی PnL امروز بهطرز عجیبی خوب است.» این هم باید بررسی شود. سود غیرمنتظره معمولاً یعنی یک باگ در حسابداری یا یک ریسک پنهان که هنوز هزینهاش را ندادهای.
۸. دفاع از تصمیمهای مدلسازی
یکی از سؤالهای همیشگی این است: «چرا X را انتخاب کردی؟» یک قالب چهارقسمتی برای جواب:
- فرض: چه چیزی را درباره بازار فرض کردی.
- شاهد: چه داده یا آزمایشی از آن پشتیبانی میکند.
- جایگزین: چه گزینههای دیگری بود و چرا کنارشان گذاشتی.
- ابطال: چه نتیجهای تو را قانع میکند که اشتباه کردهای.
مثال از پروژه خودمان:
«Microprice را بهعنوان Fair Value انتخاب کردم، با این فرض که عدمتعادل حجم بالای دفتر حرکت بعدی را پیشبینی میکند. وقتی آن را با Mid ساده مقایسه کردم، Mid در پنج ساعت از شش ساعت بهتر بود. Microprice اطلاعات داشت، چون بدون معاملهگر آگاه موجودی را سودده میکرد، ولی با وزن کامل بیش از حد واکنش نشان میداد. قدم بعدی من تخمین یک ضریب کوچککننده با رگرسیون است.»
یا برای «چرا Avellaneda–Stoikov کامل را پیاده نکردی؟»:
«پارامترهای γ و k در عمل سخت تخمین زده میشوند و ناپایدارند. قاعده من همان ساختار را دارد، یعنی Spread وابسته به نوسان و Skew خطی با موجودی، ولی پارامترهایش مستقیم قابلتفسیرند. اگر داده کافی برای تخمین پایدار k داشتم، مقایسه با فرمول کامل اولین آزمایشم بود.» (درسهای ۱۱ و ۱۲)
مصاحبهکننده دنبال کسی نیست که همیشه درست گفته؛ دنبال کسی است که میداند کجا ممکن است اشتباه کرده باشد و چطور میفهمد.
۹. پروژه را چطور ارائه کنی
پروژه درسهای ۲۹ تا ۳۱ یک نمونهکار کامل است، به شرطی که درست بستهبندی شود. یک ساختار ساده برای مخزن کد:
| فایل | محتوا |
|---|---|
README.md | مسئله، طراحی در یک پاراگراف، نتیجهها، محدودیتها، نحوه اجرا |
design.md | سند طراحی درس ۲۹: فرضها، کنترلها، لاگ |
mm_quote.py | تابع قیمتدهی و کنترل ریسک |
backtest.py | Backtester رویدادمحور |
analysis.ipynb | تحلیل درس ۳۱ با خروجیها |
tests/ | تستهای قطعیت، اتحاد حسابداری، سقف موجودی (درس ۳۰) |
requirements.txt | نسخههای دقیق کتابخانهها |
چند قاعده:
- بازتولیدپذیر باشد. با یک دستور و Seed ثابت، همان اعدادی که در README نوشتهای دوباره ساخته شوند.
- اعداد را با خطا گزارش کن. «Markout پنجثانیهای 2.34 با خطای استاندارد 0.71» از «استراتژی سودده است» خیلی حرفهایتر است.
- محدودیتها را خودت بگو. داده مصنوعی است، صف ساده است، رقبا واکنش نشان نمیدهند. اگر مصاحبهکننده اینها را خودش پیدا کند، امتیاز از دست میدهی؛ اگر تو بگویی، امتیاز میگیری.
- ادعای سودآوری نکن. این پروژه درباره روش است، نه درباره پول درآوردن.
- هیچ کد یا دادهای از محل کار فعلی یا قبلیات در آن نباشد. حتی یک نام متغیر خاص. این یکی خط قرمز است و مصاحبهکنندههای جدی دقیقاً به آن توجه میکنند.
ارائه دودقیقهای
وقتی میپرسند «درباره پروژهات بگو»، چیزی شبیه این:
«یک بازارساز تکبازاره ساختم و برایش یک Backtester رویدادمحور با صف، تأخیر و کارمزد نوشتم. در یک بازار مصنوعی با ۴۰٪ معاملهگر آگاه، استراتژی ۹ دلار در ساعت سود داشت، ولی وقتی تحلیلش کردم دیدم لبه بعد از کارمزد حدود ۳۵ سنت به ازای هر بیتکوین است و خطای استاندارد دو برابر خود لبه است. نتیجه با دو برابر شدن تأخیر یا یک فرض بدبینانهتر درباره صف منفی میشود. جالبترین یافته این بود که Microprice، که انتخاب اولم بود، از Mid ساده بدتر عمل کرد و فهمیدم چرا.»
در این دو دقیقه مصاحبهکننده چهار چیز میشنود: ساختی، اندازه گرفتی، به نتیجه خودت شک کردی، و از یک شکست چیزی یاد گرفتی. از اینجا به بعد، سؤالها معمولاً به سمت عمیقتر کردن همین چهار نقطه میروند و تو برای همهشان آمادهای، چون خودت ساختهای.
۱۰. تمرین این درس
گزینه مناسب هر سؤال را انتخاب کن و در پایان نتیجه را ببین.
پایان مسیر، شروع کار
۳۲ درس پیش با یک سؤال ساده شروع کردیم: چه کسی حاضر است همین الان بیتکوینش را به تو بفروشد؟ از آنجا به دفتر سفارش و Spread رسیدیم (بخش ۱)، دیدیم چرا اسپرد بهتنهایی سود نیست و موجودی و Adverse Selection چه هزینهای دارند (بخش ۲)، شهود را به مدلهای Avellaneda–Stoikov و GLFT تبدیل کردیم (بخش ۳)، زیرساخت داده ساختیم (بخش ۴)، یاد گرفتیم Backtest چطور ما را گول میزند (بخش ۵)، با Hedge و قراردادهای دائمی ریسک را جابهجا کردیم (بخش ۶)، سیستم را برای دنیای واقعی و خرابیهایش آماده کردیم (بخش ۷) و در آخر همه را در یک پروژه کنار هم گذاشتیم (بخش ۸).
اگر فقط یک چیز از این مسیر با تو بماند، امیدوارم این باشد: در بازار، عددی که اندازه نگرفتهای وجود ندارد، و عددی که خطایش را نمیدانی، نیمهکاره است.
قدم بعدی دست خودت است. پروژه را خودت از صفر بنویس، نه کپی؛ بعد یک چیز را عوض کن و ببین چه میشود. Fair Value را با رگرسیون بهتر کن، یک Hedge اضافه کن، یا Backtester را به داده واقعی وصل کن. هر کدام یک پروژه مستقل است.
درسها و مطالب تازه Quant Academy اول در کانال تلگرام و بله اعلام میشوند. اگر سؤالی داشتی یا در ساختن پروژه به مشکلی خوردی، همانجا منتظرت هستیم.
این مطلب صرفاً آموزشی است و توصیه خرید، فروش یا سرمایهگذاری نیست.