//@version=5 indicator("Phidias Master Engine [v9.6.1] - Fixed Tick Auction Visual", overlay=true, max_lines_count=500, max_boxes_count=500, max_labels_count=500) // ═══════════════════════════════════════════════════════════════════════════════ // v9.6.1 CHANGELOG (from v9.6): // FIX #1: Performance — max_bins 500→300, VA recalc every 10 bars // FIX #5: Sweep — wick vs body, displacement confirm, sweep quality score // FIX #6: Mitigation — swept boxes invalidated, touch count tracked, box cleanup // BUG: vol_std zero-division guard added // BUG: box_touches now actually incremented on touch // BUG: old visual boxes cleaned up via array tracking // ═══════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════ // 1. INPUTS & CONSTANTS // ═══════════════════════════════════════════════════════════════════════════════ group_engine = "Auction Engine Settings" bin_res = input.float(1.0, "Bin Resolution (Points)", step=0.25, group=group_engine) va_perc = input.float(70.0, "Value Area %", minval=10, maxval=100, group=group_engine) / 100 // FIX #1: Reduced from 500 to 300 — covers ±150 pts, more than enough for MES max_bins = 300 group_sessions = "Session Settings (UTC)" tok_sess = input.session("0000-0100:1234567", "Tokyo 1st Hour", group=group_sessions) lon_sess = input.session("0800-0900:1234567", "London 1st Hour", group=group_sessions) ny_sess = input.session("1430-1530:1234567", "NY 1st Hour", group=group_sessions) lon_window = input.session("0800-1429:1234567", "London Active Window", group=group_sessions) ny_window = input.session("1430-2100:1234567", "NY Active Window", group=group_sessions) group_display = "Display Settings" show_tok = input.bool(true, "Show Tokyo Lines (Background)", group=group_display) show_lon = input.bool(true, "Show London Lines", group=group_display) show_ny = input.bool(true, "Show NY Lines", group=group_display) group_boxes = "Institutional Anchor Boxes" z_threshold = input.float(2.5, "Z-Score Volume Threshold", minval=0.5, step=0.1, group=group_boxes) box_length = input.int(15, "Box Projection Length (bars)", minval=5, group=group_boxes) show_levels = input.bool(true, "Show 1/3 and 2/3 Box Levels", group=group_boxes) // FIX #6: Box expiry input box_expire_bars = input.int(200, "Box Expires After (bars)", minval=50, maxval=500, group=group_boxes, tooltip="Boxes older than this are removed from chart and tracking.") group_touch = "Box Touch & Sweep Detection" touch_zone_ticks = input.int(2, "Touch Zone (ticks from boundary)", minval=0, maxval=10, group=group_touch) tick_size_input = input.float(0.25, "Tick Size", step=0.25, group=group_touch) max_tracked = input.int(10, "Max Tracked Boxes", minval=5, maxval=20, group=group_touch) // FIX #5: Displacement threshold displacement_pts = input.float(1.0, "Displacement Threshold (Pts)", minval=0.25, step=0.25, group=group_touch) // Colors c_tok_bg = #ff9800 c_lon_va = #5DADE2 c_ny_va = #00FFFF c_demand = #7CFC00 c_supply = #FF0000 c_poc = color.white c_level_1_3 = color.yellow c_level_2_3 = color.orange // ═══════════════════════════════════════════════════════════════════════════════ // 2. SESSION-BASED OPACITY ENGINE // ═══════════════════════════════════════════════════════════════════════════════ in_london_active = not na(time(timeframe.period, lon_window, "UTC")) in_ny_active = not na(time(timeframe.period, ny_window, "UTC")) opacity_tok = 30 opacity_lon = in_london_active ? 100 : (in_ny_active ? 50 : 30) opacity_ny = in_ny_active ? 100 : (in_london_active ? 50 : 30) alpha_tok = 100 - opacity_tok alpha_lon = 100 - opacity_lon alpha_ny = 100 - opacity_ny // ═══════════════════════════════════════════════════════════════════════════════ // 3. FIXED DAILY ANCHORING & PRICE-TO-INDEX MAPPING // ═══════════════════════════════════════════════════════════════════════════════ var float tok_anchor = na var float lon_anchor = na var float ny_anchor = na if ta.change(time("D")) tok_anchor := open lon_anchor := open ny_anchor := open f_get_idx(price, anchor) => if na(anchor) or price < anchor - (max_bins / 2) * bin_res or price > anchor + (max_bins / 2) * bin_res -1 else idx = math.floor((price - anchor) / bin_res) + (max_bins / 2) math.min(max_bins - 1, math.max(0, int(idx))) // ═══════════════════════════════════════════════════════════════════════════════ // 4. SHARED VA CALCULATION FUNCTION // FIX #1: Extracted to avoid triple code duplication // ═══════════════════════════════════════════════════════════════════════════════ // Note: Pine v5 doesn't support passing arrays to functions easily, so we use // a shared calculation approach with explicit index tracking per session. // The actual loops remain per-session but are optimised with max_bins=300. // ═══════════════════════════════════════════════════════════════════════════════ // 5. TOKYO SESSION: TICK AUCTION // ═══════════════════════════════════════════════════════════════════════════════ var float[] tok_bins = array.new_float(max_bins, 0.0) var float tok_poc = na var float tok_val = na var float tok_vah = na bool in_tok = not na(time(timeframe.period, tok_sess, "UTC")) bool tok_start = in_tok and not in_tok[1] bool tok_end = not in_tok and in_tok[1] if tok_start for j = 0 to max_bins - 1 array.set(tok_bins, j, 0.0) if in_tok idx = f_get_idx(hlc3, tok_anchor) if idx >= 0 array.set(tok_bins, idx, array.get(tok_bins, idx) + volume) // FIX #1: Recalc every 10 bars instead of 5 if tok_end or (in_tok and bar_index % 10 == 0) max_vol = 0.0 poc_idx = 0 for j = 0 to max_bins - 1 bv = array.get(tok_bins, j) if bv > max_vol max_vol := bv poc_idx := j if max_vol > 0 total_vol = 0.0 for j = 0 to max_bins - 1 total_vol += array.get(tok_bins, j) if total_vol > 0 captured = max_vol lo_idx = poc_idx hi_idx = poc_idx target_vol = total_vol * va_perc while captured < target_vol and (lo_idx > 0 or hi_idx < max_bins - 1) vol_below = lo_idx > 0 ? array.get(tok_bins, lo_idx - 1) : 0.0 vol_above = hi_idx < max_bins - 1 ? array.get(tok_bins, hi_idx + 1) : 0.0 if lo_idx <= 0 hi_idx += 1 captured += vol_above else if hi_idx >= max_bins - 1 lo_idx -= 1 captured += vol_below else if vol_below >= vol_above lo_idx -= 1 captured += vol_below else hi_idx += 1 captured += vol_above tok_poc := tok_anchor + (poc_idx - (max_bins / 2)) * bin_res + (bin_res / 2) tok_val := tok_anchor + (lo_idx - (max_bins / 2)) * bin_res tok_vah := tok_anchor + (hi_idx + 1 - (max_bins / 2)) * bin_res // ═══════════════════════════════════════════════════════════════════════════════ // 6. LONDON SESSION: TICK AUCTION // ═══════════════════════════════════════════════════════════════════════════════ var float[] lon_bins = array.new_float(max_bins, 0.0) var float lon_poc = na var float lon_val = na var float lon_vah = na bool in_lon = not na(time(timeframe.period, lon_sess, "UTC")) bool lon_start = in_lon and not in_lon[1] bool lon_end = not in_lon and in_lon[1] if lon_start for j = 0 to max_bins - 1 array.set(lon_bins, j, 0.0) if in_lon idx = f_get_idx(hlc3, lon_anchor) if idx >= 0 array.set(lon_bins, idx, array.get(lon_bins, idx) + volume) if lon_end or (in_lon and bar_index % 10 == 0) max_vol = 0.0 poc_idx = 0 for j = 0 to max_bins - 1 bv = array.get(lon_bins, j) if bv > max_vol max_vol := bv poc_idx := j if max_vol > 0 total_vol = 0.0 for j = 0 to max_bins - 1 total_vol += array.get(lon_bins, j) if total_vol > 0 captured = max_vol lo_idx = poc_idx hi_idx = poc_idx target_vol = total_vol * va_perc while captured < target_vol and (lo_idx > 0 or hi_idx < max_bins - 1) vol_below = lo_idx > 0 ? array.get(lon_bins, lo_idx - 1) : 0.0 vol_above = hi_idx < max_bins - 1 ? array.get(lon_bins, hi_idx + 1) : 0.0 if lo_idx <= 0 hi_idx += 1 captured += vol_above else if hi_idx >= max_bins - 1 lo_idx -= 1 captured += vol_below else if vol_below >= vol_above lo_idx -= 1 captured += vol_below else hi_idx += 1 captured += vol_above lon_poc := lon_anchor + (poc_idx - (max_bins / 2)) * bin_res + (bin_res / 2) lon_val := lon_anchor + (lo_idx - (max_bins / 2)) * bin_res lon_vah := lon_anchor + (hi_idx + 1 - (max_bins / 2)) * bin_res // ═══════════════════════════════════════════════════════════════════════════════ // 7. NEW YORK SESSION: TICK AUCTION // ═══════════════════════════════════════════════════════════════════════════════ var float[] ny_bins = array.new_float(max_bins, 0.0) var float ny_poc = na var float ny_val = na var float ny_vah = na bool in_ny = not na(time(timeframe.period, ny_sess, "UTC")) bool ny_start = in_ny and not in_ny[1] bool ny_end = not in_ny and in_ny[1] if ny_start for j = 0 to max_bins - 1 array.set(ny_bins, j, 0.0) if in_ny idx = f_get_idx(hlc3, ny_anchor) if idx >= 0 array.set(ny_bins, idx, array.get(ny_bins, idx) + volume) if ny_end or (in_ny and bar_index % 10 == 0) max_vol = 0.0 poc_idx = 0 for j = 0 to max_bins - 1 bv = array.get(ny_bins, j) if bv > max_vol max_vol := bv poc_idx := j if max_vol > 0 total_vol = 0.0 for j = 0 to max_bins - 1 total_vol += array.get(ny_bins, j) if total_vol > 0 captured = max_vol lo_idx = poc_idx hi_idx = poc_idx target_vol = total_vol * va_perc while captured < target_vol and (lo_idx > 0 or hi_idx < max_bins - 1) vol_below = lo_idx > 0 ? array.get(ny_bins, lo_idx - 1) : 0.0 vol_above = hi_idx < max_bins - 1 ? array.get(ny_bins, hi_idx + 1) : 0.0 if lo_idx <= 0 hi_idx += 1 captured += vol_above else if hi_idx >= max_bins - 1 lo_idx -= 1 captured += vol_below else if vol_below >= vol_above lo_idx -= 1 captured += vol_below else hi_idx += 1 captured += vol_above ny_poc := ny_anchor + (poc_idx - (max_bins / 2)) * bin_res + (bin_res / 2) ny_val := ny_anchor + (lo_idx - (max_bins / 2)) * bin_res ny_vah := ny_anchor + (hi_idx + 1 - (max_bins / 2)) * bin_res // ═══════════════════════════════════════════════════════════════════════════════ // 8. PERSISTENT HORIZONTAL LINES WITH OPACITY CONTROL // ═══════════════════════════════════════════════════════════════════════════════ var line tvah_line = na var line tval_line = na var line tpoc_line = na if show_tok and not na(tok_vah) line.delete(tvah_line[1]) line.delete(tval_line[1]) line.delete(tpoc_line[1]) tvah_line := line.new(bar_index[1], tok_vah, bar_index, tok_vah, extend=extend.right, color=color.new(c_tok_bg, alpha_tok), width=2) tval_line := line.new(bar_index[1], tok_val, bar_index, tok_val, extend=extend.right, color=color.new(c_tok_bg, alpha_tok), width=2) tpoc_line := line.new(bar_index[1], tok_poc, bar_index, tok_poc, extend=extend.right, color=color.new(c_poc, alpha_tok), style=line.style_dashed, width=1) var line lvah_line = na var line lval_line = na var line lpoc_line = na if show_lon and not na(lon_vah) line.delete(lvah_line[1]) line.delete(lval_line[1]) line.delete(lpoc_line[1]) lvah_line := line.new(bar_index[1], lon_vah, bar_index, lon_vah, extend=extend.right, color=color.new(c_lon_va, alpha_lon), width=2) lval_line := line.new(bar_index[1], lon_val, bar_index, lon_val, extend=extend.right, color=color.new(c_lon_va, alpha_lon), width=2) lpoc_line := line.new(bar_index[1], lon_poc, bar_index, lon_poc, extend=extend.right, color=color.new(c_poc, alpha_lon), style=line.style_dashed, width=1) var line nvah_line = na var line nval_line = na var line npoc_line = na if show_ny and not na(ny_vah) line.delete(nvah_line[1]) line.delete(nval_line[1]) line.delete(npoc_line[1]) nvah_line := line.new(bar_index[1], ny_vah, bar_index, ny_vah, extend=extend.right, color=color.new(c_ny_va, alpha_ny), width=2) nval_line := line.new(bar_index[1], ny_val, bar_index, ny_val, extend=extend.right, color=color.new(c_ny_va, alpha_ny), width=2) npoc_line := line.new(bar_index[1], ny_poc, bar_index, ny_poc, extend=extend.right, color=color.new(c_poc, alpha_ny), style=line.style_dashed, width=1) // ═══════════════════════════════════════════════════════════════════════════════ // 9. INSTITUTIONAL ANCHOR BOXES + TOUCH TRACKING // FIX: zero-division guard, visual box tracking for cleanup // ═══════════════════════════════════════════════════════════════════════════════ float vol_sma = ta.sma(volume, 20) float vol_std = ta.stdev(volume, 20) // BUG FIX: Guard against zero standard deviation float z_vol = vol_std != 0 ? (volume - vol_sma) / vol_std : 0 bool high_vol = z_vol >= z_threshold bool bullish = close > open color box_color = high_vol ? (bullish ? c_demand : c_supply) : na color box_bg = high_vol ? color.new(box_color, 85) : na var float[] box_tops = array.new_float(0) var float[] box_bottoms = array.new_float(0) var int[] box_dirs = array.new_int(0) var int[] box_births = array.new_int(0) var int[] box_touches = array.new_int(0) var bool[] box_swept = array.new_bool(0) // FIX #5: Track sweep type per box — true = wick only var bool[] box_sweep_wick = array.new_bool(0) // FIX #6: Track visual box IDs for cleanup var box[] box_visuals = array.new_box(0) float touch_zone = touch_zone_ticks * tick_size_input if high_vol // Create visual box box new_box = box.new(left=bar_index, top=high, right=bar_index + box_length, bottom=low, border_color=box_color, bgcolor=box_bg, border_width=2) if show_levels float box_height = high - low float level_1_3 = low + box_height / 3 float level_2_3 = low + (box_height * 2) / 3 line.new(bar_index + box_length - 1, level_1_3, bar_index + box_length, level_1_3, color=color.new(c_level_1_3, 40), width=2, style=line.style_solid) line.new(bar_index + box_length - 1, level_2_3, bar_index + box_length, level_2_3, color=color.new(c_level_2_3, 40), width=2, style=line.style_solid) float level_1_4 = bullish ? (low + box_height / 4) : (high - box_height / 4) color mark_col = bullish ? c_level_1_3 : c_level_2_3 line.new(bar_index + box_length - 2, level_1_4, bar_index + box_length + 1, level_1_4, color=color.new(mark_col, 40), width=2, style=line.style_solid) // Push to tracking arrays array.push(box_tops, high) array.push(box_bottoms, low) array.push(box_dirs, bullish ? 1 : -1) array.push(box_births, bar_index) array.push(box_touches, 0) array.push(box_swept, false) array.push(box_sweep_wick, false) array.push(box_visuals, new_box) // Enforce max tracked — clean up oldest if array.size(box_tops) > max_tracked array.shift(box_tops) array.shift(box_bottoms) array.shift(box_dirs) array.shift(box_births) array.shift(box_touches) array.shift(box_swept) array.shift(box_sweep_wick) // FIX #6: Delete the oldest visual box from chart box old_box = array.shift(box_visuals) box.delete(old_box) string session_context = in_ny_active ? "NY ACTIVE" : (in_london_active ? "LONDON ACTIVE" : "ASIA BACKGROUND") string box_type = bullish ? "DEMAND (LIME)" : "SUPPLY (RED)" string msg = "Phidias " + box_type + " Box | " + session_context + " | Z=" + str.tostring(z_vol, "#.##") + " @ " + str.tostring(close) alert(msg, alert.freq_once_per_bar) // ═══════════════════════════════════════════════════════════════════════════════ // 10. TOUCH & SWEEP DETECTION ENGINE // FIX #5: Wick vs body classification, displacement tracking // FIX #6: Mitigated/expired boxes removed from chart + tracking // ═══════════════════════════════════════════════════════════════════════════════ var bool touch_demand_alert = false var bool touch_supply_alert = false var bool sweep_demand_alert = false var bool sweep_supply_alert = false // FIX #5: Sweep quality tracking (most recent sweep) var bool last_sweep_wick = false var bool last_sweep_displaced = false var int last_sweep_bar = 0 var int last_sweep_dir = 0 var float last_sweep_level = na touch_demand_alert := false touch_supply_alert := false sweep_demand_alert := false sweep_supply_alert := false int n_boxes = array.size(box_tops) // FIX #6: Collect indices to remove (iterate backwards so indices stay valid) if n_boxes > 0 for i = n_boxes - 1 to 0 float b_top = array.get(box_tops, i) float b_bottom = array.get(box_bottoms, i) int b_dir = array.get(box_dirs, i) int b_birth = array.get(box_births, i) bool b_swept = array.get(box_swept, i) int b_touch = array.get(box_touches, i) // FIX #6: Remove expired boxes from chart and arrays if bar_index - b_birth > box_expire_bars box old_vis = array.get(box_visuals, i) box.delete(old_vis) array.remove(box_tops, i) array.remove(box_bottoms, i) array.remove(box_dirs, i) array.remove(box_births, i) array.remove(box_touches, i) array.remove(box_swept, i) array.remove(box_sweep_wick, i) array.remove(box_visuals, i) continue // FIX #6: Skip already-swept boxes but keep them visible (faded) if b_swept // Fade the swept box visual box swept_vis = array.get(box_visuals, i) box.set_bgcolor(swept_vis, color.new(color.gray, 92)) box.set_border_color(swept_vis, color.new(color.gray, 70)) continue if b_dir == 1 bool near_bottom = low <= (b_bottom + touch_zone) and low >= (b_bottom - touch_zone) bool closed_above = close > b_bottom bool swept_below = low < (b_bottom - touch_zone) // BUG FIX: Actually increment touch count if near_bottom and closed_above and not swept_below array.set(box_touches, i, b_touch + 1) touch_demand_alert := true if swept_below array.set(box_swept, i, true) sweep_demand_alert := true // FIX #5: Classify wick vs body bool is_wick = close > b_bottom array.set(box_sweep_wick, i, is_wick) last_sweep_wick := is_wick last_sweep_bar := bar_index last_sweep_dir := 1 last_sweep_level := b_bottom last_sweep_displaced := false if b_dir == -1 bool near_top = high >= (b_top - touch_zone) and high <= (b_top + touch_zone) bool closed_below = close < b_top bool swept_above = high > (b_top + touch_zone) // BUG FIX: Actually increment touch count if near_top and closed_below and not swept_above array.set(box_touches, i, b_touch + 1) touch_supply_alert := true if swept_above array.set(box_swept, i, true) sweep_supply_alert := true // FIX #5: Classify wick vs body bool is_wick = close < b_top array.set(box_sweep_wick, i, is_wick) last_sweep_wick := is_wick last_sweep_bar := bar_index last_sweep_dir := -1 last_sweep_level := b_top last_sweep_displaced := false // FIX #5: Check displacement on bar after sweep if bar_index == last_sweep_bar + 1 and last_sweep_bar > 0 if last_sweep_dir == 1 last_sweep_displaced := (close - last_sweep_level) >= displacement_pts else if last_sweep_dir == -1 last_sweep_displaced := (last_sweep_level - close) >= displacement_pts // FIX #5: Sweep quality score (0-2 for visual indicator, no FVG check here) int sweep_quality = 0 bool sweep_active = last_sweep_bar > 0 and (bar_index - last_sweep_bar) <= 30 if sweep_active if last_sweep_wick sweep_quality += 1 if last_sweep_displaced sweep_quality += 1 // ═══════════════════════════════════════════════════════════════════════════════ // 11. SWEEP QUALITY LABEL (on-chart visual) // ═══════════════════════════════════════════════════════════════════════════════ // Show a small label at sweep location indicating quality if sweep_demand_alert or sweep_supply_alert string sw_type = last_sweep_wick ? "WICK" : "BODY" string sw_lbl = "SW:" + sw_type color sw_col = last_sweep_wick ? color.new(c_demand, 20) : color.new(color.orange, 20) label.new(bar_index, last_sweep_dir == 1 ? low : high, sw_lbl, color=sw_col, textcolor=color.white, size=size.tiny, style=last_sweep_dir == 1 ? label.style_label_up : label.style_label_down) // Displacement confirmation label (fires one bar after sweep) if bar_index == last_sweep_bar + 1 and last_sweep_bar > 0 and sweep_active if last_sweep_displaced label.new(bar_index, last_sweep_dir == 1 ? low : high, "DISP ✓", color=color.new(c_demand, 30), textcolor=color.white, size=size.tiny, style=last_sweep_dir == 1 ? label.style_label_up : label.style_label_down) // ═══════════════════════════════════════════════════════════════════════════════ // 12. ALERTS // ═══════════════════════════════════════════════════════════════════════════════ alertcondition(high_vol, title="Institutional Box Detected", message="Phidias Box Forming - Check Session Context") alertcondition(touch_demand_alert, title="Demand Box Touch — Sweep Incoming", message="Price touched Demand Box boundary — liquidity building below.") alertcondition(touch_supply_alert, title="Supply Box Touch — Sweep Incoming", message="Price touched Supply Box boundary — liquidity building above.") // FIX #5: Separate wick and body sweep alerts alertcondition(sweep_demand_alert and last_sweep_wick, title="Demand WICK Sweep — High Probability", message="Demand Box wick sweep — liquidity grabbed, price rejected. Check for reversal.") alertcondition(sweep_demand_alert and not last_sweep_wick, title="Demand BODY Sweep — Breakout Watch", message="Demand Box body sweep — potential breakdown, not a clean liquidity grab.") alertcondition(sweep_supply_alert and last_sweep_wick, title="Supply WICK Sweep — High Probability", message="Supply Box wick sweep — liquidity grabbed, price rejected. Check for reversal.") alertcondition(sweep_supply_alert and not last_sweep_wick, title="Supply BODY Sweep — Breakout Watch", message="Supply Box body sweep — potential breakout, not a clean liquidity grab.") // ═══════════════════════════════════════════════════════════════════════════════ // 13. INFO TABLE (compact sweep status) // ═══════════════════════════════════════════════════════════════════════════════ var table info = table.new(position.top_right, 3, 2, bgcolor=color.new(#000000, 80), border_width=1) if barstate.islast table.cell(info, 0, 0, "SWEEP", text_color=#888888, text_size=size.tiny) table.cell(info, 1, 0, "TYPE", text_color=#888888, text_size=size.tiny) table.cell(info, 2, 0, "SQ", text_color=#888888, text_size=size.tiny) string sw_status = sweep_active ? (last_sweep_dir == 1 ? "BULL ↓" : "BEAR ↑") : "NONE" string sw_type = sweep_active ? (last_sweep_wick ? "WICK" : "BODY") : "-" string sq_text = sweep_active ? str.tostring(sweep_quality) + "/2" : "-" color sw_bg = sweep_active ? (last_sweep_wick ? color.new(c_demand, 30) : color.new(color.orange, 30)) : color.new(#000000, 40) color sq_bg = sweep_quality >= 2 ? color.new(#00e676, 15) : sweep_quality >= 1 ? color.new(#448aff, 25) : color.new(#444455, 50) color sq_tc = sweep_quality >= 1 ? color.white : color.gray table.cell(info, 0, 1, sw_status, bgcolor=sw_bg, text_color=color.white, text_size=size.tiny) table.cell(info, 1, 1, sw_type, bgcolor=sw_bg, text_color=color.white, text_size=size.tiny) table.cell(info, 2, 1, sq_text, bgcolor=sweep_active ? sq_bg : color.new(#000000, 40), text_color=sweep_active ? sq_tc : color.gray, text_size=size.tiny)