Skip to content

Instantly share code, notes, and snippets.

@mphinance
Created September 1, 2025 18:57
Show Gist options
  • Select an option

  • Save mphinance/c510b77822efba56fc15df9025a62a9e to your computer and use it in GitHub Desktop.

Select an option

Save mphinance/c510b77822efba56fc15df9025a62a9e to your computer and use it in GitHub Desktop.
// This work is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International  
// https://creativecommons.org/licenses/by-nc-sa/4.0/
// © BigBeluga
//@version=6
// 👨‍🏫 MPH NOTE: //@version=6 tells TradingView's servers which version of the Pine Script language to use. Different versions have different features and rules, so this line is essential for the script to run correctly.
// This script is an implementation of a two-pole filter used as an oscillator.
// Unlike a simple moving average, a two-pole filter provides a very smooth output
// with minimal lag, which makes it ideal for creating responsive oscillators.
indicator("Two-Pole Oscillator [BigBeluga]", max_labels_count = 500, max_lines_count = 500)
// 👨‍🏫 MPH NOTE: The indicator() function is the first command in any script. It sets up the basic properties, like its name, and tells TradingView whether it should appear on the main chart (overlay=true) or in a separate panel below it (like this one, since overlay is not set to true).
// 👨‍🏫 MPH NOTE: What is an Oscillator?
// An oscillator is a type of technical indicator that moves back and forth within a given range. They are designed to help you spot when a price might be "overbought" (too high) or "oversold" (too low) so you can look for potential reversals. This particular oscillator is normalized, which means its values are scaled to always stay between -1 and +1, making it easy to read.
// INPUTS ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――{
// This section allows the user to configure key parameters for the indicator.
int   length   = input.int(20, minval=1, title="Filter Length") // This controls the smoothness of the indicator line. A higher value will make the line less reactive to sudden, small price changes, resulting in a "smoother" line. A lower value makes the line more reactive.
bool  disp_lvl = input.bool(true, "Levels") // A simple toggle to show or hide the buy/sell levels on the chart.
color up_color = input.color(#55ffda, "", inline = "color")
color dn_color = input.color(#8c5bff, "", inline = "color")
// 👨‍🏫 MPH NOTE: The var keyword is very important here. It means the variable is only initialized on the very first bar of the chart. On all subsequent bars, it remembers its previous value. This is how we can draw lines that persist across multiple bars. na simply means "not applicable" or "no value yet".
var buy_line = line(na) // A persistent variable to hold a reference to the buy signal line. The 'var' keyword means it's initialized only once.
var sell_line = line(na) // A persistent variable to hold a reference to the sell signal line.
// }
// CALCULATIONS――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――{
// This section contains all the mathematical and logical operations that drive the indicator.
// 👨‍🏫 MPH NOTE: What is a Simple Moving Average (SMA)?
// Think of a Simple Moving Average like a coffee filter. When you pour in the choppy, raw price data, it smooths out some of the lumps and gives you a cleaner result. The ta.sma() function is the built-in command for doing that.
float sma1   = ta.sma(close, 25) // A simple moving average of the close price, used as a baseline for normalization.
// 👨‍🏫 MPH NOTE: This is the most complex part. It normalizes the price.
// Imagine you have a bunch of photos taken in different lighting—some are too dark, some are too bright. To compare them, you adjust their brightness and contrast so they all fit into the same scale from 0 to 100.
// This line does the same thing for price data. It takes a wild, unpredictable price chart and scales it to a perfectly predictable range from -1 to +1, which is perfect for an oscillator.
float sma_n1 = ((close - sma1) - ta.sma(close - sma1, 25)) / ta.stdev(close - sma1, 25) // This is a complex but crucial step. It de-trends the price and then normalizes it using a standard deviation. This transforms the raw price into a value that oscillates around zero, which is the basis for a zero-centered oscillator.
float area   = ta.sma(high-low, 100) // This calculates the average true range of the candles, which is later used to position the buy/sell signal labels.
// Two-pole smooth filter function
// 👨‍🏫 MPH NOTE: This function defines the "double smoothing" process.
// If the SMA is like a single coffee filter, the "two-pole" filter is like running the coffee through that same filter a second time. The first pass gets rid of a lot of the grittiness, and the second pass makes it incredibly smooth and clean. That's the core idea here.
f_two_pole_filter(source, length) =>
    var float smooth1 = na // The first smoothing pass.
    var float smooth2 = na // The second smoothing pass, which creates the final, ultra-smooth output.
    alpha = 2.0 / (length + 1) // 👨‍🏫 MPH NOTE: Think of alpha as the filter's strength. A smaller alpha value means it filters slowly and very smoothly. A larger alpha means it filters faster but isn't as smooth.
    // First smoothing pass: a standard Exponential Moving Average (EMA).
    if na(smooth1)
        smooth1 := source
    else
        smooth1 := (1 - alpha) * smooth1 + alpha * source
    // Second smoothing pass: applying the EMA to the result of the first EMA.
    // This is the core of the "two-pole" filter.
    // IMPORTANT: This second smoothing greatly reduces the lag and makes the oscillator respond quickly to changes, with less of the "overshoot" common in single-pass smoothing.
    if na(smooth2)
        smooth2 := smooth1
    else
        smooth2 := (1 - alpha) * smooth2 + alpha * smooth1
// Oscillator
two_p = f_two_pole_filter(sma_n1, length) // The main oscillator line.
two_pp = two_p[4] // 👨‍🏫 MPH NOTE: two_p[4] is a "lagged" value. Imagine you and a friend are walking, but your friend is always 4 steps behind you. By comparing where you are now (two_p) to where your friend was (two_pp), you can tell if you've crossed paths.
// Colors
// This section uses Pine Script's color.from_gradient function to create smooth color transitions.
color buy_col1  = color.from_gradient(two_p, -1, 0.5, up_color, na)
color buy_col2  = color.from_gradient(two_p, -1, 0.5, color.new(up_color, 50), na)
color sell_col1 = color.from_gradient(two_p, -0.5, 1, na, dn_color)
color sell_col2 = color.from_gradient(two_p, -0.5, 1, na, color.new(dn_color, 50))
color color     = two_p > two_pp 
                  ? color.from_gradient(two_p, -1,1, up_color, color.new(up_color, 0)) 
                  : color.from_gradient(two_p, -1,1,color.new(dn_color, 0), dn_color)
// Signals
// IMPORTANT: This defines the actual buy and sell conditions.
bool buy  = ta.crossover(two_p, two_pp) and two_p < 0 and barstate.isconfirmed // 👨‍🏫 MPH NOTE: A buy signal is when your fast-walking line crosses above your slow-walking friend's line, but only while you're both in the "valley" (the negative zone).
bool sell = ta.crossunder(two_p, two_pp) and two_p > 0 and barstate.isconfirmed // 👨‍🏫 MPH NOTE: A sell signal is when your fast-walking line crosses below your friend's line, but only while you're both on a "mountain peak" (the positive zone).
// 👨‍🏫 MPH NOTE: barstate.isconfirmed is a safety check. It ensures the signal is only triggered after the current candle has officially closed. Think of it as waiting for the final bell to ring before you declare a winner.
// }
// PLOT ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――{
// This section is responsible for all the visual elements plotted on the chart.
// 👨‍🏫 MPH NOTE: bar_index is a built-in variable that counts the bars on the chart, starting from the very beginning. We use it to tell Pine Script where (on which bar) to draw our lines and labels.
if buy //and two_p < -0.5
    sell_line := line(na) // Resets the sell line when a buy signal is found.
    if disp_lvl // A condition to check if the user wants to display the levels.
// 👨‍🏫 MPH NOTE: force_overlay = true is important because this is an oscillator that normally runs in its own panel. This command allows us to draw lines and labels from the oscillator's logic directly onto the main price chart above.
        buy_line := line.new(
                             bar_index-1 // Start the line on the previous bar
                             , low[1] - area // Position the line below the previous low, offset by the average candle range.
                             , bar_index // End the line on the current bar
                             , low[1] - area
                             , force_overlay = true // Force the line to appear on the main chart, not in a sub-pane.
                             , color = buy_col1 // Use the defined buy color.
                             , style = line.style_dashed // Use a dashed style for the line.
                             )
    label.new(bar_index-1, low[1] - area // Plot a tiny label to mark the start of the buy level.
             , color = buy_col1, style = label.style_label_up, force_overlay = true, size = size.tiny)
// This logic checks if the price has crossed below the buy signal line, which could indicate a failed signal or a new downtrend.
if ta.crossunder(low, buy_line.get_y1()) and barstate.isconfirmed
    label.new(
               bar_index-1
             , buy_line.get_y1()
             , color = color.new(up_color, 100)
             , style = label.style_label_center
             , force_overlay = true
             , size = size.large
             , text = "✖"
             , textcolor = up_color
             )
    buy_line := line(na) // Deletes the line after the price has crossed it.
if sell //and two_p > 0.5
    buy_line := line(na) // Resets the buy line when a sell signal is found.
    if disp_lvl
        sell_line := line.new(
                             bar_index-1
                             , high[1] + area // Positions the line above the previous high.
                             , bar_index
                             , high[1] + area
                             , force_overlay = true
                             , color = sell_col1
                             , style = line.style_dashed
                             )
    label.new(bar_index-1, high[1] + area // Plots a tiny label for the sell level.
             , color = sell_col1, style = label.style_label_down, force_overlay = true, size = size.tiny)
// This logic checks if the price has crossed above the sell signal line.
if ta.crossover(high, sell_line.get_y1()) and barstate.isconfirmed
    label.new(
               bar_index-1
             , sell_line.get_y1()
             , color = color.new(dn_color, 100)
             , style = label.style_label_center
             , force_overlay = true
             , size = size.large
             , text = "✖"
             , textcolor = dn_color
             )
    sell_line := line(na) // Deletes the line after the price has crossed it.
// IMPORTANT: This switch statement is a compact way to manage and extend the buy and sell lines on every bar. It checks if a buy_line or sell_line exists, and if it does, it extends its endpoint to the current bar.
switch
    not na(buy_line)  => buy_line. set_x2(bar_index)
    not na(sell_line) => sell_line.set_x2(bar_index)
// 👨‍🏫 MPH NOTE: The plotshape() function is used to draw simple shapes on the chart. Here, we're drawing circles for the buy and sell signals. Two shapes are drawn for each signal with slightly different sizes and colors to create a nice visual "glow" effect.
plotshape(buy ? two_p[1] : na, "Buy", shape.circle, location.absolute, buy_col2, -1, size = size.small) // Plots a small circle for buy signals.
plotshape(buy ? two_p[1] : na, "Buy", shape.circle, location.absolute, buy_col1, -1, size = size.tiny) // Plots a tiny circle on top of the small one for a layered effect.
plotshape(sell ? two_p[1] : na, "Sell", shape.circle, location.absolute, sell_col2, -1, size = size.small) // Plots a small circle for sell signals.
plotshape(sell ? two_p[1] : na, "Sell", shape.circle, location.absolute, sell_col1, -1, size = size.tiny) // Plots a tiny circle on top of the small one for a layered effect.
p11 = plot(1, color = color.new(chart.fg_color, 80)) // Plot of the +1 level.
plot(0.5, color = color.new(chart.fg_color, 50)) // Plot of the +0.5 level.
p00 = plot(0, color = color.new(bar_index % 2 == 0 ? chart.fg_color : na, 0)) // Plot of the 0 level.
plot(-0.5, color = color.new(chart.fg_color, 50)) // Plot of the -0.5 level.
p_1 = plot(-1, color = color.new(chart.fg_color, 80)) // Plot of the -1 level.
// 👨‍🏫 MPH NOTE: The fixed levels (+1, +0.5, 0, -0.5, -1) show that this is a normalized oscillator. Its values will always stay within this range, which makes it easy to spot overbought and oversold conditions.
fill(p11, p00, 2, -1, color.new(chart.fg_color, 80), na) // Fills the area from +1 down to 0.
fill(p_1, p00, 1, -2, na, color.new(chart.fg_color, 80)) // Fills the area from -1 up to 0.
p1 = plot(two_p, color = color, linewidth = 1) // Plots the main oscillator line.
p2 = plot(two_pp, display = display.none) // A hidden plot of the lagged oscillator line, used only for the fill.
fill(p1, p2, two_p, two_pp, color, na) // Fills the area between the main oscillator and its lagged version to create the visual effect.
// }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment