//@version=6 // ============================================================================= // IPDA Macro Clock // ============================================================================= // // Marks candles whose clock time reduces to 3, 6 or 9, inside your macro // windows, and tells you when the next one is due. // // TWO KINDS OF CERTAINTY, KEPT APART // // The time of a candle is known the moment it opens. So an IPDA mark can be // painted live and will never move — that is a fact about the clock, not a // guess about price. // // A pivot is the opposite. It needs `rightBars` candles to close after it // before it exists at all, so anything shown before that is provisional. The // script this replaces drew those provisional marks on every historical bar as // well as the live one, which left the chart covered in "(F)" markers that had // never been swings. Here the forming marker is a single label on the current // bar and nowhere else, and it disappears the moment the bar closes without // confirming. // // HOW OFTEN THIS FIRES, so the filter cannot flatter itself: the digital root // preserves divisibility by three, so "reduces to 3, 6 or 9" means "divisible // by three". On a one-minute chart that is 54% of all candles. Inside one of // the macro windows below it is about 18 of 34 candles with all three digits // on, and about 6 with only the 9. The table carries a live count so the figure // is always on screen rather than assumed. // // ============================================================================= indicator("IPDA Macro Clock", overlay = true, max_labels_count = 500, max_lines_count = 500) // ----------------------------------------------------------------- the rule gCalc = "1 · The rule" i_tz = input.string("America/New_York", "Timezone", group = gCalc, tooltip = "All times are read in this zone. New York is UTC-4 in summer and UTC-5 in winter; the zone name handles the switch, a fixed offset would not.") i_method = input.string("Combined, then minute", "How the digit is worked out", options = ["Combined, then minute", "Combined only", "Minute only"], group = gCalc, tooltip = "Combined, then minute — the original rule: try root(root(hour) + root(minute)) first, and fall back to root(minute). Combined only — the first test on its own, which DISCARDS the minute rule: 13:42 gives nothing, because 4 + 6 reduces to 1, even though the minute 4+2 is 6. Minute only — just root(minute), the way most people work it out in their head. The panel shows which of the two tests fired, so the route is never a guess.") gDigits = "2 · Which digits count" i_use3 = input.bool(true, "3", inline = "d", group = gDigits) i_use6 = input.bool(true, "6", inline = "d", group = gDigits) i_use9 = input.bool(true, "9", inline = "d", group = gDigits, tooltip = "Turning two of these off is the strongest way to quieten the chart. The three digits occur about equally often, so dropping two is not a biased choice — it simply shows a third as many.") // ------------------------------------------------------------ macro windows gM = "3 · Macro windows" i_macroOnly = input.bool(true, "Only inside the windows below", group = gM) m02 = input.bool(true, "02:42 - 03:15", group = gM) m03 = input.bool(true, "03:42 - 04:15", group = gM) m04 = input.bool(true, "04:42 - 05:15", group = gM) m05 = input.bool(true, "05:42 - 06:15", group = gM) m06 = input.bool(true, "06:42 - 07:15", group = gM) m07 = input.bool(true, "07:42 - 08:15", group = gM) m08 = input.bool(true, "08:42 - 09:15", group = gM) m09 = input.bool(true, "09:42 - 10:15", group = gM) m10 = input.bool(true, "10:42 - 11:15", group = gM) m11 = input.bool(true, "11:42 - 12:15", group = gM) m12 = input.bool(true, "12:42 - 13:15", group = gM) m13 = input.bool(true, "13:42 - 14:15", group = gM) m14 = input.bool(true, "14:42 - 15:15", group = gM) m15 = input.bool(true, "15:42 - 16:15", group = gM) // ------------------------------------------------------------- what to show gShow = "4 · What to show" i_showNumber = input.bool(true, "Number on the candle", group = gShow) i_showLine = input.bool(false, "Vertical line", group = gShow, tooltip = "Quieter than numbers when several candles qualify in a row.") i_showColour = input.bool(false, "Colour the candle", group = gShow, tooltip = "Leave off until the digit and macro filters are tight. With all three digits on a one-minute chart this colours more than half your candles, and then an uncoloured candle is the exception rather than the mark.") i_pivotOnly = input.bool(false, "Only on candles that are also a swing", group = gShow, tooltip = "Off: marks appear live, the moment the candle opens, and never move. On: marks wait for the swing to confirm, which takes `right bars` candles — later, but only real swings.") i_maxMarks = input.int(40, "Keep the last X marks", minval = 1, maxval = 300, group = gShow) gCol = "5 · Colours" i_col3 = input.color(color.new(#42bda8, 0), "3", inline = "c", group = gCol) i_col6 = input.color(color.new(#f2b544, 0), "6", inline = "c", group = gCol) i_col9 = input.color(color.new(#e0556d, 0), "9", inline = "c", group = gCol) i_sizeStr = input.string("small", "Text size", options = ["tiny","small","normal","large"], group = gCol) gSwing = "6 · Swing (fractal)" i_left = input.int(2, "Left bars", minval = 1, group = gSwing) i_right = input.int(2, "Right bars", minval = 1, group = gSwing) i_preview = input.bool(true, "Warn on the forming candle", group = gSwing, tooltip = "One marker, on the current candle only, while it still might become a swing. It is provisional by nature — the swing cannot exist until `right bars` candles have closed — so it is drawn hollow and removed again if it does not confirm.") gNext = "7 · Next IPDA time" i_showNext = input.bool(true, "Show the countdown", group = gNext) i_warnMins = input.int(2, "Alert this many minutes before", minval = 0, maxval = 60, group = gNext, tooltip = "An alert on the moment itself arrives too late to prepare for it.") gTbl = "8 · Panel" i_showTable = input.bool(true, "Show panel", group = gTbl) i_tablePos = input.string("Top Right", "Position", options = ["Top Right","Top Left","Bottom Right","Bottom Left","Middle Right"], group = gTbl) i_showRate = input.bool(true, "Show how often the filter fires", group = gTbl, tooltip = "The share of candles on this chart that passed every filter. A filter letting four in five through is not selecting anything, and this is the only way to see that without counting by hand.") labelSize = i_sizeStr == "tiny" ? size.tiny : i_sizeStr == "small" ? size.small : i_sizeStr == "large" ? size.large : size.normal // ============================================================================= // THE RULE — one implementation, used by every part of the script // ============================================================================= // Digital root: add the digits until one is left. 0 stays 0, which matters — // minute :00 has no root and so never qualifies on the minute alone. f_root(int n) => n > 0 ? (n - 1) % 9 + 1 : 0 // Is this digit one you have switched on? f_allowed(int d) => (d == 3 and i_use3) or (d == 6 and i_use6) or (d == 9 and i_use9) // The digit for a given hour and minute, or 0 for none. // Returns the digit and which test produced it: 1 = hour+minute, 2 = minute. f_digitVia(int h, int m) => byHour = f_root(f_root(h) + f_root(m)) byMinute = f_root(m) int out = 0 int via = 0 if i_method == "Combined only" if f_allowed(byHour) out := byHour via := 1 else if i_method == "Minute only" if f_allowed(byMinute) out := byMinute via := 2 else if f_allowed(byHour) out := byHour via := 1 else if f_allowed(byMinute) out := byMinute via := 2 [out, via] f_digit(int h, int m) => [d, _v] = f_digitVia(h, m) d // Is this hour and minute inside an enabled macro window? The windows run from // :42 to :15 of the following hour, so each one straddles the hour boundary and // has to be tested on both sides. f_win(int h, int m, int startHour, bool on) => on and ((h == startHour and m >= 42) or (h == (startHour + 1) % 24 and m <= 15)) f_inMacro(int h, int m) => f_win(h, m, 2, m02) or f_win(h, m, 3, m03) or f_win(h, m, 4, m04) or f_win(h, m, 5, m05) or f_win(h, m, 6, m06) or f_win(h, m, 7, m07) or f_win(h, m, 8, m08) or f_win(h, m, 9, m09) or f_win(h, m, 10, m10) or f_win(h, m, 11, m11) or f_win(h, m, 12, m12) or f_win(h, m, 13, m13) or f_win(h, m, 14, m14) or f_win(h, m, 15, m15) // Everything the chart needs about one timestamp. // On the first few bars `time[i_right]` has no value yet. Left unguarded that // feeds na into hour() and every comparison downstream answers na instead of // false, which Pine then paints as though it were true. f_evaluate(int ts) => if na(ts) [0, false, false] else h = hour(ts, i_tz) m = minute(ts, i_tz) d = f_digit(h, m) inWin = not i_macroOnly or f_inMacro(h, m) [d, inWin, d != 0 and inWin] f_colourFor(int d) => d == 3 ? i_col3 : d == 6 ? i_col6 : d == 9 ? i_col9 : color.gray // ============================================================================= // THIS BAR // ============================================================================= [digit, inWindow, qualifies] = f_evaluate(time) [_dg, digitVia] = f_digitVia(hour(time, i_tz), minute(time, i_tz)) viaText = digitVia == 1 ? "hour+minute" : digitVia == 2 ? "minute" : "" // ---- swings. A pivot is only real once `i_right` candles have closed after it. ph = ta.pivothigh(high, i_left, i_right) pl = ta.pivotlow(low, i_left, i_right) isPivotHigh = not na(ph) isPivotLow = not na(pl) [pivotDigitH, _winH, pivotQualH] = f_evaluate(time[i_right]) [pivotDigitL, _winL, pivotQualL] = f_evaluate(time[i_right]) // ============================================================================= // MARKS // ============================================================================= var label[] marks = array.new_label() var line[] rules = array.new_line() f_trimLabels(label[] arr, int keep) => while array.size(arr) > keep label.delete(array.shift(arr)) f_trimLines(line[] arr, int keep) => while array.size(arr) > keep line.delete(array.shift(arr)) // Where the mark goes, and what it says. // pivotOnly off → on the candle itself, live, never moves // pivotOnly on → on the pivot candle, `i_right` bars back, once confirmed drawNow = not i_pivotOnly and qualifies and barstate.isconfirmed drawPivotH = i_pivotOnly and isPivotHigh and pivotQualH drawPivotL = i_pivotOnly and isPivotLow and pivotQualL if drawNow and i_showNumber lb = label.new(bar_index, low, str.tostring(digit), xloc = xloc.bar_index, yloc = yloc.belowbar, style = label.style_none, textcolor = f_colourFor(digit), size = labelSize) array.push(marks, lb) f_trimLabels(marks, i_maxMarks) if drawPivotH and i_showNumber lb = label.new(bar_index - i_right, ph, str.tostring(pivotDigitH) + "\n▼", xloc = xloc.bar_index, yloc = yloc.abovebar, style = label.style_none, textcolor = f_colourFor(pivotDigitH), size = labelSize) array.push(marks, lb) f_trimLabels(marks, i_maxMarks) if drawPivotL and i_showNumber lb = label.new(bar_index - i_right, pl, "▲\n" + str.tostring(pivotDigitL), xloc = xloc.bar_index, yloc = yloc.belowbar, style = label.style_none, textcolor = f_colourFor(pivotDigitL), size = labelSize) array.push(marks, lb) f_trimLabels(marks, i_maxMarks) if i_showLine and (drawNow or drawPivotH or drawPivotL) int off = drawNow ? 0 : i_right int d = drawNow ? digit : (drawPivotH ? pivotDigitH : pivotDigitL) ln = line.new(bar_index - off, low, bar_index - off, high, xloc = xloc.bar_index, extend = extend.both, color = color.new(f_colourFor(d), 70), width = 1) array.push(rules, ln) f_trimLines(rules, i_maxMarks) // Candle colour is a global call, so the condition goes in the value. barcolor(i_showColour and qualifies ? f_colourFor(digit) : na, title = "IPDA candle") // ============================================================================= // THE FORMING SWING — one marker, on the live candle, nowhere else // ============================================================================= // // Kept in a single `var` slot rather than an array. The old script pushed these // onto a list on every bar, history included, so marks that had never become // swings stayed on the chart looking exactly like ones that had. Here the // previous marker is deleted before a new one is drawn, and deleted again the // moment the candle stops qualifying — so what is on screen is only ever the // candle in front of you. var label formingMark = na if barstate.islast if not na(formingMark) label.delete(formingMark) formingMark := na if i_preview and qualifies bool couldBeHigh = high >= ta.highest(high, i_left + 1) bool couldBeLow = low <= ta.lowest(low, i_left + 1) if couldBeHigh or couldBeLow formingMark := label.new(bar_index, couldBeHigh ? high : low, couldBeHigh ? str.tostring(digit) + "?\n▽" : "△\n" + str.tostring(digit) + "?", xloc = xloc.bar_index, yloc = couldBeHigh ? yloc.abovebar : yloc.belowbar, style = label.style_none, textcolor = color.new(f_colourFor(digit), 35), size = labelSize) // ============================================================================= // THE NEXT ONE // ============================================================================= // // Steps forward in whole candles of this chart's timeframe, so the answer is // "the next candle that qualifies" rather than "the next minute that does". var int nextTs = na var int nextDgt = 0 if barstate.islast and i_showNext int step = timeframe.in_seconds(timeframe.period) * 1000 nextTs := na nextDgt := 0 if step > 0 for i = 1 to 600 int ts = time + i * step [d, _w, ok] = f_evaluate(ts) if ok nextTs := ts nextDgt := d break minsToNext = na(nextTs) ? na : math.max(0, math.round((nextTs - timenow) / 60000.0)) // ============================================================================= // HOW OFTEN THIS FIRES // ============================================================================= var int barsSeen = 0 var int barsQualified = 0 if barstate.isconfirmed barsSeen += 1 if qualifies barsQualified += 1 hitRate = barsSeen > 0 ? barsQualified / barsSeen * 100 : 0.0 // ============================================================================= // PANEL // ============================================================================= f_pos(string s) => s == "Top Right" ? position.top_right : s == "Top Left" ? position.top_left : s == "Bottom Right" ? position.bottom_right : s == "Bottom Left" ? position.bottom_left : position.middle_right var table panel = table.new(f_pos(i_tablePos), 2, 5, border_width = 1, bgcolor = color.new(#1e222d, 0), frame_color = #363a45, frame_width = 1) f_row(table t, int r, string k, string v, color vc) => table.cell(t, 0, r, k, text_color = #9aa3b2, text_size = size.small, text_halign = text.align_left) table.cell(t, 1, r, v, text_color = vc, text_size = size.small, text_halign = text.align_right) if barstate.islast and i_showTable nowStr = str.format("{0,number,00}:{1,number,00}", hour(time, i_tz), minute(time, i_tz)) f_row(panel, 0, "Now", nowStr, color.white) f_row(panel, 1, "This candle", digit == 0 ? "—" : str.tostring(digit) + " · " + viaText + (inWindow ? "" : " (outside macro)"), digit != 0 and inWindow ? f_colourFor(digit) : #6b7280) if i_showNext f_row(panel, 2, "Next", na(nextTs) ? "none today" : str.format("{0,number,00}:{1,number,00} · {2}", hour(nextTs, i_tz), minute(nextTs, i_tz), nextDgt), na(nextTs) ? #6b7280 : f_colourFor(nextDgt)) f_row(panel, 3, "In", na(minsToNext) ? "—" : str.tostring(minsToNext) + " min", not na(minsToNext) and minsToNext <= i_warnMins ? #f2b544 : color.white) if i_showRate f_row(panel, 4, "Fires on", str.tostring(hitRate, "#.#") + "% of candles", hitRate > 50 ? #e0556d : hitRate > 25 ? #f2b544 : #42bda8) // ============================================================================= // ALERTS // ============================================================================= alertcondition(drawNow, "IPDA candle", "IPDA candle on {{ticker}} {{interval}} at {{time}}") alertcondition(drawPivotH, "IPDA swing high", "IPDA swing HIGH confirmed on {{ticker}} {{interval}} at {{close}}") alertcondition(drawPivotL, "IPDA swing low", "IPDA swing LOW confirmed on {{ticker}} {{interval}} at {{close}}") alertcondition(not na(minsToNext) and minsToNext <= i_warnMins and minsToNext > 0, "IPDA time approaching", "IPDA time coming up on {{ticker}} {{interval}}")