Files
rufinomec-afk a6a9164788 feat: Footprint Chart MT5 v1.85 — EA nativo MQL5
Porta fiel do YuClusters em MQL5 puro via CCanvas, sem dependências externas.

Funcionalidades:
- Clusters bid/ask por nível com POC, modo delta, painel inferior
- Ferramentas de desenho: Zona e Volume Profile de região (right-drag)
- Painéis de configuração via right-click com color picker inline (160 cores)
- Zoom vertical/horizontal, pan, eixo de preço customizado
- Menu de configurações (formação + cores) com gear button
2026-06-09 16:48:37 -03:00

1996 lines
81 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//+------------------------------------------------------------------+
//| 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 <Canvas\Canvas.mqh>
//--- 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<c.level_count;i++) if(MathAbs(c.levels[i].price-price)<1e-8) return i;
if(c.level_count>=2000) return -1;
int pos=c.level_count;
for(int i=0;i<c.level_count;i++) if(price<c.levels[i].price){pos=i;break;}
for(int i=c.level_count;i>pos;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;i<c.level_count;i++){double t=MathMax(c.levels[i].ask,c.levels[i].bid);if(t>mx){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<c.price_low) c.price_low=b;
}
void ActiveReset()
{
g_active.open_time=0;g_active.close_time=0;g_active.open_price=0;g_active.close_price=0;
g_active.delta=0;g_active.total_volume=0;g_active.price_high=0;g_active.price_low=0;
g_active.poc_price=0;g_active.is_closed=false;g_active.level_count=0;g_active_valid=false;
}
int HIdx(int logical) { return (g_history_start + logical) % InpHistoryBars; }
void CloseActive(string reason)
{
if(!g_active_valid) return;
CCalcPoc(g_active); g_active.is_closed=true; g_active.close_time=(datetime)(g_last_tick_msc/1000);
if(g_history_count < InpHistoryBars)
{
g_history[g_history_count] = g_active;
g_history_count++;
}
else
{
g_history[g_history_start] = g_active;
g_history_start = (g_history_start + 1) % InpHistoryBars;
}
ActiveReset();
}
bool ShouldClose(ulong msc)
{
if(!g_active_valid) return false;
if(g_close_mode=="delta") return MathAbs(g_active.delta)>=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(bid<g_last_bid_price)g_last_is_buy=false;g_last_bid_price=bid;}
else if(mid>0){if(mid>g_last_mid_price)g_last_is_buy=true;else if(mid<g_last_mid_price)g_last_is_buy=false;}
if(mid>0) 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;i<count;i++)
{
double p,v; bool b;
g_last_tick_msc=ticks[i].time_msc;
if(ParseTick(ticks[i],p,v,b)) ProcessTick(p,v,b,ticks[i].time_msc);
}
if(count>0) 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;li<c.level_count;li++)
{
double d=MathMax(c.levels[li].ask,c.levels[li].bid);
if(d>local_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<c.level_count;li++)
{
double price=c.levels[li].price;
double ask_v=c.levels[li].ask;
double bid_v=c.levels[li].bid;
double dom =MathMax(ask_v,bid_v);
bool is_ask=(ask_v>=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=(price<body_low-g_step*0.5)||(price>body_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_high<candle_top)
g_canvas.Line(cx_mid,y_high,cx_mid,candle_top,wick_clr);
if(c.price_low <body_low -g_tick_size*0.5&&y_low >candle_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<c.level_count;li++){total_bid+=c.levels[li].bid;total_ask+=c.levels[li].ask;}
// Layout dinâmico: usa o espaço disponível do painel
int panel_top = chart_h + 2; // topo do painel (logo abaixo do separador)
int ts_h = 12; // reserva p/ timestamp no fundo
int panel_bot = g_canvas_h - ts_h; // fundo disponível
int panel_h = panel_bot - panel_top; // altura total usável
if(panel_h < 10) return;
int bar_zone_h = panel_h * 65 / 100; // 65% p/ barras de volume
int bar_base = panel_top + bar_zone_h; // piso das barras
int pw = cw; // largura total da coluna (sem margens)
int half_w = pw / 2;
int bid_bh=MathMax(2,(int)(total_bid/MathMax(max_side,1)*bar_zone_h));
int ask_bh=MathMax(2,(int)(total_ask/MathMax(max_side,1)*bar_zone_h));
// Bid (esquerda) e Ask (direita) — sem margem interna
g_canvas.FillRectangle(cx, bar_base-bid_bh, cx+half_w-1, bar_base, Argb(0xD22626,255));
g_canvas.FillRectangle(cx+half_w,bar_base-ask_bh, cx+pw-1, bar_base, Argb(0x2563EB,255));
// Label de volume dentro das barras
double dom_vol=MathMax(total_bid,total_ask);
string vol_lbl=(dom_vol>=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){g_menu_tab=0;Redraw();return;}
if(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<mid)g_form_view=0;else g_form_view=1;Redraw();return;}
cy+=38+16;
if(my>=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;zi<g_zone_count;zi++)
{
if(!g_zones[zi].active) continue;
if(g_zones[zi].sobre_clusters) continue; // será desenhado após os clusters
// ci_from = older (menor índice) = lado ESQUERDO (X menor)
// ci_to = newer (maior índice) = lado DIREITO (X maior)
int x1=ClusterToX(g_zones[zi].ci_from);
int x2=ClusterToX(g_zones[zi].ci_to)+g_rdr_cw;
if(g_zones[zi].raio_dir) x2=g_rdr_chart_x1;
if(g_zones[zi].raio_esq) x1=0;
if(x1<0) x1=0;
if(x2<=x1||x2<0) continue;
double prange=pmax-pmin;
int y1=chart_h-1-(int)((g_zones[zi].price_high-pmin)/prange*(chart_h-4));
int y2=chart_h-1-(int)((g_zones[zi].price_low -pmin)/prange*(chart_h-4));
if(y1>y2){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;zi<g_zone_count;zi++)
{
if(!g_zones[zi].active) continue;
int x1=ClusterToX(g_zones[zi].ci_from);
int x2=ClusterToX(g_zones[zi].ci_to)+g_rdr_cw;
if(g_zones[zi].raio_dir) x2=g_rdr_chart_x1;
if(g_zones[zi].raio_esq) x1=0;
if(x1<0) x1=0;
if(x2<=x1) continue;
double prange=pmax-pmin;
int y1=chart_h-1-(int)((g_zones[zi].price_high-pmin)/prange*(chart_h-4));
int y2=chart_h-1-(int)((g_zones[zi].price_low -pmin)/prange*(chart_h-4));
if(y1>y2){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&&ci<g_history_count;ci++)
{
int hi=HIdx(ci);
for(int li=0;li<g_history[hi].level_count;li++)
{
double p=g_history[hi].levels[li].price;
if(p<price_low-g_step||p>price_high+g_step) continue;
double bin=MathRound(p/g_step)*g_step;
int idx=-1;
for(int k=0;k<vp.count;k++) if(MathAbs(vp.prices[k]-bin)<1e-8){idx=k;break;}
if(idx<0)
{
if(vp.count>=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;i<vp.count;i++)
{
double t=vp.vol_bid[i]+vp.vol_ask[i];
if(t>mx){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<vp.count;i++) if(MathAbs(vp.prices[i]-vp.poc_price)<1e-8){poc_vol=vp.vol_bid[i]+vp.vol_ask[i];break;}
double cum=poc_vol, target=total_vol*(vp.va_pct/100.0);
double up=vp.poc_price+g_step, lo=vp.poc_price-g_step;
while(cum<target)
{
double up_v=0,lo_v=0;
for(int i=0;i<vp.count;i++)
{
if(MathAbs(vp.prices[i]-up)<1e-8) up_v=vp.vol_bid[i]+vp.vol_ask[i];
if(MathAbs(vp.prices[i]-lo)<1e-8) lo_v=vp.vol_bid[i]+vp.vol_ask[i];
}
if(up_v<=0&&lo_v<=0) break;
if(up_v>=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;vi<g_vp_count;vi++)
{
if(!g_vprofiles[vi].active||!g_vprofiles[vi].show_va||g_vprofiles[vi].poc_price<=0) continue;
int vah_y=chart_h-1-(int)((g_vprofiles[vi].vah_price-pmin)/prange*(chart_h-4));
int val_y=chart_h-1-(int)((g_vprofiles[vi].val_price-pmin)/prange*(chart_h-4));
if(vah_y>val_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;vi<g_vp_count;vi++)
{
if(!g_vprofiles[vi].active||g_vprofiles[vi].count<=0) continue;
// Filtro de passe: sobre_clusters==behind significa que é o passe errado
if(g_vprofiles[vi].sobre_clusters==behind) continue;
int x1=ClusterToX(g_vprofiles[vi].ci_from);
int x2=ClusterToX(g_vprofiles[vi].ci_to)+g_rdr_cw;
if(x1<0) x1=0;
if(x2<=x1) continue;
// Painel do histograma: largura máxima 140px a partir de x1
int bar_max=MathMin(140, x2-x1);
int bx1=x1, bx2=x1+bar_max;
color fundo=(color)0x080C16;
g_canvas.FillRectangle(bx1,0,bx2,chart_h,Argb(fundo,255));
if(g_vp_panel&&vi==g_vp_sel)
g_canvas.Rectangle(bx1-1,0,bx2+1,chart_h-1,Argb(0xFFFFFF,255));
g_canvas.Rectangle(bx1,0,bx2,chart_h-1,Argb(g_vprofiles[vi].clr_borda,255));
double max_vol=1;
for(int i=0;i<g_vprofiles[vi].count;i++){double t=g_vprofiles[vi].vol_bid[i]+g_vprofiles[vi].vol_ask[i];if(t>max_vol)max_vol=t;}
double max_delta=1;
for(int i=0;i<g_vprofiles[vi].count;i++){double d=MathAbs(g_vprofiles[vi].vol_ask[i]-g_vprofiles[vi].vol_bid[i]);if(d>max_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;i<g_vprofiles[vi].count;i++)
{
double price=g_vprofiles[vi].prices[i];
int yt=chart_h-1-(int)((price+g_step*0.5-pmin)/prange*(chart_h-4));
int yb=chart_h-1-(int)((price-g_step*0.5-pmin)/prange*(chart_h-4));
if(yt>yb){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_y<chart_h)
{
for(int xx=bx1;xx<=bx2;xx++) g_canvas.PixelSet(xx,poc_y,Argb(g_vprofiles[vi].clr_poc,255));
for(int xx=bx2;xx<cw;xx++) g_canvas.PixelSet(xx,poc_y,Argb(g_vprofiles[vi].clr_poc,255));
}
}
// VAH/VAL — linhas pontilhadas por todo o gráfico
if(g_vprofiles[vi].show_va&&g_vprofiles[vi].poc_price>0)
{
int vah_y=chart_h-1-(int)((g_vprofiles[vi].vah_price-pmin)/prange*(chart_h-4));
for(int xx=0;xx<cw;xx+=8) g_canvas.Line(xx,vah_y,MathMin(xx+4,cw),vah_y,Argb(0x94A3B8,255));
int val_y=chart_h-1-(int)((g_vprofiles[vi].val_price-pmin)/prange*(chart_h-4));
for(int xx=0;xx<cw;xx+=8) g_canvas.Line(xx,val_y,MathMin(xx+4,cw),val_y,Argb(0x94A3B8,255));
}
// VWAP — linha pontilhada por todo o gráfico
if(g_vprofiles[vi].show_vwap&&g_vprofiles[vi].vwap_price>0)
{
int vy=chart_h-1-(int)((g_vprofiles[vi].vwap_price-pmin)/prange*(chart_h-4));
if(vy>=0&&vy<chart_h)
for(int xx=bx2;xx<cw;xx+=8) g_canvas.Line(xx,vy,MathMin(xx+5,cw),vy,Argb(0xFF4081,255));
}
}
}
//+------------------------------------------------------------------+
//| Painel de configuração de zona |
//+------------------------------------------------------------------+
void ZonePanelPos(int &px, int &py)
{
int pw=244, ph=205;
px=g_zpx; py=g_zpy;
if(px+pw>g_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;zi<g_zone_count;zi++)
{
if(!g_zones[zi].active) continue;
int x1=ClusterToX(g_zones[zi].ci_from);
int x2=ClusterToX(g_zones[zi].ci_to)+g_rdr_cw;
if(g_zones[zi].raio_dir) x2=g_rdr_chart_x1;
if(g_zones[zi].raio_esq) x1=0;
if(x1<0) x1=0;
if(mx<x1||mx>x2) 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<PICK_ROWS;r++)
for(int c=0;c<PICK_COLS;c++)
{
int idx=r*PICK_COLS+c; if(idx>=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;i<g_zone_count-1;i++) g_zones[i]=g_zones[i+1];
g_zone_count--;
g_zone_panel=false; g_zone_sel=-1; Redraw(); return;
}
}
//+------------------------------------------------------------------+
//| Painel de configuração de VP |
//+------------------------------------------------------------------+
void VPPanelPos(int &px, int &py)
{
int pw=290, ph=280;
px=g_vppx; py=g_vppy;
if(px+pw>g_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<g_vp_count;vi++)
{
if(!g_vprofiles[vi].active) continue;
int x1=ClusterToX(g_vprofiles[vi].ci_from);
int x2=ClusterToX(g_vprofiles[vi].ci_to)+g_rdr_cw;
if(x1<0) x1=0;
if(mx>=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_vp_count-1;i++) g_vprofiles[i]=g_vprofiles[i+1];
g_vp_count--;
g_vp_panel=false; g_vp_sel=-1; Redraw(); return;
}
}
//+------------------------------------------------------------------+
//| Ferramentas de desenho — seleção por Ctrl+drag |
//+------------------------------------------------------------------+
void HandleSelectionToolbar(int mx, int my)
{
bool in_tb=(mx>=g_seltb_x && mx<g_seltb_x+100 && my>=g_seltb_y && my<g_seltb_y+26);
if(!in_tb){ g_sel_ready=false; Redraw(); return; }
int lx=mx-g_seltb_x;
int sx1=MathMin(g_sel_x0,g_sel_x1), sy1=MathMin(g_sel_y0,g_sel_y1);
int sx2=MathMax(g_sel_x0,g_sel_x1), sy2=MathMax(g_sel_y0,g_sel_y1);
if(lx<37) // Zona
{
if(g_zone_count<ZONE_MAX)
{
int ci_r=XToClusterIdx(sx2); // lado direito = cluster mais novo
int ci_l=XToClusterIdx(sx1); // lado esquerdo = cluster mais antigo
if(ci_r>ci_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_count<VP_MAX)
{
int ci_r=XToClusterIdx(sx2);
int ci_l=XToClusterIdx(sx1);
if(ci_r>ci_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;x<g_canvas_w;x+=col_step)
g_canvas.Line(x,0,x,chart_h,Argb(0x131820,255));
// Separador painel inferior
g_canvas.Line(0,chart_h,g_canvas_w,chart_h,Argb(0x1E293B,255));
if(g_history_count==0&&!g_active_valid)
{
TxtOut(g_canvas_w/2-80,g_canvas_h/2,"Aguardando dados...",Argb(InpColorText,255),"Arial",14);
g_canvas.Update(); return;
}
int chart_x1 = g_canvas_w - axis_w;
int visible = chart_x1 / col_step + 2;
int last_idx = g_history_count - 1 - g_scroll;
int first_idx = MathMax(0, last_idx - visible + 1);
// Range de preço dos clusters visíveis
double pmax=-DBL_MAX, pmin=DBL_MAX;
for(int ci=first_idx;ci<=last_idx;ci++)
{
if(g_history[HIdx(ci)].price_high>pmax)pmax=g_history[HIdx(ci)].price_high;
if(g_history[HIdx(ci)].price_low <pmin)pmin=g_history[HIdx(ci)].price_low;
}
if(g_active_valid)
{
if(g_active.price_high>pmax)pmax=g_active.price_high;
if(g_active.price_low <pmin)pmin=g_active.price_low;
}
if(pmax<=pmin){g_canvas.Update();return;}
// Viewport vertical: SEMPRE max_levels * g_step — igual ao React (rowHeight=zoomV define o viewport)
// Isso garante zoom out real: g_zoom_v pequeno → muitos níveis visíveis → clusters viram linhas
{
int max_levels=MathMax(4,(chart_h-4)/MathMax(g_zoom_v,1));
double view_range=(double)max_levels*g_step;
// Centro seguro: usa price_high/low do cluster ativo (nunca poc_price=0)
double center=(g_active_valid&&g_active.price_high>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;li<g_history[HIdx(ci)].level_count;li++)
{ b+=g_history[HIdx(ci)].levels[li].bid; a+=g_history[HIdx(ci)].levels[li].ask; }
if(b>max_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<pmax)
{
double prange=pmax-pmin;
int yc=chart_h-1-(int)((g_current_price-pmin)/prange*(chart_h-4));
for(int x=0;x<chart_x1;x+=10)
g_canvas.Line(x,yc,MathMin(x+6,chart_x1),yc,Argb(0x00E5FF,255));
// tag na barra de preço
g_canvas.FillRectangle(chart_x1+2,yc-8,g_canvas_w-2,yc+8,Argb(0x00E5FF,255));
TxtOut(chart_x1+4,yc-5,DoubleToString(g_current_price,_Digits),Argb(InpColorBg,255),"Arial Bold",InpFontSize);
}
// Info atual (step/delta)
string info="Step: "+DoubleToString(g_step_mult,1)+"x | Delta: "+DoubleToString(g_delta_max,0);
TxtOut(40,5,info,Argb(InpColorText,255),"Arial",InpFontSize);
DrawSelectionOverlay();
DrawZonePanel();
DrawVPPanel();
DrawGearBtn();
if(g_menu_open) DrawMenu();
if(g_pick_open) DrawColorPicker();
g_canvas.Update();
}
//+------------------------------------------------------------------+
//| OnInit |
//+------------------------------------------------------------------+
int OnInit()
{
g_tick_size = SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_SIZE);
if(g_tick_size<=0) g_tick_size=_Point;
g_step_mult = InpStepMultiplier;
g_delta_max = InpDeltaMax;
g_step = g_tick_size * g_step_mult;
g_zoom_v = InpLevelHeight;
g_view_mode = InpViewMode;
g_close_mode = InpCloseMode;
g_color_ask = InpColorAsk;
g_color_bid = InpColorBid;
g_color_poc = InpColorPoc;
g_form_step = InpStepMultiplier;
g_form_delta = InpDeltaMax;
g_form_view = (int)InpViewMode;
g_form_mode = InpCloseMode;
ChartSetInteger(0,CHART_MODE,CHART_LINE);
ChartSetInteger(0,CHART_COLOR_BACKGROUND,(color)InpColorBg);
ChartSetInteger(0,CHART_COLOR_FOREGROUND,(color)InpColorBg);
ChartSetInteger(0,CHART_COLOR_CHART_LINE,(color)InpColorBg);
ChartSetInteger(0,CHART_COLOR_CHART_UP, (color)InpColorBg);
ChartSetInteger(0,CHART_COLOR_CHART_DOWN,(color)InpColorBg);
ChartSetInteger(0,CHART_COLOR_BID, (color)InpColorBg);
ChartSetInteger(0,CHART_COLOR_ASK, (color)InpColorBg);
ChartSetInteger(0,CHART_COLOR_LAST, (color)InpColorBg);
ChartSetInteger(0,CHART_SHOW_GRID, false);
ChartSetInteger(0,CHART_SHOW_VOLUMES, false);
ChartSetInteger(0,CHART_SHOW_OHLC, false);
ChartSetInteger(0,CHART_SHOW_BID_LINE, false);
ChartSetInteger(0,CHART_SHOW_ASK_LINE, false);
ChartSetInteger(0,CHART_SHOW_LAST_LINE, false);
ChartSetInteger(0,CHART_SHOW_PERIOD_SEP, false);
ChartSetInteger(0,CHART_CROSSHAIR_TOOL, false);
ChartSetInteger(0,CHART_SHOW_DATE_SCALE, false); // remove eixo de tempo no fundo
ChartSetInteger(0,CHART_SHOW_PRICE_SCALE, false); // remove eixo de preço nativo (usamos o nosso)
ChartSetInteger(0,CHART_SHIFT, false); // remove margem direita nativa do MT5
ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,1);
ChartSetInteger(0,CHART_EVENT_MOUSE_WHEEL,1);
ChartSetInteger(0,CHART_CONTEXT_MENU,false);
g_canvas_w=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS);
g_canvas_h=(int)ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS);
if(!g_canvas.CreateBitmapLabel(0,0,g_canvas_name,0,0,g_canvas_w,g_canvas_h,COLOR_FORMAT_XRGB_NOALPHA))
{ Print("TesteFootprint: erro canvas ",GetLastError()); return INIT_FAILED; }
ObjectSetInteger(0,g_canvas_name,OBJPROP_ZORDER,10);
ObjectSetInteger(0,g_canvas_name,OBJPROP_BACK,false);
ObjectSetInteger(0,g_canvas_name,OBJPROP_SELECTABLE,false);
ObjectSetInteger(0,g_canvas_name,OBJPROP_SELECTED,false);
ObjectSetInteger(0,g_canvas_name,OBJPROP_HIDDEN,true);
ObjectSetInteger(0,g_canvas_name,OBJPROP_XDISTANCE,0);
ObjectSetInteger(0,g_canvas_name,OBJPROP_YDISTANCE,0);
ActiveReset();
LoadHistory();
Redraw();
EventSetTimer(1);
return INIT_SUCCEEDED;
}
void OnTimer() { ResizeCanvas(); Redraw(); }
void OnDeinit(const int reason)
{
EventKillTimer();
g_canvas.Destroy();
ObjectDelete(0,g_canvas_name);
ChartSetInteger(0,CHART_MODE,CHART_CANDLES);
ChartSetInteger(0,CHART_SHOW_GRID,true);
ChartSetInteger(0,CHART_SHOW_OHLC,true);
ChartSetInteger(0,CHART_SHOW_BID_LINE,true);
ChartSetInteger(0,CHART_SHOW_PERIOD_SEP,true);
ChartSetInteger(0,CHART_CROSSHAIR_TOOL,true);
ChartSetInteger(0,CHART_CONTEXT_MENU,true);
ChartRedraw(0);
}
void OnTick()
{
MqlTick tick;
if(!SymbolInfoTick(_Symbol,tick)) return;
if(tick.time_msc<=(long)g_last_tick_msc) return;
g_last_tick_msc=tick.time_msc;
double p,v; bool b;
if(ParseTick(tick,p,v,b)) ProcessTick(p,v,b,tick.time_msc);
ResizeCanvas();
ulong now=GetTickCount64();
if(now-g_last_draw_msc>=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<axis_x0 && my>=0 && my<chart_h)
{
if(!g_pan_drag)
{ g_pan_drag=true; g_pan_y0=my; g_pan_off0=g_pan_offset; return; }
int dy=my-g_pan_y0;
double price_per_px=g_last_prange/MathMax(1,chart_h-4);
g_pan_offset=g_pan_off0+(double)dy*price_per_px; // dy>0=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)
{
if(!g_vaxis_drag)
{ g_vaxis_drag=true; g_vdrag_y0=my; g_vdrag_z0=g_zoom_v; return; }
int nz=MathMax(1,MathMin(200,(int)(g_vdrag_z0*(1.0+(g_vdrag_y0-my)*0.01))));
if(nz!=g_zoom_v){g_zoom_v=nz;Redraw();}
return;
}
// Drag no painel inferior → zoom horizontal (igual React timeAxis drag)
if(!g_vaxis_drag && my>=chart_h && mx<axis_x0)
{
if(!g_haxis_drag)
{ g_haxis_drag=true; g_hdrag_x0=mx; g_hdrag_zh0=g_zoom_h; return; }
double factor=1.0+(mx-g_hdrag_x0)*0.008;
int nz=MathMax(1,MathMin(800,(int)((double)g_hdrag_zh0*factor)));
if(nz!=g_zoom_h){g_zoom_h=nz;Redraw();}
return;
}
return;
}
//--- Mouse Wheel ---
if(id==CHARTEVENT_MOUSE_WHEEL)
{
// sparam = "flags;x;y;delta"
string parts[]; int n=StringSplit(sparam,';',parts);
int wheel=(n>=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;
}
}