feedback
← Back to Learn

Indicator Best Practices: Standardization

Raw values are hard to threshold. Standardized values tell you how unusual the current reading is — which is what actually matters.

The problem with raw indicator values

Suppose you want to build a strategy that goes long when "buying pressure is high." You use net taker volume as your signal. The raw value might be 8,420 BTC in a quiet week and 85,000 BTC during a volatile period. What threshold do you set? Any fixed number will be too sensitive half the time and too slow the other half.

The same problem shows up with price-derived indicators: an SMA difference of 500 USDT means something very different at $20,000 BTC vs $90,000 BTC.

Standardization converts a raw value into a question: "How far is this from normal, measured in standard deviations?" This makes the reading comparable across different market conditions and different symbols.

Rolling Z-score: the most useful method

A Z-score measures how many standard deviations the current value is from the recent mean:

z = (value - rolling_mean) / rolling_std

With a 30-day rolling window, a Z-score of +2.0 means the current reading is 2 standard deviations above the past month's average — unusual, but not extreme. A Z-score of +3.5 is extremely high by recent standards.

Z-scores have a useful property: they're roughly unit-free and comparable across symbols. A TI Z-score of +2.0 on BTCUSDT means roughly the same thing as a TI Z-score of +2.0 on ETHUSDT — both are unusually high relative to their own recent history.

In a strategy, implementing a rolling Z-score looks like this:

window = 720  # 30 days × 24h for 1h interval
df['z'] = (
    (df['raw_indicator'] - df['raw_indicator'].rolling(window).mean())
    / df['raw_indicator'].rolling(window).std()
)

Blave alpha indicators are already standardized

You don't need to standardize Blave's proprietary indicators yourself. They are already delivered as normalized scores comparable across symbols and time periods:

IndicatorStandardized?Notes
Taker Intensity (TI)Standardized against rolling recent history; comparable across symbols
Holder Concentration (HC)Standardized; positive = long concentration, negative = short concentration
Whale Hunter (WH)OI and volume change, each standardized separately
Liquidation (LQ)Standardized liquidation intensity
Market Sentiment (MS)Spot-futures spread, standardized
Capital Shortage (CS)Stablecoin utilization, standardized
Market Direction (MD)BTC-wide directional signal, standardized
Squeeze Momentum (SM)Fixed 1d period; includes scolor directional label

This is why threshold strategies using Blave indicators use values like ENTRY_TH = 1.693 rather than a raw volume number — that threshold has a consistent meaning: "enter when TI is 1.7 standard deviations above its recent average."

Standardized values degrade over lookback windows. Because these indicators are normalized against a rolling window, the same absolute reading can have different implications in very stable vs very volatile market regimes. A TI of +2.0 during a low-volatility month may represent a much smaller raw buying imbalance than a TI of +2.0 during a high-volatility month. Always interpret Blave indicator readings in the context of the current market environment.

When you do need to standardize yourself

Not all indicators need standardization. An SMA crossover signal (SMA_fast > SMA_slow) is already binary — there's nothing to standardize. The issue arises when you use an indicator's magnitude as a threshold.

Example 1 — SMA difference as a threshold

Using the raw price-minus-SMA as a signal breaks across price regimes:

# ✗ Breaks when BTC moves from $20k to $90k
signal[df['Close'] - df['SMA_50'] > 500] = 1.0

# ✓ Z-score of the same difference stays meaningful
diff = df['Close'] - df['SMA_50']
df['diff_z'] = (diff - diff.rolling(720).mean()) / diff.rolling(720).std()
signal[df['diff_z'] > 1.5] = 1.0

Example 2 — MACD across multiple symbols

MACD values are in price units — they're not comparable across different symbols:

# ✗ MACD = 800 means very different things on BTCUSDT vs 2330.TW
signal[df['MACD'] > 800] = 1.0

# ✓ Standardize per symbol before using as threshold
df['macd_z'] = (df['MACD'] - df['MACD'].rolling(720).mean()) / df['MACD'].rolling(720).std()
signal[df['macd_z'] > 1.0] = 1.0

Example 3 — Combining two indicators

RSI (0–100) and raw OI (billions of USD) can't be combined directly. Standardize both first:

# ✗ OI drowns out RSI entirely
combined = df['RSI'] + df['OI']

# ✓ Both as Z-scores, then combine
rsi_z = (df['RSI'] - df['RSI'].rolling(720).mean()) / df['RSI'].rolling(720).std()
oi_z  = (df['OI']  - df['OI'].rolling(720).mean())  / df['OI'].rolling(720).std()
combined = rsi_z + oi_z
signal[combined > 1.5] = 1.0

Example 4 — Shareholder count (Taiwan stocks)

The number of shareholders in a given bracket (e.g., holders of 1,000–5,000 shares) differs massively between large-cap and small-cap stocks. TSMC may have hundreds of thousands of retail holders; a small-cap stock might have a few thousand. The raw count cannot be used as a threshold across different stocks.

What matters is the change relative to that stock's own recent history — is retail participation increasing unusually fast? Standardize the count (or its rate of change) against each stock's rolling window:

# Raw retail holder count is not comparable across stocks
# ✗ meaningless as a cross-stock threshold
signal[df['retail_holders'] > 50000] = 1.0

# ✓ Standardize per stock: how unusual is the current count?
df['holders_z'] = (
    (df['retail_holders'] - df['retail_holders'].rolling(52).mean())
    / df['retail_holders'].rolling(52).std()   # 52 weeks rolling window
)
# Z-score > 2: retail unusually crowded → potential contrarian short signal
signal[df['holders_z'] > 2.0] = 0.0
signal[df['holders_z'] < -1.0] = 1.0

The same principle applies to margin balance (margin_balance), foreign institutional net buy/sell (foreign_net), or any other Taiwan stock data field that varies in absolute scale by company size.

Other standardization methods

MethodFormulaBest for
Rolling Z-score(x − mean) / stdMost indicators; preserves outlier information
Percentile rankrank / count over rolling windowBounded output [0, 1]; robust to extreme outliers
Min-max(x − min) / (max − min)Simple but sensitive to extreme values in the window

For most Type A strategies, rolling Z-score is the right default. Percentile rank is useful when you want a bounded signal (e.g., "top 10% of historical readings") without being affected by extreme outliers that can skew a Z-score.

← Back to Learn