//+------------------------------------------------------------------+ //| 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; } }