//@version=6 indicator("Cycle POC Block", shorttitle="Cycle POC", overlay=true, max_lines_count=500, max_labels_count=500, max_boxes_count=500) // ═══════════════════════════════════════════════════════════════════════════ // The volume cluster of the cycle you pick, carried forward as a block, so you // can see how the NEXT cycle reacts to it. // // THE JOB, in the owner's words: the point of control of the Asia session, of // London, of New York AM and PM, and of the 90, 30 and 10 minute cycles. When // London trades, how does it react to Asia's cluster, on the one minute chart. // And: it does not have to touch the point of control itself; it can reject at // the bottom of that structure too. // // ── THE GRID IS HIS, NOT A GENERIC ONE ───────────────────────────────────── // Taken from his own SMT indicator so the two agree to the minute. All New // York time, DST automatic. Anchor 02:30, end 15:59. // // 10M 10 min 81/day 02:30-02:39 ... 15:50-15:59 // 30M 30 min 27/day 02:30-02:59 ... 15:30-15:59 // 90M 90 min 9/day LO1 02:30-03:59 LO2 04:00-05:29 LO3 05:30-06:59 // AM1 07:00-08:29 AM2 08:30-09:59 AM3 10:00-11:29 // PM1 11:30-12:59 PM2 13:00-14:29 PM3 14:30-15:59 // Asia 18:00-02:29 . London 02:30-06:59 . NY AM 07:00-11:29 . NY PM 11:30-15:59 // 16:00-17:59 has no session and no cycle, by design. // // Three 10M make a 30M, three 30M a 90M, three 90M a session, three sessions // 02:30-16:00. The nine 90M windows are written out rather than divided, which // is how his own file does it: there is then no division for a fraction to get // lost in. // // No key carries a day component and none is needed. Between 16:00 and 02:30 // the key is 0, which closes whatever was open, so today's first cycle can // never be read as a continuation of yesterday's. // // ── WHY A BLOCK AND NOT A LINE ───────────────────────────────────────────── // The point of control is one bin, and which bin wins is mostly luck. Measured // on NQ over 4,246 sessions (2022-2026): the second-heaviest bin reaches 98.9% // of the peak at the median, and in 94% of sessions it is within 5% of it. A // single point-of-control line therefore puts a level on noise. The EDGE of // the cluster is a real feature; the exact peak is not. So the block is the // object and the point of control is a hairline inside it, drawn thin on // purpose: it should not look like the truth. // // Block height, same measurement: edge at 85% of peak -> 4.0 pt (4% of range) // edge at 70% -> 9.5 pt (10%) <- default // edge at 50% -> 19.5 pt (20%) // // Only about 2% of sessions touch the edge without also reaching the point of // control, so the block is not a source of new opportunities. The gain is that // you are there EARLIER — the edge sits roughly five points in front of the // middle, which on an eight-point ATR is most of a Rejection Block's stop. // // ONE BLOCK BY DEFAULT: the previous cycle's, running across the live cycle. // The cycle in progress is NOT drawn unless you ask for it — it still moves, // and a moving block beside a fixed one is read wrongly at speed. // // NOT A SIGNAL. This draws a zone and says when price comes back to it. What // the reaction means is his read; the entry is the Rejection Block. // ═══════════════════════════════════════════════════════════════════════════ // ───────────────────────── which cycle ───────────────────────── gC = "Cycle" mode = input.string("Sessions", "Cycle", options=["Sessions", "90M", "30M", "10M"], group=gC, tooltip="One at a time. Four sets of blocks on a one-minute chart is unreadable, and the whole point is to see which one reacts best.") // ───────────────────────── the cluster ───────────────────────── gP = "Cluster" edgePct = input.float(70, "Cluster edge (% of the peak)", minval=10, maxval=99, step=5, group=gP, tooltip="Walk out from the heaviest bin while bins stay above this share of it. 85% gives about 4 points on NQ, 70% about 9.5, 50% about 19.5. Below 50% it stops being a level and becomes an area.") gapTol = input.int(2, "Bins allowed to dip below", minval=0, maxval=20, group=gP, tooltip="A chance dip of one or two bins must not cut the block short. Without this a noisy profile is sliced at a random price.") binTicks = input.int(4, "Bin size (ticks)", minval=1, maxval=200, group=gP, tooltip="4 ticks is 1 point on NQ. Smaller bins are more exact and noisier.") useVol = input.bool(true, "Weight by volume", group=gP, tooltip="Off = a time profile, counting minutes instead of contracts. Measured on NQ: three quarters of a volume profile IS the time profile, so they usually land close together. Off is also the fallback on a symbol with no volume.") // ───────────────────────── how long a block lives ───────────────────────── gL = "Block lifetime" lifeMode = input.string("Its own next cycle", "A block runs for", group=gL, options=["Its own next cycle", "Until mitigated"], tooltip="A block is the level FOR one cycle: the 10:30-10:59 cluster is what 11:00-11:29 has to deal with, and when that cycle is over the block is history. Looking ten back then gives you a staircase, not ten bands all running to now. Switch to 'Until mitigated' if you want a block to keep running until price has dealt with it.") mitRule = input.string("Body close through", " └ mitigated by", group=gL, options=["Wick touch", "Wick through", "Body close through"], tooltip="Only read in 'Until mitigated'. Wick touch = price trades into the block at all. Wick through = a wick reaches the FAR edge, measured from the side price approached from. Body close through = a candle closes beyond that far edge. Wick touch ends the block at the moment it gets interesting, so it is rarely the one you want.") // ───────────────────────── carrying it forward ───────────────────────── gF = "Carry forward" keepN = input.int(1, "Blocks to keep on the chart", minval=1, maxval=30, group=gF, tooltip="1 = only the previous cycle's block, running across the live cycle. That is the default because it is what was asked for and because one block is readable. Counted in cycles, so raising it means different things per grid: three blocks is four and a half hours on 90M, an hour and a half on 30M, and half an hour on 10M.") armATR = input.float(0.5, "Arm once price is this far from the block (x ATR)", minval=0.0, step=0.25, group=gF, tooltip="The block sleeps until price has left it. Only then does a return count as a return.") atrLen = input.int(120, "ATR length", minval=5, group=gF) stopAtTouch = input.bool(false, "Stop extending once it has been touched", group=gF) // ───────────────────────── style ───────────────────────── gV = "Style" colLive = input.color(color.new(color.gray, 75), "Cycle in progress", group=gV) colBlk = input.color(color.new(color.rgb(0, 110, 180), 82), "Block", inline="b", group=gV) colEdge = input.color(color.new(color.rgb(0, 110, 180), 35), "edges", inline="b", group=gV) colTouch = input.color(color.new(color.rgb(230, 170, 40), 70), "Price inside it", group=gV) colPocLn = input.color(color.new(color.rgb(0, 110, 180), 55), "POC hairline", group=gV) showPoc = input.bool(true, "Show the POC hairline inside the block", group=gV, tooltip="Thin on purpose. In 94% of sessions the runner-up bin is within 5% of the peak, so the exact middle is close to arbitrary — it should not look like the truth.") showLive = input.bool(false, "Also show the cycle in progress", group=gV, tooltip="Off by default. The running block still moves, and a block that moves next to one that is fixed is the kind of thing you misread at speed. The finished block is the one you trade; this one is only here if you want to watch a cluster form.") showLbl = input.bool(true, "Labels", group=gV) lblSzIn = input.string("small", " └ size", options=["tiny","small","normal"], group=gV) lblSz = lblSzIn == "tiny" ? size.tiny : lblSzIn == "small" ? size.small : size.normal atr = ta.atr(atrLen) binSize = binTicks * syminfo.mintick barMs = timeframe.in_seconds() * 1000 edgeFrac = edgePct / 100.0 NY = "America/New_York" // ═══════════════════════ which cycle is this bar in ═══════════════════════ // A key of 0 means "no cycle" and closes whatever was open. That is what // separates one trading day from the next: between 16:00 and 02:30 there is // nothing, so no day component is needed anywhere below. int l_min = hour(time, NY) * 60 + minute(time, NY) // Asia crosses midnight, so it has to come from a session string rather than // from a minute range. bool inAsia = timeframe.isintraday and not na(time(timeframe.period, "1800-0230:1234567", NY)) f_hhmm(int m) => int h = m / 60 int mm = m % 60 // Built by hand: zero-padding patterns are not guaranteed in Pine's format // subset, and a compile error over a cosmetic label is a poor trade. (h < 10 ? "0" : "") + str.tostring(h) + ":" + (mm < 10 ? "0" : "") + str.tostring(mm) int cycId = 0 string cycNm = "" if mode == "Sessions" if inAsia cycId := 1 cycNm := "Asia" else if l_min >= 150 and l_min <= 419 cycId := 2 cycNm := "London" else if l_min >= 420 and l_min <= 689 cycId := 3 cycNm := "NY AM" else if l_min >= 690 and l_min <= 959 cycId := 4 cycNm := "NY PM" else if mode == "90M" // Written out, not divided — his own file's choice and the right one. if l_min >= 150 and l_min <= 239 cycId := 2 cycNm := "LO1" else if l_min >= 240 and l_min <= 329 cycId := 3 cycNm := "LO2" else if l_min >= 330 and l_min <= 419 cycId := 4 cycNm := "LO3" else if l_min >= 420 and l_min <= 509 cycId := 5 cycNm := "AM1" else if l_min >= 510 and l_min <= 599 cycId := 6 cycNm := "AM2" else if l_min >= 600 and l_min <= 689 cycId := 7 cycNm := "AM3" else if l_min >= 690 and l_min <= 779 cycId := 8 cycNm := "PM1" else if l_min >= 780 and l_min <= 869 cycId := 9 cycNm := "PM2" else if l_min >= 870 and l_min <= 959 cycId := 10 cycNm := "PM3" else if mode == "30M" if l_min >= 150 and l_min <= 959 cycId := (l_min - 150) / 30 + 1 cycNm := "30M " + f_hhmm(150 + (cycId - 1) * 30) else if l_min >= 150 and l_min <= 959 cycId := (l_min - 150) / 10 + 1 cycNm := "10M " + f_hhmm(150 + (cycId - 1) * 10) var int prevId = -1 bool cycleEnded = cycId != prevId // Telt elke cyclusgrens. Een blok weet daardoor hoe oud het is in CYCLI, en // dat is de eenheid waarin "loopt voor een cyclus" is uitgedrukt. var int cycCount = 0 if cycleEnded cycCount += 1 // ═══════════════════════ the running profile ═══════════════════════ // A map keyed by bin index, so the price range need not be known in advance. var map bins = map.new() var int cycStart = na var string liveName = "" var array bPoc = array.new_float() var array bTop = array.new_float() var array bBot = array.new_float() var array bName = array.new_string() var array bArmed = array.new_bool() var array bHit = array.new_bool() var array bBirth = array.new_int() // de cyclusteller bij geboorte var array bSide = array.new_int() // 1 = prijs kwam van boven, -1 van onder, 0 onbekend var array bDone = array.new_bool() // rechterrand ligt vast var array bBox = array.new_box() var array bLine = array.new_line() var array