Implementation shortfall और DIY TCA: मापें कि execution आपको असल में कितना महंगा पड़ता है
हर संस्थागत डेस्क के पास एक transaction cost analysis pipeline होती है। क्रिप्टो बॉट चलाने वाला लगभग कोई भी नहीं रखता। सामान्य सेटअप fills को लॉग करता है, fees जोड़ता है, और backtest PnL तथा live PnL के बीच के अंतर को "slippage" कह देता है — एक अकेला, अस्पष्ट अवशेष जो latency, spread, impact, adverse selection, और हर उस अनफिल्ड ऑर्डर को सोख लेता है जो भाग गया। जिस लागत को आप एक अकेली संख्या के रूप में मापते हैं, उसे आप ठीक नहीं कर सकते। इसे ठीक से मापने की मशीनरी 1988 से मौजूद है, यह मालिकाना नहीं है, और आपके अपने fill लॉग्स पर यह मोटे तौर पर 200 लाइनों का Python है। यह लेख उसे बनाता है।
फायदा कोई सुंदर डैशबोर्ड नहीं है। आपके backtest में एक cost model होता है — एक slippage constant, एक fill probability, एक impact coefficient — और उसका हर पैरामीटर फिलहाल एक अनुमान है। live fills पर TCA ही एकमात्र ground truth है जिसके सापेक्ष उन पैरामीटरों को कैलिब्रेट किया जा सकता है। हमने इस लूप का सिमुलेशन वाला हिस्सा Fill simulation: the ladder from close-price fantasy to queue-aware reality में बनाया था; यह लेख मापन वाला हिस्सा बनाता है।
कागज़ बनाम हकीकत: Perold ने असल में क्या मापा
यह मूलभूत तरकीब André Perold की देन है (1988, "The Implementation Shortfall: Paper Versus Reality," Journal of Portfolio Management 14(3), 4–9)। दो पोर्टफोलियो समानांतर चलाइए। paper portfolio हर निर्णय को तुरंत, असीमित आकार में, शून्य लागत पर, निर्णय के क्षण में प्रचलित कीमत पर निष्पादित करता है। real portfolio वह है जो आपके बॉट ने असल में किया: partial fills, पीछे भागती quotes, रद्द किए गए बचे हिस्से, fees। implementation shortfall इन दोनों के रिटर्न का अंतर है।
यह परिभाषा इसलिए मायने रखती है क्योंकि यह छिपाने से इनकार कर देती है। एक fee statement commissions दिखाता है। एक fill-बनाम-limit-price रिपोर्ट कुछ भी नहीं दिखाती (आप अपनी limit से बदतर कभी fill नहीं होते — रचना से ही)। paper portfolio आपसे हर चीज़ का हिसाब लेता है: निर्णय और arrival के बीच का drift, जो spread आपने पार किया, जो impact आपने पैदा किया, और — निर्णायक रूप से — वे ऑर्डर जो कभी fill नहीं हुए जबकि कीमत भाग गई। Wagner और Edwards (1993, "Best Execution," Financial Analysts Journal 49(1), 65–71) ने दिखने वाली fees को हिमशैल की नोक कहा; किसी भी turnover वाली चीज़ के लिए, डूबा हुआ हिस्सा हावी रहता है।
नोटेशन तय करें। एक parent order: side (buy/sell), size । decision price वह mid है जो आपकी strategy ने तब देखा जब signal चला। Fills के रूप में आते हैं, कुल filled quantity के साथ। horizon पर (parent पूरा, रद्द, या टाइमआउट), mid है। स्पष्ट fees हैं। मुद्रा शब्दों में implementation shortfall:
जिसे paper notional से भाग देकर basis points में सामान्यीकृत किया जाता है। धनात्मक का मतलब आपने चुकाया। पहला पद यह है कि आपके fills की लागत paper portfolio के सापेक्ष कितनी रही; दूसरा Perold का opportunity cost है — बचा हुआ अनफिल्ड हिस्सा उस कीमत पर मार्क किया गया जहाँ तक वह भाग गया; तीसरा वही एकमात्र हिस्सा है जिसे आपका exchange statement स्वीकार करता है।
एक convention नोट: उद्योग "arrival price" कहता है, और अधिकांश equity TCA में arrival का मतलब वह mid है जब ऑर्डर बाज़ार तक पहुँचा। एक बॉट के लिए, decision time और arrival time आपकी अपनी आंतरिक latency तथा rate-limit queueing के कारण अलग होते हैं — एक असली, मापने योग्य लागत। इसलिए हम दोनों timestamps और दोनों prices रखते हैं, और decomposition को उन्हें अलग करने देते हैं।
डिकंपोजिशन: delay, impact, timing, opportunity, fees
एक अकेली IS संख्या आपको बताती है कि execution महंगा है। यह आपको नहीं बताती कि क्यों, और अलग-अलग components के लिए उपाय पूरी तरह अलग हैं — delay cost और impact cost को आप एक ही बदलाव से हल नहीं करते। Robert Kissell का विस्तारित implementation shortfall (Kissell, 2006, "The Expanded Implementation Shortfall: Understanding Transaction Cost Components," Journal of Trading 1(3), 6–16; पुस्तक-लंबाई में The Science of Algorithmic Trading and Portfolio Management, Academic Press, 2013 में विकसित) कुल को components में बाँटता है, जिनमें से हर एक order lifecycle के एक अलग चरण को जिम्मेदार ठहराया जा सकता है। practitioner वाला संस्करण, arrival mid (पहले exchange acknowledgment पर mid) का उपयोग करते हुए:
यह पहचान ठीक-ठीक Perold की परिभाषा तक टेलीस्कोप करती है — पदों का विस्तार करें और रद्द हो जाता है। हर टुकड़े का एक अलग मालिक है:
Delay cost : signal चलने और आपके पहले child order के exchange पर live होने के बीच का price drift। यह आपका infrastructure है — serialization, network, rate-limit queues, risk checks। अगर आपके signal में असली short-horizon alpha है, तो delay cost वही जगह है जहाँ से वह पहले रिसता है; लगातार धनात्मक mean कहता है कि आपके पहुँचने से पहले ही बाज़ार आपके पक्ष में सरक जाता है — क्षय होता momentum alpha, या कोई आपसे तेज़ जो उसी signal पर ट्रेड कर रहा है।
Trading cost : आपके fills ने arrival के सापेक्ष क्या चुकाया — पार किया गया spread प्लस market impact प्लस intra-schedule drift। यह execution algorithm का रिपोर्ट कार्ड है, और वह मात्रा जिसे execution research असल में मॉडल करती है। Almgren, Thum, Hauptmann और Li (2005, "Direct Estimation of Equity Market Impact," Risk 18(7), 58–62) ने इसे Citigroup की डेस्कों से मोटे तौर पर 700,000 US equity ऑर्डरों पर मापा (दिसंबर 2001–जून 2003) और पाया कि trading cost daily volatility और participation rate के साथ स्केल करती है — temporary impact trade rate में एक power law का पालन करती है जिसका exponent 3/5 के करीब है, permanent impact size में लगभग linear है। जब हम कैलिब्रेट करेंगे तो हम उनके functional form का पुनः उपयोग करेंगे।
Timing risk: mean decomposition में यह कोई पद नहीं है बल्कि उसके इर्द-गिर्द का variance है। एक parent को समय पर फैलाने से अपेक्षित impact घटता है और आप volatility के संपर्क में आ जाते हैं; बची हुई position वाले schedule के लिए cost का standard deviation की तरह स्केल करता है। यह ठीक वही trade-off है जिसे Almgren–Chriss framework ऑप्टिमाइज़ करता है। आपकी TCA रिपोर्ट में यह parents के बीच IS के dispersion के रूप में दिखता है — हर mean के बगल में standard deviation रिपोर्ट कीजिए, वरना सारा ध्यान mean को मिलेगा और सारा पैसा tails को।
Opportunity cost : terminal price पर मार्क की गई अनफिल्ड quantity। passive strategies के लिए यह नियमित रूप से सबसे बड़ा और सबसे कम जाँचा गया component होता है, और यही वह पद है जो पूरे framework को ईमानदार बनाता है — इस पर और traps वाले खंड में, क्योंकि इसे छोड़ देना ही अकेला सबसे आम तरीका है जिससे लोग TCA के साथ खुद को धोखा देते हैं।
Fees : स्पष्ट हिस्सा। क्रिप्टो में, fees को discounts के बाद लॉग कीजिए (VIP tiers, fill-time price पर परिवर्तित token rebates) और maker rebates को signed रखिए — एक negative fee डेटा है, शोर नहीं।
एक हल किया हुआ उदाहरण
BTC खरीदिए। signal पर चलता है; paper notional $600,000। पहला child order mid के साथ acked होता है। अगले दो मिनटों में, 8 BTC VWAP 60,072 पर fill होते हैं; कीमत दूर की ओर trend कर रही है, algo अपनी limit का पालन करता है, और बचे 2 BTC mid पर रद्द कर दिए जाते हैं। filled notional पर blended fees 2.5 bps।
| Component | Formula | USD | bps of paper |
|---|---|---|---|
| Delay | 120 | 2.0 | |
| Trading cost | 480 | 8.0 | |
| Opportunity | 456 | 7.6 | |
| Fees | 120 | 2.0 | |
| Total IS | 1,176 | 19.6 |
exchange statement $120 दिखाता है। निर्णय को लागू करने की असल लागत $1,176 थी — दस गुना, जिसके दो सबसे बड़े components fee-आधारित लेखांकन के लिए अदृश्य हैं। इसका मोटे तौर पर 40% उस quantity से आया जिसने कभी ट्रेड ही नहीं किया। सिर्फ fills का विश्लेषण करने वाली एक TCA रिपोर्ट इस parent को 8 bps देती और इसे ठीक-ठाक कह देती।

Markouts: adverse selection की कीमत लगाना
implementation shortfall parent order को ग्रेड करता है। यह अलग-अलग fills की गुणवत्ता के बारे में कुछ नहीं कहता — खासकर यह कि क्या आप व्यवस्थित रूप से ऐसे counterparties के साथ ट्रेड करते हैं जो कुछ ऐसा जानते हैं जो आप नहीं जानते। इसे markouts से मापा जाता है: हर fill को उसके होने के बाद एक निश्चित horizon पर mid से मार्क करें।
जहाँ समय पर fill price है और समय पर mid है। यह horizon पर fill का प्रति-इकाई mark-to-market PnL है, और पर इसकी एक साफ शारीरिक रचना है: एक maker fill आधे-spread पर शुरू होता है (आपने bid पर खरीदा, mid आपके ऊपर है); एक taker fill आधे-spread पर शुरू होता है। जैसे-जैसे बढ़ता है, जो होता है वह ट्रेड का information content है:
- s sniping और stale-quote pickoff को अलग करता है। अगर आपके maker fills fill होने के एक सेकंड बाद ही पानी के नीचे हैं, तो तेज़ participants आपकी quotes को ठीक उसी क्षण मार रहे हैं जब वे mispriced हो जाती हैं — आपका quote update loop उनके trigger loop से धीमा है। यह markout एक latency diagnostic है, strategy diagnostic नहीं।
- s क्लासिक adverse selection मापता है: वे fills जिनके बाद कीमत आपके price के आर-पार लगातार चलती रही। एक market maker के लिए यह वह लागत है जिसे spread capture को हराना चाहिए; equity microstructure संबंधित मात्रा को realized spread कहता है, जिसे SEC Rule 605 रिपोर्टिंग में 5-मिनट के horizon पर संस्थागत किया गया है। क्रिप्टो तेज़ चलती है; 10–60s समतुल्य बैंड है।
- s आपको बताता है कि microstructure horizon के परे fills आपके ख़िलाफ़ momentum रखते हैं या नहीं — taker strategies के लिए, यह कि 60s पर आपके signal की alpha उस spread-प्लस-impact से ज़्यादा है या नहीं जो आपने घुसने के लिए चुकाई। एक taker markout curve जो bps पर शुरू होती है और कभी शून्य पार नहीं करती, वह एक ऐसी strategy है जो ऐसे entries के लिए भुगतान करती है जिन्हें उसकी alpha फंड नहीं कर सकती।
maker/taker असममितता दो curves में passive trading का पूरा अर्थशास्त्र है। एक maker curve जो bps (आधा-spread) पर शुरू होकर 60s तक bps तक क्षय होती है, कहती है: आप spread कैप्चर करते हैं और उससे ज़्यादा वापस दे देते हैं, और कोई rebate tier इसे ठीक नहीं करता। वही curve जो bps पर स्थिर होती है, कहती है कि quoting engine अपनी कमाई करता है। यह ठीक वही है जिसे भोले touch-fill backtests मान कर हटा देते हैं — एक simulator जो आपको तब fill करता है जब भी कीमत आपकी limit को छूती है, इस बात को नज़रअंदाज़ करता है कि fill होना गलत होने से सहसंबंधित है, यही वजह है कि fill-simulation ladder के queue-aware पायदान को एक input के रूप में मापे गए markouts चाहिए, एक धारणा के रूप में नहीं।

गणना आपके अपने mid stream पर एक merge_asof है:
import pandas as pd
def markouts(fills: pd.DataFrame, mids: pd.DataFrame,
horizons=("1s", "10s", "60s")) -> pd.DataFrame:
"""fills: [ts, price, qty, side, liquidity]; mids: [ts, mid].
Both UTC-indexed and sorted. Mid stream must be from YOUR captured
feed, not candles reconstructed later."""
fills = fills.sort_values("ts").reset_index(drop=True)
mids = mids.sort_values("ts")
out = fills.copy()
for h in horizons:
probe = fills[["ts"]].copy()
probe["ts"] = probe["ts"] + pd.Timedelta(h)
m = pd.merge_asof(probe, mids, on="ts", direction="backward")
out[f"mo_{h}"] = (fills["side"] * (m["mid"].values - fills["price"])
/ fills["price"] * 1e4)
return out
def markout_report(mo: pd.DataFrame) -> pd.DataFrame:
"""Qty-weighted markouts by liquidity flag. Weighting matters:
a 0.001 BTC fill and a 2 BTC fill are not equal evidence."""
cols = [c for c in mo.columns if c.startswith("mo_")]
def agg(g):
w = g["qty"] / g["qty"].sum()
return pd.Series({c: (g[c] * w).sum() for c in cols}
| {"n": len(g), "qty": g["qty"].sum()})
return mo.groupby("liquidity").apply(agg)
रिपोर्ट को symbol, दिन के घंटे, और mid से quote दूरी के हिसाब से और आगे स्लाइस कीजिए। maker strategy के लिए सबसे कार्रवाई-योग्य कट markout-by-queue-position-at-fill है: fresh-queue के आगे वाले fills उन fills से बहुत अलग कीमत रखते हैं जहाँ level आपके आर-पार बह गया।
pipeline: क्या लॉग करें
TCA logging layer पर मरती है, math layer पर नहीं। ऊपर का math ऐसी संख्याएँ माँगता है जिन्हें अधिकांश बॉट फेंक देते हैं, और इनमें से किसी को भी बाद में exchange history से पुनर्निर्मित नहीं किया जा सकता। जो बातें अनिवार्य हैं:
- Decision mid, आपके अपने feed से, signal time पर। exchange candle close नहीं, बाद का पुनर्निर्माण नहीं। benchmark है "वह कीमत जो मेरी strategy निर्णय लेते समय मानती थी" — उस क्षण केवल आपकी process ही उसे जानती है।
- दोनों timestamps: decision time और first-ack time, वरना delay cost अमापनीय है और चुपचाप trading cost में मिल जाता है।
- हर fill exchange timestamp, fee, और maker/taker flag के साथ — आपका स्थानीय receive time आपकी अपनी inbound latency से दूषित है।
- रद्द और समाप्त हुए parents, बाकी सब की तरह लॉग किए गए। शून्य fills वाले parents तालिका की सबसे महंगी पंक्तियाँ हैं।
- एक persistent mid stream (या L1 stream) 100–250ms की granularity पर, इतनी देर तक रखा गया कि markouts की गणना हो सके। अगर आप fill simulator के लिए पहले से books रिकॉर्ड करते हैं, तो यह मुफ्त है।
दो flat तालिकाएँ काफी हैं:
PARENTS = {
"parent_id": "str",
"strategy": "str",
"symbol": "str",
"venue": "str",
"algo": "str", # twap | pov | sniper | quote | ...
"side": "int8", # +1 buy, -1 sell
"qty": "float64", # parent size, base units
"limit_px": "float64", # NaN for unconstrained
"decision_ts": "datetime64[ns, UTC]",
"decision_mid": "float64", # mid your feed showed at decision_ts
"arrival_ts": "datetime64[ns, UTC]", # first exchange ack
"arrival_mid": "float64",
"end_ts": "datetime64[ns, UTC]", # filled / cancelled / expired
"terminal_mid": "float64",
"mkt_volume": "float64", # market volume over [arrival_ts, end_ts]
"sigma_bps": "float64", # realized vol estimate at decision time
}
FILLS = {
"parent_id": "str",
"ts": "datetime64[ns, UTC]", # exchange timestamp
"price": "float64",
"qty": "float64",
"fee": "float64", # quote ccy, post-discount, signed
"liquidity": "str", # maker | taker
"venue": "str",
}
attribution function decomposition की सीधी नकल है:
import numpy as np
def is_decomposition(p: pd.Series, fills: pd.DataFrame) -> dict:
"""Expanded implementation shortfall for one parent, bps of paper
notional. Sign convention: positive = cost."""
s, X = p["side"], p["qty"]
paper = X * p["decision_mid"]
x = fills["qty"].sum()
delay = s * X * (p["arrival_mid"] - p["decision_mid"])
trade = s * ((fills["price"] - p["arrival_mid"]) * fills["qty"]).sum()
oppty = s * (X - x) * (p["terminal_mid"] - p["arrival_mid"])
fees = fills["fee"].sum()
bps = lambda v: 1e4 * v / paper
return {
"parent_id": p["parent_id"],
"delay_bps": bps(delay), "trade_bps": bps(trade),
"oppty_bps": bps(oppty), "fees_bps": bps(fees),
"is_bps": bps(delay + trade + oppty + fees),
"fill_ratio": x / X,
"participation": x / max(p["mkt_volume"], x),
}
def tca_table(parents: pd.DataFrame, fills: pd.DataFrame) -> pd.DataFrame:
fg = dict(tuple(fills.groupby("parent_id")))
empty = fills.iloc[0:0]
rows = [is_decomposition(p, fg.get(p["parent_id"], empty))
for _, p in parents.iterrows()]
return parents.merge(pd.DataFrame(rows), on="parent_id")
और attribution queries, जहाँ TCA लेखांकन होना बंद करके research बनना शुरू करता है:
tca = tca_table(parents, fills)
def wavg(g: pd.DataFrame, col: str) -> float:
w = g["qty"] * g["decision_mid"] # notional weights
return (g[col] * w).sum() / w.sum()
comp = ["delay_bps", "trade_bps", "oppty_bps", "fees_bps", "is_bps"]
by_strat = tca.groupby("strategy").apply(
lambda g: pd.Series({c: wavg(g, c) for c in comp}
| {"is_std": g["is_bps"].std(), "n": len(g)}))
done = tca[tca["fill_ratio"] > 0.99]
done["pov_bin"] = pd.qcut(done["participation"], 6)
impact_curve = done.groupby("pov_bin").apply(lambda g: wavg(g, "trade_bps"))
tca["hour"] = tca["arrival_ts"].dt.hour
by_hour = tca.groupby("hour").apply(lambda g: wavg(g, "is_bps"))
by_venue = tca.groupby("venue").apply(
lambda g: pd.Series({"is_bps": wavg(g, "is_bps"),
"is_std": g["is_bps"].std(),
"fill_ratio": g["fill_ratio"].mean(), "n": len(g)}))
Query 4 वह input तालिका है जिसके ऊपर एक smart order router ऑप्टिमाइज़ करता है — per-venue TCA के बिना routing करना fee schedule से routing करना है, यानी सबसे छोटे cost component से (Smart order routing in crypto इसी तालिका को अपना शुरुआती बिंदु मानता है)। markout module के साथ मिलकर, यही वादा किया गया ~200 लाइनें हैं।
लूप बंद करना: backtest cost model को कैलिब्रेट करना
यहीं pipeline अपनी कीमत वसूल करती है। आपका backtest संख्याएँ मुखर करता है: slippage_bps = 5, एक fill probability curve, एक impact coefficient। इनमें से हर एक live execution के बारे में एक दावा है, और TCA तालिका live execution है। लूप: TCA से मापें, cost model फिट करें, फिट किए गए model के साथ backtest चलाएँ, ट्रेड करें, फिर से मापें।

taker cost के लिए, कोई खुद का आविष्कार करने के बजाय Almgren et al. (2005) से functional form उधार लीजिए। उनका नतीजा — cost daily volatility गुणा participation rate की एक power के समानुपाती — एक दो-पैरामीटर model देता है:
equity अनुमान को एक prior के रूप में लेते हुए। इसे binned means पर फिट कीजिए, raw parents पर नहीं — अलग-अलग parent costs पर शोर हावी होता है (वही timing-risk पद है), और raw data पर एक log-log regression खुशी-खुशी शोर को फिट कर लेगा:
done = tca[(tca["fill_ratio"] > 0.99) & (tca["participation"] > 0)]
done["norm_cost"] = done["trade_bps"] / done["sigma_bps"]
bins = done.groupby(pd.qcut(done["participation"], 8)).agg(
pov=("participation", "mean"), cost=("norm_cost", "mean"))
bins = bins[bins["cost"] > 0] # can't log a negative bin
b, log_a = np.polyfit(np.log(bins["pov"]), np.log(bins["cost"]), 1)
def taker_cost_bps(participation, sigma_bps, a=np.exp(log_a), b=b):
return a * sigma_bps * participation ** b
फिर error bars के बारे में ईमानदार रहिए। Almgren की टीम के पास 700,000 ऑर्डर थे और फिर भी उन्होंने temporary-impact exponent को के रूप में रिपोर्ट किया; 2,000 parents वाले बॉट को हर symbol के लिए स्वतंत्र रूप से का अनुमान लगाने की छूट नहीं है। व्यावहारिक व्यवस्था: से सामान्यीकृत करने के बाद symbols के बीच pool कीजिए, को 0.6 की ओर ज़ोर से shrink कीजिए (या बस उसे तय कर दीजिए और केवल फिट कीजिए), और मासिक रूप से refit कीजिए। अगर आपका फिट किया गया ऊपर की ओर drift करता है, तो या तो आपका footprint बढ़ गया या बाज़ार पतला हो गया — किसी भी हाल में backtest को यह जानना ज़रूरी था।
maker strategies के लिए कैलिब्रेशन के लक्ष्य अलग हैं और बाकी दो modules से आते हैं:
- Fill probability: fill simulator का queue model प्रति (spread-distance, queue-position) सेल की भविष्यवाणी करता है; आपका parent log प्रति सेल realized fill ratios देता है। असहमति एक ग्रिड संदर्भ के साथ संलग्न simulator बग है।
- Adverse selection: simulator की निहित "fills exchangeable हैं" धारणा को मापे गए markout table से बदलिए — घंटे में दूरी पर एक simulated maker fill मापे गए को एक तात्कालिक mark-to-market haircut के रूप में साथ रखता है। यह अकेला बदलाव एक ऐसे maker backtest, जो मतिभ्रम करता है, और एक ऐसे के बीच का अंतर है जो सटीक ट्रैक करता है; यह fill-simulation लेख में चिह्नित छूटा हुआ कैलिब्रेशन input था।
- Cost dispersion: backtest में IS का वितरण फीड कीजिए, उसका mean नहीं। एक cost model जो केवल mean को शिफ्ट करता है, timing risk जो drawdowns पैदा करती है, उन्हें पुनरुत्पादित नहीं कर सकता; per-parent IS पर फिट किया गया एक lognormal भी एक constant को हरा देता है।
पूरा slippage-curve उपचार — functional forms, regime conditioning, power law कब टूटता है — अपने आप में एक लेख है: Slippage curves and cost models for backtests। यहाँ मुद्दा स्थापत्य-संबंधी है: उस लेख के models इस लेख की तालिकाओं के बिना unfittable हैं।
Traps
TCA साहित्य दशकों पुराना है, और इसे धोखा देने के तरीके भी उतने ही पुराने हैं। तीन failure modes अधिकांश आत्म-धोखे के लिए ज़िम्मेदार हैं।
Benchmark gaming. arrival price के अलावा किसी भी benchmark को गले लगाया जा सकता है। क्लासिक है VWAP: interval VWAP के सापेक्ष ग्रेड किया गया algorithm उसे एक basis point के भीतर ट्रैक कर सकता है जबकि position arrival के सापेक्ष बीस bleed करती है, क्योंकि benchmark उसी कीमत के साथ drift करता है जिसे आप धकेल रहे हैं — और सार्थक participation पर आपके अपने prints ही VWAP हैं, इसलिए उसे ट्रैक करना खुद का ग्रेड करने वाला होमवर्क है। हमने benchmark की राजनीति को TWAP vs VWAP vs POV में विच्छेदित किया; TCA-पक्ष का नियम सरल है: benchmarks ट्रेडिंग से पहले चुने जाते हैं, और IS-बनाम-arrival हमेशा गणना किया जाता है, तब भी जब एक scheduler को उसके scheduling benchmark के सापेक्ष ग्रेड किया जा रहा हो। सूक्ष्म संस्करण है arrival gaming: अगर वह component जो decision_ts सेट करता है short-term momentum देख सकता है, तो वह delay पद को चापलूसी करने के लिए "decisions" का समय तय कर सकता है। decision timestamps signal layer के हैं, किसी भी execution logic के चलने से पहले लॉग किए गए।
filled-only विश्लेषण में survivorship bias. अपने cost विश्लेषण को fills पर conditioned कीजिए और passive execution मुफ्त लगती है। ठोस रूप से: 100 passive buy parents, mid से एक tick नीचे। साठ fill होते हैं, और — passive होने के कारण — arrival से औसतन 3 bps बेहतर कीमतों पर fill होते हैं: मापी गई "cost" bps, गर्व करने लायक एक रिपोर्ट। जो चालीस कभी fill नहीं हुए वे ठीक वही थे जहाँ कीमत दूर उछल गई; उन्हें cancel पर 25 bps प्रतिकूल मार्क कीजिए और ईमानदार संख्या है bps। filled-only रिपोर्ट और सच्ची रिपोर्ट 11 bps और चिह्न से भिन्न हैं। यह कोई corner case नहीं है — यह passive trading की कार्यप्रणाली है: fill होना कीमत के आपके आर-पार आने से सहसंबंधित है, जो वही conditioning है जो touch-fill backtests को कल्पना बनाती है। Opportunity cost IS का कोई वैकल्पिक परिष्करण नहीं है; यह वह पद है जो पूरे मापन को selection के ख़िलाफ़ बचाता है। इसी bias का एक taker संस्करण है: वे IOC ऑर्डर जो चूक गए, वे ऑर्डर जो rate limits या risk checks द्वारा अस्वीकृत हो गए — अगर चूक लॉग नहीं की गई, तो चूकने की लागत अमापित है, और वह ठीक उन तेज़ बाज़ारों में सबसे बड़ी होती है जहाँ आपकी strategy को ट्रेड सबसे ज़्यादा चाहिए था।
विविध foot-guns, संक्षेप में: exchange candles से पुनर्निर्मित arrival mids (जब मायने रखता है ठीक तभी आपका feed और candle असहमत होते हैं); notional weights के बिना per-parent bps का औसत निकालना (हज़ार dust fills एक असली ऑर्डर को वोट में हरा देते हैं); pre-discount लॉग की गई fees; fill से भिन्न venue से लिए गए markout mids (cross-venue basis adverse selection का भेस धरे); और perps पर, funding accrual को execution window में bleed होने देना — funding एक cost है, पर execution cost नहीं, और इन्हें मिलाना दोनों विश्लेषणों को ज़हरीला बना देता है।
इस हफ्ते क्या करें
अपने बॉट में दो log तालिकाएँ जोड़िए — ऊपर का schema एक column सूची है, एक project नहीं। कुछ भी backfill मत कीजिए; ईमानदार लॉग्स के दो हफ्ते एक साल के पुनर्निर्माणों को हरा देते हैं। decomposition और तीन markout horizons चलाइए। आप सीखेंगे कि कौन सा component हावी है (लगभग कभी fees नहीं), क्या आपके passive fills अपनी spread capture से परे प्रतिकूल रूप से चुने गए हैं, और आपके backtest का cost constant मापी गई curve से कितनी दूर बैठा है। फिर फिट की गई curve को वापस simulator में जोड़िए और वह backtest फिर से चलाइए जिसने आपसे पहले-पहल यह strategy चलाने को कहा था। Perold का paper portfolio, अड़तीस साल बाद, अब भी वही एकमात्र ईमानदार प्रतिद्वंद्वी है जो आपके असली के पास है।
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.