//@version=6 // ============================================================================= // CYCLE SMT // // SMT op het 02:30-raster uit Time Cycles — AMD, met de datahygiëne die de // meeste SMT-scripts missen. // // WAAROM DIT BESTAAT. Een SMT vergelijkt jouw grafiek met een gekoppeld // symbool. Dat gaat stuk op punten die je op de chart niet ziet. // // WAT DIT SCRIPT WEL DOET: // * STRIKT > en <. Twee gelijke waarden zijn géén veeg. Met >= leest een // bevroren feed als permanente bevestiging, in beide richtingen tegelijk — // die fout zit in meerdere populaire SMT-scripts. // * ECHTE XOR. Precies één van de twee markten veegt. Vegen ze allebei, // dan is er geen divergentie, ook niet als het er zwak uitziet. // * EEN SMT DIE OPLOST WORDT GRIJS, niet gewist. Hij bestond echt en // verdween daarna; dat is informatie, geen vergissing. // // WAT IK EERST FOUT DEED, want het staat hier als waarschuwing. // Ik liet alleen bars van het koppel meetellen waarvan de tijdstempel exact // klopte (gaps_on plus een gelijkheidstest). Dat leek zorgvuldig en sloopte // het script: sloeg dat filter de minuut over waarin ES zijn low zette, dan // kende het script die low nooit, bleef de XOR waar en bleef de SMT eeuwig // staan. De denkfout zat in WAAR ik het toepaste. Herhaalde data is gevaarlijk // als je één bar met één andere VERGELIJKT; bij het OPTELLEN van een extreem // over een cyclus is ze ongevaarlijk, want een herhaalde waarde kan een lopend // minimum niet verlagen. De voorzorg was juist, de plek niet. // // HET RASTER is dat van Time Cycles — AMD: 02:30 (minuut 150) tot en met // 15:59 (minuut 959), opgedeeld in 10M, 30M en 90M, plus de vier sessies die // vanaf 18:00 tellen zodat Asia niet om middernacht doormidden valt. // De SMT-niveaus vallen dus precies samen met de boxen die je al ziet. // // ALLE TIJDEN NEW YORK, zomer- en wintertijd automatisch. // ============================================================================= indicator("Cycle SMT", "Cycle SMT", overlay = true, max_lines_count = 500, max_labels_count = 500) // Staat rechtsonder op de chart, zodat je altijd kunt zien welke versie er // werkelijk draait. const string VERSIE = "v8" // ── instellingen ───────────────────────────────────────────────────────────── const string G_PAIR = "Gekoppeld symbool" const string G_LVL = "Cycli" const string G_VIEW = "Weergave" pairMode = input.string("Automatisch", "Koppeling", options = ["Automatisch", "Handmatig"], inline = "p", group = G_PAIR) pairIn = input.symbol("CME_MINI:ES1!", "", inline = "p", group = G_PAIR, tooltip = "Automatisch: NQ↔ES, MNQ↔MES, YM/RTY→ES, zelfde contract als de grafiek. " + "Handmatig: het symbool dat je hier kiest.") inverse = input.bool(false, "Omgekeerd (DXY, 6E)", group = G_PAIR, tooltip = "Aan voor symbolen die TEGEN je grafiek in bewegen. Dan hoort een " + "nieuwe high bij jou tegenover een nieuwe LOW bij hem.") use10 = input.bool(true, "10M", inline = "lv", group = G_LVL) use30 = input.bool(true, "30M", inline = "lv", group = G_LVL) use90 = input.bool(true, "90M", inline = "lv", group = G_LVL) useSe = input.bool(true, "Sessie", inline = "lv", group = G_LVL) bullCol = input.color(#1C6B4B, "Bullish", inline = "c", group = G_VIEW) bearCol = input.color(#B4462A, "Bearish", inline = "c", group = G_VIEW) doneCol = input.color(#7A8691, "Opgelost", group = G_VIEW, tooltip = "De kleur van een SMT waarvan de divergentie alsnog verdween " + "doordat het gekoppelde symbool hetzelfde niveau ook wegnam. Hij blijft " + "staan, gestippeld en dof — niet gewist, want het is geen vergissing maar " + "een signaal met een einde.") lineW = input.int(2, "Lijndikte", minval = 1, maxval = 4, group = G_VIEW) lblSize = input.string("Klein", "Labelgrootte", options = ["Piepklein", "Klein", "Normaal"], group = G_VIEW) mono = input.bool(true, "Monospace lettertype", group = G_VIEW, tooltip = "Pine kent maar twee lettertypes: het standaard schreefloze en " + "monospace. Monospace zet alle cijfers even breed, dus prijzen onder " + "elkaar lopen recht. Meer keus is er niet — dit is geen beperking van " + "dit script maar van TradingView.") maxKeep = input.int(30, "Hoeveel SMT's bewaren", minval = 5, maxval = 120, group = G_VIEW) // ── het gekoppelde symbool ─────────────────────────────────────────────────── autoPair = switch syminfo.root "NQ" => "ES" "ES" => "NQ" "MNQ" => "MES" "MES" => "MNQ" "YM" => "ES" "MYM" => "MES" "RTY" => "ES" "M2K" => "MES" => "" // Als functie geschreven en niet als ternaire over meerdere regels: Pine staat // een regelvervolg alleen toe binnen open haakjes. Een los uitgesmeerde // vraagteken-dubbelepunt geeft "end of line without line continuation". f_pairSym() => string s = "" bool indexFut = syminfo.root == "YM" or syminfo.root == "MYM" or syminfo.root == "RTY" or syminfo.root == "M2K" if pairMode == "Handmatig" s := pairIn else if autoPair == "" s := "" else if indexFut s := "CME_MINI:" + autoPair + "1!" else s := str.replace(syminfo.tickerid, syminfo.root, autoPair) s f_pairName() => string s = "" if pairMode == "Handmatig" s := array.last(str.split(pairIn, ":")) else if autoPair == "" s := "n.v.t." else s := autoPair s pairSym = f_pairSym() pairName = f_pairName() // gaps_off, en dat is na een fout een bewuste keuze. // // Met gaps_on komt elke minuut waarin het koppel niets deed als LEEG binnen. // Dat klinkt netjes, maar voor het opbouwen van een cyclus-extreem is het // schadelijk: valt de minuut weg waarin ES zijn low zette, dan kent het script // die low nooit. Met gaps_off krijg je in zo'n minuut de laatst bekende waarde // — en die kan een lopend minimum niet verlagen, dus hij doet geen kwaad. // // Kort: herhaalde data is gevaarlijk bij VERGELIJKEN, ongevaarlijk bij // OPTELLEN van een extreem. Ik paste de juiste voorzorg op de verkeerde plek toe. [pH, pL, pT] = request.security(pairSym != "" ? pairSym : syminfo.tickerid, timeframe.period, [high, low, time], gaps = barmerge.gaps_off, lookahead = barmerge.lookahead_off, ignore_invalid_symbol = true) // Nog uitsluitend een statuslampje: loopt de feed, of staat hij stil? Het raakt // de meting niet meer aan. bool pairLive = pairSym != "" and not na(pH) and not na(pL) and not na(pT) and pT == time bool pairOk = pairSym != "" and not na(pH) and not na(pL) // ── het raster van Time Cycles — AMD ───────────────────────────────────────── tz = "America/New_York" mnts = hour(time, tz) * 60 + minute(time, tz) inRas = mnts >= 150 and mnts <= 959 k10 = inRas ? int((mnts - 150) / 10) + 1 : 0 k30 = inRas ? int((mnts - 150) / 30) + 1 : 0 k90 = inRas ? int((mnts - 150) / 90) + 1 : 0 // Sessies tellen vanaf 18:00, anders knipt middernacht Asia doormidden. tmins = mnts >= 1080 ? mnts - 1080 : mnts + 360 kSes = tmins <= 509 ? 1 : tmins <= 779 ? 2 : tmins <= 1049 ? 3 : tmins <= 1319 ? 4 : 0 // De dag erbij in de sleutel, anders is 10M-cyclus 7 van vandaag gelijk aan die // van gisteren en wordt er nooit een cyclus afgesloten. dayKey = year(time, tz) * 10000 + month(time, tz) * 100 + dayofmonth(time, tz) // Voor sessies telt de handelsdag, die om 18:00 begint. sesDay = mnts >= 1080 ? dayKey + 1 : dayKey f_key(int k, int d) => k == 0 ? 0 : d * 1000 + k const int N = 4 levelName(int i) => i == 0 ? "10M" : i == 1 ? "30M" : i == 2 ? "90M" : "Sessie" levelOn(int i) => i == 0 ? use10 : i == 1 ? use30 : i == 2 ? use90 : useSe // ── toestand per cyclusniveau ──────────────────────────────────────────────── // Arrays in plaats van losse variabelen, want `var` in een functie krijgt in // Pine state per AANROEPPLEK — een lus zou alle vier de niveaus laten delen. var int[] curKey = array.new_int(N, 0) var float[] cHi = array.new_float(N, na) // grafiek, lopende cyclus var float[] cLo = array.new_float(N, na) var int[] cHiB = array.new_int(N, na) var int[] cLoB = array.new_int(N, na) var float[] pHi = array.new_float(N, na) // grafiek, vórige cyclus var float[] pLo = array.new_float(N, na) var int[] pHiB = array.new_int(N, na) var int[] pLoB = array.new_int(N, na) var float[] qcHi = array.new_float(N, na) // koppel, lopende cyclus var float[] qcLo = array.new_float(N, na) var float[] qpHi = array.new_float(N, na) // koppel, vórige cyclus var float[] qpLo = array.new_float(N, na) var bool[] firedBu = array.new_bool(N, false) var bool[] firedBe = array.new_bool(N, false) // OPGELOST. Veegt het gekoppelde symbool alsnog hetzelfde niveau weg, dan is // de divergentie voorbij — de XOR is niet meer waar. Binnen één cyclus kan dat // niet terugdraaien, want een extreem loopt maar één kant op. Dus zodra het // gebeurt staat het vast. // // De lijn wordt dan grijs en bevriest op dat punt. NIET verwijderd. Een SMT die // later oplost is geen vergissing die je wegpoetst; hij is een feit met een // einddatum. Wissen zou precies doen wat de indicators van maandag deden. var bool[] loseBu = array.new_bool(N, false) var bool[] loseBe = array.new_bool(N, false) // De oorspronkelijke tooltip-tekst, zelf bewaard. // // Pine heeft label.get_text, get_x en get_y, maar GEEN label.get_tooltip. Ik // nam aan dat die er wel was omdat de andere getters bestaan, en dat kostte een // versie. Wat je niet kunt teruglezen moet je dus zelf onthouden. var string[] tipBu = array.new_string(N, "") var string[] tipBe = array.new_string(N, "") var line[] lns = array.new_line() var label[] lbs = array.new_label() // De LEVENDE tekening per niveau en per richting. // // Zolang de cyclus openstaat groeit zijn extreem nog. De lijn moet dat volgen, // anders wijst hij naar de eerste kaars die de vorige low wegnam in plaats van // naar het werkelijke dieptepunt — en dan meet je de twee uiteinden van je // eigen lijn met verschillende maatstaven. // // Het SIGNAAL blijft wel staan op het moment dat het viel: de alert gaat één // keer af, op de bar waar de divergentie ontstond. Alleen het eindpunt van de // tekening schuift mee, en zodra de cyclus sluit staat hij stil. var line[] liveLnBu = array.new_line(N, na) var label[] liveLbBu = array.new_label(N, na) var line[] liveLnBe = array.new_line(N, na) var label[] liveLbBe = array.new_label(N, na) // Strikt. Twee gelijke waarden zijn geen veeg — dat is het verschil tussen // een echte divergentie en een bevroren datafeed. f_sweptHigh(float now, float ref) => not na(now) and not na(ref) and now > ref f_sweptLow (float now, float ref) => not na(now) and not na(ref) and now < ref f_xor(bool a, bool b) => (a and not b) or (not a and b) f_lblSize() => lblSize == "Piepklein" ? size.tiny : lblSize == "Normaal" ? size.normal : size.small // Label op het MIDDEN van de lijn. // // Eerder stond hier een omweg via time[off] om de tijd van een echte bar te // pakken. Dat was een fout met twee kanten. Pine kan de terugkijklengte van een // serie niet bepalen als je hem met een berekende index benadert — dat geeft // "Pine cannot determine the referencing length of series", en dan moet je // max_bars_back ophogen, wat het script onnodig zwaar maakt. En het was // overbodig: de lijn tekent al op xloc.bar_index, dus het label kan dat ook. // Geen tijdstempel, geen historie-aanroep, geen probleem. f_midBar(int b1, int b2) => int(math.floor((b1 + b2) * 0.5)) // Maakt de tekening. Belandt NIET meteen in de geschiedenis: zolang de cyclus // loopt blijft hij "levend" en wordt hij bijgewerkt. Pas bij het sluiten van de // cyclus gaat hij op de stapel en geldt de opruimgrens. Zo kan de opruiming // nooit een lijn weghalen die nog bijgewerkt moet worden. f_make(bool isBull, int lvl, float y1, int b1, float y2, int b2, string chartTxt, string pairTxt) => col = isBull ? bullCol : bearCol line ln = line.new(b1, y1, b2, y2, xloc = xloc.bar_index, color = col, width = lineW, style = line.style_dotted) string kop = (isBull ? "Bullish" : "Bearish") + " SMT · " + levelName(lvl) string mij = syminfo.ticker + ": " + chartTxt string hem = pairName + ": " + pairTxt string tip = str.format("{0}\n{1}\n{2}", kop, mij, hem) label lb = label.new(f_midBar(b1, b2), isBull ? math.min(y1, y2) : math.max(y1, y2), levelName(lvl), xloc = xloc.bar_index, yloc = yloc.price, style = isBull ? label.style_label_up : label.style_label_down, color = color.new(color.white, 100), textcolor = col, size = f_lblSize(), textalign = text.align_center, text_font_family = mono ? font.family_monospace : font.family_default, tooltip = tip) [ln, lb, tip] // Het eindpunt meeschuiven met het extreem van de lopende cyclus. f_follow(line ln, label lb, bool isBull, float y2, int b2) => if not na(ln) int b1 = line.get_x1(ln) float y1 = line.get_y1(ln) line.set_xy2(ln, b2, y2) if not na(lb) label.set_x(lb, f_midBar(b1, b2)) label.set_y(lb, isBull ? math.min(y1, y2) : math.max(y1, y2)) // De divergentie is opgelost: grijs maken en bevriezen. De tooltip vertelt wat // er gebeurd is, zodat je bij terugkijken niet hoeft te raden waarom hij dof is. f_resolve(line ln, label lb, string baseTip) => if not na(ln) line.set_color(ln, doneCol) line.set_style(ln, line.style_dashed) if not na(lb) label.set_textcolor(lb, doneCol) label.set_tooltip(lb, str.format("{0}\n\nOPGELOST: {1} nam hetzelfde niveau alsnog weg. Geen divergentie meer.", baseTip, pairName)) // Bij het sluiten van de cyclus: de tekening staat stil en gaat op de stapel. f_retire(line ln, label lb) => if not na(ln) array.push(lns, ln) array.push(lbs, lb) if array.size(lns) > maxKeep line.delete(array.shift(lns)) label.delete(array.shift(lbs)) // ── per bar bijwerken en toetsen ───────────────────────────────────────────── bool firedBullBar = false bool firedBearBar = false for i = 0 to N - 1 int k = 0 if i == 0 k := f_key(k10, dayKey) else if i == 1 k := f_key(k30, dayKey) else if i == 2 k := f_key(k90, dayKey) else k := f_key(kSes, sesDay) if k != array.get(curKey, i) // Cyclus gesloten. Eerst de levende tekeningen bevriezen: hun extreem // staat nu vast, dus ze hoeven niet meer mee te schuiven. f_retire(array.get(liveLnBu, i), array.get(liveLbBu, i)) f_retire(array.get(liveLnBe, i), array.get(liveLbBe, i)) array.set(liveLnBu, i, na) array.set(liveLbBu, i, na) array.set(liveLnBe, i, na) array.set(liveLbBe, i, na) // Verzegel de cyclus als "vorige" en begin opnieuw. array.set(pHi, i, array.get(cHi, i)) array.set(pLo, i, array.get(cLo, i)) array.set(pHiB, i, array.get(cHiB, i)) array.set(pLoB, i, array.get(cLoB, i)) array.set(qpHi, i, array.get(qcHi, i)) array.set(qpLo, i, array.get(qcLo, i)) array.set(cHi, i, na) array.set(cLo, i, na) array.set(cHiB, i, na) array.set(cLoB, i, na) array.set(qcHi, i, na) array.set(qcLo, i, na) array.set(firedBu, i, false) array.set(firedBe, i, false) array.set(loseBu, i, false) array.set(loseBe, i, false) array.set(curKey, i, k) if k != 0 // Grafiek bijwerken — die is er altijd. if na(array.get(cHi, i)) or high > array.get(cHi, i) array.set(cHi, i, high) array.set(cHiB, i, bar_index) if na(array.get(cLo, i)) or low < array.get(cLo, i) array.set(cLo, i, low) array.set(cLoB, i, bar_index) // Het koppel bijwerken, op ELKE bar. // // Hier stond een filter dat alleen bars doorliet waarvan de tijdstempel // exact klopte. Dat was fout, en het brak het hele script: sloeg dat // filter de minuut over waarin ES zijn low zette, dan kwam die low // nooit in het cyclus-extreem. Gevolg: ES "veegde" volgens het script // nooit, de XOR bleef waar, en de SMT bleef eeuwig staan. // // De denkfout: ik bestreed het herhaalde-waarde-probleem op de verkeerde // plek. Dat probleem bestaat bij het VERGELIJKEN van één bar met één // andere. Bij het OPTELLEN van een extreem over een cyclus bestaat het // niet — een herhaalde waarde kan een lopend minimum niet verlagen en // een lopend maximum niet verhogen. Hij verandert simpelweg niets. // // Dus: alle bars erin, en het extreem klopt vanzelf. if not na(pH) and not na(pL) if na(array.get(qcHi, i)) or pH > array.get(qcHi, i) array.set(qcHi, i, pH) if na(array.get(qcLo, i)) or pL < array.get(qcLo, i) array.set(qcLo, i, pL) // Toetsen: lopende cyclus tegen de vorige, voor beide markten. // Bewust uitgeschreven in losse regels in plaats van één lange // voorwaarde: Pine's regelvervolg is kieskeurig en dit leest beter. float myPrevH = array.get(pHi, i) float myPrevL = array.get(pLo, i) float itPrevH = array.get(qpHi, i) float itPrevL = array.get(qpLo, i) float itCurH = array.get(qcHi, i) float itCurL = array.get(qcLo, i) bool havePrev = not na(myPrevH) and not na(myPrevL) bool havePair = not na(itPrevH) and not na(itPrevL) and not na(itCurH) and not na(itCurL) bool ready = levelOn(i) and havePrev and havePair if ready and barstate.isconfirmed bool myHigh = f_sweptHigh(array.get(cHi, i), myPrevH) bool myLow = f_sweptLow(array.get(cLo, i), myPrevL) // Omgekeerd symbool: mijn high hoort tegenover zijn LOW te staan. bool itHigh = inverse ? f_sweptLow(itCurL, itPrevL) : f_sweptHigh(itCurH, itPrevH) bool itLow = inverse ? f_sweptHigh(itCurH, itPrevH) : f_sweptLow(itCurL, itPrevL) // Bearish SMT: aan de bovenkant veegt er precies één. if f_xor(myHigh, itHigh) and not array.get(firedBe, i) array.set(firedBe, i, true) firedBearBar := true [ln, lb, tp] = f_make(false, i, array.get(pHi, i), array.get(pHiB, i), array.get(cHi, i), array.get(cHiB, i), myHigh ? "nieuwe high" : "hield onder de vorige high", itHigh ? "nieuwe high" : "hield onder de vorige high") array.set(liveLnBe, i, ln) array.set(liveLbBe, i, lb) array.set(tipBe, i, tp) // Bullish SMT: aan de onderkant veegt er precies één. if f_xor(myLow, itLow) and not array.get(firedBu, i) array.set(firedBu, i, true) firedBullBar := true [ln2, lb2, tp2] = f_make(true, i, array.get(pLo, i), array.get(pLoB, i), array.get(cLo, i), array.get(cLoB, i), myLow ? "nieuwe low" : "hield boven de vorige low", itLow ? "nieuwe low" : "hield boven de vorige low") array.set(liveLnBu, i, ln2) array.set(liveLbBu, i, lb2) array.set(tipBu, i, tp2) // Is de divergentie inmiddels opgelost? Dan grijs en bevroren. // Dit wordt elke bar opnieuw gekeken, niet alleen bij het vuren. if array.get(firedBe, i) and not array.get(loseBe, i) and not f_xor(myHigh, itHigh) array.set(loseBe, i, true) f_resolve(array.get(liveLnBe, i), array.get(liveLbBe, i), array.get(tipBe, i)) if array.get(firedBu, i) and not array.get(loseBu, i) and not f_xor(myLow, itLow) array.set(loseBu, i, true) f_resolve(array.get(liveLnBu, i), array.get(liveLbBu, i), array.get(tipBu, i)) // Nog lopend? Dan het eindpunt meeschuiven met het extreem van de // cyclus — niet naar de kaars die toevallig als eerste doorbrak. // Opgeloste SMT's schuiven niet meer mee; die staan stil. if array.get(firedBe, i) and not array.get(loseBe, i) f_follow(array.get(liveLnBe, i), array.get(liveLbBe, i), false, array.get(cHi, i), array.get(cHiB, i)) if array.get(firedBu, i) and not array.get(loseBu, i) f_follow(array.get(liveLnBu, i), array.get(liveLbBu, i), true, array.get(cLo, i), array.get(cLoB, i)) // ── statusregel, zodat je ziet of de koppeling leeft ───────────────────────── var table st = table.new(position.bottom_right, 1, 1, border_width = 0) if barstate.islast string vink = not pairOk ? "⚠ " : (pairLive ? "✓ " : "· ") string omg = inverse ? " (omgekeerd)" : "" string basis = pairSym == "" ? "geen koppeling" : vink + pairName + omg // Het versienummer erbij. Zonder dat kun je van buitenaf niet zien welke // versie er op de chart staat, en dan ga je zoeken naar een fout in code // die er helemaal niet op staat. Dat kostte een ronde. string txt = basis + " " + VERSIE table.cell(st, 0, 0, txt, text_size = size.tiny, text_color = pairOk ? color.new(color.gray, 20) : bearCol, bgcolor = color.new(color.white, 100), text_font_family = mono ? font.family_monospace : font.family_default) // Alerts op de gebeurtenis zelf, en uitsluitend op een gesloten kaars — een // alert die intrabar vuurt kan terugkomen op een bar die alsnog anders sluit. alertcondition(firedBullBar, "Cycle SMT — Bullish", "Bullish Cycle SMT op {{ticker}}") alertcondition(firedBearBar, "Cycle SMT — Bearish", "Bearish Cycle SMT op {{ticker}}")