Indicator · Pine Script v6 · On the chart

Gann Square of Nine: Planetary Degrees

by blueprintresearch-dev· code credited to BlueprintResearch / Javonnii

This indicator maps planetary positions onto W.D.

View on GitHub

GitHub stars42on the repository
Pine versionv6current
Lines of code812
Settings38inputs
LicenceNone statedfile: MPL-2.0
Last update2 weeks ago12 Oct 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart, then adjust the settings with the gear icon next to its name.

Source Code

812 lines of Pine Script v6 from scripts/gann square of nine: planetary degrees.pine, published under MPL-2.0 (file header). Keep the author's credit and licence lines when you reuse it.

gann square of nine: planetary degrees.pine
//   ════════════════════════════════════════════════════════════
//      BLUEPRINT RESEARCH • GANN SQUARE OF NINE: PLANETARY DEGREES
//   ════════════════════════════════════════════════════════════
//
//                  ★       ★       ★
//                      ╱   │   ╲
//                     ─    ☉    ─
//                      ╲   │   ╱
//                  ★       ★       ★
//
//   ════════════════════════════════════════════════════════════
//
// @description  Maps planetary positions to Gann's Square of Nine grid
//               with pivot high/low degree tracking, target degree alerts,
//               and future degree projections. Supports both geocentric
//               and heliocentric calculations.
//
// @author       BlueprintResearch (Javonnii)
// @version      2.0
//
// @license      DUAL LICENSE:
//
//               ▸ SQUARE OF NINE TABLE IMPLEMENTATION (Lines ~455-655):
//                 Mozilla Public License 2.0 (MPL 2.0)
//                 Adapted from ThiagoSchmitz's "Gann Square of 9" (Feb 2023)
//                 https://www.tradingview.com/script/cSzck7Kl-Gann-Square-of-9/
//
//               ▸ ALL OTHER CODE:
//                 Creative Commons Attribution-NonCommercial-ShareAlike 4.0
//                 (CC BY-NC-SA 4.0)
//                 https://creativecommons.org/licenses/by-nc-sa/4.0/
//                 © 2026 BlueprintResearch / Javonnii
//
// @theory       Uses VSOP87, ELP2000-82, and Meeus algorithms via
//               Blueprint Research ephemeris libraries for accurate
//               planetary longitude calculations.
//
// @features     • Square of Nine grid with planetary degree mapping
//               • Pivot high/low detection with degree labels
//               • User-defined target degree alerts with bgcolor
//               • Future degree projections (up to 500 bars)
//               • Retrograde indicator (red text on So9)
//               • Geocentric and heliocentric modes
//
// @accuracy     High accuracy using VSOP87/ELP2000-82 truncated series.
//               Within 1-10 arcseconds for most planets.
//
// @open_source  This library is part of an open-source alternative to
//               proprietary astronomical libraries. Study, modify, and
//               share freely. We believe knowledge of the cosmos belongs
//               to everyone.
//
// @credits      Square of Nine grid visualization adapted from
//               ThiagoSchmitz's "Gann Square of 9" (Feb 2023)
//               https://www.tradingview.com/script/cSzck7Kl-Gann-Square-of-9/
//
// ════════════════════════════════════════════════════════════════
//   © 2026 BlueprintResearch / Javonnii
//   DUAL LICENSE: CC BY-NC-SA 4.0 (original code) + MPL 2.0 (So9 table)
// ════════════════════════════════════════════════════════════════
//


//@version=6
indicator("Gann Square of Nine: Planetary Degrees", overlay=true, max_labels_count=500, max_bars_back=500, max_lines_count=500)

// ═══════════════════════════════════════════════════════════════════
// Library Imports
// ═══════════════════════════════════════════════════════════════════
import BlueprintResearch/lib_ephemeris/1 as eph

// ═══════════════════════════════════════════════════════════════════════════
// SECTION: BLUEPRINT RESEARCH ORIGINAL CODE
// LICENSE: CC BY-NC-SA 4.0 - Non-Commercial Use Only
// © 2026 BlueprintResearch / Javonnii
// ═══════════════════════════════════════════════════════════════════════════

// ═══════════════════════════════════════════════════════════════════
// User Inputs
// ═══════════════════════════════════════════════════════════════════

// Tooltips
tooltip_anchor = "Select the timestamp of a significant high or low pivot on any timeframe. Pivots generated after this timestamp will be plotted on the So9."
tooltip_pivot_type = "Select based on the pivot at your timestamp. High for downward spiral, Low for upward."
tooltip_pivot_sens = "Pivot Point Highs: Determined by counting bars with lower highs on both sides of a high point. Pivot Point Lows: Determined by counting bars with higher lows on both sides of a low point."
tooltip_overlay = "Enter degree to plot square overlay. This makes it easier to draw lines to track angle relationship from the entered degree."

// ─────────────────────────────────────────────────────────────────────
// 1. Anchor Point
// ─────────────────────────────────────────────────────────────────────
group1 = "1. Anchor Point"

dateTime = input.time(0, "Anchor Timestamp", group=group1, confirm=true, tooltip=tooltip_anchor)
selectHL = input.string("Low", "Pivot Type", options=["High", "Low"], group=group1, tooltip=tooltip_pivot_type)

// ─────────────────────────────────────────────────────────────────────
// 2. Planet Selection
// ─────────────────────────────────────────────────────────────────────
group2 = "2. Planet Selection"

planet_in = input.string("☉︎ Sun", "Planet",
    options=["☉︎ Sun", "☽︎ Moon", "☿ Mercury", "♀ Venus", "🜨 Earth", "♂ Mars", "♃ Jupiter", "♄ Saturn", "⛢ Uranus", "♆ Neptune", "♇ Pluto"],
    group=group2)
geo = input.bool(true, "Geocentric", group=group2, tooltip="Unchecked for heliocentric coordinates")

// ─────────────────────────────────────────────────────────────────────
// 3. Target Degree Alerts
// ─────────────────────────────────────────────────────────────────────
group3 = "3. Target Degree Alerts"

preset_degrees = input.string("None", "Preset So9 Degrees",
    options=["None", "0° Zero", "45°", "90°", "135°", "180°", "225°", "270°", "315°"],
    group=group3,
    tooltip="Select a preset So9 degree list, or choose 'None' to use only custom degrees")

target_degrees_str = input.text_area("", "Custom Degrees",
    group=group3,
    tooltip="Enter custom degrees separated by commas or newlines.\nThese are combined with preset degrees above.")

show_degree_alerts = input.bool(true, "Enable Degree Alerts",
    group=group3,
    tooltip="Trigger alerts when planet crosses target degrees")

show_future_degrees = input.bool(true, "Show Future Degree Projections",
    group=group3,
    tooltip="Display when planet will reach each target degree")

look_ahead = input.int(250, "Bars to Look Ahead",
    minval=50, maxval=500,
    group=group3,
    tooltip="Number of future bars to scan for degree arrivals")

tz_input = input.string("UTC", "Timezone for Future Dates",
    options=["UTC", "America/New_York", "America/Chicago", "America/Denver",
             "America/Los_Angeles", "Europe/London", "Europe/Paris",
             "Asia/Tokyo", "Asia/Hong_Kong", "Australia/Sydney"],
    group=group3,
    tooltip="Timezone for formatting future date labels")

show_future_datetime = input.bool(true, "Show Date/Time Labels",
    group=group3,
    tooltip="Toggle visibility of date/time labels on future projection markers")

future_line_color = input.color(color.yellow, "Future Projection Color",
    group=group3, inline="proj_colors")

crossed_line_color = input.color(color.gray, "Already Crossed Color",
    group=group3, inline="proj_colors",
    tooltip="Color for degrees that have already been crossed since anchor")

// ─────────────────────────────────────────────────────────────────────
// 4. Pivot Detection
// ─────────────────────────────────────────────────────────────────────
group4 = "4. Pivot Detection"

pivotHighBars = input.int(title="Pivot High Bars", defval=10, minval=1, group=group4, tooltip=tooltip_pivot_sens)
pivotLowBars = input.int(title="Pivot Low Bars", defval=10, minval=1, group=group4)

hide_pivot_labels = input.bool(false, "Hide Pivot Labels", group=group4)
rounding = input.int(0, "Degree Rounding Precision", minval=0, group=group4)

// ─────────────────────────────────────────────────────────────────────
// 5. Visual Style
// ─────────────────────────────────────────────────────────────────────
group5 = "5. Visual Style"

labelColorH = input.color(color.lime, "High Pivot Color", group=group5, inline="pivot_colors")
labelColorL = input.color(color.red, "Low Pivot Color", group=group5, inline="pivot_colors")
labelColorS = input.color(color.orange, "Equal Pivots", group=group5, inline="pivot_colors")

TextColor = input.color(#000000, "So9 Text Color", group=group5)

iTxtSize = input.string("Large", "Label Text Size", options=["Auto", "Tiny", "Small", "Normal", "Large"], group=group5)
vTxtSize = iTxtSize == "Auto" ? size.auto : iTxtSize == "Tiny" ? size.tiny : iTxtSize == "Small" ? size.small : iTxtSize == "Normal" ? size.normal : iTxtSize == "Large" ? size.large : size.small

// ─────────────────────────────────────────────────────────────────────
// 6. So9 Grid Overlay
// ─────────────────────────────────────────────────────────────────────
group6 = "6. So9 Grid Overlay"

overlay = input.int(0, "Overlay Pivot Degree", group=group6, tooltip=tooltip_overlay)

plotSquareOL = input.bool(false, "Show Square Overlay", group=group6, inline="square_ol")
SquareOLCol = input.color(color.white, "", group=group6, inline="square_ol")

plotFortyFiveOL = input.bool(false, "Show 45° Overlay", group=group6, inline="45_ol", tooltip="Plot the 45 degree angles from entered degree")
FortyFiveOLCol = input.color(color.aqua, "", group=group6, inline="45_ol")

// ─────────────────────────────────────────────────────────────────────
// 7. So9 Table
// ─────────────────────────────────────────────────────────────────────
group7 = "7. So9 Table"

var int levels = 9
var string tablePosition = input.string("Top Right", "Table Position",
    options=["Top Right", "Middle Right", "Bottom Right", "Top Left", "Middle Left", "Bottom Left"],
    group=group7)

var float width = input.float(0, "Cell Width", group=group7, inline="cell_size")
var float height = input.float(0, "Cell Height", group=group7, inline="cell_size")

var int frameWidth = input.int(1, "Frame Width", group=group7)

var color backgroundColor = input.color(color.new(color.black, 100), "Background", group=group7, inline="table_colors")
var color frameColor = input.color(color.new(color.white, 40), "Frame", group=group7, inline="table_colors")

var string tableTextSize = input.string(size.normal, "Table Font Size",
    options=[size.auto, size.tiny, size.small, size.normal, size.large, size.huge],
    group=group7, inline="table_text")
var color tableTextColor = input.color(color.new(color.white, 0), "", group=group7, inline="table_text")

// ─────────────────────────────────────────────────────────────────────
// 8. So9 Diagonals
// ─────────────────────────────────────────────────────────────────────
group8 = "8. So9 Diagonals"

var bool showDiagonal = input.bool(true, "Show Diagonals", group=group8)
var color diagonalColor = input.color(color.new(#004b8d, 35), "Diagonal Cross", group=group8, inline="diag_colors")
var color cardinalColor = input.color(color.new(#801922, 35), "Cardinal Cross", group=group8, inline="diag_colors")

// ═══════════════════════════════════════════════════════════════════
// Constants
// ═══════════════════════════════════════════════════════════════════
increase = 1

// Extract the first character from planet string
planet_symbol = str.substring(planet_in, 0, 1)
planet_string = planet_symbol

centricSign = geo ? "𝒢" : "ℋ"

// Persistent variables
var bool dateTimeReached = false
var float[] pivotHighsArray = array.new_float()
var float[] pivotLowsArray = array.new_float()
var float anchor_degree = na
var float prev_planet_long = na
var array<float> crossed_degrees_history = array.new_float()
var bool any_degree_crossed = false

col_out = planet_in == "☉︎ Sun" ? color.yellow : planet_in == "☽︎ Moon" ? color.silver : planet_in == "☿ Mercury" ? color.maroon : planet_in == "♀ Venus" ? color.green : planet_in == "🜨 Earth" ? color.blue : planet_in == "♂ Mars" ? color.red : planet_in == "♃ Jupiter" ? color.orange : planet_in == "♄ Saturn" ? color.gray : planet_in == "⛢ Uranus" ? color.navy : planet_in == "♆ Neptune" ? color.yellow : planet_in == "♇ Pluto" ? color.black : na

// ═══════════════════════════════════════════════════════════════════
// Helper Functions
// ═══════════════════════════════════════════════════════════════════

// Function to get planet longitude using lib_ephemeris
f_get_planet_longitude(string planet_str, bool use_geo, int timestamp_ms) =>
    planet_enum = eph.string_to_planet(planet_str)
    eph.get_longitude(planet_enum, timestamp_ms, use_geo)

// Function to get planet speed (for retrograde detection)
f_get_planet_speed(string planet_str, int timestamp_ms) =>
    planet_enum = eph.string_to_planet(planet_str)
    eph.get_speed(planet_enum, timestamp_ms)

// Check if planet can retrograde (Sun, Moon, Earth cannot from geocentric view)
can_retrograde(string planet_str) =>
    not (str.contains(planet_str, "Sun") or str.contains(planet_str, "Moon") or str.contains(planet_str, "Earth"))

// Parse degree string (handles commas and newlines)
parse_degrees(string deg_str) =>
    array<float> degrees = array.new_float()
    if str.length(deg_str) > 0
        // Replace newlines with commas for unified parsing
        normalized_str = str.replace_all(deg_str, "\n", ",")
        parts = str.split(normalized_str, ",")
        for i = 0 to array.size(parts) - 1
            part = str.trim(array.get(parts, i))
            if str.length(part) > 0
                deg_val = str.tonumber(part)
                if not na(deg_val)
                    // Normalize to 0-360 range
                    normalized_deg = deg_val % 360
                    if normalized_deg < 0
                        normalized_deg += 360
                    array.push(degrees, normalized_deg)
    degrees

// Get predefined So9 degree array based on dropdown selection
get_preset_degrees(string preset) =>
    array<float> degrees = array.new_float()
    switch preset
        "0° Zero" =>
            array.push(degrees, 2.0), array.push(degrees, 11.0), array.push(degrees, 28.0)
            array.push(degrees, 53.0), array.push(degrees, 86.0), array.push(degrees, 127.0)
            array.push(degrees, 176.0), array.push(degrees, 233.0), array.push(degrees, 298.0)
        "45°" =>
            array.push(degrees, 3.0), array.push(degrees, 13.0), array.push(degrees, 31.0)
            array.push(degrees, 57.0), array.push(degrees, 91.0), array.push(degrees, 133.0)
            array.push(degrees, 183.0), array.push(degrees, 241.0), array.push(degrees, 307.0)
        "90°" =>
            array.push(degrees, 4.0), array.push(degrees, 15.0), array.push(degrees, 34.0)
            array.push(degrees, 61.0), array.push(degrees, 96.0), array.push(degrees, 139.0)
            array.push(degrees, 190.0), array.push(degrees, 249.0), array.push(degrees, 316.0)
        "135°" =>
            array.push(degrees, 5.0), array.push(degrees, 17.0), array.push(degrees, 37.0)
            array.push(degrees, 65.0), array.push(degrees, 101.0), array.push(degrees, 145.0)
            array.push(degrees, 197.0), array.push(degrees, 257.0), array.push(degrees, 325.0)
        "180°" =>
            array.push(degrees, 6.0), array.push(degrees, 19.0), array.push(degrees, 40.0)
            array.push(degrees, 69.0), array.push(degrees, 106.0), array.push(degrees, 151.0)
            array.push(degrees, 204.0), array.push(degrees, 265.0), array.push(degrees, 334.0)
        "225°" =>
            array.push(degrees, 7.0), array.push(degrees, 21.0), array.push(degrees, 43.0)
            array.push(degrees, 73.0), array.push(degrees, 111.0), array.push(degrees, 157.0)
            array.push(degrees, 211.0), array.push(degrees, 273.0), array.push(degrees, 343.0)
        "270°" =>
            array.push(degrees, 8.0), array.push(degrees, 23.0), array.push(degrees, 46.0)
            array.push(degrees, 77.0), array.push(degrees, 116.0), array.push(degrees, 163.0)
            array.push(degrees, 218.0), array.push(degrees, 281.0), array.push(degrees, 352.0)
        "315°" =>
            array.push(degrees, 9.0), array.push(degrees, 25.0), array.push(degrees, 49.0)
            array.push(degrees, 81.0), array.push(degrees, 121.0), array.push(degrees, 169.0)
            array.push(degrees, 225.0), array.push(degrees, 289.0)
    degrees

// Detect if longitude crossed a target degree between two values
// Handles 360/0 boundary wrapping and both direct/retrograde motion
degree_crossed(float prev_lon, float curr_lon, float target_deg) =>
    result = false
    if not na(prev_lon) and not na(curr_lon)
        // Normalize all values to 0-360
        prev = prev_lon % 360
        prev := prev < 0 ? prev + 360 : prev
        curr = curr_lon % 360
        curr := curr < 0 ? curr + 360 : curr
        target = target_deg % 360
        target := target < 0 ? target + 360 : target

        // Calculate movement delta
        delta = curr - prev

        // Handle wrap-around (planet crossed 360/0 boundary)
        if delta > 180
            delta -= 360
        else if delta < -180
            delta += 360

        // Check if target was crossed
        if delta > 0  // Forward motion
            // Normalize target relative to prev
            t_rel = target - prev
            if t_rel < 0
                t_rel += 360
            if t_rel <= delta and t_rel > 0
                result := true
        else if delta < 0  // Retrograde motion
            // Normalize target relative to curr
            t_rel = prev - target
            if t_rel < 0
                t_rel += 360
            if t_rel <= math.abs(delta) and t_rel > 0
                result := true
    result

// Calculate pivot highs and lows
ph = ta.pivothigh(pivotHighBars, pivotHighBars)
pl = ta.pivotlow(pivotLowBars, pivotLowBars)

var float planet_long = na
// Combine preset degrees with custom degrees
var array<float> target_degrees = array.new_float()
preset_deg_array = get_preset_degrees(preset_degrees)
custom_deg_array = parse_degrees(target_degrees_str)

// Add preset degrees
if array.size(preset_deg_array) > 0
    for i = 0 to array.size(preset_deg_array) - 1
        deg = array.get(preset_deg_array, i)
        if not array.includes(target_degrees, deg)
            array.push(target_degrees, deg)

// Add custom degrees (avoiding duplicates)
if array.size(custom_deg_array) > 0
    for i = 0 to array.size(custom_deg_array) - 1
        deg = array.get(custom_deg_array, i)
        if not array.includes(target_degrees, deg)
            array.push(target_degrees, deg)

// Check if this planet can retrograde
planet_can_retro = can_retrograde(planet_in)

if (time > dateTime)  // Exclude labels when time == dateTime
    // Calculate planetary degree
    planet_long := f_get_planet_longitude(planet_in, geo, time)

    // Only calculate degrees if it's after dateTime
    degree1 = math.round(planet_long[pivotHighBars], rounding)
    degree2 = math.round(planet_long[pivotLowBars], rounding)

    // Handle degree 0 as 360
    degree1 := degree1 == 0 ? 360 : degree1
    degree2 := degree2 == 0 ? 360 : degree2

    // **For Pivots High**
    if not na(ph) and not na(degree1)
        array.push(pivotHighsArray, degree1)
        if not hide_pivot_labels
            label.new(bar_index[pivotHighBars], ph, str.tostring(degree1) + "°\n" + planet_string, style=label.style_label_down, textcolor=labelColorH, size=vTxtSize, color=color.new(labelColorH, 100))

    // **For Pivots Low**
    if not na(pl) and not na(degree2)
        array.push(pivotLowsArray, degree2)
        if not hide_pivot_labels
            label.new(bar_index[pivotLowBars], pl, planet_string + "\n" + str.tostring(degree2) + "°", style=label.style_label_up, color=color.new(labelColorH, 100), textcolor=labelColorL, size=vTxtSize)

// ═══════════════════════════════════════════════════════════════════
// Retrograde Detection
// ═══════════════════════════════════════════════════════════════════
// Get speed for retrograde detection (negative speed = retrograde)
// Sun, Moon, Earth cannot retrograde from geocentric perspective
planet_speed = planet_can_retro ? f_get_planet_speed(planet_in, time) : 0.0
is_retrograde = planet_can_retro and planet_speed < 0

// ═══════════════════════════════════════════════════════════════════
// Target Degree Crossing Detection
// ═══════════════════════════════════════════════════════════════════
// Reset the crossing flag each bar
any_degree_crossed := false

// Reset historical tracking on anchor bar
if time == dateTime
    array.clear(crossed_degrees_history)

// Check for degree crossings after anchor
if time > dateTime and not na(prev_planet_long) and not na(planet_long) and array.size(target_degrees) > 0
    for i = 0 to array.size(target_degrees) - 1
        target_deg = array.get(target_degrees, i)
        if degree_crossed(prev_planet_long, planet_long, target_deg)
            any_degree_crossed := true
            // Track historical crossings (only add if not already present)
            if not array.includes(crossed_degrees_history, target_deg)
                array.push(crossed_degrees_history, target_deg)
            // Dynamic alert with specific degree
            if show_degree_alerts
                alert(planet_string + " crossed " + str.tostring(target_deg) + "° on " + syminfo.ticker, alert.freq_once_per_bar_close)

// Update previous value for next bar
prev_planet_long := planet_long

// Background color for bars where target degrees were crossed
bgcolor(any_degree_crossed ? color.new(future_line_color, 50) : na)

if (time == dateTime)
    dateTimeReached := true
    planet_long_ = f_get_planet_longitude(planet_in, geo, time)

    anchor_degree := math.round(planet_long_, rounding)
    anchor_degree := anchor_degree == 0 ? 360 : anchor_degree

    // Adjust the label position higher above the high
    label_y_position = high + (low * 0.0333)

    // Create the label
    if not hide_pivot_labels
        anchor_label = label.new(bar_index, label_y_position, str.tostring(anchor_degree) + "°" + "\n" + centricSign +  " " + planet_string + "\n" + "⚓", style=label.style_label_down, textcolor=SquareOLCol, size=vTxtSize, color=color.new(labelColorH, 100))

        // Create a vertical line from high to the label
        line.new(bar_index, low, bar_index, label_y_position, width=1, color=color.new(SquareOLCol, 0), style=line.style_dashed)

// ═══════════════════════════════════════════════════════════════════════════
// SECTION: SQUARE OF NINE TABLE IMPLEMENTATION
// LICENSE: Mozilla Public License 2.0 (MPL 2.0)
// ATTRIBUTION: Adapted from ThiagoSchmitz's "Gann Square of 9" (Feb 2023)
//              https://www.tradingview.com/script/cSzck7Kl-Gann-Square-of-9/
// SCOPE: Table type, building functions, spiral algorithm, and visualization
// ═══════════════════════════════════════════════════════════════════════════

type tableCells
    int column
    int row

var string position = switch tablePosition
    "Top Right" => position.top_right
    "Middle Right" => position.middle_right
    "Bottom Right" => position.bottom_right
    "Top Left" => position.top_left
    "Middle Left" => position.middle_left
    => position.bottom_left
var int size = levels * 2 + 1
var table tab = table.new(position, size, size, backgroundColor, frameColor, frameWidth, frameColor, frameWidth)
var tableCells[] coordinates = array.new<tableCells>()
var float[] values = array.new_float()
var float[] diagonalValues = array.new_float()

// Functions
createCell(int c, int r, float v, color bgcolor=na) =>
    table.cell(tab, c, r, str.tostring(v), width, height, tableTextColor, text_size=tableTextSize, bgcolor=bgcolor)

isDiagonal(int c, int l) => c % l == 1

setCenterRowAndColumnBackground(table tab, int size, color bgcolor) =>
    int center = math.floor(size / 2)
    // Set the center column background
    for r = 0 to size - 1
        table.cell_set_bgcolor(tab, center, r, bgcolor)
    // Set the center row background
    for c = 0 to size - 1
        table.cell_set_bgcolor(tab, c, center, bgcolor)

getDiagonalColor() => showDiagonal ? diagonalColor : na

createCellWithValueAndColor(int column, int row, float value, color col) =>
    createCell(column, row, value, col)
    pivot = tableCells.new(column, row)
    array.push(coordinates, pivot)
    array.push(values, value)

overlay_plot(time_, degree) =>
    _sqrt = math.sqrt(time_)
    _deg_value = _sqrt + (degree)
    _result = (_deg_value * _deg_value)
    _result

// Build Square Table
var float[] overlayValues = array.new_float()
var float[] overlayValues2 = array.new_float()

if (time == dateTime)
    int currentColumn = levels
    int currentRow = levels
    int doubleLevels = levels * 2
    float startingValue = 1.0
    int count = 1
    int layer = 2

    if selectHL == "Low"
        createCell(currentColumn, currentRow, startingValue, diagonalColor)
        array.push(diagonalValues, startingValue)

        for i = 1 to doubleLevels by 1
            int sign = i % 2 == 1 ? -1 : 1
            int limit = i == 1 ? i / 2 : i

            for j = 0 to i * 2 - 1 by 1
                startingValue := startingValue + increase
                count := count + 1
                layer := math.sqrt(count) == layer + 1 ? layer + 2 : layer
                isDiagonal = isDiagonal(count, layer)
                col = isDiagonal ? getDiagonalColor() : na
                currentColumn := j < limit ? currentColumn + sign : currentColumn
                currentRow := j >= limit ? currentRow + sign : currentRow
                createCellWithValueAndColor(currentColumn, currentRow, startingValue, col)
                if isDiagonal
                    array.push(diagonalValues, startingValue)

            if i == doubleLevels
                for j = 0 to i - 1 by 1
                    startingValue := startingValue + increase
                    count := count + 1
                    isDiagonal = isDiagonal(count, layer)
                    col = isDiagonal ? getDiagonalColor() : na
                    currentColumn := currentColumn - sign
                    createCellWithValueAndColor(currentColumn, currentRow, startingValue, col)
                    if isDiagonal
                        array.push(diagonalValues, startingValue)

    if selectHL == "High"
        createCell(currentColumn, currentRow, startingValue, diagonalColor)
        array.push(diagonalValues, startingValue)

        for i = 1 to doubleLevels by 1
            int sign = i % 2 == 1 ? -1 : 1
            int limit = i == 1 ? i / 2 : i

            for j = 0 to i * 2 - 1 by 1
                startingValue := startingValue + increase
                if startingValue < 0  // Prevent negative values for "High"
                    break
                count := count + 1
                layer := math.sqrt(count) == layer + 1 ? layer + 2 : layer
                isDiagonal = isDiagonal(count, layer)
                col = isDiagonal ? getDiagonalColor() : na
                currentColumn := j < limit ? currentColumn + sign : currentColumn
                currentRow := j >= limit ? currentRow + sign : currentRow
                createCellWithValueAndColor(currentColumn, currentRow, startingValue, col)
                if isDiagonal
                    array.push(diagonalValues, startingValue)

            if i == doubleLevels
                for j = 0 to i - 1 by 1
                    startingValue := startingValue + increase
                    if startingValue < 0  // Prevent negative values for "High"
                        break
                    count := count + 1
                    isDiagonal = isDiagonal(count, layer)
                    col = isDiagonal ? getDiagonalColor() : na
                    currentColumn := currentColumn - sign
                    createCellWithValueAndColor(currentColumn, currentRow, startingValue, col)
                    if isDiagonal
                        array.push(diagonalValues, startingValue)

    setCenterRowAndColumnBackground(tab, size, cardinalColor)

if barstate.islast
    // Highlight target degrees on So9 grid (rendered first so pivots/anchor can override)
    if array.size(target_degrees) > 0
        for i = 0 to array.size(target_degrees) - 1
            target_deg = array.get(target_degrees, i)
            target_cell_idx = array.indexof(values, target_deg)
            if target_cell_idx >= 0
                coord = array.get(coordinates, target_cell_idx)
                table.cell_set_bgcolor(tab, coord.column, coord.row, color.new(future_line_color, 50))

    // Process High Pivots
    if array.size(pivotHighsArray) > 0
        for i = 0 to array.size(pivotHighsArray) - 1
            pivotHigh = array.get(pivotHighsArray, i)
            pivot_high_cell = array.indexof(values, pivotHigh)
            if pivot_high_cell >= 0
                coord = array.get(coordinates, pivot_high_cell)
                if array.includes(pivotLowsArray, pivotHigh)
                    table.cell_set_bgcolor(tab, coord.column, coord.row, labelColorS)
                else
                    table.cell_set_bgcolor(tab, coord.column, coord.row, labelColorH)
                table.cell_set_text_color(tab, coord.column, coord.row, TextColor)

    // Process Low Pivots
    if array.size(pivotLowsArray) > 0
        for i = 0 to array.size(pivotLowsArray) - 1
            pivotLow = array.get(pivotLowsArray, i)
            pivot_low_cell = array.indexof(values, pivotLow)
            if pivot_low_cell >= 0
                coord = array.get(coordinates, pivot_low_cell)
                if array.includes(pivotHighsArray, pivotLow)
                    table.cell_set_bgcolor(tab, coord.column, coord.row, labelColorS)
                else
                    table.cell_set_bgcolor(tab, coord.column, coord.row, labelColorL)
                table.cell_set_text_color(tab, coord.column, coord.row, TextColor)

    anchor_deg = array.indexof(values, anchor_degree)
    if anchor_deg >= 0
        coord = array.get(coordinates, anchor_deg)
        table.cell_set_bgcolor(tab, coord.column, coord.row, SquareOLCol)
        table.cell_set_text_color(tab, coord.column, coord.row, TextColor)
        table.cell_set_text(tab, coord.column, coord.row, text="⚓")

    square_overlay = array.from(.5, 1, 1.5, 2)
    forty_five = array.from(.25, .75, 1.25, 1.75)

    if overlay > 0 and plotSquareOL
        for i = 0 to array.size(square_overlay) - 1
            deg = array.get(square_overlay, i)
            result = overlay_plot(overlay, deg)
            array.push(overlayValues, result)

        for i = 0 to array.size(overlayValues) - 1
            cell_time = array.get(overlayValues, i)
            index = array.indexof(values, math.floor(cell_time))
            if index >= 0
                coord = array.get(coordinates, index)
                table.cell_set_bgcolor(tab, coord.column, coord.row, bgcolor=SquareOLCol)
                table.cell_set_text_color(tab, coord.column, coord.row, text_color=TextColor)

    if overlay > 0 and plotFortyFiveOL
        for i = 0 to array.size(forty_five) - 1
            deg = array.get(forty_five, i)
            result = overlay_plot(overlay, deg)
            array.push(overlayValues2, result)

        for i = 0 to array.size(overlayValues2) - 1
            cell_time = array.get(overlayValues2, i)
            index = array.indexof(values, math.floor(cell_time))
            if index >= 0
                coord = array.get(coordinates, index)
                table.cell_set_bgcolor(tab, coord.column, coord.row, bgcolor=FortyFiveOLCol)
                table.cell_set_text_color(tab, coord.column, coord.row, text_color=TextColor)

if barstate.isrealtime or barstate.islast
    current_planet_degree = planet_long
    current_planet_degree := current_planet_degree == 0 ? 360 : current_planet_degree
    index = array.indexof(values, math.round(current_planet_degree))
    if index >= 0 and not barstate.isconfirmed
        coord = array.get(coordinates, index)
        table.cell_set_bgcolor(tab, coord.column, coord.row, col_out)
        // Use red text if retrograde, otherwise normal text color
        so9_text_color = is_retrograde ? color.red : TextColor
        table.cell_set_text_color(tab, coord.column, coord.row, so9_text_color)
        // Add retrograde symbol if retrograde
        retro_indicator = is_retrograde ? "℞" : ""
        table.cell_set_text(tab, coord.column, coord.row, text=centricSign + planet_symbol + retro_indicator)
        table.cell_set_tooltip(tab, coord.column, coord.row, tooltip=planet_string + (is_retrograde ? " (Retrograde)" : ""))

// ═══════════════════════════════════════════════════════════════════════════
// END SECTION: SQUARE OF NINE TABLE IMPLEMENTATION (MPL 2.0)
// ═══════════════════════════════════════════════════════════════════════════

// ═══════════════════════════════════════════════════════════════════════════
// SECTION: FUTURE DEGREE PROJECTIONS (BLUEPRINT RESEARCH ORIGINAL)
// LICENSE: CC BY-NC-SA 4.0 - Non-Commercial Use Only
// © 2026 BlueprintResearch / Javonnii
// ═══════════════════════════════════════════════════════════════════════════

// ═══════════════════════════════════════════════════════════════════
// Future Degree Projections
// ═══════════════════════════════════════════════════════════════════

// State variables for future projection visualization
var array<line> degree_lines = array.new_line()
var array<label> degree_labels = array.new_label()

// Calculate proportional line height
line_height = ta.atr(14) * 2

if show_future_degrees and (barstate.islast or barstate.isrealtime) and array.size(target_degrees) > 0
    // Clean up previous markers (check size before cleanup)
    if array.size(degree_lines) > 0
        for i = 0 to array.size(degree_lines) - 1
            line.delete(array.get(degree_lines, i))
        array.clear(degree_lines)
    if array.size(degree_labels) > 0
        for i = 0 to array.size(degree_labels) - 1
            label.delete(array.get(degree_labels, i))
        array.clear(degree_labels)

    bar_gap_seconds = timeframe.in_seconds(timeframe.period)
    bar_gap = bar_gap_seconds * 1000

    float prev_lon = planet_long

    for i = 1 to look_ahead
        future_time = time + (bar_gap * i)

        // Get future planet position
        future_lon = f_get_planet_longitude(planet_in, geo, future_time)

        // Get future retrograde status (only for planets that can retrograde)
        future_speed = planet_can_retro ? f_get_planet_speed(planet_in, future_time) : 0.0
        future_retrograde = planet_can_retro and future_speed < 0

        // Check EACH target degree for crossing (show ALL occurrences)
        for j = 0 to array.size(target_degrees) - 1
            target_deg = array.get(target_degrees, j)
            if degree_crossed(prev_lon, future_lon, target_deg)
                // Determine if this degree was already crossed historically
                was_crossed = array.includes(crossed_degrees_history, target_deg)

                // Use different styling for already-crossed degrees
                line_col = was_crossed ? crossed_line_color : future_line_color
                label_bg = was_crossed ? crossed_line_color : future_line_color

                // Calculate position
                crossing_time = future_time
                line_top = close + line_height
                line_bottom = close - line_height

                // Only draw if event is truly in the future
                if crossing_time > time
                    // Draw vertical dotted line
                    new_line = line.new(crossing_time, line_bottom, crossing_time, line_top,
                        xloc=xloc.bar_time,
                        color=line_col,
                        width=1,
                        style=line.style_dotted)
                    array.push(degree_lines, new_line)

                    // Top label: planet symbol + degree + retrograde indicator
                    retro_marker = future_retrograde ? "℞" : ""
                    top_label_text = planet_string + retro_marker + "\n" + str.tostring(target_deg) + "°"
                    new_top_label = label.new(crossing_time, line_top, top_label_text,
                        style=label.style_text_outline,
                        color=label_bg,
                        textcolor=TextColor,
                        xloc=xloc.bar_time,
                        size=vTxtSize,
                        text_font_family=font.family_monospace)
                    array.push(degree_labels, new_top_label)

                    // Bottom label: formatted date/time (only if toggle enabled)
                    if show_future_datetime
                        crossing_date = str.format_time(crossing_time, "MMM d, yy\nHH:mm", timezone=tz_input)
                        new_bottom_label = label.new(crossing_time, line_bottom, crossing_date,
                            style=label.style_text_outline,
                            color=label_bg,
                            textcolor=TextColor,
                            xloc=xloc.bar_time,
                            size=vTxtSize,
                            text_font_family=font.family_monospace)
                        array.push(degree_labels, new_bottom_label)

        prev_lon := future_lon

// ═══════════════════════════════════════════════════════════════════
// Alert Conditions
// ═══════════════════════════════════════════════════════════════════

// Static alertcondition for target degree crossing
alertcondition(any_degree_crossed and show_degree_alerts,
    title="Planet Degree Crossing",
    message="Planet has crossed a target degree")



// ═══════════════════════════════════════════════════════════════════════════
// LICENSE SUMMARY
// ═══════════════════════════════════════════════════════════════════════════
// This script contains code under two licenses:
//
// 1. SQUARE OF NINE TABLE (~Lines 455-670):
//    Mozilla Public License 2.0 - https://mozilla.org/MPL/2.0/
//    You may use, modify, and distribute this section under MPL 2.0 terms.
//    Adapted from ThiagoSchmitz's "Gann Square of 9"
//
// 2. ALL OTHER CODE:
//    CC BY-NC-SA 4.0 - https://creativecommons.org/licenses/by-nc-sa/4.0/
//    You may share and adapt for NON-COMMERCIAL purposes only.
//    You must give attribution and use the same license for derivatives.
//
// Commercial use of any CC BY-NC-SA sections requires written permission
// from BlueprintResearch / Javonnii.
// ═══════════════════════════════════════════════════════════════════════════

Overview

Gann's Square of Nine grid using astronomical ephemeris algorithms. It tracks pivot highs and lows with corresponding planetary longitude degrees, projects future target degrees ahead on the chart, and supports geocentric and heliocentric calculations. Key inputs allow users to select specific planets, adjust degree rounding, define custom degree targets, set bar look-ahead distances, and configure display colors. The script draws degree labels and projection lines on the main price chart alongside an optional table panel.

Details

Published by

blueprintresearch-dev on GitHub

Code credit

BlueprintResearch / Javonnii, as written in the script's header

Type

Indicator, Pine Script v6

Draws

On the price chart

Category

Support & resistance, Screeners & dashboards

Markets

Any symbol

Timeframes

Any timeframe

Risk Notes

No repainting, lookahead or stacked entries were found in its code or README. That says nothing about how well it works: test it first.

Settings

The inputs you can change from the gear icon on the chart. Defaults are the author's.

SettingTypeDefaultVariable
Anchor Timestamptime0dateTime
Pivot TypestringLowselectHL
Planetstring☉︎ Sunplanet_in
Geocentricbooltruegeo
Preset So9 DegreesstringNonepreset_degrees
Custom Degreestexttarget_degrees_str
Enable Degree Alertsbooltrueshow_degree_alerts
Show Future Degree Projectionsbooltrueshow_future_degrees
Bars to Look Aheadint250look_ahead
Timezone for Future DatesstringUTCtz_input
Show Date/Time Labelsbooltrueshow_future_datetime
Future Projection Colorcolorcolor.yellowfuture_line_color
Show all 38 settings
SettingTypeDefaultVariable
Already Crossed Colorcolorcolor.graycrossed_line_color
Pivot High Barsint10pivotHighBars
Pivot Low Barsint10pivotLowBars
Hide Pivot Labelsboolfalsehide_pivot_labels
Degree Rounding Precisionint0rounding
High Pivot Colorcolorcolor.limelabelColorH
Low Pivot Colorcolorcolor.redlabelColorL
Equal Pivotscolorcolor.orangelabelColorS
So9 Text Colorcolor#000000TextColor
Label Text SizestringLargeiTxtSize
Overlay Pivot Degreeint0overlay
Show Square OverlayboolfalseplotSquareOL
SquareOLColcolorcolor.whiteSquareOLCol
Show 45° OverlayboolfalseplotFortyFiveOL
FortyFiveOLColcolorcolor.aquaFortyFiveOLCol
Table PositionstringTop RighttablePosition
Cell Widthfloat0width
Cell Heightfloat0height
Frame Widthint1frameWidth
Backgroundcolorcolor.new(color.black, 100)backgroundColor
Framecolorcolor.new(color.white, 40)frameColor
Table Font Sizestringsize.normaltableTextSize
tableTextColorcolorcolor.new(color.white, 0)tableTextColor
Show DiagonalsbooltrueshowDiagonal
Diagonal Crosscolorcolor.new(#004b8d, 35)diagonalColor
Cardinal Crosscolorcolor.new(#801922, 35)cardinalColor
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

575 stars

This overlay indicator scans a pasted list of symbols for ICT liquidity sweep patterns using prior swing sweeps. The screening method looks back one, two, or three swings and can list long and short signals in a results table with adjustable rows, position, an

Indicator v5 SMC / ICT / price actionScreeners & dashboards
Updated 1 year ago · MPL-2.0 GitHub repository of ICT Liquidity Sweep Pattern Scanner
330 stars

This indicator monitors multiple cryptocurrency symbols for trading signals. It calculates and plots entry prices, take profit, and stop loss levels for each symbol including BTCUSDT, ETHUSDT, BNBUSDT and others. Price crossovers trigger buy, sell or close ale

Indicator v5 Support & resistanceScreeners & dashboards
Updated 2 months ago · None stated GitHub repository of Multi Alert Long/Short [Alorse]

BZ-tracker

by Allen1212
303 stars

The BZ-tracker indicator marks ARK Invest buy and sell transactions on stock charts with labels. It plots green labels for purchases and red labels for sales, each annotated with a multiplier such as X1 or X2 to show the number of ARK ETFs trading the stock on

Indicator v4 Support & resistanceScreeners & dashboards
Updated 2 years ago · None stated GitHub repository of BZ-tracker
257 stars

This indicator overlays an intraday SPX and ES chart with a midnight session range, regular-trading-hours opening levels, and a VWAP projection box. It draws the midnight high, mid, and low, flags edge rejections within an absolute tolerance, and plots optiona

Indicator v6 TrendSupport & resistance
Updated 6 weeks ago · None stated GitHub repository of SPX Midnight Box + Snag Logic

Golden Bullet Strategy

by TradersPost
218 stars

The Golden Bullet Strategy is an overlay signal indicator based on Smart Money Concepts and time-based liquidity models. It identifies liquidity sweeps of a designated reference session range during a specified hunt window. It then confirms Market Structure Sh

Indicator v6 SMC / ICT / price actionSessions & time
Updated 3 months ago · None stated GitHub repository of Golden Bullet Strategy

More indicators

Trade Your Charts With a Regulated Broker

Compare brokers that connect to TradingView, with spreads, minimum deposits and leverage side by side.

Compare brokers