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.
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 Level | Interpretation | Implication |
|---|---|---|
| Very low (< 5) | Large stablecoin reserves vs BTC cap | High buying power — bullish potential |
| Low (5–10) | Moderate stablecoin supply | Neutral to bullish |
| High (10–20) | Stablecoins deployed / low reserves | Reduced buying power |
| Very high (> 20) | Capital fully deployed | Bearish — limited fuel |
This notebook:
- Fetches BTC market cap and stablecoin supply data
- Computes SSR and its z-score relative to historical average
- Generates signals at SSR extremes
- Backtests the strategy
- Visualizes SSR with price and signal annotations
- Exports the dataset
!pip install numpy pandas matplotlib seaborn requests scipy --quietimport 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:
- BTC Price with SSR Signals: Shows the BTC price over time, with 'BUY' and 'SELL' signals annotated on the chart.
- SSR and Stablecoin Supply: Displays the Stablecoin Supply Ratio and, if available, the stablecoin supply in billion USD.
- 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()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.