Skip to content

Volatility-Adjusted Returns Strategy

Rank stocks by risk-adjusted returns rather than raw returns.

Strategy Overview

High returns aren't meaningful without considering risk. Adjust returns for volatility to find truly superior stocks.

The Signal

Text Only
signal sharpe_momentum:
  ret_60 = ret(prices, 60)
  vol_60 = rolling_std(ret(prices, 1), 60) * sqrt(252)

  // Risk-adjusted return
  sharpe = ret_60 / vol_60

  emit zscore(sharpe)

Complete Strategy

Text Only
data:
  source = "prices_with_sectors.parquet"
  format = parquet

signal vol_adjusted:
  // 60-day returns
  ret_60 = ret(prices, 60)

  // 60-day realized volatility
  vol_60 = rolling_std(ret(prices, 1), 60) * sqrt(252)

  // Sharpe-like ratio
  risk_adj_ret = ret_60 / vol_60

  emit neutralize(zscore(risk_adj_ret), by=sectors)

portfolio vol_adjusted:
  weights = rank(vol_adjusted).long_short(
    top = 0.2,
    bottom = 0.2,
    cap = 0.03
  )

  constraints:
    gross_exposure = 2.0
    net_exposure = 0.0

  costs = tc.bps(10)

  backtest rebal=21 from 2010-01-01 to 2024-12-31

Variations

Information Ratio Style

Text Only
signal info_ratio:
  // Excess return vs market
  stock_ret = ret(prices, 60)
  market_ret = ret(market, 60)
  excess_ret = stock_ret - market_ret

  // Tracking error
  residual = ret(prices, 1) - ret(market, 1)
  te = rolling_std(residual, 60) * sqrt(252)

  emit zscore(excess_ret / te)

Sortino-Based

Text Only
signal sortino_momentum:
  ret_60 = ret(prices, 60)

  // Downside deviation only
  daily_ret = ret(prices, 1)
  negative_ret = where(daily_ret < 0, daily_ret, 0)
  downside_vol = rolling_std(negative_ret, 60) * sqrt(252)

  sortino = ret_60 / downside_vol

  emit zscore(sortino)

Expected Results

Text Only
Vol-Adjusted vs Raw Momentum
============================

                    Raw Mom   Vol-Adjusted
Annual Return:      6.8%      7.2%
Annual Volatility:  10.5%     9.8%
Sharpe Ratio:       0.65      0.73

See Also