Skip to content

Beta & Relative Strength Indicators

This module provides beta calculation and market regime detection based on relative strength analysis.

Core Functions

beta

Beta and relative strength regime analysis module.

Implements Mike McGlone's approach to regime detection: - Relative strength ratio (asset/benchmark) vs its moving average - Rolling beta calculation using covariance/variance - Regime signal: "risk-on" when ratio above MA, "risk-off" when below

Public API

compute_rolling_beta(asset_returns, benchmark_returns, window=60) -> pd.Series compute_relative_strength(asset_close, benchmark_close) -> pd.Series compute_regime_signal(ratio, ma_period=200) -> dict analyze_beta_regime(asset_df, benchmark_df, ma_period=200, beta_window=60) -> dict

compute_rolling_beta(asset_returns, benchmark_returns, window=60)

Compute rolling beta of asset relative to benchmark.

Beta = Cov(asset_returns, benchmark_returns) / Var(benchmark_returns)

Parameters:

Name Type Description Default
asset_returns Series

Series of asset percentage returns.

required
benchmark_returns Series

Series of benchmark percentage returns.

required
window int

Rolling window size for beta calculation (default: 60 periods).

60

Returns:

Type Description
Series

Series of rolling beta values. NaN where insufficient history.

Raises:

Type Description
ValueError

If window < 2.

Source code in src/stockcharts/indicators/beta.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def compute_rolling_beta(
    asset_returns: pd.Series,
    benchmark_returns: pd.Series,
    window: int = 60,
) -> pd.Series:
    """Compute rolling beta of asset relative to benchmark.

    Beta = Cov(asset_returns, benchmark_returns) / Var(benchmark_returns)

    Args:
        asset_returns: Series of asset percentage returns.
        benchmark_returns: Series of benchmark percentage returns.
        window: Rolling window size for beta calculation (default: 60 periods).

    Returns:
        Series of rolling beta values. NaN where insufficient history.

    Raises:
        ValueError: If window < 2.
    """
    if window < 2:
        raise ValueError("window must be >= 2")

    # Align the series by index
    aligned = pd.DataFrame(
        {
            "asset": asset_returns,
            "benchmark": benchmark_returns,
        }
    ).dropna()

    if len(aligned) < window:
        return pd.Series([float("nan")] * len(asset_returns), index=asset_returns.index)

    # Rolling covariance and variance
    rolling_cov = aligned["asset"].rolling(window=window).cov(aligned["benchmark"])
    rolling_var = aligned["benchmark"].rolling(window=window).var()

    # Beta = Cov / Var
    beta = rolling_cov / rolling_var

    # Reindex to original asset index
    return beta.reindex(asset_returns.index)

compute_relative_strength(asset_close, benchmark_close)

Compute relative strength ratio of asset vs benchmark.

Relative Strength = Asset Price / Benchmark Price

This ratio shows whether the asset is outperforming (rising ratio) or underperforming (falling ratio) the benchmark.

Parameters:

Name Type Description Default
asset_close Series

Series of asset closing prices.

required
benchmark_close Series

Series of benchmark closing prices.

required

Returns:

Type Description
Series

Series of relative strength ratios.

Source code in src/stockcharts/indicators/beta.py
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def compute_relative_strength(
    asset_close: pd.Series,
    benchmark_close: pd.Series,
) -> pd.Series:
    """Compute relative strength ratio of asset vs benchmark.

    Relative Strength = Asset Price / Benchmark Price

    This ratio shows whether the asset is outperforming (rising ratio)
    or underperforming (falling ratio) the benchmark.

    Args:
        asset_close: Series of asset closing prices.
        benchmark_close: Series of benchmark closing prices.

    Returns:
        Series of relative strength ratios.
    """
    # Align by index
    aligned = pd.DataFrame(
        {
            "asset": asset_close,
            "benchmark": benchmark_close,
        }
    ).dropna()

    if aligned.empty:
        return pd.Series(dtype=float)

    ratio = aligned["asset"] / aligned["benchmark"]
    return ratio

compute_regime_signal(ratio, ma_period=200)

Determine regime signal based on ratio vs its moving average.

Parameters:

Name Type Description Default
ratio Series

Series of relative strength ratios.

required
ma_period int

Moving average period for regime detection (default: 200).

200

Returns:

Type Description
dict

dict with keys: - 'ratio': current ratio value - 'ma': current moving average value - 'regime': "risk-on" if ratio > ma, "risk-off" if ratio <= ma - 'pct_from_ma': percentage distance from MA - 'ratio_series': full ratio series - 'ma_series': full MA series

Source code in src/stockcharts/indicators/beta.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def compute_regime_signal(
    ratio: pd.Series,
    ma_period: int = 200,
) -> dict:
    """Determine regime signal based on ratio vs its moving average.

    Args:
        ratio: Series of relative strength ratios.
        ma_period: Moving average period for regime detection (default: 200).

    Returns:
        dict with keys:
            - 'ratio': current ratio value
            - 'ma': current moving average value
            - 'regime': "risk-on" if ratio > ma, "risk-off" if ratio <= ma
            - 'pct_from_ma': percentage distance from MA
            - 'ratio_series': full ratio series
            - 'ma_series': full MA series
    """
    if len(ratio) < ma_period:
        return {
            "ratio": float("nan"),
            "ma": float("nan"),
            "regime": "insufficient-data",
            "pct_from_ma": float("nan"),
            "ratio_series": ratio,
            "ma_series": pd.Series(dtype=float),
        }

    ma_series = ratio.rolling(window=ma_period).mean()

    current_ratio = ratio.iloc[-1]
    current_ma = ma_series.iloc[-1]

    if pd.isna(current_ratio) or pd.isna(current_ma):
        regime = "insufficient-data"
        pct_from_ma = float("nan")
    else:
        regime = "risk-on" if current_ratio > current_ma else "risk-off"
        pct_from_ma = ((current_ratio - current_ma) / current_ma) * 100

    return {
        "ratio": current_ratio,
        "ma": current_ma,
        "regime": regime,
        "pct_from_ma": pct_from_ma,
        "ratio_series": ratio,
        "ma_series": ma_series,
    }

analyze_beta_regime(asset_df, benchmark_df, ma_period=200, beta_window=60)

Complete beta regime analysis for an asset vs benchmark.

Combines relative strength ratio, regime signal, and rolling beta.

Parameters:

Name Type Description Default
asset_df DataFrame

DataFrame with 'Close' column for the asset.

required
benchmark_df DataFrame

DataFrame with 'Close' column for the benchmark.

required
ma_period int

Moving average period for regime detection (default: 200).

200
beta_window int

Rolling window for beta calculation (default: 60).

60

Returns:

Type Description
dict

dict with keys: - 'relative_strength': current RS ratio - 'rs_ma': RS moving average value - 'regime': "risk-on" or "risk-off" - 'pct_from_ma': percentage distance from MA - 'rolling_beta': current rolling beta value - 'asset_price': current asset price - 'benchmark_price': current benchmark price - 'rs_series': full RS ratio series - 'rs_ma_series': full RS MA series - 'beta_series': full rolling beta series

Source code in src/stockcharts/indicators/beta.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
def analyze_beta_regime(
    asset_df: pd.DataFrame,
    benchmark_df: pd.DataFrame,
    ma_period: int = 200,
    beta_window: int = 60,
) -> dict:
    """Complete beta regime analysis for an asset vs benchmark.

    Combines relative strength ratio, regime signal, and rolling beta.

    Args:
        asset_df: DataFrame with 'Close' column for the asset.
        benchmark_df: DataFrame with 'Close' column for the benchmark.
        ma_period: Moving average period for regime detection (default: 200).
        beta_window: Rolling window for beta calculation (default: 60).

    Returns:
        dict with keys:
            - 'relative_strength': current RS ratio
            - 'rs_ma': RS moving average value
            - 'regime': "risk-on" or "risk-off"
            - 'pct_from_ma': percentage distance from MA
            - 'rolling_beta': current rolling beta value
            - 'asset_price': current asset price
            - 'benchmark_price': current benchmark price
            - 'rs_series': full RS ratio series
            - 'rs_ma_series': full RS MA series
            - 'beta_series': full rolling beta series
    """
    # Extract close prices
    asset_close = asset_df["Close"]
    benchmark_close = benchmark_df["Close"]

    # Compute relative strength
    rs_ratio = compute_relative_strength(asset_close, benchmark_close)

    # Compute regime signal
    regime_result = compute_regime_signal(rs_ratio, ma_period=ma_period)

    # Compute returns for beta calculation
    asset_returns = asset_close.pct_change()
    benchmark_returns = benchmark_close.pct_change()

    # Compute rolling beta
    beta_series = compute_rolling_beta(asset_returns, benchmark_returns, window=beta_window)
    current_beta = beta_series.iloc[-1] if len(beta_series) > 0 else float("nan")

    return {
        "relative_strength": regime_result["ratio"],
        "rs_ma": regime_result["ma"],
        "regime": regime_result["regime"],
        "pct_from_ma": regime_result["pct_from_ma"],
        "rolling_beta": current_beta,
        "asset_price": asset_close.iloc[-1] if len(asset_close) > 0 else float("nan"),
        "benchmark_price": benchmark_close.iloc[-1] if len(benchmark_close) > 0 else float("nan"),
        "rs_series": regime_result["ratio_series"],
        "rs_ma_series": regime_result["ma_series"],
        "beta_series": beta_series,
    }