← लेखों की सूची पर वापस जाएँ
June 12, 2026
5 मिनट का पठन

जोखिम-सजग पोज़िशन साइज़िंग के लिए कॉन्फ़ॉर्मल प्रेडिक्शन

जोखिम-सजग पोज़िशन साइज़िंग के लिए कॉन्फ़ॉर्मल प्रेडिक्शन
#uncertainty
#conformal-prediction
#risk
#position-sizing
#statistics
#algorithmic-trading
🧠
Part 4 of 4 · Collection
Deep Learning for Markets

हर पोज़िशन साइज़िंग फ़ॉर्मूले को अनिश्चितता के अनुमान की ज़रूरत होती है। Kelly criterion को जीतने की संभावना और payoff ratio चाहिए (देखें रणनीतियों के लिए Kelly criterion)। Mean-variance optimization को covariance matrix चाहिए। VaR को returns के distribution की ज़रूरत होती है। इन सभी को डेटा-जनरेटिंग प्रक्रिया के बारे में मान्यताओं की आवश्यकता होती है — ऐसी मान्यताएं जिन्हें वित्तीय बाज़ार अक्सर तोड़ देते हैं।

Conformal prediction कुछ अलग पेश करता है: बिना किसी पैरामीट्रिक वितरण संबंधी मान्यता के, फ़िनाइट-सैंपल कवरेज गारंटी वाले प्रेडिक्शन इंटरवल्स। यदि आप 90% कवरेज मांगते हैं, तो आपको कम से कम 90% कवरेज मिलता है — चाहे returns Gaussian हों, fat-tailed हों, skewed हों, या heteroskedastic हों। एकमात्र आवश्यकता exchangeability है (या, जैसा हम देखेंगे, इससे कमज़ोर शर्तें)।

यह लेख सिद्धांत, प्रमुख वेरिएंट्स, और Python में पोज़िशन साइज़िंग के लिए एक व्यावहारिक implementation को कवर करता है।

मूल विचार: Nonconformity Scores

एक क्वांटाइल थ्रेशोल्ड के साथ चिह्नित nonconformity (residual) scores का वितरण — conformal कैलिब्रेशन का मूल

Conformal prediction यह मापकर काम करता है कि एक नया observation पिछले डेटा की तुलना में कितना "अजीब" है। इस अजीबपन को एक nonconformity score द्वारा मापा जाता है — कोई भी फ़ंक्शन जो यह मापता है कि एक data point बाकी डेटा में देखे गए पैटर्न के कितना कम अनुरूप है।

Regression के लिए (जैसे returns जैसी एक continuous value की भविष्यवाणी), सबसे सरल nonconformity score absolute residual है:

Ri=Yiμ^(Xi)R_i = |Y_i - \hat{\mu}(X_i)|

जहां μ^\hat{\mu} कोई भी point predictor है (linear regression, random forest, neural network — इससे फ़र्क नहीं पड़ता) और (Xi,Yi)(X_i, Y_i) एक data point है।

मुख्य अंतर्दृष्टि: यदि data points (X1,Y1),,(Xn,Yn),(Xn+1,Yn+1)(X_1, Y_1), \ldots, (X_n, Y_n), (X_{n+1}, Y_{n+1}) exchangeable हैं, तो R1,,Rn,Rn+1R_1, \ldots, R_n, R_{n+1} में Rn+1R_{n+1} का rank {1,,n+1}\{1, \ldots, n+1\} पर uniformly distributed है। यह विशुद्ध रूप से एक combinatorial तथ्य है — इसे XX या YY के distribution के बारे में किसी मान्यता की आवश्यकता नहीं है।

इस rank uniformity से, हम फ़िनाइट-सैंपल कवरेज वाले प्रेडिक्शन इंटरवल्स बना सकते हैं।

Split Conformal Prediction

Split conformal prediction: डेटा को training और calibration folds में बांटा गया है, calibration residuals प्रेडिक्शन इंटरवल्स बना रहे हैं

Split conformal prediction (Papadopoulos et al., 2002; Lei et al., 2018) सबसे व्यावहारिक वेरिएंट है। Algorithm सरल है:

चरण 1. डेटा को एक training set Dtrain\mathcal{D}_{\text{train}} और एक calibration set Dcal={(X1,Y1),,(Xn,Yn)}\mathcal{D}_{\text{cal}} = \{(X_1, Y_1), \ldots, (X_n, Y_n)\} में बांटें।

चरण 2. Dtrain\mathcal{D}_{\text{train}} पर कोई भी model μ^\hat{\mu} फ़िट करें।

चरण 3. Calibration set पर nonconformity scores की गणना करें:

Ri=Yiμ^(Xi),i=1,,nR_i = |Y_i - \hat{\mu}(X_i)|, \quad i = 1, \ldots, n

चरण 4. एक वांछित miscoverage level α(0,1)\alpha \in (0, 1) के लिए, q^\hat{q} को R1,,RnR_1, \ldots, R_n का (1α)(n+1)n\frac{\lceil (1 - \alpha)(n + 1) \rceil}{n} empirical quantile मानें। स्पष्ट रूप से, यह (1α)(n+1)\lceil (1 - \alpha)(n + 1) \rceil-वां सबसे छोटा residual है (और (1α)(n+1)>n\lceil (1 - \alpha)(n + 1) \rceil > n होने पर, यानी बहुत छोटे nn के लिए, q^=+\hat{q} = +\infty)।

चरण 5. एक नए point Xn+1X_{n+1} के लिए प्रेडिक्शन इंटरवल है:

C(Xn+1)=[μ^(Xn+1)q^,  μ^(Xn+1)+q^]C(X_{n+1}) = \left[\hat{\mu}(X_{n+1}) - \hat{q}, \; \hat{\mu}(X_{n+1}) + \hat{q}\right]

कवरेज गारंटी

Calibration data और नए test point की exchangeability के तहत:

P(Yn+1C(Xn+1))1α\mathbb{P}\left(Y_{n+1} \in C(X_{n+1})\right) \geq 1 - \alpha

यह एक फ़िनाइट-सैंपल गारंटी है — कोई asymptotic approximation नहीं। यह किसी भी model μ^\hat{\mu}, डेटा के किसी भी distribution, और किसी भी sample size nn के लिए लागू होती है। यदि μ^\hat{\mu} एक ख़राब predictor है, तो intervals बस अधिक चौड़े होंगे। कवरेज गारंटी फिर भी बनी रहती है।

जब scores में कोई ties न हों तो एक upper bound भी है: P(Yn+1C(Xn+1))1α+1n+1\mathbb{P}(Y_{n+1} \in C(X_{n+1})) \leq 1 - \alpha + \frac{1}{n+1}, इसलिए कवरेज बेवजह conservative नहीं है।

यह ट्रेडिंग के लिए क्यों मायने रखता है

पारंपरिक प्रेडिक्शन इंटरवल्स, मान लीजिए किसी linear regression से, Gaussian errors मान लेते हैं। डेटा के बल्क पर कैलिब्रेट किया गया एक Gaussian interval, जब वास्तविक residuals fat-tailed होते हैं (जैसे कि कुछ degrees of freedom वाला Student-tt), तो tails को बुरी तरह गलत आंक सकता है: केंद्रीय द्रव्यमान Gaussian से पतला होता है, इसलिए एक variance-matched Gaussian interval केंद्र के पास over-cover करता है फिर भी tails में under-cover करता है, और एक tail-fit interval इसका उल्टा करता है। मुद्दा एक एकल जादुई संख्या नहीं है — मुद्दा यह है कि एक पैरामीट्रिक interval का realized coverage एक distributional assumption पर निर्भर करता है जिसे आपने verify नहीं किया है।

Conformal प्रेडिक्शन इंटरवल्स इसे टाल देते हैं। ये तब स्वचालित रूप से चौड़े हो जाते हैं जब model अनिश्चित होता है, और असली error distribution की परवाह किए बिना marginal coverage बनाए रखते हैं। एक trader के लिए, इसका मतलब है:

  • यदि आप positions का आकार interval width के व्युत्क्रमानुपाती (inversely proportional) रखते हैं, तो जब model अनिश्चित होता है तो आप स्वचालित रूप से exposure कम कर देते हैं
  • कवरेज गारंटी का मतलब है कि आपके risk estimates ईमानदार हैं — यदि आप कहते हैं "90% realized returns इस interval के भीतर आएंगे," तो यह कथन सांख्यिकीय रूप से मान्य है (marginally, exchangeability के तहत)।

Full Conformal और Jackknife+

Split conformal सरल है लेकिन डेटा बर्बाद करता है: calibration set को training के लिए इस्तेमाल नहीं किया जा सकता। दो विकल्प इसे संबोधित करते हैं।

Full Conformal Prediction

Full conformal prediction (Vovk et al., 2005) training और calibration दोनों के लिए सारा डेटा उपयोग करता है। Yn+1Y_{n+1} के हर candidate value yy के लिए:

  1. Dataset को (Xn+1,y)(X_{n+1}, y) से augment करें।
  2. Augmented dataset पर model को refit करें।
  3. सभी nonconformity scores की गणना करें।
  4. यदि (Xn+1,y)(X_{n+1}, y) के लिए score बहुत extreme नहीं है तो yy को prediction set में शामिल करें।

Prediction set है:

C(Xn+1)={y:{i:RiyRn+1y}n+1>α}C(X_{n+1}) = \left\{y : \frac{|\{i : R_i^y \geq R_{n+1}^y\}|}{n+1} > \alpha \right\}

जहां RiyR_i^y augmented dataset के साथ गणना किए गए nonconformity scores हैं।

Full conformal सबसे tight intervals प्रदान करता है लेकिन अधिकांश models के लिए computationally निषेधात्मक है — आपको grid पर हर candidate yy के लिए model refit करना होगा। Return prediction के लिए, इसका मतलब हो सकता है प्रति prediction हज़ारों refits।

Jackknife+ (Barber et al., 2021)

Jackknife+ एक संतुलन बनाता है। यह leave-one-out (LOO) residuals का उपयोग करता है लेकिन LOO folds में fitted model की variability को ध्यान में रखता है।

मान लीजिए μ^i\hat{\mu}_{-i} point ii को छोड़कर सारे डेटा पर trained model को दर्शाता है। LOO nonconformity score को एकल absolute residual से परिभाषित करें:

Ri=Yiμ^i(Xi)R_i = |Y_i - \hat{\mu}_{-i}(X_i)|

Jackknife+ prediction interval फिर test point पर LOO predictions से बनाया जाता है, इन residuals द्वारा widen किया जाता है:

C(Xn+1)=[qα ⁣{μ^i(Xn+1)Ri},    q1α+ ⁣{μ^i(Xn+1)+Ri}]C(X_{n+1}) = \left[\, q_{\alpha}^{-}\!\left\{\hat{\mu}_{-i}(X_{n+1}) - R_i\right\}, \;\; q_{1-\alpha}^{+}\!\left\{\hat{\mu}_{-i}(X_{n+1}) + R_i\right\} \right]

यहां q1α+{vi}q_{1-\alpha}^{+}\{v_i\} set {vi}i=1n\{v_i\}_{i=1}^n का (1α)(n+1)\lceil (1-\alpha)(n+1)\rceil-वां सबसे छोटा मान दर्शाता है, और qα{vi}q_{\alpha}^{-}\{v_i\} α(n+1)\lfloor \alpha(n+1)\rfloor-वां सबसे छोटा मान। Lower bound हर LOO prediction से residual को घटाता है; upper bound उसे जोड़ता है। यह असममिति (asymmetry) ही पूरा मुद्दा है — दोनों bounds को μ^i+Ri\hat{\mu}_{-i} + R_i पर collapse करना lower bound को prediction से ऊपर रख देगा, जो गलत है।

Jackknife+ कम से कम 12α1 - 2\alpha की कवरेज गारंटी प्रदान करता है (split conformal के 1α1-\alpha से थोड़ा कमज़ोर), लेकिन यह training और calibration दोनों के लिए सारा डेटा उपयोग करता है। व्यवहार में कवरेज आमतौर पर 1α1-\alpha के करीब होता है।

सीमित डेटा पर trained trading models के लिए (जैसे, सिर्फ कुछ सौ observations वाले regime-specific models), jackknife+ अक्सर सबसे अच्छा विकल्प होता है — यह calibration के लिए दुर्लभ डेटा का त्याग नहीं करता। इसकी क़ीमत है nn model refits।

वित्तीय Time Series की समस्या: Non-Exchangeability

वित्तीय time series में non-exchangeability: regime shift वाली एक non-stationary series, distribution के drift होने पर coverage का टूटना

मानक conformal गारंटी को exchangeability की आवश्यकता होती है: (Z1,,Zn+1)(Z_1, \ldots, Z_{n+1}) का joint distribution permutations के तहत invariant होता है। i.i.d. डेटा के लिए, यह trivially सत्य है।

वित्तीय time series exchangeable नहीं होती हैं। Returns निम्नलिखित प्रदर्शित करते हैं:

  • Volatility clustering: उच्च-volatility periods के बाद उच्च-volatility periods आते हैं (GARCH प्रभाव)।
  • Momentum और mean reversion: returns या squared returns में autocorrelation।
  • Regime changes: समय के साथ distribution बदलता है (bull बनाम bear markets)।

यदि आप naively split conformal को time series पर लागू करते हैं — random calibration split का उपयोग करते हुए — तो आप temporal structure का उल्लंघन करते हैं। एक शांत 2017 से calibration scores एक volatile 2020 की अनिश्चितता को प्रतिबिंबित नहीं करेंगे। आपकी कवरेज गारंटी टूट जाती है।

Adaptive Conformal Inference (ACI)

Adaptive conformal inference: एक prediction interval जो realized coverage को target की ओर track करने वाले feedback loop के माध्यम से चौड़ा और संकरा होता है

Gibbs और Candes (2021, NeurIPS) ने distribution shift और non-exchangeable डेटा से निपटने के लिए Adaptive Conformal Inference (ACI) पेश किया। विचार सुरुचिपूर्ण है: एक fixed coverage level का उपयोग करने के बजाय, हाल के intervals ने वास्तविक outcome को cover किया या नहीं, इसके आधार पर target miscoverage level को ऑनलाइन adapt करें, और प्रत्येक step पर score distribution से quantile को फिर से derive करें।

ACI Algorithm

ACI interval width को सीधे adjust नहीं करता। यह एक adaptive miscoverage parameter αt\alpha_t बनाए रखता है और इससे conformal quantile को फिर से गणना करता है। प्रत्येक time step tt पर:

  1. Conformal threshold की गणना वर्तमान residual set (calibration scores, plus अब तक realized कोई भी scores) के empirical (1αt)(1 - \alpha_t)-quantile के रूप में करें: q^t=Quantile^1αt({Rj})\hat{q}_t = \widehat{\text{Quantile}}_{1-\alpha_t}(\{R_j\})
  2. Features XtX_t observe करें, interval Ct(Xt)=[μ^(Xt)q^t,  μ^(Xt)+q^t]C_t(X_t) = [\hat{\mu}(X_t) - \hat{q}_t, \; \hat{\mu}(X_t) + \hat{q}_t] produce करें।
  3. वास्तविक मान YtY_t observe करें और error indicator की गणना करें: errt=1{YtCt(Xt)}\text{err}_t = \mathbf{1}\{Y_t \notin C_t(X_t)\}
  4. Level को update करें (width को नहीं):

αt+1=clip ⁣(αt+γ(αerrt),  0,  1)\alpha_{t+1} = \text{clip}\!\left(\alpha_t + \gamma\,(\alpha - \text{err}_t),\; 0,\; 1\right)

जहां γ>0\gamma > 0 एक step size है और α\alpha target miscoverage है। यदि एक interval miss हुआ (errt=1\text{err}_t = 1), तो αt\alpha_t घटता है, जो अगले quantile को ऊपर धकेलता है और interval को widen करता है; यदि इसने cover किया, तो αt\alpha_t बढ़ता है और intervals tighten होते हैं। महत्वपूर्ण रूप से, यहां γ\gamma probability units में है — यह [0,1][0,1] में एक level को नज-करता है, raw return units में threshold को नहीं — इसलिए वही γ\gamma समझदारी से व्यवहार करता है चाहे residuals 10310^{-3} के order पर हों या नहीं।

ACI के लिए कवरेज गारंटी

ACI एक long-run कवरेज गारंटी प्रदान करता है जो किसी distributional model पर निर्भर नहीं करती:

1Tt=1TerrtααT+1α1γT\left|\frac{1}{T}\sum_{t=1}^{T} \text{err}_t - \alpha\right| \leq \frac{|\alpha_{T+1} - \alpha_1|}{\gamma T}

चूंकि αt\alpha_t को [0,1][0,1] पर clip किया जाता है, numerator एक स्थिरांक के 1/γ1/\gamma गुना से बाउंडेड है, इसलिए right-hand side O(1/T)O(1/T) है और empirical miscoverage frequency α\alpha की ओर converge करती है। सटीक कथन: ACI यह गारंटी देता है कि arbitrary (adversarial सहित) sequences के लिए long-run empirical miscoverage frequency α\alpha की ओर converge करती है, बशर्ते adapted levels bounded रहें — जिसे clip सुनिश्चित करता है। यह coverage frequency की गारंटी है, interval की informativeness की नहीं: एक सच में adversarial sequence के तहत intervals uninformatively wide हो सकते हैं फिर भी coverage target को hit करते हुए।

Dynamically-Tuned ACI (DtACI)

Gibbs और Candes (2024, JMLR) ने step size γ\gamma की dynamic tuning के साथ ACI को परिष्कृत किया। γ\gamma को fix करने के बजाय, वे एक candidate set Γ={γ1,,γK}\Gamma = \{\gamma_1, \ldots, \gamma_K\} बनाए रखते हैं और उन्हें एक expert-aggregation rule के माध्यम से combine करते हैं, जो उस γ\gamma को प्राथमिकता देता है जिसका हाल का coverage target के सबसे करीब है।

यह एक व्यावहारिक समस्या को हल करता है: एक बड़ा γ\gamma regime changes के लिए तेज़ी से adapt होता है लेकिन volatile interval widths उत्पन्न करता है; एक छोटा γ\gamma stable है लेकिन adapt करने में धीमा। DtACI इनका स्वचालित रूप से trade-off करता है।

यह ट्रेडिंग के लिए क्यों मायने रखता है

एक market-making strategy पर विचार करें जो return forecasting model का उपयोग करती है। शांत बाज़ारों के दौरान, conformal intervals tight होते हैं — model confident है, और आप बड़े positions ले सकते हैं। जब volatility spike होती है (earnings season, FOMC announcements, geopolitical shocks), ACI level adapt होता है और intervals कुछ time steps के भीतर widen हो जाते हैं। आपकी position sizing उसके जवाब में सिकुड़ती है, बिना किसी explicit volatility model या regime-detection logic के।

यह uncertainty quantification को एक first-class signal के रूप में मानना है, न कि बाद में सोचा गया कुछ।

Conformal Intervals के साथ Position Sizing

एक calibrated uncertainty interval को position size से मैप करना: एक tight interval एक बड़ी position चलाता है, एक wide interval एक छोटी position

अब आइए conformal prediction को concrete position sizing से जोड़ें। मुख्य variable symmetric absolute-residual case के सापेक्ष prediction interval की half-width है। Symmetric interval [μ^(Xt)q^t,  μ^(Xt)+q^t][\hat{\mu}(X_t) - \hat{q}_t, \; \hat{\mu}(X_t) + \hat{q}_t] के साथ, full width wt=2q^tw_t = 2\hat{q}_t है। फ़ॉर्मूलों और code को consistent रखने के लिए, हम पूरे लेख में हर चीज़ को full width wtw_t के मुकाबले मापते हैं।

Inverse-Width Sizing

सबसे सरल तरीका: size को interval width के व्युत्क्रमानुपाती रखें।

position_sizet=kwt\text{position\_size}_t = \frac{k}{w_t}

जहां kk आपके risk budget के अनुसार कैलिब्रेट किया गया एक scaling constant है। जब model confident है (narrow interval), आप एक बड़ी position लेते हैं। जब अनिश्चित है (wide interval), आप एक छोटी लेते हैं।

यह volatility targeting (size1/σ\text{size} \propto 1/\sigma) के समान है, लेकिन एक महत्वपूर्ण अंतर के साथ: conformal interval width एक distribution-free uncertainty measure है, न कि एक parametric volatility estimate। यह coverage गारंटी के तहत predictive uncertainty को capture करता है, सिर्फ return variance को नहीं।

Edge-Ratio Sizing और No-Trade Filter

शुद्ध inverse-width sizing signal की खुद की strength को नज़रअंदाज़ करती है। एक स्वाभाविक refinement edge ratio से scale करता है — interval width के सापेक्ष point prediction:

et=μ^(Xt)wte_t = \frac{|\hat{\mu}(X_t)|}{w_t}

यह एक signal-to-noise ratio का conformal analog है: expected return को एक distribution-free uncertainty measure से divide किया गया। हम इसे sizing और no-trade filter दोनों के लिए उपयोग करते हैं।

Filter सिद्धांतबद्ध है। यदि interval zero को straddle करता है,

lowert<0<uppert,\text{lower}_t < 0 < \text{upper}_t,

तो (1α)(1-\alpha) prediction interval में positive और negative दोनों returns शामिल हैं — realized return में आपकी prediction के विपरीत sign हो सकता है। एक minimum edge threshold θ\theta परिभाषित करें और केवल तभी trade करें जब et>θe_t > \theta। ध्यान दें कि "ete_t इतना बड़ा है कि interval अब zero को straddle नहीं करता" का geometric content ठीक et>1/2e_t > 1/2 है (क्योंकि interval तब zero को clear करता है जब μ^>q^t=wt/2|\hat{\mu}| > \hat{q}_t = w_t/2)। θ\theta को et=μ^/wte_t = |\hat{\mu}|/w_t के वास्तविक scale पर backtesting के माध्यम से चुनें; daily-return residuals के लिए ete_t आमतौर पर 1/21/2 से काफी नीचे होता है, इसलिए एक बहुत छोटा θ\theta लगभग सभी trades को स्वीकार कर सकता है और एक बड़ा किसी को भी नहीं। इसे अपने डेटा पर calibrate करें।

"Conformal Kelly" के बारे में

Conformal intervals को Kelly fraction f=pb(1p)bf^* = \frac{pb - (1-p)}{b} पर जोड़ना आकर्षक है। लेकिन ff^* पहले से ही एक जीतने की संभावना pp और एक payoff ratio bb से derived एक complete, bounded fraction है; इसे μ^/q^\hat{\mu}/\hat{q} जैसे unbounded ratio से गुणा करने का कोई decision-theoretic justification नहीं है — यह ff^* से independently 1 से अधिक हो सकता है या sign flip कर सकता है, और यह उस edge को double-count करता है जिसे ff^* पहले से encode करता है। इसलिए हम "conformal Kelly" multiplier प्रस्तुत नहीं करते।

यदि आप interval से Kelly चलाना चाहते हैं, तो आपको वास्तव में इससे pp और bb derive करने होंगे, जिसके लिए interval के भीतर distribution के बारे में एक explicit assumption की आवश्यकता है (conformal intervals जानबूझकर इसके बारे में कुछ नहीं कहते — Limitations देखें)। उदाहरण के लिए, interval के भीतर मान ली गई एक shape के तहत आप pP(return>0)p \approx \mathbb{P}(\text{return} > 0) और interval geometry से एक payoff ratio का अनुमान लगा सकते हैं — लेकिन उस assumption को ज़ोर से flag करें, क्योंकि यह ठीक उसी parametric commitment को फिर से introduce करता है जिससे conformal prediction बचना चाहता था।

ईमानदार, कम-assumption वाला विकल्प है edge ratio et=μ^/wte_t = |\hat{\mu}|/w_t को एक fractional-Kelly shrinkage के रूप में उपयोग करना: जब expected return interval के सापेक्ष बड़ा हो तो size बढ़ाएं, जब छोटा हो तो घटाएं, और इसे एक hard cap के ऊपर लागू करें — स्पष्ट रूप से एक heuristic के रूप में, "Kelly fraction" के रूप में नहीं।

Python Implementation

यहां एक व्यावहारिक implementation है। हम split/prefit path और temporal (EnbPI) path दोनों दिखाते हैं, क्योंकि इस लेख का पूरा मुद्दा यही है कि वित्तीय डेटा non-exchangeable है।

Setup और Data Preparation

import numpy as np
import pandas as pd
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.model_selection import KFold

from mapie.regression import MapieRegressor, MapieTimeSeriesRegressor
from mapie.subsample import BlockBootstrap


def prepare_features(prices: pd.Series, lookback: int = 20) -> pd.DataFrame:
    """Create features from a price series."""
    df = pd.DataFrame()
    returns = prices.pct_change()

    for lag in range(1, lookback + 1):
        df[f"ret_lag_{lag}"] = returns.shift(lag)

    for window in [5, 10, 20]:
        df[f"ret_mean_{window}"] = returns.rolling(window).mean().shift(1)
        df[f"ret_std_{window}"] = returns.rolling(window).std().shift(1)
        df[f"ret_skew_{window}"] = returns.rolling(window).skew().shift(1)

    df["target"] = returns.shift(-1)

    return df.dropna()

MAPIE के साथ Split Conformal (Prefit)

Split/prefit conformal के लिए, cv="prefit" को method="base" (naive split-conformal estimator) की आवश्यकता होती है। method="plus" option CV+/jackknife+ estimator है और यह cv="prefit" के साथ incompatible है — इसे इसके बजाय एक cross-validation object चाहिए। हम यहां सही combination का उपयोग करते हैं, और sizing को full width पर standardize करते हैं।

def split_conformal_sizing(
    prices: pd.Series,
    alpha: float = 0.1,
    k: float = 1e-3,          # scaling constant, in width units
    max_position: float = 1.0,
    min_edge: float = 0.05,   # threshold on |pred| / width
) -> pd.DataFrame:
    """
    Position sizing using split (prefit) conformal prediction intervals.

    Sizing rule (consistent with the prose):
        edge_t = |pred_t| / width_t
        size_t = clip(k / width_t, 0, max_position)   # inverse-width
        size_t = 0 if edge_t < min_edge               # no-trade filter
    """
    df = prepare_features(prices)
    X = df.drop(columns=["target"])
    y = df["target"]

    n_train = int(len(X) * 0.6)
    n_cal = int(len(X) * 0.2)

    X_train, y_train = X.iloc[:n_train], y.iloc[:n_train]
    X_cal, y_cal = X.iloc[n_train:n_train + n_cal], y.iloc[n_train:n_train + n_cal]
    X_test, y_test = X.iloc[n_train + n_cal:], y.iloc[n_train + n_cal:]

    base_model = GradientBoostingRegressor(
        n_estimators=200, max_depth=3, learning_rate=0.05, random_state=42,
    )
    base_model.fit(X_train, y_train)

    mapie = MapieRegressor(estimator=base_model, cv="prefit", method="base")
    mapie.fit(X_cal, y_cal)

    y_pred, y_intervals = mapie.predict(X_test, alpha=alpha)
    lower = y_intervals[:, 0, 0]
    upper = y_intervals[:, 1, 0]
    width = upper - lower

    raw_size = k / np.where(width > 0, width, np.inf)
    position_size = np.clip(raw_size, 0.0, max_position)

    edge_ratio = np.abs(y_pred) / np.where(width > 0, width, np.inf)
    position_size = np.where(edge_ratio < min_edge, 0.0, position_size)

    position_size = position_size * np.sign(y_pred)

    return pd.DataFrame({
        "prediction": y_pred,
        "lower": lower,
        "upper": upper,
        "width": width,
        "edge_ratio": edge_ratio,
        "position_size": position_size,
        "actual": y_test.values,
    }, index=X_test.index)

EnbPI के साथ Time-Series Conformal

चूंकि returns non-exchangeable हैं, ऊपर दिया गया random/prefit split केवल एक baseline है। MAPIE का MapieTimeSeriesRegressor, method="enbpi" (Xu & Xie, 2021) के साथ, temporal dependence के लिए designed block bootstrap और residual updating का उपयोग करता है। यह वह tool है जो इस लेख के अपने argument से मेल खाता है।

def enbpi_sizing(
    prices: pd.Series,
    alpha: float = 0.1,
    k: float = 1e-3,
    max_position: float = 1.0,
    min_edge: float = 0.05,
) -> pd.DataFrame:
    """Position sizing with EnbPI (block-bootstrap, time-series conformal)."""
    df = prepare_features(prices)
    X = df.drop(columns=["target"])
    y = df["target"]

    n_train = int(len(X) * 0.7)
    X_train, y_train = X.iloc[:n_train], y.iloc[:n_train]
    X_test, y_test = X.iloc[n_train:], y.iloc[n_train:]

    base_model = GradientBoostingRegressor(
        n_estimators=200, max_depth=3, learning_rate=0.05, random_state=42,
    )

    cv = BlockBootstrap(n_resamplings=30, length=20, overlapping=False, random_state=42)
    mapie_ts = MapieTimeSeriesRegressor(base_model, method="enbpi", cv=cv, agg_function="mean")
    mapie_ts.fit(X_train, y_train)

    y_pred, y_intervals = mapie_ts.predict(X_test, alpha=alpha, ensemble=True)
    lower = y_intervals[:, 0, 0]
    upper = y_intervals[:, 1, 0]
    width = upper - lower

    raw_size = k / np.where(width > 0, width, np.inf)
    position_size = np.clip(raw_size, 0.0, max_position)
    edge_ratio = np.abs(y_pred) / np.where(width > 0, width, np.inf)
    position_size = np.where(edge_ratio < min_edge, 0.0, position_size)
    position_size = position_size * np.sign(y_pred)

    return pd.DataFrame({
        "prediction": y_pred, "lower": lower, "upper": upper,
        "width": width, "edge_ratio": edge_ratio,
        "position_size": position_size, "actual": y_test.values,
    }, index=X_test.index)

Adaptive Conformal Inference (Online)

Live trading के लिए हम true ACI को शुरू से implement करते हैं: miscoverage level αt\alpha_t बनाए रखें, इसे additively update करें, और हर step पर residual set से quantile को फिर से derive करें। दो finite-sample details महत्वपूर्ण हैं:

  • Order statistic का उपयोग करें, interpolated quantile का नहीं। np.quantile डिफ़ॉल्ट रूप से interpolate करता है, जो आवश्यक rank से थोड़ा नीचे गिर सकता है और undercover कर सकता है; method="higher" (equivalently "inverted_cdf") pass करें।
  • जब आवश्यक rank nn से अधिक हो जाए (छोटा nn, high target coverage), तो सही threshold ++\infty है (interval = पूरी line), largest residual पर clamp नहीं। Clamping चुपचाप 1αt\ge 1-\alpha_t गारंटी को तोड़ देता है।
class AdaptiveConformalSizer:
    """
    Online position sizing with Adaptive Conformal Inference (Gibbs & Candes,
    2021). Updates the miscoverage LEVEL alpha_t and re-derives the quantile
    from the residual set each step -- gamma is in probability units.
    """

    def __init__(self, base_model, alpha=0.1, gamma=0.02,
                 max_position=1.0, min_edge=0.05, k=1e-3):
        self.base_model = base_model
        self.alpha_target = alpha     # target miscoverage
        self.alpha_t = alpha          # adaptive miscoverage level
        self.gamma = gamma            # step size, in [0, 1] units
        self.max_position = max_position
        self.min_edge = min_edge
        self.k = k
        self.residuals = []
        self.q_hat = np.inf
        self.coverage_history = []

    @staticmethod
    def _conformal_quantile(residuals, alpha_t):
        """(1 - alpha_t) conformal quantile via the order statistic."""
        n = len(residuals)
        if n == 0:
            return np.inf
        rank = int(np.ceil((1.0 - alpha_t) * (n + 1)))
        if rank > n:                  # required order statistic does not exist
            return np.inf             # -> interval is the whole line
        level = rank / n
        return float(np.quantile(residuals, min(level, 1.0), method="higher"))

    def calibrate(self, X_cal, y_cal):
        preds = self.base_model.predict(X_cal)
        self.residuals = list(np.abs(np.asarray(y_cal) - preds))
        self.q_hat = self._conformal_quantile(self.residuals, self.alpha_t)

    def predict_and_size(self, X_t) -> dict:
        mu_hat = self.base_model.predict(np.asarray(X_t).reshape(1, -1))[0]
        lower, upper = mu_hat - self.q_hat, mu_hat + self.q_hat
        width = upper - lower                      # = 2 * q_hat

        edge_ratio = abs(mu_hat) / width if np.isfinite(width) and width > 0 else 0.0

        if edge_ratio < self.min_edge:
            size = 0.0
        else:
            size = min(self.k / width, self.max_position) if width > 0 else 0.0

        return {
            "prediction": mu_hat, "lower": lower, "upper": upper,
            "width": width, "edge_ratio": edge_ratio,
            "position_size": size * np.sign(mu_hat),
            "alpha_t": self.alpha_t, "q_hat": self.q_hat,
        }

    def update(self, X_t, y_t: float):
        """ACI update: adapt the LEVEL, then re-derive the quantile."""
        mu_hat = self.base_model.predict(np.asarray(X_t).reshape(1, -1))[0]
        residual = abs(y_t - mu_hat)

        covered = int(residual <= self.q_hat)
        err_t = 1 - covered

        self.alpha_t = float(np.clip(
            self.alpha_t + self.gamma * (self.alpha_target - err_t), 0.0, 1.0
        ))

        self.residuals.append(residual)
        self.q_hat = self._conformal_quantile(self.residuals, self.alpha_t)

        self.coverage_history.append(covered)

    @property
    def running_coverage(self) -> float:
        if not self.coverage_history:
            return float("nan")
        return float(np.mean(self.coverage_history))

सब कुछ मिलाकर: Backtest Loop

def backtest_aci_sizing(prices: pd.Series, alpha=0.1, gamma=0.02) -> pd.DataFrame:
    """Backtest position sizing with Adaptive Conformal Inference."""
    df = prepare_features(prices)
    X = df.drop(columns=["target"]).values
    y = df["target"].values
    index = df.index

    n_train = int(len(X) * 0.5)
    n_cal = int(len(X) * 0.2)

    X_train, y_train = X[:n_train], y[:n_train]
    X_cal, y_cal = X[n_train:n_train + n_cal], y[n_train:n_train + n_cal]
    X_test, y_test = X[n_train + n_cal:], y[n_train + n_cal:]
    test_index = index[n_train + n_cal:]

    model = GradientBoostingRegressor(
        n_estimators=200, max_depth=3, learning_rate=0.05, random_state=42,
    )
    model.fit(X_train, y_train)

    sizer = AdaptiveConformalSizer(base_model=model, alpha=alpha, gamma=gamma)
    sizer.calibrate(X_cal, y_cal)

    records = []
    for i in range(len(X_test)):
        result = sizer.predict_and_size(X_test[i])
        result["actual"] = y_test[i]
        result["pnl"] = result["position_size"] * y_test[i]
        records.append(result)
        sizer.update(X_test[i], y_test[i])   # online residual + level update

    results = pd.DataFrame(records, index=test_index)
    results["cumulative_pnl"] = results["pnl"].cumsum()
    results["running_coverage"] = (
        ((results["lower"] <= results["actual"]) &
         (results["actual"] <= results["upper"])).expanding().mean()
    )
    return results

परिणामों का मूल्यांकन

def evaluate(results: pd.DataFrame, alpha: float = 0.1):
    """Print evaluation metrics for conformal position sizing."""
    covered = ((results["actual"] >= results["lower"]) &
               (results["actual"] <= results["upper"]))

    print(f"Target coverage:      {1 - alpha:.1%}")
    print(f"Empirical coverage:   {covered.mean():.1%}")
    print(f"Mean interval width:  {results['width'].mean():.6f}")
    print(f"Median position size: {results['position_size'].abs().median():.4f}")
    print(f"Fraction no-trade:    {(results['position_size'] == 0).mean():.1%}")
    print(f"Total PnL (bps):      {results['pnl'].sum() * 10000:.1f}")
    sd = results['pnl'].std()
    sharpe = results['pnl'].mean() / sd * np.sqrt(252) if sd > 0 else float('nan')
    print(f"Sharpe (annualized):  {sharpe:.2f}")

व्यावहारिक विचार

Nonconformity Score का चयन

Absolute residual Yμ^(X)|Y - \hat{\mu}(X)| डिफ़ॉल्ट है, लेकिन यह मानता है कि prediction interval को point prediction के चारों ओर symmetric होना चाहिए। वित्तीय returns के लिए, asymmetric intervals अक्सर अधिक मायने रखते हैं:

  • Conformalized Quantile Regression (CQR): levels α/2\alpha/2 और 1α/21 - \alpha/2 पर quantile regressors फ़िट करें, फिर conformalize करें (Romano et al., 2019)। Intervals अपने आकार को local distribution के अनुसार adapt करते हैं — drawdowns के दौरान downside पर wider, rallies के दौरान upside पर wider। (CQR के साथ interval अब symmetric नहीं है, इसलिए wtw_t असली upper-minus-lower width है — हर जगह wtw_t को denominator के रूप में इस्तेमाल करना जारी रखें।)
  • Normalized scores: Ri=Yiμ^(Xi)/σ^(Xi)R_i = |Y_i - \hat{\mu}(X_i)| / \hat{\sigma}(X_i), जहां σ^\hat{\sigma} एक local volatility estimate है। यह conditionally adaptive intervals produce करता है — low-volatility regimes के दौरान tighter, high-volatility regimes के दौरान wider — marginal coverage बनाए रखते हुए।

Calibration Set का आकार

Split conformal की कवरेज गारंटी किसी भी calibration set size nn के लिए लागू होती है, लेकिन nn बढ़ने पर interval width घटती है। बहुत छोटे nn के लिए आवश्यक order statistic मौजूद नहीं हो सकता, ऐसे में honest threshold ++\infty है (uninformative लेकिन valid interval) — largest residual पर clamp करना चुपचाप गारंटी को void कर देता है। व्यावहारिक ट्रेडिंग के लिए:

  • n100n \geq 100 calibration points reasonably tight intervals देते हैं।
  • स्थिर quantile estimation के लिए n500n \geq 500 को प्राथमिकता दी जाती है।
  • ACI के साथ, calibration set केवल initialization के लिए है; online level updates बाकी काम संभालते हैं।

Retraining Frequency

Base model μ^\hat{\mu} stale हो सकता है। दो approaches:

  1. समय-समय पर retrain करें (जैसे, monthly) और conformal quantile को फिर से calibrate करें।
  2. ACI का उपयोग करें और adaptive level को model staleness की भरपाई करने दें — model के residuals बढ़ने पर intervals स्वचालित रूप से widen होते हैं।

Option 2 सरल और आश्चर्यजनक रूप से effective है। Conformal layer एक safety net की तरह काम करता है: भले ही model degrade हो जाए, long-run ACI coverage frequency फिर भी target की ओर converge करती है।

Transaction Costs

Conformal intervals transaction costs के साथ एक उपयोगी तरीके से interact करते हैं। जब intervals wide हों (high uncertainty), positions छोटे होते हैं, इसलिए turnover कम होता है। जब intervals narrow हों (model confident है), positions बढ़ते हैं — लेकिन model के सही होने की संभावना अधिक होती है, इसलिए turnover चुकाने लायक होता है।

आप transaction costs को सीधे no-trade filter में भी शामिल कर सकते हैं:

trade only if μ^(Xt)cost>θwt\text{trade only if } |\hat{\mu}(X_t)| - \text{cost} > \theta \cdot w_t

यह सुनिश्चित करता है कि आप तभी trade करें जब expected net edge conformal width द्वारा scaled एक threshold से अधिक हो — हर जगह जैसा वही wtw_t denominator इस्तेमाल करते हुए।

पारंपरिक तरीकों से तुलना

गुण Gaussian CI Bootstrap CI Conformal PI
Distribution assumption Normal errors i.i.d. + asymptotic कोई नहीं (exchangeability)
Finite-sample गारंटी नहीं नहीं हाँ
किसी भी model के साथ काम करता है नहीं हाँ हाँ
Heteroskedasticity के लिए adapt करता है नहीं आंशिक रूप से CQR / normalized scores के साथ
Distribution shift को handle करता है नहीं नहीं ACI / EnbPI variant
Computational cost कम मध्यम Split: कम; jackknife+: O(n)O(n) refits; full: निषेधात्मक

Bootstrap केवल अपनी गारंटी में "asymptotic" है; यह फिर भी i.i.d./exchangeable डेटा और smoothness मानता है, इसलिए इसका distribution-assumption cell "assumption-free" नहीं है। और एकल "conformal" column variants में बहुत अलग costs छिपाता है, जिसे cost row अब स्पष्ट कर देती है।

सीमाएं

Conformal prediction जादू नहीं है। ट्रेडिंग के लिए प्रमुख सीमाएं:

  1. Marginal, conditional नहीं, कवरेज। गारंटी यह है कि P(Yn+1C(Xn+1))1α\mathbb{P}(Y_{n+1} \in C(X_{n+1})) \geq 1 - \alpha marginally है — XX और YY दोनों की randomness पर averaged। एक विशिष्ट conditioning value X=xX = x के लिए, local coverage 1α1 - \alpha से ऊपर या नीचे हो सकता है। Conformalized quantile regression इसे आंशिक रूप से संबोधित करता है।

  2. Exchangeability एक वास्तविक आवश्यकता है। Split conformal के लिए, calibration data और test point exchangeable होने चाहिए। वित्तीय डेटा नहीं है। ACI और EnbPI इसे एक long-run गारंटी तक ढीला करते हैं, लेकिन short-run coverage भटक सकता है।

  3. Interval width एक probability density नहीं है। एक conformal interval आपको बताता है कि YY 1α1-\alpha probability के साथ कहां गिरेगा, लेकिन यह interval के भीतर distribution के बारे में कुछ नहीं कहता। यह एक full predictive distribution का विकल्प नहीं है — जो ठीक इसीलिए है कि इससे Kelly pp और bb derive करने के लिए एक अतिरिक्त, explicit assumption की आवश्यकता है।

  4. कचरा अंदर, बाहर ज़्यादा चौड़ा। एक ख़राब base model wide intervals produce करता है। Conformal prediction कवरेज की गारंटी देता है लेकिन उपयोगिता की नहीं। यदि आपके model में कोई predictive power नहीं है, तो intervals इतने wide होंगे कि position sizer कभी trade नहीं करेगा।

सारांश

Conformal prediction uncertainty quantification के लिए एक rigorous, distribution-free framework प्रदान करता है जो position sizing में स्वाभाविक रूप से फिट बैठता है:

  • Split conformal static, offline calibration के लिए finite-sample coverage के साथ।
  • Jackknife+ जब calibration data दुर्लभ हो और आप सभी observations का उपयोग करना चाहते हों (nn refits की क़ीमत पर)।
  • Adaptive conformal inference / EnbPI non-stationary markets के साथ online trading के लिए।
  • Position sizing inverse interval width और एक edge-ratio no-trade filter के माध्यम से — और, यदि आप Kelly तक जाते हैं, तो केवल pp और bb को ईमानदारी से derive करने के बाद, किसी unjustified multiplier के ज़रिये नहीं।

पैरामीट्रिक विकल्पों पर मुख्य लाभ: आपको कभी भी किसी parametric distributional assumption को specify या verify करने की ज़रूरत नहीं है। Intervals construction द्वारा ईमानदार हैं (marginally, exchangeability के तहत)। एक systematic trader के लिए, इसका मतलब है model risk का एक स्रोत कम — और एक ऐसे business में जहां model risk अस्तित्वगत है, यह मायने रखता है।


संदर्भ:

  • Papadopoulos, H., Proedrou, K., Vovk, V., Gammerman, A. (2002). Inductive confidence machines for regression. ECML.
  • Vovk, V., Gammerman, A., Shafer, G. (2005). Algorithmic Learning in a Random World. Springer.
  • Lei, J., G'Sell, M., Rinaldo, A., Tibshirani, R.J., Wasserman, L. (2018). Distribution-free predictive inference for regression. JASA.
  • Xu, C., Xie, Y. (2021). Conformal prediction interval for dynamic time-series (EnbPI). ICML.
  • Barber, R.F., Candes, E.J., Ramdas, A., Tibshirani, R.J. (2021). Predictive inference with the jackknife+. Annals of Statistics.
  • Gibbs, I., Candes, E.J. (2021). Adaptive conformal inference under distribution shift. NeurIPS.
  • Gibbs, I., Candes, E.J. (2024). Conformal inference for online prediction with arbitrary distribution shifts. JMLR.
  • Romano, Y., Patterson, E., Candes, E.J. (2019). Conformalized quantile regression. NeurIPS.
  • Cordier, T. et al. (2022). MAPIE: an open-source library for distribution-free uncertainty quantification. arXiv:2207.12274.
blog.disclaimer

Authors

Eugen Soloviov
Eugen Soloviov

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.

Newsletter

बाज़ार से आगे रहें

AI ट्रेडिंग इनसाइट्स, मार्केट एनालिसिस और प्लेटफ़ॉर्म अपडेट के लिए हमारे न्यूज़लेटर को सब्सक्राइब करें।

हम आपकी गोपनीयता का सम्मान करते हैं। किसी भी समय अनसब्सक्राइब करें।