Multi-Task Learning for Simultaneous Price, Volume, and Volatility Prediction
Multi-task learning (MTL) को आम तौर पर एक दावे के साथ बेचा जाता है: correlated targets के बीच encoder साझा करें और primary task बेहतर हो जाता है। Trading में correlated targets स्पष्ट हैं — returns, volume और realized volatility सभी उसी order flow से निकलते हैं — और इस दावे की लगभग कभी जांच नहीं होती। दिलचस्प सवाल यह नहीं है कि tasks संबंधित हैं या नहीं। सवाल यह है कि shared gradients सहमत हैं या नहीं, और उन folds पर क्या होता है जहां वे सहमत नहीं होते।
यह लेख दो चीजों को केंद्र में रखता है जिन्हें अधिकांश MTL लेख footnote की तरह रखते हैं:
- Loss balancing ही experiment है, detail नहीं। Fixed weights, Kendall uncertainty weighting और GradNorm तीन अलग models हैं। तीनों को उन्हीं folds पर चलाएं और प्रत्येक के primary-task metric के साथ learned weights report करें।
- Metric देखने से पहले negative transfer मापा जा सकता है। Shared encoder पर task gradients के बीच cosine similarity training के दौरान बताती है कि auxiliary tasks representation को उस दिशा में खींच रहे हैं या नहीं जहां primary task जाना चाहता है। Cosines का sign करें, फिर जांचें कि उस fold पर sign ने outcome का अनुमान लगाया था या नहीं।
Pipeline की बाकी हर चीज — volatility process, training loop, leakage controls, validation protocol — इस blog में कहीं और cover है और यहां फिर से derive करने के बजाय link की गई है।
Setup

Input features (OHLCV, technical indicators, order flow) दिए हों, तो तीन targets हैं:
- Task 1 (primary): next-period return
- Task 2 (auxiliary): next-period log volume
- Task 3 (auxiliary): next-period realized volatility
Multi-task model तीनों को एक साथ produce करता है, , और multi-task risk per-task risks का weighted sum है:
पूरा लेख और इस बात पर है कि per-task gradients एक-दूसरे के साथ क्या करते हैं।
Joint training मदद क्यों कर सकती है, एक paragraph में। Auxiliary tasks shared representation को एक से अधिक market phenomena समझाने के लिए बाध्य करते हैं, जो एक साथ capacity control और inductive bias है; और volume तथा volatility सीधे observe किए जाते हैं, जबकि "expected return" नहीं, इसलिए auxiliary heads primary head से अधिक साफ gradient signal देते हैं। एक model से अनेक outputs निकालने का तर्क — interpretability machinery के साथ — multi-horizon forecasting के लिए temporal fusion transformers में विस्तार से दिया गया है, जो multi-horizon quantiles के लिए यही shared-encoder-many-heads तर्क इस्तेमाल करता है।
Architecture, संक्षेप में
Hard parameter sharing: shared encoder , task-specific heads को feed करता है, इसलिए । यही version यहां मापा गया है, क्योंकि यही वह version है जहां पर gradient conflict अच्छी तरह परिभाषित है।
Soft parameter sharing हर task को coupling penalty के साथ अपना encoder देता है — अधिक parameters, अधिक flexibility, और conflict मापने के लिए कोई एक shared parameter vector नहीं। Cross-stitch networks बीच में बैठते हैं, हर level पर learned matrix के माध्यम से per-task features मिलाते हैं। यदि hard sharing conflict दिखाए तो दोनों को आजमाना उचित है, और दोनों नीचे के measurement के scope से बाहर हैं।
महत्वपूर्ण Experiment: तीन Loss-Balancing Schemes

Naive loss scale-sensitive है। यदि return loss करीब और volume loss करीब हो, तो volume gradient का मालिक बन जाता है और return head भूखा रह जाता है। तीन responses हैं:
Fixed weights. हर target standardize करने के बाद रखें। ईमानदार baseline — यदि यह जीतता है, तो adaptive schemes केवल ceremony हैं।
Uncertainty weighting (Kendall et al., 2018). हर task के लिए homoscedastic noise scale सीखें:
High-uncertainty tasks को अपने-आप कम weight मिलता है; term trivial solution को रोकता है। ध्यान दें कि यह training-time loss-weighting device है, predictive interval नहीं — जिस uncertainty से वास्तव में position size की जा सकती है, उसके लिए conformal prediction देखें।
GradNorm (Chen et al., 2018). Loss scales के बजाय gradient magnitudes संतुलित करें। प्रत्येक step पर और mean निकालें, relative training rate निकालें और update करें। तब सभी tasks loss scale की परवाह किए बिना comparable rates पर train होते हैं।
MTL-specific code heads, list-returning forward और loss aggregation है। Linear/BatchNorm/ReLU/Dropout stack, Adam/cosine/clip boilerplate और epoch loop DeepLOB में दिखाए गए standard pattern हैं और यहां छोड़े गए हैं।
import torch
import torch.nn as nn
class MultiTaskTradingModel(nn.Module):
"""Hard parameter sharing: one encoder, K heads."""
def __init__(self, encoder: nn.Module, repr_dim: int, n_tasks: int = 3):
super().__init__()
self.shared_encoder = encoder # any MLP/CNN/GRU trunk
self.task_heads = nn.ModuleList(
nn.Linear(repr_dim, 1) for _ in range(n_tasks)
)
def forward(self, x):
h = self.shared_encoder(x)
return [head(h).squeeze(-1) for head in self.task_heads]
def shared_repr(self, x):
return self.shared_encoder(x)
class UncertaintyWeightedLoss(nn.Module):
"""Kendall et al. (2018) homoscedastic weighting."""
def __init__(self, n_tasks: int = 3):
super().__init__()
self.log_vars = nn.Parameter(torch.zeros(n_tasks)) # log(sigma^2)
def forward(self, losses: list) -> torch.Tensor:
return sum(
torch.exp(-self.log_vars[i]) * loss + self.log_vars[i]
for i, loss in enumerate(losses)
)
def get_weights(self) -> list:
with torch.no_grad():
return [torch.exp(-lv).item() for lv in self.log_vars]
UncertaintyWeightedLoss में parameters हैं, इसलिए इसे model के साथ optimizer में जाना चाहिए: optim.Adam(list(model.parameters()) + list(uw.parameters()), ...)। इसे भूलना uncertainty weighting "चलाने" और चुपचाप fixed weights चलाने का सबसे सामान्य तरीका है।
क्या report करना है
हर scheme के लिए, हर fold पर: अंतिम learned task weights, primary-task metric, और — क्योंकि weighting scheme model choice है — चुनने से पहले कितनी schemes compare की गईं।
| Scheme | Primary-task metric vs single-task | |||
|---|---|---|---|---|
| Fixed () | 1.00 | 1.00 | 1.00 | — |
| Uncertainty weighting | — | — | — | — |
| GradNorm | — | — | — | — |
तीन schemes गुणा कई folds पहले से एक छोटा model search है। यहां report किया गया कोई भी improvement deflated Sharpe और multiple testing में वर्णित multiple-testing correction से बचना चाहिए, तभी उसका कोई अर्थ होगा।
Negative Transfer: Gradients का Sign करें

यह हिस्सा बचाए रखने लायक है। Negative transfer तब होता है जब auxiliary tasks primary task को बदतर बनाते हैं, और इसका direct diagnostic है: shared parameter space में task gradients के बीच का angle।
इसे केवल shared encoder पर मापें — heads construction से task-specific हैं और हमेशा trivially "agree" करते हैं।
import torch.nn.functional as F
def shared_grad(model, x, y, task_idx, criterion=nn.MSELoss()):
"""Gradient of task `task_idx` w.r.t. the shared encoder, flattened."""
model.zero_grad(set_to_none=True)
loss = criterion(model(x)[task_idx], y)
loss.backward()
return torch.cat([
p.grad.detach().flatten()
for p in model.shared_encoder.parameters()
if p.grad is not None
])
def task_conflict(model, x, y_by_task, task_names):
"""Pairwise cosine similarity between per-task shared-encoder gradients."""
grads = {
name: shared_grad(model, x, y_by_task[name], i)
for i, name in enumerate(task_names)
}
return {
(a, b): F.cosine_similarity(
grads[a].unsqueeze(0), grads[b].unsqueeze(0)
).item()
for i, a in enumerate(task_names)
for b in task_names[i + 1:]
}
इसे training के दौरान fixed cadence पर held-out batch पर call करें, केवल अंत में एक बार नहीं। एक pair शुरुआत में aligned और encoder के specialize होने पर divergent हो सकता है; training-end का एक number यह छिपा देता है।
ढूंढने योग्य finding — और किसी भी दिशा में publish करने योग्य:
| Pair | cos sim, early training | cos sim, late training | MTL helped primary task? |
|---|---|---|---|
| return ↔ volume | — | — | — |
| return ↔ volatility | — | — | — |
| volume ↔ volatility | — | — | — |
यदि volume और volatility gradients आपस में agree करें, जबकि दोनों return gradient से conflict करें, तो सही निष्कर्ष है कि दोनों auxiliary tasks एक coherent block बनाते हैं जिससे return task संबंधित नहीं है — और fix task grouping है, अधिक capacity नहीं। जब conflict वास्तविक हो, standard remedies PCGrad (Yu et al., 2020) है, जो हर conflicting gradient को दूसरे के normal plane पर project करता है; CAGrad (Liu et al., 2021), जो ऐसा descent direction खोजता है जो किसी task को नुकसान न पहुंचाए; या auxiliary task को पूरी तरह हटाना।
ध्यान दें कि जानबूझकर क्या अनुपस्थित है: target value से रंगा हुआ shared representation का t-SNE plot। यह decorative है — ऊपर के cosine numbers वह सब कहते हैं जिसकी ओर embedding इशारा करता, और numbers में कहते हैं।
Validation Protocol

लापरवाह protocol में ऊपर का measurement बेकार है, और MTL सामान्य traps को बदतर बनाता है क्योंकि leak करने के लिए एक के बजाय तीन targets हैं।
Real data, simulator नहीं। Targets actual OHLCV/trade data से आने चाहिए। Hardcoded GARCH toy ऐसी volatility बनाता है जो construction से ही returns के साथ correlated है, और यही test के अंतर्गत बात है — experiment अपने generator को मापेगा। यदि fitted volatility process चाहिए, तो crypto के लिए GARCH volatility forecasting real BTC/ETH पर maximum likelihood से GARCH(1,1) fit करता है और standardized residuals validate करता है, जबकि asymmetric GARCH और leverage effect बताता है कि Gaussian symmetric-response simulator पहले ही crypto volatility को गलत क्यों बताता है। Synthetic data तभी उचित है जब वह controlled ground truth दे — एक ज्ञात, author-set task correlation जिसे recover करना हो — जो यहां के experiment से अलग है।
Scalers केवल train पर fit करें। हर training fold के भीतर feature scaler और तीनों target scalers fit करें और validation पर लागू करें; split से पहले global fit_transform training में test-set moments leak करता है। यह exact failure look-ahead bias taxonomy में दर्ज है।
Purged, embargoed walk-forward folds। एक 80/20 chronological split MTL improvement को fold effect से अलग नहीं कर सकता — यही walk-forward optimization का पूरा तर्क है, जो तीन splits से तीन conclusions दिखाता है। spread modeling with machine learning का expanding-window purged_walk_forward generator फिर से इस्तेमाल करें: यह हर boundary के दोनों ओर horizon rows का gap छोड़ता है, जो यहां महत्वपूर्ण है क्योंकि overlapping realized-volatility windows boundary के पार leak करते हैं, भले return target leak न करे।
एक classical baseline। यदि MTL net तीन single-task nets को हराता है तो भी कुछ सिद्ध नहीं हुआ, यदि per-target gradient-boosting या ridge model चारों को हरा देता है। उन्हीं folds और features पर LightGBM या ridge से प्रति target एक model fit करें और उसी table में report करें।
| Model | Primary-task metric | Notes |
|---|---|---|
| Ridge, per target | — | Classical baseline |
| LightGBM, per target | — | Classical baseline |
| Single-task MLP, per target | — | Three separate nets |
| MTL, best loss scheme | — | One net, three heads |
यहां MTL को Worth It क्या बनाएगा

वे conditions जिनमें MTL को जीतना चाहिए, ऊपर के folds के विरुद्ध check की जाने वाली hypotheses के रूप में, checklist के रूप में नहीं:
- Auxiliary labels primary label से cleaner हैं। Volume सीधे observe किया जाता है; "expected return" नहीं। यदि return head अधिकतर noise fit कर रहा है, तो auxiliary heads से आने वाला gradient signal objective का एकमात्र well-posed भाग है।
- Encoder capacity की तुलना में training data सीमित है, इसलिए auxiliary constraint केवल parameters के लिए compete करने के बजाय वास्तविक regularization करता है।
- Inference latency महत्वपूर्ण है और एक forward pass तीन से बेहतर है।
और इसके विरुद्ध मामला भी उतना ही testable है: यदि measured cos_sim(return, ·) values लगातार negative हैं, तो shared encoder primary task से दूर खींचा जा रहा है और auxiliary heads regularizer नहीं, tax हैं।
Conclusion

Returns, volume और volatility एक ही microstructure से आते हैं, इसलिए shared representation एक reasonable prior है — लेकिन prior result नहीं है। यह setup वास्तव में दो चीजें स्थापित कर सकता है: data कौन-सी loss-balancing scheme पसंद करता है (learned weights report करके, केवल winner का नाम नहीं) और shared encoder पर task gradients agree करते हैं या नहीं, training के दौरान मापा हुआ, targets correlated हैं इस तथ्य से assumed नहीं।
यदि purged walk-forward folds दिखाएं कि MTL net per-target gradient-boosting model को हरा नहीं पा रहा, तो यही finding है और इसे उसी रूप में publish किया जाएगा — template है ईमानदार negative। Negative transfer पर negative result भी negative transfer के बारे में एक result है।
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.