Been working on a “Sentiment Pulse Oscillator” for a little while now and just wanted to share it with everyone. This indicator combines RSI, CCI, and OBV into one normalized reading between –1 and +1. It smooths the result, shades the background into sentiment zones, and plots vertical lines labeled “BUY” or “SELL” whenever the sentiment crosses into strong bullish or bearish alignment. The idea is to simplify multiple indicators into a single clean view of momentum, trend, and volume pressure. It’s been working well in my testing, but I know it’s far from perfect, so I’d love any suggestions on how to make it more accurate, responsive, or visually clear.
Here is the code:

Here is the code:
Ruby:
declare lower;
# === INPUTS ===
input rsiLength = 14;
input cciLength = 20;
input obvLength = 20;
input cciNormFactor = 200.0; # Scale for CCI --> [-1,1]
input zScale = 3.0; # number of stdevs to map OBV z-score
input wRSI = 1.0;
input wCCI = 1.0;
input wOBV = 1.0;
input smoothlength = 3; # Smoothing for final oscillator
input strongThreshold = 0.7;
input mildThreshold = 0.15;
input showcomponents = yes;
input showclouds = yes;
# === Raw component series ===
def rsi = RSI(rsilength);
def cci = CCI(CCIlength);
def obv = onbalancevolume();
# === RSI normalization: 0..100 -> -1..1 ===
def rsiNorm = (rsi - 50) /50;
def rsiNormClip = if rsinorm > 1 then 1 else if rsinorm < -1 then -1 else rsiNorm;
# === CCI normalization: divide by factor then clip ===
def ccinorm = cci / ccinormFactor;
def ccinormclip = if ccinorm > 1 then 1 else if ccinorm < -1 then -1 else ccinorm;
# === OBV normalization via z-score then scale & clip ===
def obvMean = Average(obv, obvlength);
def obvStDev = stdev(obv, obvlength);
def obvz = if obvstdev != 0 then (obv - obvmean) / obvstdev else 0;
def obvnorm = obvz / zscale;
def obvnormclip = if obvnorm > 1 then 1 else if obvnorm < -1 then -1 else obvnorm;
# === Combine with weights ===
def totalweight = wrsi + wcci + wobv;
def sentRaw = (wrsi * rsinormclip + wcci *ccinormclip + wobv * obvnormclip) / totalweight;
# === Smoothlength & final clip to [-1, 1] ===
def sent = expaverage(sentraw, smoothlength);
def sentfinal = if sent > 1 then 1 else if sent < -1 then -1 else sent;
# === Main plot ===
plot sentiment = sentfinal;
sentiment.setDefaultColor(color.cyan);
sentiment.setLineWeight(3);
# === Zero Line ===
plot zeroline = 0;
zeroline.setDefaultColor(color.white);
Zeroline.SetLineWeight(3);
# === Optional component plots ===
plot RSI_Line = if showComponents then rsiNormClip else Double.NaN;
RSI_Line.SetDefaultColor(Color.YELLOW);
RSI_Line.SetLineWeight(1);
plot CCI_Line = if showComponents then cciNormClip else Double.NaN;
CCI_Line.SetDefaultColor(Color.MAGENTA);
CCI_Line.SetLineWeight(1);
plot OBV_Line = if showComponents then obvNormClip else Double.NaN;
OBV_Line.SetDefaultColor(Color.RED);
OBV_Line.SetLineWeight(1);
# === Clouds (5 zones) ===
plot top1 = if showClouds then 1 else Double.NaN;
plot topStrong = if showClouds then strongThreshold else Double.NaN;
plot topMild = if showClouds then mildThreshold else Double.NaN;
plot botMild = if showClouds then -mildThreshold else Double.NaN;
plot botStrong = if showClouds then -strongThreshold else Double.NaN;
plot bot1 = if showClouds then -1 else Double.NaN;
# Strong Bull (0.7..1)
AddCloud(topStrong, top1, Color.DARK_GREEN, Color.DARK_GREEN);
# Mild Bull (0.15..0.7)
AddCloud(topMild, topStrong, Color.LIGHT_GREEN, Color.LIGHT_GREEN);
# Neutral (-0.15..0.15)
AddCloud(botMild, topMild, Color.LIGHT_GRAY, Color.LIGHT_GRAY);
# Mild Bear (-0.7..-0.15)
AddCloud(botStrong, botMild, Color.PINK, Color.PINK);
# Strong Bear (-1..-0.7)
AddCloud(bot1, botStrong, Color.DARK_RED, Color.DARK_RED);
# === Strong alignment cross detection ===
def prevSent = sentFinal[1];
def crossStrongUp = prevSent < strongThreshold and sentFinal >= strongThreshold;
def crossStrongDown = prevSent > -strongThreshold and sentFinal <= -strongThreshold;
# === Vertical lines ===
AddVerticalLine(crossStrongUp, "BUY", Color.GREEN, Curve.SHORT_DASH);
AddVerticalLine(crossStrongDown, "SELL", Color.RED, Curve.SHORT_DASH);
# === Label ===
AddLabel(yes, "Sentiment: " + Round(sentFinal, 2), if sentFinal > 0 then Color.GREEN else if sentFinal < 0 then Color.RED else Color.GRAY);
Last edited by a moderator: