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

مسیر Quant Developer · بخش ۲: اقتصاد و ریسک Market Making · درس ۷

Spread Capture، کارمزد و PnL

سود و زیان بازارساز را دقیق حساب می‌کنیم: ناخالص و خالص، تحقق‌یافته و تحقق‌نیافته، اثر کارمزد Maker و Taker، و تجزیه PnL به سهم Spread، سهم موجودی و سهم کارمزد.

سطح: متوسط۱۳ دقیقه مطالعه

۱. یک عدد، چند معنی

«امروز چقدر سود کردیم؟» ساده‌ترین سؤال یک سیستم معاملاتی است و جوابش بسته به اینکه چه چیزی را بشماری، کاملاً فرق می‌کند. پیش از هر چیز این اصطلاح‌ها را از هم جدا کنیم:

  • PnL (Profit and Loss): سود و زیان.
  • Gross PnL یا سود ناخالص: سود و زیان معاملات پیش از کارمزد.
  • Net PnL یا سود خالص: بعد از کارمزد (و ریبیت).
  • Realized PnL یا سود تحقق‌یافته: سود و زیان بخشی از موجودی که بسته شده است؛ یعنی خریدی که فروخته‌ای یا برعکس.
  • Unrealized PnL یا سود تحقق‌نیافته: سود و زیان کاغذی موجودی‌ای که هنوز در دست داری.
  • Mark-to-Market: ارزش‌گذاری موجودی باز با قیمت فعلی بازار. قیمتی که با آن ارزش‌گذاری می‌کنی را Mark می‌گویند.

در درس ششم PnL را به شکل «نقد به‌علاوه موجودی ضرب در قیمت پایانی» حساب کردیم. آن عدد درست بود، ولی نمی‌گفت پول از کجا آمد و کجا رفت. این درس همین را باز می‌کند.

۲. کارمزد Maker و Taker، با عدد

در درس ۳ دیدیم Maker و Taker کارمزد متفاوتی دارند. در این درس همان نرخ‌های فرضی درس ۱ را به کار می‌بریم:

نقشنرخ فرضیبرای 0.1 BTC در قیمت حدود 100,000
Makerریبیت ۰.۰۱٪ (۱ basis point)حدود ۱ دلار دریافتی
Takerکارمزد ۰.۰۵٪ (۵ basis point)حدود ۵ دلار پرداختی
Fee = Price × Quantity × Rate

کارمزد روی ارزش معامله (Notional) گرفته می‌شود، نه روی سود. حالا این را با درآمد Spread مقایسه کن: اگر 0.1 BTC را با نیم‌اسپرد ۱۰ دلاری بخری، سهم Spread در همان یک معامله هم 0.1 × 10 = 1 دلار است. یعنی:

  • ریبیت Maker به اندازه کل درآمد Spread هر معامله است.
  • اگر به‌جای ریبیت، کارمزد Maker ۰.۰۲٪ (۲ دلار) بپردازی، هر معامله پیش از هر حرکت قیمتی زیان‌ده است.
  • یک معامله Taker (۵ دلار) درآمد Spread پنج معامله Maker را می‌بلعد.

این همان نتیجه جدول درس ۱ است، این بار در مقیاس یک معامله. در بازارسازی با Spread باریک، کارمزد یک جزء فرعی نیست؛ اغلب بزرگ‌ترین جزء است.

۳. Realized و Unrealized با روش میانگین بها

برای جدا کردن سود تحقق‌یافته از تحقق‌نیافته، باید بدانی موجودی‌ات را «به چه قیمتی» خریده‌ای. رایج‌ترین روش در سیستم‌های معاملاتی میانگین بها (Average Cost) است:

New Avg = (Old Qty × Old Avg + Fill Qty × Fill Price) / (Old Qty + Fill Qty)

این فرمول فقط وقتی به کار می‌رود که موجودی را بزرگ‌تر می‌کنی (خرید روی Long، یا فروش روی Short). وقتی موجودی را کوچک می‌کنی، میانگین بها ثابت می‌ماند و سود تحقق می‌یابد. برای موجودی Long:

Realized = Closed Qty × (Sell Price − Avg Cost)

و برای باقی‌مانده موجودی:

Unrealized = Position × (Mark − Avg Cost)

Mark معمولاً Mid یا Fair Value درس ۵ است. نسخه محافظه‌کارانه‌تر، Long را با Best Bid و Short را با Best Ask ارزش‌گذاری می‌کند؛ یعنی قیمتی که واقعاً می‌توانی فوراً با آن خارج شوی.

۴. مثال: سه معامله و یک ارزش‌گذاری

بازار کمی پایین می‌رود و بازارساز سه معامله Maker انجام می‌دهد، هر کدام 0.1 BTC. ستون Mid، قیمت میانی بازار در لحظه هر معامله است:

رویدادمعاملهMidموجودیمیانگین بهاRealized
۱خرید در 99,990100,0000.199,990صفر
۲خرید در 99,97099,9800.299,980صفر
۳فروش در 100,00099,9900.199,980۲ دلار سود
ارزش‌گذاری—99,9600.199,980۲ دلار سود

قدم به قدم:

  • بعد از دو خرید، میانگین بها (99,990 + 99,970) / 2 = 99,980 است.
  • فروش در 100,000، 0.1 BTC را می‌بندد: 0.1 × (100,000 − 99,980) = 2 دلار سود تحقق‌یافته.
  • 0.1 BTC باقی‌مانده با Mid پایانی 99,960 ارزش‌گذاری می‌شود: 0.1 × (99,960 − 99,980)، یعنی ۲ دلار زیان تحقق‌نیافته.
Realized PnL
+2.00 USDT
Unrealized PnL
−2.00 USDT
Gross PnL
0.00 USDT
Net PnL (با ریبیت حدود 3 دلار)
+3.00 USDT

سود ناخالص دقیقاً صفر است و تمام سود خالص از ریبیت آمده است. اگر همین سه معامله با کارمزد Maker ۰.۰۲٪ انجام می‌شد (حدود ۲ دلار برای هر معامله)، نتیجه خالص حدود ۶ دلار زیان بود.

یک نکته مهم: گزارش «۲ دلار سود تحقق‌یافته» به تنهایی گمراه‌کننده است. داشبوردی که فقط Realized را نشان دهد، استراتژی‌ای را سودده نشان می‌دهد که زیان‌هایش را در موجودی باز پنهان کرده است. همیشه Realized و Unrealized را با هم ببین.

۵. تجزیه PnL: سهم Spread و سهم موجودی

تقسیم Realized و Unrealized یک تقسیم حسابداری است. برای فهم اقتصادی استراتژی، تجزیه دیگری مفیدتر است:

Gross PnL = Spread Capture + Inventory PnL

Spread Capture یا سهم Spread: هر معامله در لحظه انجام، چقدر از Mid بهتر بوده است.

Spread Capture = s × Qty × (Mid − Price)

که s برای خرید برابر ۱ و برای فروش برابر منفی ۱ است. خرید زیر Mid و فروش بالای Mid هر دو سهم مثبت دارند.

Inventory PnL یا سهم موجودی: سود و زیان ناشی از نگه داشتن موجودی، وقتی Mid بین دو رویداد حرکت می‌کند.

Inventory PnL = Σ Position × ΔMid

برای مثال بالا:

بازهSpread Captureموجودی در بازهتغییر MidInventory PnL
رویداد ۱0.1 × 10 = ۱ دلار
۱ تا ۲0.1100,000 به 99,980۲ دلار زیان
رویداد ۲0.1 × 10 = ۱ دلار
۲ تا ۳0.299,980 به 99,990۲ دلار سود
رویداد ۳0.1 × 10 = ۱ دلار
۳ تا ارزش‌گذاری0.199,990 به 99,960۳ دلار زیان
جمع۳ دلار سود۳ دلار زیان

سهم Spread سه دلار، سهم موجودی سه دلار زیان، و جمع صفر؛ همان Gross PnL بخش ۴. با اضافه شدن کارمزد، تصویر کامل این است:

Net PnL = Spread Capture + Inventory PnL − Fees = 3 − 3 − (−3) = 3

(کارمزد اینجا منفی است چون ریبیت گرفته‌ایم.) حالا داستان روشن است: استراتژی Spread را درست گرفت، ولی همه‌اش را در موجودی پس داد. این الگو، یعنی خریدن درست قبل از پایین رفتن قیمت، دقیقاً همان چیزی است که درس ۸ اندازه‌اش را می‌گیرد.

۶. بستن موجودی با سفارش Taker

فرض کن بعد از ارزش‌گذاری، سیستم ریسک تصمیم بگیرد 0.1 BTC باقی‌مانده را فوراً ببندد. Mid برابر 99,960 است و Best Bid برابر 99,950. فروش Market یعنی:

  • Realized: 0.1 × (99,950 − 99,980)، یعنی ۳ دلار زیان. جمع Realized از ۲ دلار سود به ۱ دلار زیان می‌رسد.
  • Spread Capture این معامله: 0.1 × (99,950 − 99,960)، یعنی ۱ دلار زیان. این بار ما نیم‌اسپرد را پرداختیم.
  • کارمزد Taker: 0.1 × 99,950 × 0.05% ≈ 5 دلار پرداختی.
پیش از بستنبعد از بستن
موجودی0.1 BTCصفر
Gross PnLصفر۱ دلار زیان
کارمزد خالص۳ دلار ریبیت۲ دلار پرداختی
Net PnL۳ دلار سود۳ دلار زیان

حذف ریسک 0.1 BTC، ۶ دلار هزینه داشت: ۱ دلار نیم‌اسپرد و ۵ دلار کارمزد Taker. این قیمت «خروج فوری» است و توضیح می‌دهد چرا بازارسازها ترجیح می‌دهند موجودی را با Skew (درس ۶) و به‌عنوان Maker کم کنند و Taker شدن را برای وضعیت اضطراری نگه دارند. گزینه‌های دیگر پوشش ریسک را در درس ۲۱ می‌بینیم.

۷. در کد: محاسبه PnL از فهرست معاملات

تابع زیر فهرست معاملات را می‌گیرد و با روش میانگین بها موجودی، سود تحقق‌یافته، سود تحقق‌نیافته و کارمزد را حساب می‌کند. حالت عبور از صفر (مثلاً فروش بیشتر از موجودی Long که آن را به Short تبدیل می‌کند) هم پوشش داده شده است.

# Hypothetical fee rates in basis points (1 bp = 0.01%); negative = rebate
FEE_BPS = {"maker": -1.0, "taker": 5.0}

def pnl_report(fills, mark):
    """fills: (side, price, qty, liquidity). Average-cost accounting."""
    pos = avg = realized = fees = 0.0
    for side, price, qty, liq in fills:
        signed = qty if side == "buy" else -qty
        fees += abs(signed) * price * FEE_BPS[liq] / 10_000
        if pos * signed >= 0:                 # opening or adding: update average cost
            avg = (avg * abs(pos) + price * abs(signed)) / (abs(pos) + abs(signed))
            pos += signed
        else:                                 # reducing (or flipping) the position
            closed = min(abs(signed), abs(pos))
            realized += closed * (price - avg) * (1 if pos > 0 else -1)
            pos += signed
            if abs(pos) < 1e-12:
                pos, avg = 0.0, 0.0
            elif pos * signed > 0:            # flipped: remainder opens at this price
                avg = price
    unrealized = pos * (mark - avg)
    gross = realized + unrealized
    return dict(pos=round(pos, 8), avg=avg, realized=realized,
                unrealized=unrealized, gross=gross, fees=fees, net=gross - fees)

fills = [
    ("buy", 99_990, 0.1, "maker"),
    ("buy", 99_970, 0.1, "maker"),
    ("sell", 100_000, 0.1, "maker"),
]
for key, val in pnl_report(fills, mark=99_960).items():
    print(f"{key:10s} {val:10.2f}")

print("--- after closing with a taker sell at 99,950")
for key, val in pnl_report(fills + [("sell", 99_950, 0.1, "taker")], mark=99_960).items():
    print(f"{key:10s} {val:10.2f}")

خروجی:

pos              0.10
avg          99980.00
realized         2.00
unrealized      -2.00
gross            0.00
fees            -3.00
net              3.00
--- after closing with a taker sell at 99,950
pos              0.00
avg              0.00
realized        -1.00
unrealized       0.00
gross           -1.00
fees             2.00
net             -3.00

همان اعداد بخش ۴ و ۶. چند نکته درباره این کد:

  • علامت کارمزد: اینجا کارمزد مثبت یعنی پرداختی و منفی یعنی ریبیت؛ پس net = gross - fees. در سیستم واقعی این قرارداد علامت را یک بار تعریف و همه‌جا رعایت کن؛ اشتباه علامت کارمزد از رایج‌ترین باگ‌های گزارش PnL است.
  • اعداد اعشاری: این کد برای آموزش با float کار می‌کند. همان‌طور که در درس ۳ گفتیم، سیستم واقعی قیمت و حجم را به‌صورت عدد صحیح (Tick و Lot) یا Decimal نگه می‌دارد. خط round(pos, 8) و شرط 1e-12 فقط وصله‌ای برای خطای گرد کردن float است.
  • روش‌های دیگر: به‌جای میانگین بها می‌شود از FIFO استفاده کرد (اولین خرید، اولین فروش). جمع Gross PnL در هر دو روش یکی است، ولی تقسیمش بین Realized و Unrealized فرق می‌کند. قواعد حسابداری یا مالیاتی ممکن است یکی را الزامی کنند.

۸. تمرین این درس

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

سؤال ۱: بازارساز 0.2 BTC در 99,990 می‌خرد، سپس 0.1 BTC در 100,010 می‌فروشد. Mid فعلی 100,000 است. Realized و Unrealized چقدر است؟
سؤال ۲: یک سفارش Maker به حجم 0.1 BTC در قیمت 100,000 با ریبیت ۰.۰۱٪ پر می‌شود. اثر کارمزد چیست؟
سؤال ۳: در مثال بخش ۴، سود ناخالص صفر ولی سود خالص مثبت بود. چرا؟
سؤال ۴: Mid برابر 99,960 است و بازارساز 0.1 BTC را با فروش Market در 99,950 می‌فروشد. Spread Capture این معامله چقدر است؟

درس بعدی

در مثال این درس، Spread را درست گرفتیم و همه‌اش را در موجودی پس دادیم. آیا این بدشانسی بود؟ در درس هشتم می‌بینیم معمولاً نه: Adverse Selection یعنی بخشی از کسانی که با تو معامله می‌کنند، بیشتر از تو می‌دانند. و با Markout یاد می‌گیریم اندازه این زیان را برای هر معامله، در افق‌های زمانی مختلف، اندازه بگیریم.

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