diff --git a/mql5/TesteFootprint.mq5 b/mql5/TesteFootprint.mq5 new file mode 100644 index 0000000..a810dc1 --- /dev/null +++ b/mql5/TesteFootprint.mq5 @@ -0,0 +1,1995 @@ +//+------------------------------------------------------------------+ +//| TesteFootprint.mq5 | +//| Footprint Chart nativo MT5 — porta fiel do React | +//+------------------------------------------------------------------+ +#property copyright "TesteFootprint" +#property version "1.85" +#property description "Footprint Chart" +#property stacksize 4194304 + +#include + +//--- Formação +input group "=== Formação ===" +input string InpCloseMode = "delta"; // Modo: delta|volume|range|time +input double InpDeltaMax = 800; // Delta máximo por cluster +input double InpVolumeMax = 100000; // Volume máximo (modo volume) +input double InpRangePoints = 50; // Range em pontos (modo range) +input int InpTimeSeconds = 300; // Tempo em segundos (modo time) +input double InpStepMultiplier = 75.0; // Agrupamento de níveis — 75 para USTEC +input int InpHistoryBars = 200; // Clusters no buffer circular +input int InpHistoryHours = 4; // Horas de histórico ao iniciar + +//--- Exibição +input group "=== Exibição ===" +enum ENUM_VIEW { VIEW_BIDASK=0, VIEW_DELTA=1 }; +input ENUM_VIEW InpViewMode = VIEW_BIDASK; // Modo: Bid x Ask | Delta +input int InpClusterWidth = 120; // Largura base do cluster (px) +input int InpLevelHeight = 16; // Altura por nível (zoom vertical) +input int InpFontSize = 8; // Tamanho da fonte +input int InpBottomPanel = 100; // Altura do painel inferior (px) — igual React + +//--- Cores +input group "=== Cores ===" +input color InpColorAsk = 0x3B82F6; +input color InpColorBid = 0xEC4899; +input color InpColorPoc = clrWhite; +input color InpColorBg = 0x0B0E14; +input color InpColorText = 0x94A3B8; + +//+------------------------------------------------------------------+ +//| Estruturas | +//+------------------------------------------------------------------+ +struct SLevel { double price, ask, bid; }; + +struct SCluster +{ + datetime open_time, close_time; + double open_price, close_price, delta, total_volume; + double price_high, price_low, poc_price; + bool is_closed; + SLevel levels[2000]; + int level_count; +}; + +#define VP_LEVELS 500 +#define ZONE_MAX 20 +#define VP_MAX 5 +// Color picker grid +#define PICK_COLS 16 +#define PICK_ROWS 10 +#define PICK_SW 14 // swatch width px +#define PICK_SH 14 // swatch height px +#define PICK_GAP 2 // gap between swatches +#define PICK_PAD 8 // panel padding +#define PICK_W (PICK_PAD*2 + PICK_COLS*(PICK_SW+PICK_GAP) - PICK_GAP) // 270 +#define PICK_H (32 + PICK_PAD + PICK_ROWS*(PICK_SH+PICK_GAP) - PICK_GAP + PICK_PAD) // 206 + +struct SZone { + double price_high, price_low; + int ci_from, ci_to; // índices absolutos de histórico (ci_from <= ci_to) + color border_clr; + bool raio_dir; // estende até a direita (borda do gráfico) + bool raio_esq; // estende até a esquerda + bool sobre_clusters; // z-order: desenha sobre os clusters + bool active; +}; + +struct SVProfile { + double price_high, price_low; + int ci_from, ci_to; + // Visualizar + bool show_volume; // barras de volume + bool show_delta; // barras de delta + bool delta_espelhado; // delta espelhado (bid esquerda, ask direita) + bool volume_soma; // soma bid+ask em barra única + // Linhas + bool show_poc; + bool show_va; // value area + double va_pct; // % value area (padrão 70) + bool show_vwap; + // Z-order + bool sobre_clusters; + // Cores + color clr_bid, clr_ask; // cores das barras de volume + color clr_poc; // linha do POC + color clr_borda; // borda do painel + // Dados calculados + double prices[VP_LEVELS]; + double vol_bid[VP_LEVELS]; + double vol_ask[VP_LEVELS]; + int count; + double poc_price, vah_price, val_price, vwap_price; + bool active; +}; + +//+------------------------------------------------------------------+ +//| Globais | +//+------------------------------------------------------------------+ +SCluster g_history[200]; +int g_history_count = 0; +int g_history_start = 0; +SCluster g_active; +bool g_active_valid = false; + +double g_tick_size = 0.01; +double g_step = 1.0; +double g_step_mult = 1.0; +double g_delta_max = 800; +int g_scroll = 0; // navegação L/R (em clusters) +int g_zoom_v = 16; // altura de nível em px (drag barra de preço) +int g_zoom_h = 100; // zoom horizontal % (drag painel inferior) +ulong g_last_draw_msc = 0; + +CCanvas g_canvas; +string g_canvas_name = "TesteFootprint_BMP"; +int g_canvas_w = 0; +int g_canvas_h = 0; + +ulong g_last_tick_msc = 0; +double g_last_bid_price = 0; +double g_last_mid_price = 0; +bool g_last_is_buy = true; +double g_last_bid = 0; +double g_last_ask = 0; +double g_current_price = 0; + +// Drag: barra de preço → zoom vertical +bool g_vaxis_drag = false; +int g_vdrag_y0 = 0; +int g_vdrag_z0 = 16; + +// Drag: painel inferior → zoom horizontal +bool g_haxis_drag = false; +int g_hdrag_x0 = 0; +int g_hdrag_zh0 = 100; + +// Pan vertical (arrastar área principal) +bool g_pan_drag = false; +int g_pan_y0 = 0; +double g_pan_off0 = 0.0; +double g_pan_offset = 0.0; // offset em preço aplicado ao viewport +double g_last_prange= 1.0; // prange da última renderização (para converter px → preço) + +// Runtime overrides (alterados pelo menu em tempo real) +ENUM_VIEW g_view_mode = VIEW_BIDASK; +string g_close_mode = "delta"; +color g_color_ask = (color)0x3B82F6; +color g_color_bid = (color)0xEC4899; +color g_color_poc = clrWhite; + +// Menu de configurações +bool g_menu_open = false; +int g_menu_tab = 0; // 0=Formação 1=Cores +double g_form_step = 75.0; +double g_form_delta = 800.0; +int g_form_view = 0; +string g_form_mode = "delta"; +bool g_prev_btn = false; // estado anterior do botão (detecção de click) + +// Ferramentas de desenho — Zona e VP +SZone g_zones[ZONE_MAX]; +int g_zone_count = 0; +SVProfile g_vprofiles[VP_MAX]; +int g_vp_count = 0; + +// Estado do último Redraw — conversão pixel↔cluster/preço +int g_rdr_cw=0, g_rdr_col_step=0, g_rdr_chart_x1=0, g_rdr_chart_h=0; +int g_rdr_last_idx=0, g_rdr_active_off=0; +double g_rdr_pmin=0, g_rdr_pmax=0; + +// Seleção de área para ferramentas de desenho (right-drag ou Ctrl+left-drag) +bool g_rbtn_prev = false; +bool g_sel_drag = false; +bool g_sel_is_right= false; // true=iniciado por right-drag, false=Ctrl+left-drag +int g_sel_x0=0, g_sel_y0=0; +int g_sel_x1=0, g_sel_y1=0; +bool g_sel_ready = false; +int g_seltb_x=0, g_seltb_y=0; // posição da toolbar (para hit-test) + +// Presets de cor para o menu (cycling) +color g_bid_presets[] = {(color)0xEC4899,(color)0xFF1744,(color)0xEC464F,(color)0xE53935}; +color g_ask_presets[] = {(color)0x3B82F6,(color)0x29B6F6,(color)0x3BA6F7,(color)0x1565C0}; +color g_poc_presets[] = {clrWhite,(color)0xFFD600,(color)0x00E5FF,(color)0x76FF03}; +int g_bid_pi=0, g_ask_pi=0, g_poc_pi=0; + +// Painel de configuração de zona +int g_zone_sel = -1; // índice da zona selecionada, -1=nenhuma +bool g_zone_panel = false; // painel de config aberto +int g_zpx=0, g_zpy=0; // posição do painel (ponto de clique) + +color g_zone_clr_presets[] = {(color)0xFF4081,(color)0x3B82F6,(color)0x22C55E, + (color)0xFFD600,(color)0xEF4444,(color)0xA855F7}; +int g_zone_clr_pi=0; + +// Painel de configuração de VP +int g_vp_sel = -1; +bool g_vp_panel = false; +int g_vppx=0, g_vppy=0; + +color g_vp_clr_presets[] = {(color)0xFFD600,(color)0xF59E0B,(color)0xFF4081, + (color)0x22C55E,(color)0x00E5FF}; +int g_vp_poc_pi=0; + +// Right-click: distingue click (abre config) vs drag (inicia seleção) +bool g_rclick_wait = false; +int g_rclick_xi=0, g_rclick_yi=0; +int g_rclick_vi=-1; // índice VP sob cursor no momento do right-press +int g_rclick_zi=-1; // índice Zone sob cursor no momento do right-press + +// Color picker +enum EPICK_TARGET { PICK_NONE=0, PICK_ZONE_BORDER, + PICK_VP_BID, PICK_VP_ASK, PICK_VP_POC, + PICK_GLOBAL_BID, PICK_GLOBAL_ASK, PICK_GLOBAL_POC }; +EPICK_TARGET g_pick_target = PICK_NONE; +bool g_pick_open = false; +int g_pick_px=0, g_pick_py=0; + +color g_pick_palette[160] = { + // Row 0: Grayscale + 0x000000,0x111111,0x222222,0x333333,0x444444,0x666666,0x808080,0x999999,0xBBBBBB,0xCCCCCC,0xDDDDDD,0xEEEEEE,0xFFFFFF,0xF5F5DC,0xFFF8DC,0xFFFAF0, + // Row 1: Red + 0xFF0000,0xFF1744,0xF50057,0xE53935,0xC62828,0xB71C1C,0x8B0000,0xFF5252,0xFF8A80,0xFFCDD2,0xFF4081,0xFC3158,0xE91E63,0xC2185B,0xAD1457,0x880E4F, + // Row 2: Orange + 0xFF6600,0xFF5722,0xF4511E,0xE64A19,0xBF360C,0xFF3D00,0xFF6D00,0xFF9100,0xFFAB40,0xFF8C00,0xFFA726,0xFB8C00,0xF57C00,0xEF6C00,0xE65100,0xDD2C00, + // Row 3: Yellow/Amber + 0xFFD600,0xFFEA00,0xFFFF00,0xFFF176,0xFFEB3B,0xFFC107,0xFFB300,0xFFA000,0xFF8F00,0xFF6F00,0xF9A825,0xF57F17,0xFDD835,0xF9D71C,0xFFEE58,0xFFF9C4, + // Row 4: Green + 0x00C853,0x00E676,0x69F0AE,0x22C55E,0x4CAF50,0x43A047,0x388E3C,0x2E7D32,0x1B5E20,0x76FF03,0xB2FF59,0x8BC34A,0x558B2F,0x33691E,0x00BFA5,0x1DE9B6, + // Row 5: Teal/Cyan + 0x00E5FF,0x18FFFF,0x00B8D4,0x00838F,0x00BCD4,0x00ACC1,0x0097A7,0x006064,0x009688,0x00897B,0x00796B,0x004D40,0x80DEEA,0x4DD0E1,0x26C6DA,0x84FFFF, + // Row 6: Blue + 0x3B82F6,0x2563EB,0x1D4ED8,0x448AFF,0x2196F3,0x1976D2,0x1565C0,0x0D47A1,0x29B6F6,0x03A9F4,0x039BE5,0x0288D1,0x0277BD,0x01579B,0x82B1FF,0x40C4FF, + // Row 7: Purple/Violet + 0xA855F7,0x9333EA,0x7E22CE,0x6200EA,0x651FFF,0x7C4DFF,0xD500F9,0xAA00FF,0xCE93D8,0xBA68C8,0xAB47BC,0x9C27B0,0x8E24AA,0x7B1FA2,0x4A148C,0x311B92, + // Row 8: Pink/Magenta + 0xFF80AB,0xF48FB1,0xF06292,0xEC407A,0xE91E63,0xD81B60,0xC2185B,0xAD1457,0xFF4081,0xFF1744,0xF50057,0xC51162,0xD81B60,0xC2185B,0xAD1457,0x880E4F, + // Row 9: Trading / acentos úteis + 0xFFD600,0xEC4899,0x3B82F6,0x22C55E,0xEF4444,0xA855F7,0x00E5FF,0xF97316,0x14B8A6,0x8B5CF6,0x6366F1,0x06B6D4,0x10B981,0xF59E0B,0x84CC16,0x795548 +}; + +//+------------------------------------------------------------------+ +//| Cores | +//+------------------------------------------------------------------+ +uint Argb(color c, int a) +{ return ((uint)a<<24)|((c>>16)&0xFF)<<16|((c>>8)&0xFF)<<8|(c&0xFF); } + +// Único jeito correto de semi-transparência em XRGB_NOALPHA +uint Blend(color bc, color bg, double t) +{ + t=MathMax(0.0,MathMin(1.0,t)); + uint r=(uint)(((bg>>16)&0xFF)*(1-t)+((bc>>16)&0xFF)*t); + uint g=(uint)(((bg>>8)&0xFF)*(1-t)+((bc>>8)&0xFF)*t); + uint b=(uint)((bg&0xFF)*(1-t)+(bc&0xFF)*t); + return 0xFF000000|(r<<16)|(g<<8)|b; +} + +//+------------------------------------------------------------------+ +//| Cluster helpers | +//+------------------------------------------------------------------+ +int CFindOrInsert(SCluster &c, double price) +{ + for(int i=0;i=2000) return -1; + int pos=c.level_count; + for(int i=0;ipos;i--) c.levels[i]=c.levels[i-1]; + c.levels[pos].price=price; c.levels[pos].ask=0; c.levels[pos].bid=0; + c.level_count++; return pos; +} + +void CCalcPoc(SCluster &c) +{ + // Usa max(ask,bid) — mesma métrica do local_max/barra — para que POC = nível sempre cheio + double mx=-1; c.poc_price=(c.level_count>0)?c.levels[0].price:0; + for(int i=0;imx){mx=t;c.poc_price=c.levels[i].price;}} +} + +void CAddVol(SCluster &c, double price, double volume, bool is_buy) +{ + double b=NormalizeDouble(MathRound(price/g_step)*g_step,_Digits); + int idx=CFindOrInsert(c,b); if(idx<0) return; + if(is_buy){c.levels[idx].ask+=volume;c.delta+=volume;} + else {c.levels[idx].bid+=volume;c.delta-=volume;} + c.total_volume+=volume; c.close_price=price; + if(c.level_count==1||b>c.price_high) c.price_high=b; + if(c.level_count==1||b=g_delta_max; + if(g_close_mode=="range") return (g_active.price_high-g_active.price_low)>=InpRangePoints*g_tick_size; + if(g_close_mode=="time") return (msc/1000-(ulong)g_active.open_time)>=(ulong)InpTimeSeconds; + if(g_close_mode=="volume") return g_active.total_volume>=InpVolumeMax; + return false; +} + +void ProcessTick(double price, double volume, bool is_buy, ulong msc) +{ + g_current_price=price; + if(g_close_mode!="delta") + { + if(!g_active_valid){ActiveReset();g_active.open_time=(datetime)(msc/1000);g_active.open_price=price;g_active_valid=true;} + CAddVol(g_active,price,volume,is_buy); + if(ShouldClose(msc)) CloseActive(g_close_mode); + return; + } + double rem=volume; + while(rem>0.0) + { + if(g_active_valid&&MathAbs(g_active.delta)>=g_delta_max){CloseActive("delta");continue;} + if(!g_active_valid){ActiveReset();g_active.open_time=(datetime)(msc/1000);g_active.open_price=price;g_active_valid=true;} + double cur=g_active.delta,contrib=is_buy?rem:-rem; + if(MathAbs(cur+contrib)<=g_delta_max){CAddVol(g_active,price,rem,is_buy);if(ShouldClose(msc))CloseActive("delta");break;} + double cap=MathMax(is_buy?(g_delta_max-cur):(cur+g_delta_max),0.0); + if(cap>0) CAddVol(g_active,price,cap,is_buy); + CloseActive("delta"); rem-=cap; + } +} + +bool ClassifyTick(MqlTick &t) +{ + if((t.flags&TICK_FLAG_BUY) !=0) return true; + if((t.flags&TICK_FLAG_SELL)!=0) return false; + return g_last_is_buy; +} + +bool ParseTick(MqlTick &t, double &p, double &v, bool &b) +{ + double bid=t.bid,ask=t.ask,last=t.last; + if(bid>0) g_last_bid=bid; + if(ask>0) g_last_ask=ask; + double mid=(bid>0&&ask>0)?(bid+ask)/2.0:0; + double pb=g_last_bid_price,pm=g_last_mid_price; + if(bid>0){if(bid>g_last_bid_price)g_last_is_buy=true;else if(bid0){if(mid>g_last_mid_price)g_last_is_buy=true;else if(mid0) g_last_mid_price=mid; + p=(last>0)?last:(mid>0?mid:(bid>0?bid:ask)); + if(p<=0) return false; + double ts=(g_tick_size>0)?g_tick_size:0.01; + if(pb>0&&bid>0) v=MathMax(MathAbs(bid-pb)/ts,1.0); + else if(pm>0&&mid>0)v=MathMax(MathAbs(mid-pm)/ts,1.0); + else v=1.0; + b=ClassifyTick(t); return true; +} + +void Reload() +{ + g_step = g_tick_size * g_step_mult; + g_pan_offset = 0.0; + ActiveReset(); + g_history_count = 0; + g_history_start = 0; + g_last_bid_price = 0; + g_last_mid_price = 0; + g_last_is_buy = true; + LoadHistory(); + Redraw(); +} + +void LoadHistory() +{ + datetime from=TimeCurrent()-InpHistoryHours*3600; + Print("TesteFootprint: carregando desde ",TimeToString(from)); + MqlTick ticks[]; + int count=CopyTicksRange(_Symbol,ticks,COPY_TICKS_ALL,(ulong)from*1000,(ulong)TimeCurrent()*1000); + if(count<=0){Print("TesteFootprint: sem ticks históricos.");return;} + Print("TesteFootprint: processando ",count," ticks..."); + for(int i=0;i0) g_last_tick_msc=ticks[count-1].time_msc; + Print("TesteFootprint: ",g_history_count," clusters prontos."); +} + +//+------------------------------------------------------------------+ +//| Canvas helpers | +//+------------------------------------------------------------------+ +void TxtOut(int x, int y, string text, uint clr, string font="Arial", int sz=8) +{ + g_canvas.FontSet(font, sz); + g_canvas.TextOut(x, y, text, clr); +} + +bool ResizeCanvas() +{ + int w=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS); + int h=(int)ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS); + if(w==g_canvas_w&&h==g_canvas_h) return false; + if(w<=0||h<=0) return false; + g_canvas_w=w; g_canvas_h=h; + g_canvas.Resize(w,h); return true; +} + +//+------------------------------------------------------------------+ +//| Renderiza cluster — porta fiel do React FootprintCanvas.jsx | +//+------------------------------------------------------------------+ +void DrawCluster(SCluster &c, int cx, int cw, int chart_h, + double pmin, double pmax, double max_side, bool is_active) +{ + if(c.level_count==0) return; + double prange=pmax-pmin; if(prange<=0) return; + + int cw2 = cw - 4; // largura interna (2px margem cada lado, como colX+1 no React) + if(cw2<4) cw2=4; + + // Máximo local para proporção das barras (igual React: por cluster, não global) + double local_max=1; + for(int li=0;lilocal_max)local_max=d; + } + + // Body bounds para wick detection (open/close, igual React) + double body_low =MathMin(c.open_price,c.close_price); + double body_high=MathMax(c.open_price,c.close_price); + bool is_bull =(c.close_price>=c.open_price); + + // row_h: espaçamento real entre níveis em px — igual ao React (rowHeight=zoomV, sem cap) + double px_step=(g_step>0&&prange>0)?((double)(chart_h-4)*g_step/prange):(double)g_zoom_v; + int row_h=MathMax(2,(int)px_step); + + //--- Células de nível (bid/ask bars) --- + for(int li=0;li=bid_v); + + int yc=chart_h-1-(int)((price-pmin)/prange*(chart_h-4)); + int yt=yc-row_h/2, yb=yt+row_h-1; + if(yt<0)yt=0; if(yb>=chart_h)yb=chart_h-1; if(yt>=yb)continue; + + // Wick: fora do corpo → alpha 0.65, dentro → alpha 0.85 + // Tolerância = g_step*0.5 (igual React: actualTickSize = tick_size * stepMultiplier) + bool is_wick=(pricebody_high+g_step*0.5); + double alpha =is_wick?0.65:0.85; + + // Barra proporcional lado dominante — igual em ambos os modos (fiel ao React) + { + int bw=(int)(dom/local_max*(double)cw2); if(bw<1)bw=1; + g_canvas.FillRectangle(cx+2,yt+1,cx+2+bw-1,yb-1, + Blend(is_ask?g_color_ask:g_color_bid,InpColorBg,alpha)); + } + + // POC: borda + número — sempre, inclusive no pavio + // BidAsk: mostra dominantVal | Delta: mostra ask-bid com cor verde/vermelho + if(MathAbs(price-c.poc_price)<1e-8) + { + g_canvas.Rectangle(cx+2,yt,cx+cw2+1,yb,Argb(g_color_poc,255)); + double delta_poc=ask_v-bid_v; + string s=(g_view_mode==VIEW_DELTA) + ? ((delta_poc>=0?"+":"")+IntegerToString((int)delta_poc)) + : IntegerToString((int)dom); + uint txt_clr=(g_view_mode==VIEW_DELTA) + ? Argb(delta_poc>=0?(color)0x00E676:(color)0xFF1744,255) + : Argb(0xFFFFFF,255); + g_canvas.FontSet("Arial Bold",InpFontSize); + int tx=cx+cw2-(int)StringLen(s)*(InpFontSize-1); + if(row_h>=InpFontSize) + g_canvas.TextOut(tx,yt+(row_h-InpFontSize)/2,s,txt_clr); + } + + } + + //--- Borda OHLC do cluster (open_price → close_price = CORPO) --- + // Igual ao React: strokeRect ao redor do corpo, NÃO do high/low + int y_open =chart_h-1-(int)((c.open_price -pmin)/prange*(chart_h-4)); + int y_close =chart_h-1-(int)((c.close_price-pmin)/prange*(chart_h-4)); + int candle_top=MathMin(y_open,y_close)-row_h/2; + int candle_bot=MathMax(y_open,y_close)+row_h/2; + if(candle_top<0)candle_top=0; + if(candle_bot>=chart_h)candle_bot=chart_h-1; + if(candle_bot>candle_top) + g_canvas.Rectangle(cx,candle_top,cx+cw,candle_bot, + Blend(is_bull?(color)0x00E676:(color)0xFF1744,InpColorBg,0.55)); + + //--- Linhas de pavio (centro da coluna, igual candle do React) --- + int cx_mid=cx+cw/2; + int y_high=chart_h-1-(int)((c.price_high-pmin)/prange*(chart_h-4)); + int y_low =chart_h-1-(int)((c.price_low -pmin)/prange*(chart_h-4)); + uint wick_clr=Blend(is_bull?(color)0x00E676:(color)0xFF1744,InpColorBg,0.4); + if(c.price_high>body_high+g_tick_size*0.5&&y_highcandle_bot) + g_canvas.Line(cx_mid,candle_bot,cx_mid,y_low+row_h,wick_clr); + + + //--- Painel inferior --- + double total_bid=0,total_ask=0; + for(int li=0;li=1000)?(DoubleToString(dom_vol/1000.0,1)+"K"):IntegerToString((int)dom_vol); + int vol_fs=MathMax(10,InpFontSize+4); + g_canvas.FontSet("Arial Bold",vol_fs); + int ly=bar_base-2-vol_fs; + if(ly>panel_top) g_canvas.TextOut(cx+pw/2,ly,vol_lbl,Argb(0xFFFFFF,255),TA_CENTER|TA_TOP); + + // Bloco delta — ocupa o resto do painel até o timestamp + int delta_y = bar_base + 1; + int delta_b = panel_bot; + if(delta_b > delta_y + 4) + { + g_canvas.FillRectangle(cx,delta_y,cx+pw-1,delta_b, + Argb((c.delta>=0)?0x2563EB:0xF97316,255)); + string ds=(c.delta>=0?"+":"")+IntegerToString((int)c.delta); + g_canvas.FontSet("Arial Bold",InpFontSize); + int dh=delta_b-delta_y; + g_canvas.TextOut(cx+pw/2,delta_y+(dh-InpFontSize)/2,ds,Argb(0xFFFFFF,255),TA_CENTER|TA_TOP); + } + + // Horário no fundo + if(c.open_time>0&&g_canvas_h-11>chart_h) + { + g_canvas.FontSet("Arial",InpFontSize-1); + g_canvas.TextOut(cx+2,g_canvas_h-11, + TimeToString(c.open_time,TIME_MINUTES),Argb(InpColorText,255)); + } +} + +//+------------------------------------------------------------------+ +//| Menu de Configurações | +//+------------------------------------------------------------------+ +#define MNU_X 20 +#define MNU_Y 45 +#define MNU_W 280 +#define MNU_BG 0x0D1220 +#define MNU_CARD 0x182032 +#define MNU_BTN 0x253348 +#define MNU_GRN 0x00C853 + +void MnuLabel(int y, string txt) +{ + g_canvas.FontSet("Arial Bold",7); + g_canvas.TextOut(MNU_X+10,y,txt,Argb(0x7A8EA8,255)); +} + +void MnuToggle(int y, string l, string r, int sel) +{ + int lx=MNU_X+10, mid=MNU_X+MNU_W/2, rx=MNU_X+MNU_W-10, h=30; + g_canvas.FillRectangle(lx,y,mid,y+h,sel==0?Argb(MNU_BTN,255):Argb(MNU_CARD,255)); + g_canvas.FillRectangle(mid,y,rx,y+h,sel==1?Argb(MNU_BTN,255):Argb(MNU_CARD,255)); + g_canvas.Rectangle(lx,y,rx,y+h,Argb(0x334155,255)); + g_canvas.Line(mid,y,mid,y+h,Argb(0x334155,255)); + g_canvas.FontSet("Arial Bold",9); + g_canvas.TextOut(lx+(mid-lx)/2-(int)StringLen(l)*5/2,y+9,l,Argb(0xFFFFFF,255)); + g_canvas.TextOut(mid+(rx-mid)/2-(int)StringLen(r)*5/2,y+9,r,Argb(0xFFFFFF,255)); +} + +void MnuStepper(int y, double val) +{ + int lx=MNU_X+10, rx=MNU_X+MNU_W-10, w=rx-lx; + g_canvas.FillRectangle(lx,y,rx,y+32,Argb(MNU_CARD,255)); + g_canvas.Rectangle(lx,y,rx,y+32,Argb(0x334155,255)); + g_canvas.FillRectangle(lx+1,y+1,lx+32,y+31,Argb(MNU_BTN,255)); + g_canvas.FillRectangle(rx-32,y+1,rx-1,y+31,Argb(MNU_BTN,255)); + g_canvas.FontSet("Arial Bold",13); g_canvas.TextOut(lx+8,y+5,"−",Argb(0xFFFFFF,255)); + g_canvas.FontSet("Arial Bold",13); g_canvas.TextOut(rx-22,y+5,"+",Argb(0xFFFFFF,255)); + string sv=IntegerToString((int)val); + g_canvas.FontSet("Arial Bold",10); + g_canvas.TextOut(lx+w/2-(int)StringLen(sv)*6/2,y+9,sv,Argb(0xFFFFFF,255)); +} + +void MnuDropdown(int y, string val) +{ + int lx=MNU_X+10, rx=MNU_X+MNU_W-10; + g_canvas.FillRectangle(lx,y,rx,y+32,Argb(MNU_CARD,255)); + g_canvas.Rectangle(lx,y,rx,y+32,Argb(0x334155,255)); + string cap=val; + StringSetCharacter(cap,0,(ushort)(StringGetCharacter(cap,0)-32)); + g_canvas.FontSet("Arial",10); g_canvas.TextOut(lx+10,y+9,cap,Argb(0xFFFFFF,255)); + g_canvas.FontSet("Arial",9); g_canvas.TextOut(rx-18,y+10,"▼",Argb(0x94A3B8,255)); +} + +void MnuColorRow(int y, string lbl, color c) +{ + int rx=MNU_X+MNU_W-10; + g_canvas.FontSet("Arial Bold",9); g_canvas.TextOut(MNU_X+10,y+10,lbl,Argb(0xFFFFFF,255)); + string hs=StringFormat("#%02X%02X%02X",(c>>16)&0xFF,(c>>8)&0xFF,c&0xFF); + g_canvas.FontSet("Arial",8); g_canvas.TextOut(rx-96,y+12,hs,Argb(0x64748B,255)); + g_canvas.FillRectangle(rx-40,y+4,rx-4,y+36,Argb(c,255)); + g_canvas.Rectangle(rx-40,y+4,rx-4,y+36,Argb(0x475569,255)); +} + +void DrawGearBtn() +{ + uint bg=g_menu_open?Argb(MNU_GRN,255):Argb(0x1E293B,255); + g_canvas.FillRectangle(5,8,33,36,bg); + g_canvas.Rectangle(5,8,33,36,Argb(0x334155,255)); + g_canvas.FontSet("Arial",15); g_canvas.TextOut(9,10,"⚙",Argb(0xFFFFFF,255)); +} + +void DrawMenu() +{ + int tabY=MNU_Y+42, cy=tabY+38; + int panH=(g_menu_tab==0)?418:255; + g_canvas.FillRectangle(MNU_X,MNU_Y,MNU_X+MNU_W,MNU_Y+panH,Argb(MNU_BG,255)); + g_canvas.Rectangle(MNU_X,MNU_Y,MNU_X+MNU_W,MNU_Y+panH,Argb(0x2A364F,255)); + // Header + g_canvas.FillRectangle(MNU_X,MNU_Y,MNU_X+MNU_W,MNU_Y+34,Argb(0x080E18,255)); + g_canvas.FontSet("Arial Bold",11); g_canvas.TextOut(MNU_X+14,MNU_Y+9,"CONFIGURAÇÕES",Argb(0xFFFFFF,255)); + g_canvas.FontSet("Arial Bold",13); g_canvas.TextOut(MNU_X+MNU_W-18,MNU_Y+7,"×",Argb(0x94A3B8,255)); + // Tabs + int mid=MNU_X+MNU_W/2; + g_canvas.FillRectangle(MNU_X+10,tabY,mid-2,tabY+30,g_menu_tab==0?Argb(MNU_GRN,255):Argb(MNU_CARD,255)); + g_canvas.FillRectangle(mid+2,tabY,MNU_X+MNU_W-10,tabY+30,g_menu_tab==1?Argb(MNU_GRN,255):Argb(MNU_CARD,255)); + g_canvas.Rectangle(MNU_X+10,tabY,MNU_X+MNU_W-10,tabY+30,Argb(0x334155,255)); + g_canvas.Line(mid,tabY,mid,tabY+30,Argb(0x334155,255)); + g_canvas.FontSet("Arial Bold",9); + g_canvas.TextOut(MNU_X+30,tabY+9,"Formação",Argb(0xFFFFFF,255)); + g_canvas.TextOut(mid+22,tabY+9,"Cores",Argb(0xFFFFFF,255)); + if(g_menu_tab==0) + { + MnuLabel(cy,"MODO DE EXIBIÇÃO"); cy+=16; + MnuToggle(cy,"Bid x Ask","Delta",g_form_view); cy+=38; + MnuLabel(cy,"PASSO DO PREÇO (ticks)"); cy+=16; + MnuStepper(cy,g_form_step); cy+=40; + MnuLabel(cy,"MODO DE FECHAMENTO"); cy+=16; + MnuDropdown(cy,g_form_mode); cy+=40; + MnuLabel(cy,"DELTA MÁX"); cy+=16; + MnuStepper(cy,g_form_delta); cy+=44; + bool dirty=(g_form_step!=g_step_mult||g_form_delta!=g_delta_max||g_form_view!=(int)g_view_mode||g_form_mode!=g_close_mode); + uint abg=dirty?Argb(0x1E40AF,255):Argb(0x1A2234,255); + g_canvas.FillRectangle(MNU_X+10,cy,MNU_X+MNU_W-10,cy+34,abg); + g_canvas.Rectangle(MNU_X+10,cy,MNU_X+MNU_W-10,cy+34,Argb(0x334155,255)); + g_canvas.FontSet("Arial Bold",10); + g_canvas.TextOut(MNU_X+MNU_W/2-22,cy+10,"Aplicar",Argb(dirty?(color)0xFFFFFF:(color)0x475569,255)); + } + else + { + MnuColorRow(cy,"BID (VENDA)",g_color_bid); cy+=50; + MnuColorRow(cy,"ASK (COMPRA)",g_color_ask); cy+=50; + MnuColorRow(cy,"POC (BORDA)",g_color_poc); cy+=50; + g_canvas.FontSet("Arial",8); + g_canvas.TextOut(MNU_X+32,cy+6,"Cores salvas automaticamente",Argb(0x475569,255)); + } +} + +void HandleMenuClick(int mx, int my) +{ + // Gear button + if(mx>=5&&mx<=33&&my>=8&&my<=36) + { + g_menu_open=!g_menu_open; + if(g_menu_open){g_form_step=g_step_mult;g_form_delta=g_delta_max;g_form_view=(int)g_view_mode;g_form_mode=g_close_mode;} + Redraw(); return; + } + if(!g_menu_open) return; + // X button + if(mx>=MNU_X+MNU_W-22&&my>=MNU_Y&&my<=MNU_Y+34){g_menu_open=false;Redraw();return;} + // Tabs + int tabY=MNU_Y+42, mid=MNU_X+MNU_W/2; + if(my>=tabY&&my<=tabY+30) + { + if(mx>=MNU_X+10&&mx=mid&&mx<=MNU_X+MNU_W-10){g_menu_tab=1;Redraw();return;} + } + int cy=tabY+38; + if(g_menu_tab==0) + { + cy+=16; + if(my>=cy&&my<=cy+30){if(mx=cy&&my<=cy+32){if(mx<=MNU_X+42)g_form_step=MathMax(1,g_form_step-50);else if(mx>=MNU_X+MNU_W-42)g_form_step+=50;Redraw();return;} + cy+=40+16; + if(my>=cy&&my<=cy+32){if(g_form_mode=="delta")g_form_mode="volume";else if(g_form_mode=="volume")g_form_mode="range";else if(g_form_mode=="range")g_form_mode="time";else g_form_mode="delta";Redraw();return;} + cy+=40+16; + if(my>=cy&&my<=cy+32){if(mx<=MNU_X+42)g_form_delta=MathMax(200,g_form_delta-200);else if(mx>=MNU_X+MNU_W-42)g_form_delta+=200;Redraw();return;} + cy+=44; + if(my>=cy&&my<=cy+34&&mx>=MNU_X+10&&mx<=MNU_X+MNU_W-10) + { + g_step_mult=g_form_step; g_delta_max=g_form_delta; + g_view_mode=(ENUM_VIEW)g_form_view; g_close_mode=g_form_mode; + Reload(); return; + } + } + else + { + int rx=MNU_X+MNU_W-10; + if(my>=cy+4&&my<=cy+36&&mx>=rx-40&&mx<=rx-4){ OpenColorPicker(PICK_GLOBAL_BID, rx-40, cy+36); return; } + cy+=50; + if(my>=cy+4&&my<=cy+36&&mx>=rx-40&&mx<=rx-4){ OpenColorPicker(PICK_GLOBAL_ASK, rx-40, cy+36); return; } + cy+=50; + if(my>=cy+4&&my<=cy+36&&mx>=rx-40&&mx<=rx-4){ OpenColorPicker(PICK_GLOBAL_POC, rx-40, cy+36); return; } + } +} + +//+------------------------------------------------------------------+ +//| Eixo de preço | +//+------------------------------------------------------------------+ +void DrawPriceAxis(int x0, int chart_h, double pmin, double pmax) +{ + double prange=pmax-pmin; if(prange<=0) return; + g_canvas.FillRectangle(x0,0,g_canvas_w-1,chart_h,Argb(InpColorBg,255)); + g_canvas.Line(x0,0,x0,chart_h,Argb(0x2A364F,255)); + double step=g_step*10,nL=prange/step; + if(nL>20) step*=MathCeil(nL/20.0); + for(double p=MathCeil(pmin/step)*step;p<=pmax;p+=step) + { + int y=chart_h-1-(int)((p-pmin)/prange*(chart_h-4)); + if(y<5||y>chart_h-5) continue; + g_canvas.Line(x0,y,x0+4,y,Argb(0x475569,255)); + TxtOut(x0+7,y-5,DoubleToString(p,_Digits),Argb(0x64748B,255),"Arial",InpFontSize); + } +} + +//+------------------------------------------------------------------+ +//| Conversores pixel ↔ preço/cluster (usam estado do último Redraw) | +//+------------------------------------------------------------------+ +int PriceToY(double price) +{ + if(g_rdr_chart_h<=0||g_rdr_pmax<=g_rdr_pmin) return 0; + return g_rdr_chart_h-1-(int)((price-g_rdr_pmin)/(g_rdr_pmax-g_rdr_pmin)*(g_rdr_chart_h-4)); +} +double YToPrice(int y) +{ + if(g_rdr_chart_h<=0||g_rdr_pmax<=g_rdr_pmin) return 0; + return g_rdr_pmin+(double)(g_rdr_chart_h-1-y)*(g_rdr_pmax-g_rdr_pmin)/(g_rdr_chart_h-4); +} +// X canvas → índice absoluto do cluster no histórico +int XToClusterIdx(int x) +{ + if(g_rdr_col_step<=0) return 0; + int ref_x=g_rdr_chart_x1-g_rdr_col_step*(3+g_rdr_active_off)-g_rdr_cw; + int steps=(ref_x-x)/g_rdr_col_step; + if(steps<0) steps=0; + return MathMax(0,MathMin(g_history_count-1, g_rdr_last_idx-steps)); +} +// Índice absoluto → X esquerdo do cluster no canvas (-1 = fora da área) +int ClusterToX(int ci) +{ + if(g_rdr_col_step<=0) return -1; + int steps=g_rdr_last_idx-ci; + if(steps<0) return -1; + return g_rdr_chart_x1-g_rdr_col_step*(3+g_rdr_active_off+steps)-g_rdr_cw; +} + +//+------------------------------------------------------------------+ +//| Zona — fill antes dos clusters, borda depois | +//+------------------------------------------------------------------+ +void DrawZoneFills(int chart_h, double pmin, double pmax) +{ + for(int zi=0;ziy2){int t=y1;y1=y2;y2=t;} + y1=MathMax(0,y1); y2=MathMin(chart_h-1,y2); + uint fill=Blend(g_zones[zi].border_clr,(color)InpColorBg,0.10); + g_canvas.FillRectangle(x1,y1,x2,y2,0xFF000000|fill); + } +} +void DrawZoneBorders(int chart_h, double pmin, double pmax) +{ + for(int zi=0;ziy2){int t=y1;y1=y2;y2=t;} + y1=MathMax(0,y1); y2=MathMin(chart_h-1,y2); + if(g_zones[zi].sobre_clusters) + { + uint fill=Blend(g_zones[zi].border_clr,(color)InpColorBg,0.25); + g_canvas.FillRectangle(x1,y1,x2,y2,0xFF000000|fill); + } + // Destacar zona selecionada + if(g_zone_panel && zi==g_zone_sel) + g_canvas.Rectangle(x1-1,y1-1,x2+1,y2+1,Argb(0xFFFFFF,255)); + g_canvas.Rectangle(x1,y1,x2,y2,Argb(g_zones[zi].border_clr,255)); + } +} + +//+------------------------------------------------------------------+ +//| Volume Profile — desenhado sobre os clusters | +//+------------------------------------------------------------------+ +void CalcVProfile(SVProfile &vp, int ci_from, int ci_to, + double price_high, double price_low) +{ + vp.ci_from=ci_from; vp.ci_to=ci_to; + vp.price_high=price_high; vp.price_low=price_low; + vp.show_volume=true; vp.show_delta=false; vp.delta_espelhado=true; vp.volume_soma=false; + vp.show_poc=true; vp.show_va=true; vp.va_pct=70.0; vp.show_vwap=false; + vp.sobre_clusters=false; + vp.clr_bid=g_color_bid; vp.clr_ask=g_color_ask; vp.clr_poc=(color)0xFFD600; + vp.clr_borda=(color)0x2A364F; vp.count=0; + ArrayInitialize(vp.prices,0); ArrayInitialize(vp.vol_bid,0); ArrayInitialize(vp.vol_ask,0); + + double cum_pv=0, cum_v=0; + // Percorre clusters na faixa + for(int ci=ci_from;ci<=ci_to&&ciprice_high+g_step) continue; + double bin=MathRound(p/g_step)*g_step; + int idx=-1; + for(int k=0;k=VP_LEVELS) continue; + idx=vp.count++; + vp.prices[idx]=bin; vp.vol_bid[idx]=0; vp.vol_ask[idx]=0; + } + vp.vol_bid[idx]+=g_history[hi].levels[li].bid; + vp.vol_ask[idx]+=g_history[hi].levels[li].ask; + double vol=g_history[hi].levels[li].bid+g_history[hi].levels[li].ask; + cum_pv+=bin*vol; cum_v+=vol; + } + } + vp.vwap_price=(cum_v>0)?cum_pv/cum_v:0; + + // POC — nível com maior volume total + double mx=-1; vp.poc_price=0; + double total_vol=0; + for(int i=0;imx){mx=t;vp.poc_price=vp.prices[i];} + total_vol+=t; + } + + // VAH/VAL — expansão 70% a partir do POC + vp.vah_price=vp.poc_price; vp.val_price=vp.poc_price; + if(total_vol>0&&mx>0) + { + double poc_vol=0; + for(int i=0;i=lo_v){cum+=up_v;vp.vah_price=up;up+=g_step;} + else {cum+=lo_v;vp.val_price=lo;lo-=g_step;} + } + } + vp.active=true; +} + +// VA overlay: retângulo semi-transparente sobre TODA a largura do gráfico +// Deve ser chamado ANTES dos clusters +void DrawVAOverlays(int chart_h, double pmin, double pmax) +{ + double prange=pmax-pmin; + for(int vi=0;vival_y){int t=vah_y;vah_y=val_y;val_y=t;} + vah_y=MathMax(0,vah_y); val_y=MathMin(chart_h-1,val_y); + uint fill=Blend(g_vprofiles[vi].clr_ask,(color)InpColorBg,0.14); + g_canvas.FillRectangle(0,vah_y,g_rdr_chart_x1,val_y,fill); + } +} + +// behind=true → desenha VPs que ficam ATRÁS dos clusters (sobre_clusters=false) +// behind=false → desenha VPs que ficam NA FRENTE dos clusters (sobre_clusters=true) +void DrawVProfiles(int chart_h, double pmin, double pmax, bool behind) +{ + int cw = g_rdr_chart_x1; // largura total do gráfico (para linhas full-width) + + for(int vi=0;vimax_vol)max_vol=t;} + double max_delta=1; + for(int i=0;imax_delta)max_delta=d;} + + double prange=pmax-pmin; + color clr_bid=g_vprofiles[vi].clr_bid; + color clr_ask=g_vprofiles[vi].clr_ask; + + for(int i=0;iyb){int t=yt;yt=yb;yb=t;} + if(yb<0||yt>chart_h) continue; + yt=MathMax(0,yt); yb=MathMin(chart_h-1,yb); + + bool in_va=(g_vprofiles[vi].show_va&&price>=g_vprofiles[vi].val_price&&price<=g_vprofiles[vi].vah_price); + double alpha_out=0.30; + + if(g_vprofiles[vi].show_volume) + { + double total=g_vprofiles[vi].vol_bid[i]+g_vprofiles[vi].vol_ask[i]; + int bw=(int)(total/max_vol*(bar_max-2)); + if(bw<1&&total>0) bw=1; + if(g_vprofiles[vi].volume_soma) + { + uint c=in_va?Argb(clr_ask,255):Blend(clr_ask,fundo,alpha_out); + if(bw>0) g_canvas.FillRectangle(bx1+1,yt,bx1+bw,yb,c); + } + else + { + int bw_bid=(total>0)?(int)(g_vprofiles[vi].vol_bid[i]/total*bw):0; + int bw_ask=bw-bw_bid; + uint c_bid=in_va?Argb(clr_bid,255):Blend(clr_bid,fundo,alpha_out); + uint c_ask=in_va?Argb(clr_ask,255):Blend(clr_ask,fundo,alpha_out); + if(bw_bid>0) g_canvas.FillRectangle(bx1+1,yt,bx1+bw_bid,yb,c_bid); + if(bw_ask>0) g_canvas.FillRectangle(bx1+1+bw_bid,yt,bx1+bw,yb,c_ask); + } + } + + if(g_vprofiles[vi].show_delta) + { + double delta=g_vprofiles[vi].vol_ask[i]-g_vprofiles[vi].vol_bid[i]; + int bw=(int)(MathAbs(delta)/max_delta*(bar_max/2-2)); + if(bw<1&&delta!=0) bw=1; + bool pos=(delta>=0); + uint c=pos?(in_va?Argb(clr_ask,255):Blend(clr_ask,fundo,alpha_out)) + :(in_va?Argb(clr_bid,255):Blend(clr_bid,fundo,alpha_out)); + int cx_delta=bx1+bar_max/2; + if(g_vprofiles[vi].delta_espelhado) + { + if(pos) g_canvas.FillRectangle(cx_delta,yt,cx_delta+bw,yb,c); + else g_canvas.FillRectangle(cx_delta-bw,yt,cx_delta,yb,c); + } + else g_canvas.FillRectangle(bx1+1,yt,bx1+bw,yb,c); + } + } + + // POC — linha no painel + linha pontilhada por todo o gráfico + if(g_vprofiles[vi].show_poc&&g_vprofiles[vi].poc_price>0) + { + int poc_y=chart_h-1-(int)((g_vprofiles[vi].poc_price-pmin)/prange*(chart_h-4)); + if(poc_y>=0&&poc_y0) + { + int vah_y=chart_h-1-(int)((g_vprofiles[vi].vah_price-pmin)/prange*(chart_h-4)); + for(int xx=0;xx0) + { + int vy=chart_h-1-(int)((g_vprofiles[vi].vwap_price-pmin)/prange*(chart_h-4)); + if(vy>=0&&vyg_canvas_w) px=g_canvas_w-pw-4; + if(py+ph>g_canvas_h) py=g_canvas_h-ph-4; + if(px<0) px=4; + if(py<0) py=4; +} + +void DrawZoneCheck(int x, int y, bool val, string lbl) +{ + g_canvas.Rectangle(x,y,x+12,y+12,Argb(0x475569,255)); + if(val) + { + g_canvas.Line(x+2,y+7,x+5,y+11,Argb(0x22C55E,255)); + g_canvas.Line(x+5,y+11,x+10,y+3,Argb(0x22C55E,255)); + } + g_canvas.FontSet("Arial",9); + g_canvas.TextOut(x+16,y,lbl,Argb(0xCBD5E1,255),TA_LEFT|TA_TOP); +} + +void DrawZonePanel() +{ + if(!g_zone_panel||g_zone_sel<0||g_zone_sel>=g_zone_count) return; + int pw=244, ph=205; + int px, py; ZonePanelPos(px,py); + + g_canvas.FillRectangle(px,py,px+pw,py+ph,Argb(0x0F1520,255)); + g_canvas.Rectangle(px,py,px+pw,py+ph,Argb(0x2A364F,255)); + + // Header + g_canvas.FillRectangle(px,py,px+pw,py+30,Argb(0x080E18,255)); + g_canvas.FontSet("Arial Bold",10); + g_canvas.TextOut(px+10,py+8,StringFormat("Zona #%d",g_zone_sel+1),Argb(0xFF4081,255),TA_LEFT|TA_TOP); + g_canvas.FontSet("Arial Bold",14); + g_canvas.TextOut(px+pw-18,py+5,"×",Argb(0x94A3B8,255),TA_LEFT|TA_TOP); + + // Info preço + int cy=py+36; int lx=px+10; int vx=px+118; + g_canvas.FontSet("Arial",9); + g_canvas.TextOut(lx,cy,"Preço alto:",Argb(0x64748B,255),TA_LEFT|TA_TOP); + g_canvas.TextOut(vx,cy,DoubleToString(g_zones[g_zone_sel].price_high,2),Argb(0xE2E8F0,255),TA_LEFT|TA_TOP); + cy+=18; + g_canvas.TextOut(lx,cy,"Preço baixo:",Argb(0x64748B,255),TA_LEFT|TA_TOP); + g_canvas.TextOut(vx,cy,DoubleToString(g_zones[g_zone_sel].price_low,2),Argb(0xE2E8F0,255),TA_LEFT|TA_TOP); + cy+=22; + + // Separador + g_canvas.Line(px+4,cy,px+pw-4,cy,Argb(0x1E293B,255)); + cy+=8; + + // Cor borda + g_canvas.TextOut(lx,cy,"Cor borda:",Argb(0x64748B,255),TA_LEFT|TA_TOP); + color bc=g_zones[g_zone_sel].border_clr; + g_canvas.FillRectangle(vx,cy,vx+32,cy+14,Argb(bc,255)); + g_canvas.Rectangle(vx,cy,vx+32,cy+14,Argb(0x475569,255)); + g_canvas.FontSet("Arial",8); + g_canvas.TextOut(vx+36,cy+2,"← clique",Argb(0x475569,255),TA_LEFT|TA_TOP); + cy+=24; + + // Separador + g_canvas.Line(px+4,cy,px+pw-4,cy,Argb(0x1E293B,255)); + cy+=8; + + // Checkboxes + DrawZoneCheck(lx,cy,g_zones[g_zone_sel].raio_esq,"Raio esquerdo"); + DrawZoneCheck(px+130,cy,g_zones[g_zone_sel].raio_dir,"Raio direito"); + cy+=22; + DrawZoneCheck(px+130,cy,g_zones[g_zone_sel].sobre_clusters,"Sobre os clusters"); + cy+=26; + + // Separador + g_canvas.Line(px+4,cy,px+pw-4,cy,Argb(0x1E293B,255)); + cy+=8; + + // Botão excluir + g_canvas.FillRectangle(px+10,cy,px+pw-10,cy+24,Argb(0x7F1D1D,255)); + g_canvas.Rectangle(px+10,cy,px+pw-10,cy+24,Argb(0xEF4444,255)); + g_canvas.FontSet("Arial Bold",9); + g_canvas.TextOut(px+pw/2,cy+6,"Excluir zona",Argb(0xFFFFFF,255),TA_CENTER|TA_TOP); +} + +int HitTestZone(int mx, int my) +{ + if(g_rdr_col_step<=0) return -1; + int chart_h=g_canvas_h-InpBottomPanel; + if(my<0||my>=chart_h) return -1; + for(int zi=0;zix2) continue; + int y1=PriceToY(g_zones[zi].price_high); + int y2=PriceToY(g_zones[zi].price_low); + if(y1>y2){int t=y1;y1=y2;y2=t;} + if(my>=y1&&my<=y2) return zi; + } + return -1; +} + +void DrawColorPicker() +{ + int px=g_pick_px, py=g_pick_py; + g_canvas.FillRectangle(px,py,px+PICK_W,py+PICK_H,Argb(0x0F1520,255)); + g_canvas.Rectangle(px,py,px+PICK_W,py+PICK_H,Argb(0x475569,255)); + g_canvas.FillRectangle(px,py,px+PICK_W,py+30,Argb(0x080E18,255)); + g_canvas.FontSet("Arial Bold",9); + g_canvas.TextOut(px+8,py+9,"Selecionar Cor",Argb(0xE2E8F0,255),TA_LEFT|TA_TOP); + g_canvas.TextOut(px+PICK_W-18,py+8,"✕",Argb(0xEF4444,255),TA_LEFT|TA_TOP); + g_canvas.Line(px,py+30,px+PICK_W,py+30,Argb(0x2A364F,255)); + int n=ArraySize(g_pick_palette); + for(int r=0;r=n) break; + int sx=px+PICK_PAD+c*(PICK_SW+PICK_GAP); + int sy=py+32+PICK_PAD/2+r*(PICK_SH+PICK_GAP); + g_canvas.FillRectangle(sx,sy,sx+PICK_SW-1,sy+PICK_SH-1,Argb(g_pick_palette[idx],255)); + g_canvas.Rectangle(sx,sy,sx+PICK_SW-1,sy+PICK_SH-1,Argb(0x2A364F,255)); + } +} + +void ApplyPickedColor(color c) +{ + switch(g_pick_target) + { + case PICK_ZONE_BORDER: if(g_zone_sel>=0) g_zones[g_zone_sel].border_clr=c; break; + case PICK_VP_BID: if(g_vp_sel>=0) g_vprofiles[g_vp_sel].clr_bid=c; break; + case PICK_VP_ASK: if(g_vp_sel>=0) g_vprofiles[g_vp_sel].clr_ask=c; break; + case PICK_VP_POC: if(g_vp_sel>=0) g_vprofiles[g_vp_sel].clr_poc=c; break; + case PICK_GLOBAL_BID: g_color_bid=c; break; + case PICK_GLOBAL_ASK: g_color_ask=c; break; + case PICK_GLOBAL_POC: g_color_poc=c; break; + default: break; + } +} + +void HandleColorPickerClick(int mx, int my) +{ + int px=g_pick_px, py=g_pick_py; + if(mx>=px+PICK_W-22&&my>=py&&my<=py+30){ g_pick_open=false; Redraw(); return; } + int gx=mx-(px+PICK_PAD); + int gy=my-(py+32+PICK_PAD/2); + if(gx<0||gy<0) return; + int col=gx/(PICK_SW+PICK_GAP), row=gy/(PICK_SH+PICK_GAP); + if(col<0||col>=PICK_COLS||row<0||row>=PICK_ROWS) return; + if(gx%(PICK_SW+PICK_GAP)>=PICK_SW||gy%(PICK_SH+PICK_GAP)>=PICK_SH) return; + int idx=row*PICK_COLS+col; + if(idx>=ArraySize(g_pick_palette)) return; + ApplyPickedColor(g_pick_palette[idx]); + g_pick_open=false; + Redraw(); +} + +void OpenColorPicker(EPICK_TARGET target, int near_x, int near_y) +{ + g_pick_target=target; + g_pick_px=near_x; g_pick_py=near_y; + if(g_pick_px+PICK_W>g_canvas_w) g_pick_px=g_canvas_w-PICK_W-4; + if(g_pick_py+PICK_H>g_canvas_h) g_pick_py=g_canvas_h-PICK_H-4; + if(g_pick_px<0) g_pick_px=4; + if(g_pick_py<0) g_pick_py=4; + g_pick_open=true; + Redraw(); +} + +void HandleZonePanelClick(int mx, int my) +{ + if(g_zone_sel<0||g_zone_sel>=g_zone_count) return; + int pw=244, ph=205; + int px, py; ZonePanelPos(px,py); + + // Botão ✕ fechar + if(mx>=px+pw-22&&my>=py&&my<=py+30) + { g_zone_panel=false; g_zone_sel=-1; Redraw(); return; } + + int cy=py+36+18+22+8; // posição após preços e separador = py+84 + // Cor borda swatch (vx=px+118, cy..cy+14) + if(mx>=px+118&&mx<=px+150&&my>=cy&&my<=cy+14) + { OpenColorPicker(PICK_ZONE_BORDER, px+118, cy+16); return; } + cy+=24+8; // cy = py+116: checkboxes row 1 + + // Raio esquerdo (px+10, cy) + if(mx>=px+10&&mx<=px+22&&my>=cy&&my<=cy+12) + { g_zones[g_zone_sel].raio_esq=!g_zones[g_zone_sel].raio_esq; Redraw(); return; } + // Raio direito (px+130, cy) + if(mx>=px+130&&mx<=px+142&&my>=cy&&my<=cy+12) + { g_zones[g_zone_sel].raio_dir=!g_zones[g_zone_sel].raio_dir; Redraw(); return; } + cy+=22; // cy = py+138: checkboxes row 2 + + // Sobre os clusters (px+130, cy) + if(mx>=px+130&&mx<=px+142&&my>=cy&&my<=cy+12) + { g_zones[g_zone_sel].sobre_clusters=!g_zones[g_zone_sel].sobre_clusters; Redraw(); return; } + cy+=26+8; // cy = py+172: excluir button + + // Botão excluir + if(mx>=px+10&&mx<=px+pw-10&&my>=cy&&my<=cy+24) + { + for(int i=g_zone_sel;ig_canvas_w) px=g_canvas_w-pw-4; + if(py+ph>g_canvas_h) py=g_canvas_h-ph-4; + if(px<0) px=4; + if(py<0) py=4; +} + +void DrawVPCheck(int x, int y, bool val, string lbl) +{ + g_canvas.Rectangle(x,y,x+12,y+12,Argb(0x475569,255)); + if(val){g_canvas.Line(x+2,y+7,x+5,y+11,Argb(0x22C55E,255));g_canvas.Line(x+5,y+11,x+10,y+3,Argb(0x22C55E,255));} + g_canvas.FontSet("Arial",9); + g_canvas.TextOut(x+16,y,lbl,Argb(0xCBD5E1,255),TA_LEFT|TA_TOP); +} + +void DrawVPSwatch(int x, int y, color c, string lbl) +{ + g_canvas.FontSet("Arial",8); + g_canvas.TextOut(x,y,lbl,Argb(0x64748B,255),TA_LEFT|TA_TOP); + g_canvas.FillRectangle(x,y+12,x+28,y+26,Argb(c,255)); + g_canvas.Rectangle(x,y+12,x+28,y+26,Argb(0x475569,255)); +} + +void DrawVPPanel() +{ + if(!g_vp_panel||g_vp_sel<0||g_vp_sel>=g_vp_count) return; + int pw=290, ph=280; + int px, py; VPPanelPos(px,py); + + g_canvas.FillRectangle(px,py,px+pw,py+ph,Argb(0x0F1520,255)); + g_canvas.Rectangle(px,py,px+pw,py+ph,Argb(0x2A364F,255)); + + // Header + g_canvas.FillRectangle(px,py,px+pw,py+30,Argb(0x080E18,255)); + g_canvas.FontSet("Arial Bold",10); + g_canvas.TextOut(px+10,py+8,StringFormat("VP #%d",g_vp_sel+1),Argb(0x22C55E,255),TA_LEFT|TA_TOP); + g_canvas.FontSet("Arial Bold",14); + g_canvas.TextOut(px+pw-18,py+5,"×",Argb(0x94A3B8,255),TA_LEFT|TA_TOP); + + int cy=py+36; int lx=px+10; + + // Linha: Visualizar — botões Volume / Delta + g_canvas.FontSet("Arial",9); + g_canvas.TextOut(lx,cy,"Visualizar:",Argb(0x64748B,255),TA_LEFT|TA_TOP); + bool sv=g_vprofiles[g_vp_sel].show_volume; + bool sd=g_vprofiles[g_vp_sel].show_delta; + uint vol_bg=sv?Argb(0x1E40AF,255):Argb(0x1A2234,255); + uint del_bg=sd?Argb(0x065F46,255):Argb(0x1A2234,255); + g_canvas.FillRectangle(px+90,cy-1,px+140,cy+14,vol_bg); + g_canvas.Rectangle(px+90,cy-1,px+140,cy+14,Argb(0x334155,255)); + g_canvas.TextOut(px+100,cy,"Volume",sv?Argb(0xFFFFFF,255):Argb(0x64748B,255),TA_LEFT|TA_TOP); + g_canvas.FillRectangle(px+144,cy-1,px+192,cy+14,del_bg); + g_canvas.Rectangle(px+144,cy-1,px+192,cy+14,Argb(0x334155,255)); + g_canvas.TextOut(px+154,cy,"Delta",sd?Argb(0xFFFFFF,255):Argb(0x64748B,255),TA_LEFT|TA_TOP); + cy+=22; + + // Separador + g_canvas.Line(px+4,cy,px+pw-4,cy,Argb(0x1E293B,255)); cy+=6; + + // Cores das barras + DrawVPSwatch(lx, cy,g_vprofiles[g_vp_sel].clr_bid,"Bid"); + DrawVPSwatch(lx+50, cy,g_vprofiles[g_vp_sel].clr_ask,"Ask"); + DrawVPSwatch(lx+100, cy,g_vprofiles[g_vp_sel].clr_poc,"POC"); + cy+=34; + + // Checkboxes linha 1 + DrawVPCheck(lx, cy,g_vprofiles[g_vp_sel].volume_soma, "Soma"); + DrawVPCheck(lx+80, cy,g_vprofiles[g_vp_sel].delta_espelhado,"Espelhado"); + DrawVPCheck(lx+188, cy,g_vprofiles[g_vp_sel].sobre_clusters, "S/ clusters"); + cy+=22; + + // Separador + g_canvas.Line(px+4,cy,px+pw-4,cy,Argb(0x1E293B,255)); cy+=6; + + // POC + VA + DrawVPCheck(lx, cy,g_vprofiles[g_vp_sel].show_poc, "POC"); + DrawVPCheck(lx+60, cy,g_vprofiles[g_vp_sel].show_va, "VA"); + // VA% stepper + g_canvas.FontSet("Arial",9); + g_canvas.TextOut(lx+130,cy,StringFormat("%.0f%%",g_vprofiles[g_vp_sel].va_pct),Argb(0xE2E8F0,255),TA_LEFT|TA_TOP); + g_canvas.FillRectangle(lx+152,cy,lx+164,cy+12,Argb(0x1A2234,255)); + g_canvas.Rectangle(lx+152,cy,lx+164,cy+12,Argb(0x334155,255)); + g_canvas.TextOut(lx+154,cy+1,"–",Argb(0xF59E0B,255),TA_LEFT|TA_TOP); + g_canvas.FillRectangle(lx+166,cy,lx+178,cy+12,Argb(0x1A2234,255)); + g_canvas.Rectangle(lx+166,cy,lx+178,cy+12,Argb(0x334155,255)); + g_canvas.TextOut(lx+168,cy+1,"+",Argb(0x22C55E,255),TA_LEFT|TA_TOP); + cy+=22; + + // VWAP + DrawVPCheck(lx,cy,g_vprofiles[g_vp_sel].show_vwap,"VWAP"); + cy+=22; + + // Separador + g_canvas.Line(px+4,cy,px+pw-4,cy,Argb(0x1E293B,255)); cy+=8; + + // Botão excluir + g_canvas.FillRectangle(px+10,cy,px+pw-10,cy+24,Argb(0x7F1D1D,255)); + g_canvas.Rectangle(px+10,cy,px+pw-10,cy+24,Argb(0xEF4444,255)); + g_canvas.FontSet("Arial Bold",9); + g_canvas.TextOut(px+pw/2,cy+6,"Excluir VP",Argb(0xFFFFFF,255),TA_CENTER|TA_TOP); +} + +int HitTestVP(int mx, int my) +{ + if(g_rdr_col_step<=0) return -1; + int chart_h=g_canvas_h-InpBottomPanel; + if(my<0||my>=chart_h) return -1; + for(int vi=0;vi=x1&&mx<=x2) return vi; + } + return -1; +} + +void HandleVPPanelClick(int mx, int my) +{ + if(g_vp_sel<0||g_vp_sel>=g_vp_count) return; + int pw=290, ph=280; + int px, py; VPPanelPos(px,py); + + // Fechar × + if(mx>=px+pw-22&&my>=py&&my<=py+30){g_vp_panel=false;g_vp_sel=-1;Redraw();return;} + + int cy=py+36; int lx=px+10; + + // Botões Volume / Delta (cy=py+36) + if(my>=cy-1&&my<=cy+14) + { + if(mx>=px+90&&mx<=px+140){g_vprofiles[g_vp_sel].show_volume=!g_vprofiles[g_vp_sel].show_volume;Redraw();return;} + if(mx>=px+144&&mx<=px+192){g_vprofiles[g_vp_sel].show_delta=!g_vprofiles[g_vp_sel].show_delta;Redraw();return;} + } + cy+=22+6; // cy=py+64: cores + + // Swatches de cor (cy=py+64) + if(my>=cy+12&&my<=cy+26) + { + if(mx>=lx&&mx<=lx+28) { OpenColorPicker(PICK_VP_BID, lx, cy+28); return; } + if(mx>=lx+50&&mx<=lx+78) { OpenColorPicker(PICK_VP_ASK, lx+50, cy+28); return; } + if(mx>=lx+100&&mx<=lx+128) { OpenColorPicker(PICK_VP_POC, lx+100, cy+28); return; } + } + cy+=34; // cy=py+98: checkboxes linha 1 + + // Checkboxes: Soma / Espelhado / S/ clusters (cy=py+98) + if(my>=cy&&my<=cy+12) + { + if(mx>=lx&&mx<=lx+12) {g_vprofiles[g_vp_sel].volume_soma=!g_vprofiles[g_vp_sel].volume_soma;Redraw();return;} + if(mx>=lx+80&&mx<=lx+92) {g_vprofiles[g_vp_sel].delta_espelhado=!g_vprofiles[g_vp_sel].delta_espelhado;Redraw();return;} + if(mx>=lx+188&&mx<=lx+200) {g_vprofiles[g_vp_sel].sobre_clusters=!g_vprofiles[g_vp_sel].sobre_clusters;Redraw();return;} + } + cy+=22+6; // cy=py+126: POC/VA + + // POC / VA / stepper (cy=py+126) + if(my>=cy&&my<=cy+12) + { + if(mx>=lx&&mx<=lx+12) {g_vprofiles[g_vp_sel].show_poc=!g_vprofiles[g_vp_sel].show_poc;Redraw();return;} + if(mx>=lx+60&&mx<=lx+72) {g_vprofiles[g_vp_sel].show_va=!g_vprofiles[g_vp_sel].show_va;Redraw();return;} + if(mx>=lx+152&&mx<=lx+164) // VA% – + {g_vprofiles[g_vp_sel].va_pct=MathMax(10,g_vprofiles[g_vp_sel].va_pct-5);Redraw();return;} + if(mx>=lx+166&&mx<=lx+178) // VA% + + {g_vprofiles[g_vp_sel].va_pct=MathMin(100,g_vprofiles[g_vp_sel].va_pct+5);Redraw();return;} + } + cy+=22; // cy=py+148: VWAP + + if(my>=cy&&my<=cy+12&&mx>=lx&&mx<=lx+12) + {g_vprofiles[g_vp_sel].show_vwap=!g_vprofiles[g_vp_sel].show_vwap;Redraw();return;} + cy+=22+8; // cy=py+178: excluir + + if(mx>=px+10&&mx<=px+pw-10&&my>=cy&&my<=cy+24) + { + for(int i=g_vp_sel;i=g_seltb_x && mx=g_seltb_y && myci_l){int t=ci_r;ci_r=ci_l;ci_l=t;} + int zn=g_zone_count; + g_zones[zn].price_high=YToPrice(sy1); + g_zones[zn].price_low =YToPrice(sy2); + g_zones[zn].ci_from=ci_r; g_zones[zn].ci_to=ci_l; + g_zones[zn].border_clr=(color)0xFF4081; + g_zones[zn].raio_dir=false; g_zones[zn].raio_esq=false; + g_zones[zn].sobre_clusters=false; g_zones[zn].active=true; + g_zone_count++; + } + } + else if(lx<59) // VP + { + if(g_vp_countci_l){int t=ci_r;ci_r=ci_l;ci_l=t;} + CalcVProfile(g_vprofiles[g_vp_count],ci_r,ci_l,YToPrice(sy1),YToPrice(sy2)); + g_vp_count++; + } + } + else if(lx<78) // Desfazer último + { + if(g_vp_count>0) { g_vp_count--; g_vprofiles[g_vp_count].active=false; } + else if(g_zone_count>0){ g_zone_count--; g_zones[g_zone_count].active=false; } + } + // else ✕ → apenas fecha + + g_sel_ready=false; + Redraw(); +} + +void DrawSelectionOverlay() +{ + if(!g_sel_drag && !g_sel_ready) return; + + int x1=MathMin(g_sel_x0,g_sel_x1), y1=MathMin(g_sel_y0,g_sel_y1); + int x2=MathMax(g_sel_x0,g_sel_x1), y2=MathMax(g_sel_y0,g_sel_y1); + + // Borda verde da seleção + g_canvas.Rectangle(x1,y1,x2,y2,Argb(0x00E676,255)); + + if(!g_sel_ready) return; + + // Posiciona toolbar abaixo da seleção (ou acima se não cabe) + int tbx=x1, tby=y2+6; + if(tby+26>=g_canvas_h) tby=y1-30; + if(tbx+100>=g_canvas_w) tbx=g_canvas_w-102; + g_seltb_x=tbx; g_seltb_y=tby; + + // Fundo + borda da toolbar + g_canvas.FillRectangle(tbx,tby,tbx+99,tby+25,Argb(0x0F1520,255)); + g_canvas.Rectangle(tbx,tby,tbx+99,tby+25,Argb(0x475569,255)); + + // Separadores verticais + for(int dy=3;dy<23;dy++) + { + g_canvas.PixelSet(tbx+36,tby+dy,Argb(0x475569,255)); + g_canvas.PixelSet(tbx+58,tby+dy,Argb(0x475569,255)); + g_canvas.PixelSet(tbx+77,tby+dy,Argb(0x475569,255)); + } + + // Rótulos dos botões + g_canvas.FontSet("Arial Bold",9); + g_canvas.TextOut(tbx+5, tby+6,"Zona",Argb(0xFF4081,255),TA_LEFT|TA_TOP); + g_canvas.TextOut(tbx+41,tby+6,"VP", Argb(0x00E676,255),TA_LEFT|TA_TOP); + g_canvas.TextOut(tbx+62,tby+6,"↩", Argb(0xF59E0B,255),TA_LEFT|TA_TOP); + g_canvas.TextOut(tbx+82,tby+6,"✕", Argb(0xEF4444,255),TA_LEFT|TA_TOP); +} + +//+------------------------------------------------------------------+ +//| Redraw | +//+------------------------------------------------------------------+ +void Redraw() +{ + if(g_canvas_w<=0||g_canvas_h<=0) return; + + int cw = MathMax(1, InpClusterWidth * g_zoom_h / 100); + int col_gap = MathMax(2, cw * 15 / 140); // proporcional ao React (colGap=15*zoomH quando colWidth=140) + int col_step= cw + col_gap; + int axis_w = 70; + int chart_h = g_canvas_h - InpBottomPanel; + + g_canvas.Erase(Argb(InpColorBg,255)); + + // Grade vertical sutil + for(int x=0;xpmax)pmax=g_history[HIdx(ci)].price_high; + if(g_history[HIdx(ci)].price_low pmax)pmax=g_active.price_high; + if(g_active.price_low 0) + ?(g_active.price_high+g_active.price_low)*0.5 + :(pmax+pmin)*0.5; + pmin=center-view_range*0.5; + pmax=center+view_range*0.5; + } + + // Pan vertical: desloca o viewport pelo offset acumulado no drag + pmin += g_pan_offset; + pmax += g_pan_offset; + g_last_prange = pmax - pmin; // guarda para converter pixels → preço no drag handler + + // Salva estado do render — usado pelas ferramentas de desenho + g_rdr_cw=cw; g_rdr_col_step=col_step; g_rdr_chart_x1=chart_x1; g_rdr_chart_h=chart_h; + g_rdr_last_idx=last_idx; g_rdr_pmin=pmin; g_rdr_pmax=pmax; + g_rdr_active_off=(g_active_valid&&g_scroll==0)?1:0; + + // Camada de fundo: zonas, VA overlay e VPs "atrás" — ANTES dos clusters + DrawZoneFills(chart_h,pmin,pmax); + DrawVAOverlays(chart_h,pmin,pmax); + DrawVProfiles(chart_h,pmin,pmax,true); + + // max_side: escala global das barras do painel inferior + double max_side=1; + for(int ci=first_idx;ci<=last_idx;ci++) + { + double b=0,a=0; + for(int li=0;limax_side)max_side=b; if(a>max_side)max_side=a; + } + + // Renderiza da direita para a esquerda (com margem direita de 3 colunas) + int cx_right = chart_x1 - col_step * 3; + if(g_active_valid && g_scroll==0) + { + CCalcPoc(g_active); + DrawCluster(g_active, cx_right-cw, cw, chart_h, pmin, pmax, max_side, true); + cx_right -= col_step; + } + for(int ci=last_idx; ci>=first_idx && cx_right>0; ci--) + { + DrawCluster(g_history[HIdx(ci)], cx_right-cw, cw, chart_h, pmin, pmax, max_side, false); + cx_right -= col_step; + } + + // Camada de frente: bordas das zonas + VPs "sobre clusters" — APÓS os clusters + DrawZoneBorders(chart_h,pmin,pmax); + DrawVProfiles(chart_h,pmin,pmax,false); + + DrawPriceAxis(chart_x1, chart_h, pmin, pmax); + + // Linha do preço atual (dashed ciano) — desenhada APÓS o eixo para não ser sobrescrita + if(g_current_price>pmin && g_current_price=50){g_last_draw_msc=now;Redraw();} +} + +void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) +{ + if(id==CHARTEVENT_CHART_CHANGE){ResizeCanvas();Redraw();return;} + + //--- Teclado --- + if(id==CHARTEVENT_KEYDOWN) + { + if(lparam==37){g_scroll++;Redraw();} // ← scroll + if(lparam==39){g_scroll=MathMax(0,g_scroll-1);Redraw();} // → scroll + if(lparam==36){g_scroll=0;Redraw();} // Home + if(lparam==38){g_zoom_v=MathMin(200,g_zoom_v+2);Redraw();} // ↑ zoom V+ + if(lparam==40){g_zoom_v=MathMax(1, g_zoom_v-2);Redraw();} // ↓ zoom V- + if(lparam==219){g_step_mult=MathMax(0.5,g_step_mult/2.0);Reload();return;} // [ step÷2 + if(lparam==221){g_step_mult=MathMin(200,g_step_mult*2.0);Reload();return;} // ] step×2 + if(lparam==188){g_delta_max=MathMax(100,g_delta_max-200);Reload();return;} // , delta-200 + if(lparam==190){g_delta_max=g_delta_max+200;Reload();return;} // . delta+200 + return; + } + + //--- Mouse Move --- + if(id==CHARTEVENT_MOUSE_MOVE) + { + int mx=(int)lparam, my=(int)dparam; + bool btn = ((int)sparam&1)!=0; + bool ctrl = (TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL)&0x8000)!=0; + bool is_sel = ctrl && btn; // Ctrl+left-drag = seleção de área + int chart_h=g_canvas_h-InpBottomPanel; + int axis_x0=g_canvas_w-70; + + // Detecção de click: botão acabou de ser pressionado + bool just_pressed=(!g_prev_btn&&btn); + g_prev_btn=btn; + + bool in_gear=(mx>=5&&mx<=33&&my>=8&&my<=36); + bool in_menu=(g_menu_open&&mx>=MNU_X&&mx<=MNU_X+MNU_W&&my>=MNU_Y&&my<=MNU_Y+450); + + // Hit-test painel de zona + int zpx=0,zpy=0; if(g_zone_panel&&g_zone_sel>=0) ZonePanelPos(zpx,zpy); + bool in_zone_panel=(g_zone_panel&&mx>=zpx&&mx<=zpx+244&&my>=zpy&&my<=zpy+205); + // Hit-test painel de VP + int vppx=0,vppy=0; if(g_vp_panel&&g_vp_sel>=0) VPPanelPos(vppx,vppy); + bool in_vp_panel=(g_vp_panel&&mx>=vppx&&mx<=vppx+290&&my>=vppy&&my<=vppy+280); + + // Color picker: prioridade máxima + bool in_color_picker=(g_pick_open&&mx>=g_pick_px&&mx<=g_pick_px+PICK_W&&my>=g_pick_py&&my<=g_pick_py+PICK_H); + if(g_pick_open) + { + if(just_pressed && in_color_picker){ HandleColorPickerClick(mx,my); return; } + if(just_pressed && !in_color_picker){ g_pick_open=false; Redraw(); } + if(in_color_picker) return; // absorve hover + } + + // Rastreamento do botão direito + bool btn_r=((int)sparam&2)!=0; + bool just_pressed_r=(!g_rbtn_prev && btn_r); + g_rbtn_prev=btn_r; + + // ── Right-drag de seleção em andamento ── + if(g_sel_drag && g_sel_is_right) + { + if(!btn_r) + { + g_sel_drag=false; + if(MathAbs(g_sel_x1-g_sel_x0)>8||MathAbs(g_sel_y1-g_sel_y0)>8) + g_sel_ready=true; + Redraw(); return; + } + if(mx!=g_sel_x1||my!=g_sel_y1){ g_sel_x1=mx; g_sel_y1=my; Redraw(); } + return; + } + + // ── Ctrl+left-drag de seleção em andamento ── + if(g_sel_drag && !g_sel_is_right) + { + if(!is_sel) + { + g_sel_drag=false; + if(MathAbs(g_sel_x1-g_sel_x0)>8||MathAbs(g_sel_y1-g_sel_y0)>8) + g_sel_ready=true; + Redraw(); return; + } + if(mx!=g_sel_x1||my!=g_sel_y1){ g_sel_x1=mx; g_sel_y1=my; Redraw(); } + return; + } + + // ── Right-click-wait: pressionado sobre ferramenta, aguardando click vs drag ── + if(g_rclick_wait) + { + if(!btn_r) + { + // Solto sem arrastar → abre config da ferramenta + g_rclick_wait=false; + if(g_rclick_vi>=0) + { g_zone_panel=false; g_vp_sel=g_rclick_vi; g_vp_panel=true; g_vppx=g_rclick_xi+12; g_vppy=g_rclick_yi+12; Redraw(); } + else if(g_rclick_zi>=0) + { g_vp_panel=false; g_zone_sel=g_rclick_zi; g_zone_panel=true; g_zpx=g_rclick_xi+12; g_zpy=g_rclick_yi+12; Redraw(); } + return; + } + if(MathAbs(mx-g_rclick_xi)>10||MathAbs(my-g_rclick_yi)>10) + { + // Arrastou → converte em drag de seleção + g_rclick_wait=false; + g_sel_drag=true; g_sel_is_right=true; + g_sel_x0=g_rclick_xi; g_sel_y0=g_rclick_yi; + g_sel_x1=mx; g_sel_y1=my; g_sel_ready=false; + Redraw(); + } + return; + } + + // ── Nova pressão do botão direito ── + if(just_pressed_r && !in_zone_panel && !in_vp_panel && !in_gear && !in_menu) + { + int vi=HitTestVP(mx,my); + int zi=(vi<0)?HitTestZone(mx,my):-1; + g_rclick_xi=mx; g_rclick_yi=my; g_rclick_vi=vi; g_rclick_zi=zi; + if(vi>=0||zi>=0) + g_rclick_wait=true; // sobre ferramenta → esperar ver se é click ou drag + else + { g_sel_drag=true; g_sel_is_right=true; g_sel_x0=mx; g_sel_y0=my; g_sel_x1=mx; g_sel_y1=my; g_sel_ready=false; } + return; + } + + // ── Ctrl+left-drag start ── + if(is_sel && !g_sel_drag) + { + g_sel_drag=true; g_sel_is_right=false; + g_sel_x0=mx; g_sel_y0=my; g_sel_x1=mx; g_sel_y1=my; g_sel_ready=false; + return; + } + + if(just_pressed&&g_sel_ready) { HandleSelectionToolbar(mx,my); return; } + if(just_pressed&&in_zone_panel) { HandleZonePanelClick(mx,my); return; } + if(just_pressed&&in_vp_panel) { HandleVPPanelClick(mx,my); return; } + if(just_pressed&&(in_gear||in_menu)) { HandleMenuClick(mx,my); return; } + if(in_zone_panel||in_vp_panel||in_gear||in_menu) return; + + // Clique esquerdo fora fecha painéis abertos + if(just_pressed&&g_zone_panel){ g_zone_panel=false; g_zone_sel=-1; Redraw(); } + if(just_pressed&&g_vp_panel) { g_vp_panel=false; g_vp_sel=-1; Redraw(); } + + // Left-click em zona ou VP → apenas seleciona (destaque visual, sem abrir painel) + if(just_pressed&&!ctrl) + { + int vi=HitTestVP(mx,my); + if(vi>=0){ g_vp_sel=vi; g_vp_panel=false; Redraw(); return; } + int zi=HitTestZone(mx,my); + if(zi>=0){ g_zone_sel=zi; g_zone_panel=false; Redraw(); return; } + // Clique em vazio → deseleciona + if(g_vp_sel>=0||g_zone_sel>=0){ g_vp_sel=-1; g_zone_sel=-1; Redraw(); } + } + + if(!btn) + { + g_vaxis_drag=false; + g_haxis_drag=false; + g_pan_drag=false; + return; + } + + // Pan vertical: arrastar área principal (esquerda da barra de preço, acima do painel) + if(!g_vaxis_drag && !g_haxis_drag && !ctrl && mx=0 && my0=arrastar baixo=pmin/pmax sobem=conteúdo desce + Redraw(); return; + } + + // Drag na barra de preço (direita) → zoom vertical + if(!g_haxis_drag && mx>=axis_x0 && my>=0 && my=chart_h && mx=4)?(int)StringToInteger(parts[3]):((int)dparam>0?120:-120); + + bool ctrl=(TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL)&0x8000)!=0; + if(ctrl) + { + // Ctrl+scroll = zoom horizontal + if(wheel>0) g_zoom_h=MathMin(800,g_zoom_h+10); + else g_zoom_h=MathMax(1, g_zoom_h-10); + } + else + { + // Scroll normal = navegar clusters L/R + if(wheel>0) g_scroll=MathMax(0,g_scroll-3); + else g_scroll+=3; + g_scroll=MathMax(0,MathMin(g_scroll,g_history_count-1)); + } + Redraw(); return; + } +}