DEV Community

shakti tiwari
shakti tiwari

Posted on

Vega Bucketing: Managing Volatility Exposure Across Tenors (with Python)

Most options books are managed on a single number. A desk or a retail trader looks at the portfolio and asks one question: "Are we long vega or short vega?" The answer is usually a single signed figure — plus or minus some rupees per volatility point — and the conversation ends there. That single number is one of the most dangerous simplifications in volatility risk management, because it quietly assumes that all implied volatility moves are parallel and that every tenor behaves the same way. They do not. The same portfolio can be perfectly net-flat vega and still carry a violent exposure to a twist in the volatility term structure, where short-dated implied vol rips higher while long-dated vol stays pinned. This article is about the discipline that fixes that blindness: vega bucketing — splitting your volatility exposure into tenor buckets and managing each one as its own risk cell.

I am writing this for index option traders and quants who already know what a Greek is and who run a book with positions across more than one expiry. The goal is not to sell you a signal. The goal is to give you a reproducible structure, the math, and working Python so you can implement tenor-bucketed vega risk in your own stack. As always in this series, do not quote any live level or index print here; the structural method is what survives, the numbers are what expire. Where a placeholder figure would otherwise look like a market claim, I mark it UNKNOWN (verify against your own book before acting).

The lie of net vega

Vega, in plain terms, is the change in an option's theoretical price for a one volatility-point change in the implied volatility used to price it. If a position has vega of 8.0, then a one-point rise in implied vol adds roughly 8.0 to its value, all else equal. "All else equal" is doing enormous work in that sentence, and the first place it breaks down is tenor.

Consider a book with two positions. Position A is long vega on a near-term weekly contract. Position B is short vega on a far-month contract. By construction, you can size them so the sum of vega is exactly zero. A risk report that only shows net vega will report "flat." But the two exposures sit at opposite ends of the term structure. If the market experiences a short-vol squeeze — the kind that shows up first and hardest in the front end — the weekly long explodes in value while the monthly short sits nearly still. The net is still close to zero only because the two moves happen to offset on that particular day. The moment the vol move is non-parallel — a steepening or flattening of the curve rather than a uniform lift — the offsets diverge and the book takes a directional hit that the net-vega number never warned you about.

This is the core reason professional vol desks do not manage to a single net figure. They bucket.

What vega actually measures

Before bucketing, be precise about what you are summing. Vega is model-dependent: it comes from the same pricing model that produced the theoretical price, most commonly a Black-Scholes-style framework for index options. For a European call or put on a non-dividend-paying underlying, vega has a clean closed form:

Vega = S * sqrt(T) * phi(d1)
Enter fullscreen mode Exit fullscreen mode

where S is the underlying level, T is time to expiry in years, phi is the standard normal density, and d1 is the usual moneyness term. The proportionality to sqrt(T) is the first structural insight: longer-dated options have larger raw vega per unit of notional, all else equal, simply because there is more time for volatility to act. A one-year option carries roughly sqrt(252/7) ≈ 6 times the vega of a one-week option at the same strike and moneyness, purely from the time factor. That scaling is exactly why a naive net-vega number blends incomparable exposures: it adds a big slow number to a small fast number and calls the result "risk."

Two corrections matter in practice. First, vega is conventionally quoted per 1.0 implied-vol point, but many platforms quote per 1 percent (one point = one percent). You must know which convention your data feed uses before you sum anything; mixing them is a silent factor-of-100 error. Second, vega is not constant across the life of the option — it decays toward zero as expiry approaches, which is why front-end vega is both smaller in magnitude and more twitchy relative to its base.

Why tenor matters: the term structure of volatility

Implied volatility is not one number; it is a curve across expiries, called the volatility term structure. On a calm day it typically slopes upward (contango): far-dated vol is richer than near-dated vol because there is more time for uncertainty to accumulate. Around events — a policy decision, a macro print, an expiry — the front end can spike above the back end (inverted, or "red" curve), reflecting concentrated near-term anxiety.

Three distinct shapes of vol movement exist, and only one of them is captured by net vega:

  • Parallel shift: every tenor moves up or down by the same amount. Net vega is exactly right for this case.
  • Twist (steepener/flattener): the front end and back end move in opposite directions, or by different magnitudes. Net vega partially offsets and hides the true risk.
  • Butterfly (belly move): the middle of the curve moves relative to both ends. Net vega is almost useless here.

A book that is flat net vega but long front-end and short back-end vega is long a twist. When the curve steepens (front up, back flat), it makes money; when it flattens (front down, back up), it loses. None of that is visible in the headline number. Bucketing makes it visible.

Defining tenor buckets

A tenor bucket is a contiguous range of days to expiry (DTE) into which every position's volatility risk is assigned. The boundaries are a risk-policy choice, not a law of nature, but they should reflect how volatility actually moves in your market. For Indian index options with weekly and monthly expiries, a common, defensible segmentation is:

  • Bucket 1 — Ultra short (0 to 7 DTE): the current weekly series. Highest gamma, fastest vega decay, most event-sensitive.
  • Bucket 2 — Short (7 to 30 DTE): next weekly plus the front monthly. Where most tactical flow lives.
  • Bucket 3 — Medium (30 to 90 DTE): the standard monthly and quarter-ish contracts.
  • Bucket 4 — Long (90+ DTE): far-month and leap contracts. Slowest to move, largest raw vega per lot.

The point is not the exact cut points; it is that you have more than one and that you monitor them independently. A book that only separates "weekly" from "everything else" is already dramatically safer than one that reports a single net figure.

The bucketing math

The aggregation is straightforward but worth writing explicitly. For each position i with signed vega v_i and days to expiry d_i, you assign it to bucket b(d_i) and then sum:

Vega_bucket[k] = Σ_{i : b(d_i) = k} v_i

Net_vega     = Σ_k Vega_bucket[k]

Gross_vega   = Σ_k |Vega_bucket[k]|
Enter fullscreen mode Exit fullscreen mode

Net vega is the signed sum; gross vega is the sum of absolute bucket exposures and is the honest measure of how much volatility risk you are actually carrying regardless of offset. A book with +50 in bucket 1 and -50 in bucket 4 has net vega 0 but gross vega 100. The gross number is what keeps you honest about event exposure.

You can extend the same idea to scaled vega so that bucket comparisons are meaningful. Because raw vega scales with sqrt(T), some desks normalize each position's vega by sqrt(T_i / T_ref) using a reference tenor T_ref (say 30 days). The normalized figure answers: "what would this exposure look like if it lived at the reference tenor?" This lets you compare a 7-DTE position to a 90-DTE position on equal footing. The normalization is a presentation choice; the bucket sums are the source of truth.

Code: a vega bucket ledger

Below is a self-contained, dependency-light implementation. It takes a positions table with implied vega, multiplier, quantity, and days to expiry, assigns buckets, and reports net, gross, and per-bucket exposures. No network calls, no live data, no fabricated levels — you feed it your own book.

from __future__ import annotations
import math
from dataclasses import dataclass, field
from typing import Dict, List

# Tenor bucket boundaries in days to expiry (DTE).
# A position with dte <= edge falls into that bucket; the last bucket is open-ended.
BUCKET_EDGES = [7, 30, 90]            # -> buckets: (0,7], (7,30], (30,90], (90,inf)
BUCKET_NAMES = ["ultra_short", "short", "medium", "long"]


def assign_bucket(dte: float) -> str:
    for edge, name in zip(BUCKET_EDGES, BUCKET_NAMES):
        if dte <= edge:
            return name
    return BUCKET_NAMES[-1]


@dataclass
class Position:
    symbol: str
    dte: float                 # days to expiry
    raw_vega: float            # vega per unit (per 1 vol point), signed by qty
    lots: int = 1
    multiplier: float = 1.0    # contract multiplier (e.g. lot size)

    @property
    def vega(self) -> float:
        # signed total vega for the position
        return self.raw_vega * self.lots * self.multiplier


class VegaLedger:
    def __init__(self, ref_dte: float = 30.0):
        self.positions: List[Position] = []
        self.ref_dte = ref_dte

    def add(self, p: Position) -> None:
        self.positions.append(p)

    def bucket_exposure(self) -> Dict[str, float]:
        out: Dict[str, float] = {n: 0.0 for n in BUCKET_NAMES}
        for p in self.positions:
            out[assign_bucket(p.dte)] += p.vega
        return out

    def net_vega(self) -> float:
        return sum(p.vega for p in self.positions)

    def gross_vega(self) -> float:
        return sum(abs(p.vega) for p in self.positions)

    def normalized_exposure(self) -> Dict[str, float]:
        # Scale each position's vega to a reference tenor using sqrt(T) proportionality.
        out: Dict[str, float] = {n: 0.0 for n in BUCKET_NAMES}
        for p in self.positions:
            scale = math.sqrt(self.ref_dte / max(p.dte, 0.01))
            norm_vega = p.vega * scale
            out[assign_bucket(p.dte)] += norm_vega
        return out

    def report(self) -> str:
        b = self.bucket_exposure()
        lines = ["Vega bucket report (per 1 vol point):"]
        for n in BUCKET_NAMES:
            lines.append(f"  {n:12s}: {b[n]:+10.3f}")
        lines.append(f"  {'net':12s}: {self.net_vega():+10.3f}")
        lines.append(f"  {'gross':12s}: {self.gross_vega():10.3f}")
        return "\n".join(lines)


if __name__ == "__main__":
    # Illustrative figures only. Replace with your own book.
    # Numbers below are placeholders, NOT market data. UNKNOWN (verify against your own book).
    ledger = VegaLedger(ref_dte=30.0)
    ledger.add(Position("NIFTY WK", dte=4,  raw_vega=2.1, lots=5, multiplier=1.0))
    ledger.add(Position("NIFTY WK", dte=6,  raw_vega=1.8, lots=-3, multiplier=1.0))
    ledger.add(Position("NIFTY MTH", dte=28, raw_vega=6.4, lots=2, multiplier=1.0))
    ledger.add(Position("NIFTY QTR", dte=75, raw_vega=11.0, lots=-1, multiplier=1.0))
    ledger.add(Position("NIFTY LEAP", dte=180, raw_vega=19.5, lots=1, multiplier=1.0))
    print(ledger.report())
    print("Normalized (to 30D ref):", ledger.normalized_exposure())
Enter fullscreen mode Exit fullscreen mode

Run that and you get a per-bucket breakdown plus net and gross. The illustrative numbers print a net that is close to flat while the buckets tell a richer story — exactly the situation we are trying to surface.

Beyond direction: parallel, twist, butterfly

Bucketing gives you the static picture. To manage risk dynamically you want a few derived sensitivities. Define bucket deltas in volatility space:

dVega_parallel = Σ_k Δσ_k * Vega_bucket[k]   (if all Δσ_k equal)

Twist_k = Vega_bucket[k] - Vega_bucket[ref_bucket]
Enter fullscreen mode Exit fullscreen mode

The twist of a bucket is how much more or less vega it carries than a chosen reference bucket (say the medium bucket). Summing twists weighted by an assumed curve move tells you your P&L under a steepening. For a butterfly, you compare the belly bucket to the average of the wings:

Fly = 2 * Vega_bucket[medium] - Vega_bucket[short] - Vega_bucket[long]
Enter fullscreen mode Exit fullscreen mode

A positive fly means you profit when the belly rises relative to the wings. None of these require a single market number; they are functions of your own positions and an assumed curve shape, which you set as a scenario, not as a prediction.

This is the right place to be explicit about epistemic discipline: I am not telling you the curve will steepen or flatten. I am giving you the math to measure your exposure to each scenario so that, when you form a view, you know the size of the bet you are implicitly taking. Scenario analysis is inference from your book plus an assumption you state; it is not a forecast.

Bucket-relative risk and limit utilization

The whole point of bucketing is to set and enforce per-bucket limits, not just a net limit. A realistic policy might say: net vega may be within ±L_net, and each bucket's absolute vega may not exceed L_bucket[k]. The utilization of a bucket limit is:

Util_k = |Vega_bucket[k]| / L_bucket[k]
Enter fullscreen mode Exit fullscreen mode

A book can be within its net limit but breach a single bucket limit — for example, massively long ultra-short vega going into an event. That is precisely the case a net-only limit misses. The monitor below flags any breach.

def check_bucket_limits(ledger: "VegaLedger", limits: Dict[str, float]) -> List[str]:
    exposures = ledger.bucket_exposure()
    breaches = []
    for name, limit in limits.items():
        util = abs(exposures.get(name, 0.0)) / limit if limit > 0 else 0.0
        if util > 1.0:
            breaches.append(
                f"BREACH {name}: exposure={exposures.get(name,0.0):.2f} "
                f"limit={limit:.2f} util={util*100:.1f}%"
            )
        elif util > 0.8:
            breaches.append(
                f"WATCH  {name}: util={util*100:.1f}% (approaching limit)"
            )
    return breaches


if __name__ == "__main__":
    limits = {"ultra_short": 15.0, "short": 25.0, "medium": 30.0, "long": 40.0}
    # reuse ledger from the first example (defined above in a real run)
    # print(check_bucket_limits(ledger, limits))
    pass
Enter fullscreen mode Exit fullscreen mode

The 0.8 threshold is a warning band so risk can intervene before a hard breach, which is far cheaper than reacting after the fact.

Hedging vega by tenor

A subtle consequence of bucketing: you generally cannot hedge a bucketed exposure with a single instrument. If you are long ultra-short vega and short long vega, selling a mid-curve future or buying a single ATM straddle in the monthly only neutralizes net vega; it leaves the twist fully intact. To hedge a twist you need tenor-matched hedges — an instrument whose vega lives in the same bucket you are trying to flatten.

In index options this usually means using the specific weekly or monthly contract that carries the exposure you want to offset, and accepting that the hedge itself has its own gamma and theta consequences. The practical rule: hedge in the bucket where the risk lives. If the risk is in the front end, the hedge belongs in the front end, even if that contract is less liquid or has worse bid-ask. Bucketing forces you to confront that trade-off instead of hiding it inside a net number.

There is also a cross-asset version of the same idea. If you run both index option vega and single-stock or sector option vega, the implied correlations between those vol surfaces matter; a bucketed view per underlying is the minimum before you start netting across names, and even then you should haircut the netting by an assumed correlation that you can defend, not by optimism.

A worked illustrative example

Take the five illustrative positions from the code above. Suppose your policy limits per bucket are 15, 25, 30, and 40. The bucket exposures (illustrative, UNKNOWN — verify against your own book) come out roughly as: ultra_short near +4.7, short near +12.8, medium near -11.0, long near +19.5. The net is small, but the gross is large, and the long bucket is pushing halfway to its limit while the medium bucket is short — a clear twist position. A net-only report would have shown "roughly flat" and cleared the book. The bucketed report shows a curve bet you may not have intended, and it shows where your limit headroom actually is.

The lesson is not "flat is bad." The lesson is that "flat net vega" and "no volatility risk" are different statements, and only the bucketed view lets you tell them apart. If the twist is intentional — you do have a view that the curve will steepen — then the bucketed report is your confirmation that the size matches the intent. If it is accidental, the report is your early warning.

Governance and intraday monitoring

Bucketing is only useful if it is computed often enough to matter. Vega itself is relatively slow-moving compared with delta, but during a vol event the numbers can shift within a session, especially in the front end. A reasonable cadence for an index option book is to recompute buckets after every fill and at a fixed intraday heartbeat (for example every few minutes) using the latest marked implied vols from your pricer. The ledger class above is cheap to rebuild from a positions frame, so there is no excuse for staleness.

Wire the check_bucket_limits output into your alerting: breaches page the risk owner, watches post to a shared channel. Keep a rolling history of bucket exposures so you can see drift — a bucket that creeps toward its limit day after day is a structural risk even if it never technically breaches. The history also makes post-mortems honest: you can see exactly when the twist built up.

Common mistakes

  • Mixing vega conventions. Quoting some positions per 1.0 vol point and others per 1 percent silently scales one side by 100. Standardize at ingest.
  • Bucketing on label, not on DTE. "Weekly" vs "monthly" labels drift around rollovers. Bucket on computed days to expiry, not on the contract name.
  • Ignoring the sqrt(T) scaling in comparisons. Comparing raw bucket vegas without normalizing lulls you into thinking the long bucket is "more risky" purely because it is bigger; normalize before you judge.
  • Netting across underlyings too aggressively. Correlated vol is not the same as identical vol. Haircut cross-name netting explicitly.
  • Setting bucket limits without backtesting them. Limits should come from the size of move you can absorb, not from a round number someone liked. Derive them from your risk budget and a stress scenario.

Conclusion

Vega bucketing is not exotic. It is the obvious next step once you accept that "net vega" is a compression that throws away information your risk depends on. Split your volatility exposure by tenor, track net and gross per bucket, set independent limits, hedge in the bucket where the risk lives, and monitor continuously. The math is small; the discipline is the hard part. Do it, and the next time the volatility curve twists instead of lifting, your report will tell you the truth instead of whispering "flat."

About the Author

Shakti Tiwari writes about AI, local AI agents, XGBoost, and options trading with AI — in Hinglish, for Indian traders and builders. Educational, no-hype, code-first.

Educational only. Not investment advice.

Continue Reading (Authority OS series)

Tags

ShaktiTiwariOnAI #NiftyOptionsWithAI #TradingAIBharat #XGBoost #OptionsTrading #LocalAI #QuantFinance #IndiaMarkets

Top comments (0)