Portfolio & Risk·Position Sizing Models·Intermediate

Volatility Based Sizing

ATR-based position sizing. A complete hands-on Jupyter notebook with production-ready Python implementation, best practices, and step-by-step walkthrough for risk workflows in quantitative cryptocurrency trading.

position-sizingrisk-management

Volatility-Based Sizing

Volatility-based sizing is a critical component of risk management in trading and investment. It involves adjusting the size of a trade or investment based on the volatility of the asset being traded. The core idea is that assets with higher volatility carry more risk per unit of capital, and therefore, smaller positions should be taken to maintain a consistent level of risk across different trades.

Purpose and Importance

The primary purpose of volatility-based sizing is to equalize the risk exposure across various trading opportunities. Without it, a fixed position size might expose a trader to significantly different levels of risk when trading a highly volatile asset versus a stable one. By adjusting position size inversely to volatility, traders aim to ensure that the potential loss from a single trade, expressed in monetary terms, remains relatively constant, regardless of the asset's price fluctuations.

This approach helps in:

  • Consistent Risk Management: Ensures that each trade represents a similar 'risk unit' for the portfolio.
  • Capital Preservation: Prevents outsized losses from highly volatile moves by limiting exposure.
  • Improved Performance: By managing risk consistently, it can lead to more stable equity curves and better long-term performance.
  • Discipline: Encourages a systematic approach to trade sizing rather than arbitrary decisions.
[2]
# Import necessary libraries
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

Understanding Volatility

Volatility is a statistical measure of the dispersion of returns for a given security or market index. In most cases, the higher the volatility, the riskier the security. It is often measured as the standard deviation of returns over a specific period.

Historical Volatility

Historical volatility is calculated from past market data. It tells us how much an asset's price has fluctuated in the past. A common method is to use the standard deviation of logarithmic returns.

Formula for Standard Deviation (Volatility)

The standard deviation ($\sigma$) of a set of returns ($R_i$) is given by:

$$\sigma = \sqrt{\frac{1}{N-1} \sum_{i=1}^{N} (R_i - \bar{R})^2}$$

Where:

  • $N$ is the number of observations.
  • $R_i$ is the individual return.
  • $\bar{R}$ is the mean of the returns.

For financial applications, it's common to annualize volatility by multiplying the daily standard deviation by the square root of the number of trading days in a year (e.g., $\sqrt{252}$).

[3]
def calculate_historical_volatility(prices, lookback_period=20, annualize=True, annualization_factor=252):
    r"""
    Calculates the historical volatility of an asset.

    Inputs:
    - prices (pd.Series or np.array): A series or array of asset closing prices.
    - lookback_period (int): The number of periods to consider for volatility calculation.
    - annualize (bool): Whether to annualize the volatility.
    - annualization_factor (int): The factor to use for annualization (e.g., 252 for trading days).

    Outputs:
    - float: The calculated historical volatility.

    Formulas:
    1. Logarithmic returns: $R_t = \ln(P_t / P_{t-1})$
    2. Standard deviation of returns over the lookback period.
    3. Annualized volatility = Daily Volatility * $\sqrt{\text{annualization_factor}}$
    """
    if len(prices) < lookback_period:
        return np.nan

    # Calculate logarithmic returns
    returns = np.log(prices / prices.shift(1)).dropna()

    if len(returns) < lookback_period:
        return np.nan

    # Calculate standard deviation of returns over the lookback period
    daily_volatility = returns.rolling(window=lookback_period).std().iloc[-1]

    if annualize:
        return daily_volatility * np.sqrt(annualization_factor)
    else:
        return daily_volatility

# --- Example Demonstration of Volatility Calculation ---
print("\n--- Demonstrating Volatility Calculation ---")
# Generate mock price data
np.random.seed(42)
mock_prices = pd.Series(100 + np.cumsum(np.random.normal(0, 1, 100)))

# Calculate and display volatility
volatility = calculate_historical_volatility(mock_prices, lookback_period=20, annualize=True)
print(f"Mock Prices (last 5):\n{mock_prices.tail()}")
print(f"Calculated Annualized Volatility (20-day lookback): {volatility:.4f}")

# Demonstrate rolling volatility
mock_returns = np.log(mock_prices / mock_prices.shift(1)).dropna()
rolling_vol = mock_returns.rolling(window=20).std() * np.sqrt(252)

plt.figure(figsize=(12, 6))
plt.plot(mock_prices.index, mock_prices, label='Asset Price')
plt.title('Mock Asset Price Over Time')
plt.xlabel('Time Step')
plt.ylabel('Price')
plt.grid(True)
plt.legend()
plt.show()

plt.figure(figsize=(12, 6))
plt.plot(rolling_vol.index, rolling_vol, label='Annualized Rolling 20-day Volatility', color='orange')
plt.title('Rolling Historical Volatility of Mock Asset')
plt.xlabel('Time Step')
plt.ylabel('Volatility')
plt.grid(True)
plt.legend()
plt.show()

--- Demonstrating Volatility Calculation ---
Mock Prices (last 5):
95    89.287646
96    89.583767
97    89.844822
98    89.849935
99    89.615348
dtype: float64
Calculated Annualized Volatility (20-day lookback): 0.1209
cell output
cell output

Position Sizing Fundamentals

Before diving into volatility-based sizing, it's important to understand the basic concept of position sizing. Position sizing refers to the process of determining the number of units (shares, contracts, etc.) of an asset to trade. It's a key aspect of risk management.

A common approach is to define a maximum percentage of your total trading capital you are willing to risk on a single trade. For example, if you decide to risk no more than 1% of your capital per trade, and your capital is $100,000, then your maximum risk per trade is $1,000.

The relationship is:

$$\text{Risk Per Trade} = \text{Account Capital} \times \text{Risk Percentage}$$

Traditional fixed-dollar or fixed-share sizing methods often overlook the inherent risk differences between assets. This is where volatility-based sizing offers a more sophisticated approach.

Volatility-Based Position Sizing

Volatility-based position sizing aims to make the dollar risk per trade constant by inversely relating position size to the asset's volatility. The more volatile an asset is, the smaller the position size should be to maintain the same dollar risk. Conversely, for less volatile assets, a larger position can be taken.

The Core Idea: Equi-Risk Positions

The goal is to have each 'unit of risk' correspond to the same potential dollar loss. If an asset moves 1% per day, and another moves 5% per day, taking the same capital exposure will lead to drastically different potential daily P&L swings.

Formula for Volatility-Based Position Size

The number of units to trade can be calculated as:

$$\text{Number of Units} = \frac{\text{Account Capital} \times \text{Risk Percentage}}{\text{Asset Volatility (in dollar terms)}}$$

To express asset volatility in dollar terms, we can use the price of the asset multiplied by its percentage volatility (e.g., daily standard deviation of returns). If we want to target a specific daily risk, we use daily volatility. If we want to annualize, we use annualized volatility and adjust the risk accordingly.

A more practical formulation often uses the Average True Range (ATR) or a percentage of price movement derived from volatility:

$$\text{Number of Units} = \frac{\text{Maximum Dollar Risk Per Trade}}{\text{Volatility Measure (e.g., Daily Volatility} \times \text{Current Price})}$$

Or, more simply, if we define volatility as the expected dollar movement for a given period (e.g., 1-day standard deviation of price changes):

$$\text{Number of Units} = \frac{\text{Maximum Dollar Risk Per Trade}}{\text{Expected Price Move (e.g., } \sigma \times \text{Price} \text{ for a given period)}}$$

For this example, we'll use an annualized volatility converted to a daily dollar movement for calculation efficiency.

Let's assume our risk_per_trade_dollar is the maximum dollar amount we're willing to lose if our stop loss is hit. This needs to be consistent with the volatility measure used. For simplicity, we can interpret 'volatility' here as the expected dollar movement (e.g., the standard deviation of daily price changes, or a multiple of daily ATR, or even the annualized volatility converted to a daily dollar amount).

Here, we'll use the daily standard deviation of returns multiplied by the current price to estimate the typical dollar movement on a daily basis. So, a 1-standard deviation move in dollar terms.

[4]
def calculate_volatility_based_position_size(
    account_capital,
    risk_percentage_per_trade,
    asset_prices,
    lookback_period=20,
    stop_loss_multiplier=1 # How many 'volatility units' away is your stop loss
):
    """
    Calculates the number of units to trade using a volatility-based sizing approach.

    Inputs:
    - account_capital (float): Total trading capital.
    - risk_percentage_per_trade (float): Percentage of capital to risk per trade (e.g., 0.01 for 1%).
    - asset_prices (pd.Series): A series of asset closing prices.
    - lookback_period (int): Lookback period for calculating daily volatility.
    - stop_loss_multiplier (float): Multiplier to relate volatility to the stop-loss distance.
      For example, if your stop-loss is typically set at 2 times the daily standard deviation
      of price movement, this would be 2.

    Outputs:
    - int: The calculated number of units (shares/contracts) to trade.

    Formulas:
    1. Maximum Dollar Risk Per Trade = `account_capital * risk_percentage_per_trade`
    2. Daily Dollar Volatility = `daily_std_of_returns * current_price`
    3. Risk per Unit = `Daily Dollar Volatility * stop_loss_multiplier`
    4. Number of Units = `Maximum Dollar Risk Per Trade / Risk per Unit`
    """
    if len(asset_prices) < lookback_period + 1:
        return 0 # Not enough data to calculate volatility

    current_price = asset_prices.iloc[-1]

    # Calculate daily returns for the lookback period
    returns = np.log(asset_prices / asset_prices.shift(1)).dropna()

    if len(returns) < lookback_period:
        return 0

    # Calculate the daily standard deviation of returns
    daily_std_returns = returns.rolling(window=lookback_period).std().iloc[-1]

    # Convert daily standard deviation of returns to daily dollar volatility
    # This represents the expected 1-standard deviation dollar movement per day.
    daily_dollar_volatility = daily_std_returns * current_price

    if daily_dollar_volatility <= 0:
        return 0 # Avoid division by zero or negative volatility

    # Calculate maximum dollar risk per trade
    max_dollar_risk_per_trade = account_capital * risk_percentage_per_trade

    # Estimate the dollar risk per unit (share) based on volatility and stop loss placement.
    # If your stop loss is set at 'stop_loss_multiplier' times the daily dollar volatility,
    # then this is your 'risk per unit' for sizing purposes.
    risk_per_unit = daily_dollar_volatility * stop_loss_multiplier

    if risk_per_unit <= 0:
        return 0

    # Calculate the number of units
    number_of_units = max_dollar_risk_per_trade / risk_per_unit

    return int(round(number_of_units))

Practical Example and Demonstration

Let's demonstrate how volatility-based sizing works with a simulated price series. We will generate a mock asset price path, calculate its rolling volatility, and then determine the appropriate position size over time based on a fixed risk percentage.

We'll assume a constant account capital and risk percentage, and observe how the calculated position size adapts to changes in the asset's volatility.

[5]
# --- Simulation Parameters ---
np.random.seed(42) # for reproducibility
num_days = 252 # One trading year
initial_price = 100
account_capital = 100000 # $100,000
risk_percentage_per_trade = 0.01 # 1% risk per trade
lookback_period_vol = 20 # 20-day lookback for volatility
stop_loss_multiplier = 2 # Stop loss at 2x daily dollar volatility

# --- Generate Mock Price Data with Changing Volatility ---
# We'll simulate periods of low and high volatility
price_data = [initial_price]
current_price = initial_price

for i in range(1, num_days):
    # Simulate changing volatility
    if i < num_days / 3:
        daily_std = 0.5 # Lower volatility
    elif i < 2 * num_days / 3:
        daily_std = 1.5 # Higher volatility
    else:
        daily_std = 0.8 # Medium volatility

    # Simulate price movement
    price_change = np.random.normal(0, daily_std)
    current_price += price_change
    price_data.append(current_price)

mock_prices_df = pd.Series(price_data, name='Price')

# --- Calculate Rolling Volatility and Position Size ---
position_sizes = []
rolling_dollar_volatilities = []

for i in range(lookback_period_vol, num_days):
    current_prices_window = mock_prices_df.iloc[:i+1]

    # Calculate daily standard deviation of returns for the rolling window
    current_returns_window = np.log(current_prices_window / current_prices_window.shift(1)).dropna()
    if len(current_returns_window) >= lookback_period_vol:
        daily_std_returns = current_returns_window.rolling(window=lookback_period_vol).std().iloc[-1]
        current_price_for_calc = current_prices_window.iloc[-1]
        daily_dollar_vol = daily_std_returns * current_price_for_calc
        rolling_dollar_volatilities.append(daily_dollar_vol)
    else:
        daily_dollar_vol = np.nan # Not enough data yet
        rolling_dollar_volatilities.append(np.nan)

    size = calculate_volatility_based_position_size(
        account_capital,
        risk_percentage_per_trade,
        current_prices_window,
        lookback_period=lookback_period_vol,
        stop_loss_multiplier=stop_loss_multiplier
    )
    position_sizes.append(size)

# Align position_sizes and rolling_dollar_volatilities with the original index for plotting
# The first `lookback_period_vol` days won't have a position size or rolling vol
index_start = mock_prices_df.index[lookback_period_vol]
position_sizes_series = pd.Series(position_sizes, index=mock_prices_df.index[lookback_period_vol:])
# FIX: Removed the slicing for rolling_dollar_volatilities as it already has the correct length
rolling_dollar_volatilities_series = pd.Series(rolling_dollar_volatilities, index=mock_prices_df.index[lookback_period_vol:])

print("\n--- Simulation Results (last 10 days) ---")
results_df = pd.DataFrame({
    'Price': mock_prices_df,
    'Rolling Dollar Volatility': rolling_dollar_volatilities_series,
    'Calculated Position Size': position_sizes_series
}).tail(10)

print(results_df.to_markdown())

--- Simulation Results (last 10 days) ---
|     |   Price |   Rolling Dollar Volatility |   Calculated Position Size |
|----:|--------:|----------------------------:|---------------------------:|
| 242 | 101.177 |                    0.7777   |                        643 |
| 243 | 101.581 |                    0.775629 |                        645 |
| 244 | 102.274 |                    0.747414 |                        669 |
| 245 | 101.314 |                    0.766863 |                        652 |
| 246 | 101.046 |                    0.737002 |                        678 |
| 247 | 100.666 |                    0.736392 |                        679 |
| 248 | 100.144 |                    0.718869 |                        696 |
| 249 | 101.556 |                    0.793114 |                        630 |
| 250 | 101.88  |                    0.789345 |                        633 |
| 251 | 100.871 |                    0.803488 |                        622 |

Visualizations

1. Asset Price vs. Rolling Dollar Volatility

This plot shows the simulated asset price alongside its calculated rolling dollar volatility. You can observe how volatility changes over time, reflecting the different simulation phases (low, high, medium volatility).

[6]
import matplotlib.pyplot as plt

fig, ax1 = plt.subplots(figsize=(14, 7))

ax1.plot(mock_prices_df.index, mock_prices_df, label='Asset Price', color='blue')
ax1.set_xlabel('Time Step')
ax1.set_ylabel('Asset Price', color='blue')
ax1.tick_params(axis='y', labelcolor='blue')
ax1.grid(True, linestyle='--', alpha=0.7)

ax2 = ax1.twinx()
ax2.plot(rolling_dollar_volatilities_series.index, rolling_dollar_volatilities_series, label='Rolling Daily Dollar Volatility', color='red', linestyle='--')
ax2.set_ylabel('Daily Dollar Volatility', color='red')
ax2.tick_params(axis='y', labelcolor='red')

plt.title('Asset Price and Rolling Daily Dollar Volatility Over Time')
fig.tight_layout()
plt.legend(loc='upper left', bbox_to_anchor=(0.0, 0.95))
plt.show()
cell output

Interpretation of Visualization 1:

This chart clearly illustrates the dynamic nature of both asset price and its volatility. Notice how during periods where the Daily Dollar Volatility (red dashed line) is higher, the Asset Price (blue line) tends to show larger swings. Conversely, when volatility is lower, price movements are more subdued. This confirms our simulated data behaves as expected and sets the stage for how position sizing will react.

2. Rolling Dollar Volatility vs. Calculated Position Size

This plot directly shows the inverse relationship between asset volatility and the calculated position size. As volatility increases, the number of units to trade decreases, helping to keep the dollar risk per trade constant. Conversely, when volatility drops, the position size can be increased.

[7]
import matplotlib.pyplot as plt

fig, ax1 = plt.subplots(figsize=(14, 7))

ax1.plot(rolling_dollar_volatilities_series.index, rolling_dollar_volatilities_series, label='Rolling Daily Dollar Volatility', color='red')
ax1.set_xlabel('Time Step')
ax1.set_ylabel('Daily Dollar Volatility', color='red')
ax1.tick_params(axis='y', labelcolor='red')
ax1.grid(True, linestyle='--', alpha=0.7)

ax2 = ax1.twinx()
ax2.plot(position_sizes_series.index, position_sizes_series, label='Calculated Position Size (Units)', color='green', linestyle='-')
ax2.set_ylabel('Calculated Position Size (Units)', color='green')
ax2.tick_params(axis='y', labelcolor='green')

plt.title('Rolling Daily Dollar Volatility vs. Calculated Volatility-Based Position Size')
fig.tight_layout()
plt.legend(loc='upper right', bbox_to_anchor=(1.0, 0.95))
plt.show()
cell output

Interpretation of Visualization 2:

This visualization is key to understanding volatility-based sizing. It clearly demonstrates the inverse correlation: when the Rolling Daily Dollar Volatility (red line) increases, the Calculated Position Size (green line) decreases, and vice-versa. This inverse relationship is precisely the mechanism by which volatility-based sizing achieves its goal of normalizing risk exposure across different market conditions. By trading fewer units when an asset is more volatile, the potential dollar loss from a typical price swing (or hitting a stop-loss) remains more consistent with the fixed risk percentage set by the trader.

Conclusion

Volatility-based sizing is a robust risk management technique that moves beyond simple fixed-share or fixed-dollar position sizing. By dynamically adjusting the number of units traded inversely to an asset's volatility, traders can achieve a more consistent dollar risk exposure across various trading opportunities.

This method is crucial for:

  • Capital Protection: Reducing exposure to high-volatility assets helps prevent large, unexpected drawdowns.
  • Emotional Discipline: Standardizing risk per trade helps remove arbitrary decision-making and fosters a more systematic approach.
  • System Longevity: By managing risk effectively, trading systems are more likely to survive adverse market conditions and achieve long-term profitability.

Implementing volatility-based sizing requires a reliable measure of volatility and a clear understanding of your risk tolerance. While historical volatility is commonly used, more advanced methods might incorporate implied volatility or adaptive lookback periods for an even more responsive sizing strategy.

Annualization Factor for Different Asset Classes

The standard annualization factor of 252 trading days is typically used for traditional financial markets like stocks and forex. However, for cryptocurrencies, which often trade 7 days a week, a more appropriate annualization factor is 365 days.

Our calculate_historical_volatility function is designed to accommodate this by allowing a custom annualization_factor to be passed.

[8]
print("\n--- Demonstrating Volatility Calculation for Crypto (365-day annualization) ---")
# Generate mock price data for a crypto asset (e.g., Bitcoin)
np.random.seed(43) # Another seed for different data
crypto_prices = pd.Series(50000 + np.cumsum(np.random.normal(0, 1000, 100))) # Higher price, different std

# Calculate and display volatility with 365 annualization factor
crypto_volatility = calculate_historical_volatility(crypto_prices, lookback_period=20, annualize=True, annualization_factor=365)
print(f"Mock Crypto Prices (last 5):\n{crypto_prices.tail()}")
print(f"Calculated Annualized Volatility (20-day lookback, 365-day factor): {crypto_volatility:.4f}")

# Visualize the crypto price and its rolling volatility
crypto_returns = np.log(crypto_prices / crypto_prices.shift(1)).dropna()
crypto_rolling_vol = crypto_returns.rolling(window=20).std() * np.sqrt(365)

plt.figure(figsize=(12, 6))
plt.plot(crypto_prices.index, crypto_prices, label='Crypto Asset Price', color='purple')
plt.title('Mock Crypto Asset Price Over Time')
plt.xlabel('Time Step')
plt.ylabel('Price')
plt.grid(True)
plt.legend()
plt.show()

plt.figure(figsize=(12, 6))
plt.plot(crypto_rolling_vol.index, crypto_rolling_vol, label='Annualized Rolling 20-day Volatility (Crypto)', color='orange')
plt.title('Rolling Historical Volatility of Mock Crypto Asset')
plt.xlabel('Time Step')
plt.ylabel('Volatility')
plt.grid(True)
plt.legend()
plt.show()

--- Demonstrating Volatility Calculation for Crypto (365-day annualization) ---
Mock Crypto Prices (last 5):
95    54372.302543
96    54469.234311
97    54280.849962
98    55872.452121
99    56458.262948
dtype: float64
Calculated Annualized Volatility (20-day lookback, 365-day factor): 0.3204
cell output
cell output
Volatility Based Sizing · BitPredict