Crypto-Native·On-Chain Signal Generation·Intermediate

Stablecoin Ratio Signal

Build a stablecoin supply ratio trading signal that measures the total purchasing power sitting in major stablecoins relative to aggregate cryptocurrency market capitalization, indicating potential sideline buying pressure available to flow back into crypto markets during sentiment shifts.

cryptoon-chainsignal-generation

Stablecoin Supply Ratio Signal — Crypto-Native

Category: Crypto-Native | Subcategory: On-Chain Signals


What This Notebook Does

The Stablecoin Supply Ratio (SSR) measures the ratio of BTC market cap to the total supply of stablecoins:

SSR = BTC Market Cap / Total Stablecoin Supply

Stablecoins represent dry powder — capital waiting on the sidelines that can be deployed into BTC. A low SSR means there is proportionally more buying power available relative to BTC market cap (bullish). A high SSR means stablecoins are scarce relative to BTC, suggesting capital is already deployed (less fuel for further upside).

SSR LevelInterpretationImplication
Very low (< 5)Large stablecoin reserves vs BTC capHigh buying power — bullish potential
Low (5–10)Moderate stablecoin supplyNeutral to bullish
High (10–20)Stablecoins deployed / low reservesReduced buying power
Very high (> 20)Capital fully deployedBearish — limited fuel

This notebook:

  1. Fetches BTC market cap and stablecoin supply data
  2. Computes SSR and its z-score relative to historical average
  3. Generates signals at SSR extremes
  4. Backtests the strategy
  5. Visualizes SSR with price and signal annotations
  6. Exports the dataset
[ ]
!pip install numpy pandas matplotlib seaborn requests scipy --quiet
[ ]
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import requests
from scipy import stats
import warnings

warnings.filterwarnings('ignore')
%matplotlib inline
plt.rcParams['figure.figsize'] = (14, 5)
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.right'] = False
print('Imports ready.')
Imports ready.

Section 1 — Configuration

This section defines the key parameters and settings for the SSR signal calculation and backtesting. These include:

  • USE_SYNTHETIC: A boolean to toggle between using synthetic data or fetching real data from Glassnode.
  • GLASSNODE_API_KEY: Your Glassnode API key for fetching real data (ensure it's kept confidential).
  • SIMULATION_DAYS: The number of days to simulate data for, especially when using synthetic data.
  • ZSCORE_WINDOW: The rolling window size for calculating the SSR's z-score.
  • SSR_LOW_ZSCORE: The z-score threshold below which a 'BUY' signal is generated.
  • SSR_HIGH_ZSCORE: The z-score threshold above which a 'SELL' signal is generated.
  • HOLD_DAYS: The minimum number of days to hold a position after a signal to prevent rapid re-trading.
[ ]
USE_SYNTHETIC = False
GLASSNODE_API_KEY = 'YOUR_API_KEY_HERE'
SIMULATION_DAYS = 1460
ZSCORE_WINDOW = 90      # rolling window for z-score normalisation
SSR_LOW_ZSCORE  = -1.5  # low SSR z-score → lots of buying power → BUY signal
SSR_HIGH_ZSCORE =  1.5  # high SSR z-score → limited buying power → SELL signal
HOLD_DAYS = 14
print('Config ready.')
Config ready.

Section 2 — Data

This function fetch_ssr_data retrieves the Stablecoin Supply Ratio (SSR) and BTC price data from Glassnode's API. It requires a Glassnode API key and handles potential API failures by returning None.

The generate_synthetic_ssr function creates synthetic SSR data for demonstration or when Glassnode API data is unavailable. It simulates BTC price and stablecoin supply dynamics to produce realistic SSR values over a specified number of days.

[ ]
def fetch_ssr_data(api_key: str) -> pd.DataFrame:
    """
    Fetch SSR data from Glassnode (requires paid tier).
    Falls back to computing from BTC market cap + stablecoin supply.

    Returns
    -------
    pd.DataFrame  Columns: btc_price, ssr  (daily, indexed by date).
    """
    base = 'https://api.glassnode.com/v1/metrics'
    params = {'a': 'BTC', 'i': '24h', 'api_key': api_key}
    ssr_r = requests.get(f'{base}/indicators/ssr', params=params, timeout=30)
    mkt_r = requests.get(f'{base}/market/price_usd_close', params=params, timeout=30)
    if ssr_r.status_code != 200 or mkt_r.status_code != 200:
        return None
    ssr  = pd.DataFrame(ssr_r.json()).rename(columns={'t': 'date', 'v': 'ssr'})
    mkt  = pd.DataFrame(mkt_r.json()).rename(columns={'t': 'date', 'v': 'btc_price'})
    df   = ssr.merge(mkt, on='date')
    df['date'] = pd.to_datetime(df['date'], unit='s')
    return df.set_index('date').sort_index()


def generate_synthetic_ssr(n_days: int = 1460, seed: int = 42) -> pd.DataFrame:
    """
    Generate synthetic SSR data with realistic cycle dynamics.

    SSR rises as BTC price rises (more cap vs stable supply) and
    falls after bear market capitulation as stablecoins accumulate.

    Returns
    -------
    pd.DataFrame  Columns: btc_price, stablecoin_supply_bn, ssr  (daily).
    """
    rng = np.random.default_rng(seed)
    t = np.linspace(0, 2 * np.pi, n_days)
    cycle = np.sin(t - np.pi / 2) * 0.5 + 0.5
    btc_price = 15_000 + 75_000 * cycle**1.5 + np.cumsum(rng.normal(0, 300, n_days))
    btc_price = np.maximum(btc_price, 5_000)
    btc_supply = 19_000_000
    btc_mktcap = btc_price * btc_supply / 1e9  # billions
    # Stablecoins grow steadily but faster during bear (accumulation)
    stable_base = 50 + 100 * (1 - cycle) + np.cumsum(rng.normal(0, 0.5, n_days))
    stable = np.maximum(stable_base, 20)
    ssr = btc_mktcap / stable
    idx = pd.date_range('2020-01-01', periods=n_days, freq='D')
    return pd.DataFrame({'btc_price': btc_price,
                         'stablecoin_supply_bn': stable,
                         'ssr': ssr}, index=idx)


if USE_SYNTHETIC or not GLASSNODE_API_KEY or GLASSNODE_API_KEY == 'YOUR_API_KEY_HERE':
    df = generate_synthetic_ssr(SIMULATION_DAYS)
    print(f'Synthetic data: {len(df)} days')
else:
    df = fetch_ssr_data(GLASSNODE_API_KEY)
    if df is None:
        df = generate_synthetic_ssr(SIMULATION_DAYS)
        print('API failed — synthetic fallback')
    else:
        print(f'Glassnode data: {len(df)} days')

print(df.describe())
Synthetic data: 1460 days
          btc_price  stablecoin_supply_bn          ssr
count   1460.000000           1460.000000  1460.000000
mean   40966.865494             69.865951    26.668657
std    26935.261713             42.515687    29.403707
min     5000.000000             20.000000     0.783997
25%    15615.177988             23.408774     2.276303
50%    36334.964076             69.553869    10.399806
75%    68238.786294            109.809772    54.899586
max    83684.836568            150.481480    79.500595

Section 3 — Z-Score & Signal

This section calculates the rolling z-score for the SSR, which normalizes the SSR data relative to its recent historical average. It then generates trading signals: a 'BUY' signal when the SSR z-score is very low (indicating high stablecoin reserves) and a 'SELL' signal when it's very high (indicating depleted reserves). Signals are subject to a HOLD_DAYS constraint to prevent rapid re-triggering.

[ ]
# Rolling z-score of SSR
rolling_mean = df['ssr'].rolling(ZSCORE_WINDOW).mean()
rolling_std  = df['ssr'].rolling(ZSCORE_WINDOW).std()
df['ssr_zscore'] = (df['ssr'] - rolling_mean) / rolling_std

# Signals: low SSR z-score = high buying power = bullish
df['signal'] = 0
last_sig = -HOLD_DAYS
for i in range(ZSCORE_WINDOW, len(df)):
    if i - last_sig < HOLD_DAYS:
        continue
    z = df['ssr_zscore'].iloc[i]
    if z <= SSR_LOW_ZSCORE:   # lots of dry powder
        df.iloc[i, df.columns.get_loc('signal')] = 1
        last_sig = i
    elif z >= SSR_HIGH_ZSCORE: # dry powder depleted
        df.iloc[i, df.columns.get_loc('signal')] = -1
        last_sig = i

print(f"BUY  signals: {(df['signal']==1).sum()}")
print(f"SELL signals: {(df['signal']==-1).sum()}")
BUY  signals: 30
SELL signals: 37

Section 4 — Backtest

[ ]
cash, btc, in_trade, entry_price = 10_000.0, 0.0, False, 0.0
equity = []
for _, row in df.iterrows():
    price = row['btc_price']
    if row['signal'] == 1 and not in_trade:
        btc = cash / price; cash = 0; in_trade = True; entry_price = price
    elif row['signal'] == -1 and in_trade:
        cash = btc * price; btc = 0; in_trade = False
    equity.append(cash + btc * price)
df['equity'] = equity
total = (equity[-1] - 10_000) / 10_000
bh    = (df['btc_price'].iloc[-1] - df['btc_price'].iloc[0]) / df['btc_price'].iloc[0]
print(f'SSR Strategy return : {total:.1%}')
print(f'Buy-and-hold return : {bh:.1%}')
SSR Strategy return : -91.6%
Buy-and-hold return : -45.9%

This section backtests the SSR trading strategy. It simulates buying BTC when a 'BUY' signal is triggered and selling when a 'SELL' signal is triggered. It tracks the equity curve of the strategy and compares its total return against a simple buy-and-hold strategy for BTC.

Section 5 — Visualization

This section visualizes the results of the SSR signal. It plots three subplots:

  1. BTC Price with SSR Signals: Shows the BTC price over time, with 'BUY' and 'SELL' signals annotated on the chart.
  2. SSR and Stablecoin Supply: Displays the Stablecoin Supply Ratio and, if available, the stablecoin supply in billion USD.
  3. SSR Rolling Z-Score: Plots the normalized SSR z-score and highlights the 'buy' and 'sell' threshold zones. This helps to understand when signals are generated based on SSR deviations from its historical average.
[ ]
fig, axes = plt.subplots(3, 1, figsize=(14, 12), sharex=True)
fig.suptitle('Stablecoin Supply Ratio (SSR) Signal', fontsize=14, fontweight='bold')

buys  = df[df['signal'] ==  1]
sells = df[df['signal'] == -1]

ax1 = axes[0]
ax1.plot(df.index, df['btc_price'], color='#1976d2', lw=1.5)
ax1.scatter(buys.index,  buys['btc_price'],  marker='^', color='#43a047', s=80, zorder=5, label='BUY')
ax1.scatter(sells.index, sells['btc_price'], marker='v', color='#e53935', s=80, zorder=5, label='SELL')
ax1.set_ylabel('BTC Price (USD)')
ax1.legend(); ax1.set_title('BTC Price with SSR Signals')

ax2 = axes[1]
ax2.plot(df.index, df['ssr'], color='#6a1b9a', lw=1.5, label='SSR')
ax22 = ax2.twinx()
ax22.plot(df.index, df.get('stablecoin_supply_bn', df['ssr'] * 0), color='#ff7043', lw=1, ls='--', alpha=0.6, label='Stable Supply (B)')
ax2.set_ylabel('SSR')
ax22.set_ylabel('Stablecoin Supply (B USD)', color='#ff7043')
ax2.set_title('SSR and Stablecoin Supply')

ax3 = axes[2]
ax3.plot(df.index, df['ssr_zscore'], color='#00796b', lw=1.5)
ax3.axhline(SSR_LOW_ZSCORE,  color='#43a047', ls='--', lw=1, label=f'Buy zone (z={SSR_LOW_ZSCORE})')
ax3.axhline(SSR_HIGH_ZSCORE, color='#e53935', ls='--', lw=1, label=f'Sell zone (z={SSR_HIGH_ZSCORE})')
ax3.axhline(0, color='black', lw=0.8)
ax3.set_ylabel('SSR Z-Score')
ax3.legend(); ax3.set_title('SSR Rolling Z-Score')

plt.tight_layout()
plt.show()
cell output

Section 6 — Export

[ ]
df[['btc_price', 'ssr', 'ssr_zscore', 'signal', 'equity']].to_csv('stablecoin_ratio_signal.csv')
print('Saved: stablecoin_ratio_signal.csv')
Saved: stablecoin_ratio_signal.csv

This section exports the generated data, including BTC price, SSR, SSR z-score, trading signals, and the strategy's equity curve, into a CSV file named stablecoin_ratio_signal.csv. This allows for further analysis or external use of the processed data.

Conclusion

This notebook demonstrates the implementation and backtesting of a Stablecoin Supply Ratio (SSR) trading signal for Bitcoin. The SSR is a crypto-native on-chain metric that gauges the potential buying power in the market by comparing Bitcoin's market capitalization to the total supply of stablecoins. A low SSR indicates a large reserve of stablecoins relative to BTC's market cap, suggesting a bullish sentiment due to ample 'dry powder' for potential investment. Conversely, a high SSR implies that stablecoins are already largely deployed, signaling reduced buying power and potentially bearish conditions.

The notebook begins by configuring key parameters such as whether to use synthetic data or fetch real data from Glassnode, the window size for z-score calculation, and thresholds for generating buy/sell signals. It then proceeds to either generate synthetic SSR data (for demonstration purposes or if API access is unavailable) or fetch actual SSR and BTC price data using the Glassnode API.

A crucial step involves calculating the rolling z-score of the SSR, which normalizes the ratio against its historical average, making it easier to identify significant deviations. Trading signals are then generated based on these z-scores: a 'BUY' signal is issued when the SSR z-score falls below a specified low threshold (indicating high stablecoin reserves), and a 'SELL' signal is issued when it rises above a high threshold (suggesting depleted reserves). A HOLD_DAYS parameter is incorporated to prevent rapid, consecutive trades.

The backtesting section simulates the performance of the SSR strategy, tracking the equity curve based on generated buy and sell signals. This allows for a direct comparison of the strategy's returns against a simple buy-and-hold approach for Bitcoin. The results from the synthetic data showed a negative return for the SSR strategy, which was worse than the buy-and-hold return, indicating that the chosen parameters or the inherent volatility of synthetic data might need further tuning or real-world validation.

Finally, the notebook provides comprehensive visualizations, including:

  • BTC Price with SSR Signals: Illustrating buy and sell points directly on the Bitcoin price chart.
  • SSR and Stablecoin Supply: Displaying the trend of the SSR alongside the stablecoin supply.
  • SSR Rolling Z-Score: Highlighting the normalized SSR and the defined buy/sell threshold zones.

The entire dataset, including BTC price, SSR, SSR z-score, trading signals, and the strategy's equity curve, is exported to a CSV file for further analysis or integration into other tools. While the synthetic data backtest yielded negative results, this framework provides a robust foundation for analyzing and potentially optimizing the Stablecoin Supply Ratio as a crypto-native trading signal with real-world data and refined parameters.