Skip to content

Market volatility rebalance

Market Volatility Rebalance Example

In the fix-time-rebalance examples, we showed fixed-schedule triggers. Here we use VIX as a regime signal to switch between two different allocation portfolios dynamically.

This uses the Tree Structure concept from bt: a parent portfolio holds child portfolios as its children. Signal algos select which child receives capital each period.

How the Tree Execution Works

When a portfolio's children are other Portfolio objects (not ticker strings), the engine evaluates it as a parent node:

  1. The parent's algo stack runs first
  2. A signal algo (e.g. Signal.VIX) writes the chosen child to context.selected and its capital fraction to context.weights
  3. Action.Rebalance() reads context.selected and context.weights to allocate capital — identically to how a leaf node allocates across tickers
  4. The engine evaluates each selected child's algo stack with a fresh context

Child portfolios do not need a schedule algo — the parent controls when evaluation happens. Children just describe how to allocate when they are active.

Children ordering for Signal.VIX: the parent portfolio's children list must be ordered [low_vol_child, high_vol_child] — index 0 is activated when VIX < low, index 1 when VIX > high.

VIX Regime-Switching Example

import tiportfolio as ti

tickers = ["QQQ", "BIL", "GLD"]

data = ti.fetch_data(tickers, start="2019-01-01", end="2024-12-31")
vix_data = ti.fetch_data(["^VIX"], start="2019-01-01", end="2024-12-31")

# Child: low-volatility regime — growth-heavy allocation
low_vol_portfolio = ti.Portfolio(
    'low_vol',
    [
        ti.Select.All(),
        ti.Weigh.Ratio(weights={"QQQ": 0.8, "BIL": 0.15, "GLD": 0.05}),
        ti.Action.Rebalance(),
    ],
    tickers,
)

# Child: high-volatility regime — defensive allocation
high_vol_portfolio = ti.Portfolio(
    'high_vol',
    [
        ti.Select.All(),
        ti.Weigh.Ratio(weights={"QQQ": 0.5, "BIL": 0.4, "GLD": 0.1}),
        ti.Action.Rebalance(),
    ],
    tickers,
)

# Parent: uses VIX to route capital to the right child each month
portfolio = ti.Portfolio(
    'vix_based_rebalance',
    [
        ti.Signal.Monthly(),
        ti.Signal.VIX(high=30, low=20, data=vix_data),
        ti.Action.Rebalance(),
    ],
    [low_vol_portfolio, high_vol_portfolio],
)

result = ti.run(ti.Backtest(portfolio, data))
result.plot()
result.plot_security_weights()  # clearly shows regime transitions

VIX threshold behaviour: When VIX > high (30), high_vol_portfolio is selected. When VIX < low (20), low_vol_portfolio is selected. When VIX is between the two thresholds, the previous selection persists — no flip-flopping in the transition zone. This hysteresis prevents excessive switching during choppy markets.