//+------------------------------------------------------------------+ //| GexMap.mq5 | //| MMQ — Muhammad Minhas Qamar | //| www.mql5.com/en/articles/23410 | //+------------------------------------------------------------------+ #property copyright "MMQ — Muhammad Minhas Qamar" #property link "https://www.mql5.com/en/articles/23410" #property version "1.10" #property description "Dealer gamma-exposure (GEX) map: per-strike exposure, call/put walls, and the zero-gamma flip, drawn on the price chart at real strike levels." #property indicator_chart_window #property indicator_plots 0 #property indicator_buffers 0 #include #include #include //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ enum ENUM_GEX_SOURCE { GEX_SOURCE_CSV = 0, // read a chain CSV from MQL5\Files GEX_SOURCE_NATIVE = 1 // read the broker's native option symbols }; enum ENUM_GEX_THEME { GEX_THEME_DARK = 0, // dark labels/bars (for a dark chart) GEX_THEME_LIGHT = 1 // light labels/bars (for a light chart) }; enum ENUM_GEX_SCALE { GEX_SCALE_OWN = 0, // self-contained band: always visible on any chart GEX_SCALE_SNAP = 1 // snap to the chart's real price axis (same-underlying only) }; input ENUM_GEX_SOURCE InpSource = GEX_SOURCE_CSV; // data source input string InpCsvFile = "GEX\\gex_chain_sample.csv"; // CSV chain (in MQL5\Files) input string InpUnderlying = ""; // native: base symbol (empty = chart symbol) input double InpRiskFree = 0.045; // risk-free rate for the inversion input double InpMultiplier = 100.0; // contract multiplier (100 = US equity options) input int InpRefreshSec = 0; // native re-poll seconds (0 = manual, press R) input int InpProfileW = 260; // profile width in pixels (right margin) input ENUM_GEX_SCALE InpScaleMode = GEX_SCALE_OWN; // vertical scale: own band, or snap to price axis input double InpFillPct = 0.80; // own scale: fraction of chart height to fill input ENUM_GEX_THEME InpTheme = GEX_THEME_DARK; // label/bar theme //+------------------------------------------------------------------+ //| Globals | //+------------------------------------------------------------------+ CCanvas g_canvas; CGexProfile g_profile; string g_objName = ""; datetime g_lastRefresh = 0; bool g_haveData = false; int g_lastW = -1, g_lastH = -1; // last canvas pixel size, to detect real resizes //--- own-scale band (price -> y), recomputed each draw from the strike range double g_bandLo = 0, g_bandHi = 0; // price bounds of the profile band int g_bandTop = 0, g_bandBot = 0; // pixel bounds of the profile band //--- resolved palette (set in OnInit from the theme) uint g_panel, g_grid, g_text, g_textDim, g_pos, g_neg, g_flip, g_spot, g_wall; //+------------------------------------------------------------------+ //| Resolve the theme into a small ARGB palette once, so every draw | //| call pulls from the same consistent set of colours. The bar and | //| line colours read the same on either chart theme; only the panel | //| fill and the text swap. | //+------------------------------------------------------------------+ void ResolvePalette(void) { if(InpTheme == GEX_THEME_LIGHT) { g_panel = ColorToARGB(clrWhiteSmoke, 220); // near-opaque card g_grid = ColorToARGB(clrSilver); g_text = ColorToARGB(clrBlack); g_textDim = ColorToARGB(clrDimGray); } else { g_panel = ColorToARGB(C'22,24,30', 225); // near-opaque card g_grid = ColorToARGB(C'70,74,86'); g_text = ColorToARGB(clrWhiteSmoke); g_textDim = ColorToARGB(clrSilver); } g_pos = ColorToARGB(C'38,166,91', 235); // long-gamma (positive) green g_neg = ColorToARGB(C'207,64,64', 235); // short-gamma (negative) red g_flip = ColorToARGB(clrGold); // zero-gamma flip line g_spot = ColorToARGB(clrDeepSkyBlue); // current spot line g_wall = ColorToARGB(clrOrange); // call/put wall markers } //+------------------------------------------------------------------+ //| (Re)load the chosen data source and rebuild the profile. | //+------------------------------------------------------------------+ bool Reload(void) { OptionQuote quotes[]; bool ok = false; if(InpSource == GEX_SOURCE_CSV) { CGexProviderCSV csv; ok = csv.Load(InpCsvFile, quotes); } else { string under = (InpUnderlying == "") ? _Symbol : InpUnderlying; CGexProviderNative nat; ok = nat.Load(under, InpRiskFree, quotes); } if(!ok) return(false); g_profile.Multiplier(InpMultiplier); g_haveData = g_profile.Build(quotes); return(g_haveData); } //+------------------------------------------------------------------+ //| Set up the own-scale band: map the strike range onto a centred | //| vertical span covering InpFillPct of the chart height. Padded a | //| little beyond the strikes so spot/flip lines that sit just | //| outside the outermost strikes still land inside the band. Called | //| once per draw before any PriceY() query in own-scale mode. | //+------------------------------------------------------------------+ void SetupBand(void) { int H = g_canvas.Height(); int nk = g_profile.NStrikes(); double lo = g_profile.Strike(0); double hi = g_profile.Strike(nk - 1); double pad = 0.06 * (hi - lo); g_bandLo = lo - pad; g_bandHi = hi + pad; double fill = MathMax(0.3, MathMin(0.95, InpFillPct)); int span = (int)(H * fill); g_bandTop = (H - span) / 2; // centred vertically g_bandBot = g_bandTop + span; } //+------------------------------------------------------------------+ //| Map a price to a y pixel, either by snapping to the chart's real | //| price axis (SNAP mode, for a same-underlying chart) or across | //| the tool's own centred band (OWN mode, which is always visible | //| regardless of the chart symbol). Returns false when the price | //| falls off-screen so callers simply skip that level. | //+------------------------------------------------------------------+ bool PriceY(const double price, int &y) { if(InpScaleMode == GEX_SCALE_SNAP) { int x = 0, sub = 0; datetime t = TimeCurrent(); // any visible time works; only y is used if(!ChartTimePriceToXY(0, sub, t, price, x, y)) return(false); int h = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, 0); if(y < -50 || y > h + 50) return(false); // well off-screen: don't draw it return(true); } //--- own scale: linear map of the strike band onto [g_bandTop, g_bandBot] if(g_bandHi <= g_bandLo) return(false); double f = (price - g_bandLo) / (g_bandHi - g_bandLo); // 0 at bandLo, 1 at bandHi if(f < -0.02 || f > 1.02) return(false); // outside the padded band y = (int)MathRound(g_bandBot - f * (g_bandBot - g_bandTop)); return(true); } //+------------------------------------------------------------------+ //| Draw the GEX profile in the right margin, each bar sitting at its| //| strike's price level. In SNAP mode that level is the chart's own | //| price axis; in OWN mode it is the tool's centred band, so the | //| profile is visible on any chart. Bars grow leftward from the | //| right edge; positive (dealer long gamma) is green, negative | //| (short gamma) is red. Walls, spot and the flip are marked lines. | //+------------------------------------------------------------------+ void DrawMap(void) { int W = g_canvas.Width(); int H = g_canvas.Height(); g_canvas.Erase(0); // fully transparent: the chart shows through if(!g_haveData || g_profile.NStrikes() < 2) { DrawTitle(W, "GEX: no data. Check CSV path / source. Press R."); g_canvas.Update(true); return; } //--- own-scale mode maps the strike range onto a centred band if(InpScaleMode == GEX_SCALE_OWN) SetupBand(); //--- right-margin geometry int rightPad = 8; int baseX = W - rightPad; // bars anchor here and grow left int maxLen = MathMax(60, InpProfileW); // longest bar int labelX = baseX - maxLen - 10; // strike labels sit left of the bars //--- in own-scale mode, back the band with a faint panel so it reads as a //--- self-contained gauge rather than floating bars over the candles if(InpScaleMode == GEX_SCALE_OWN) { int px0 = labelX - 52; g_canvas.FillRectangle(px0, g_bandTop - 10, baseX + rightPad, g_bandBot + 10, g_panel); g_canvas.Rectangle(px0, g_bandTop - 10, baseX + rightPad, g_bandBot + 10, g_grid); g_canvas.LineVertical(baseX, g_bandTop - 10, g_bandBot + 10, g_grid); // zero line bars grow from } int nk = g_profile.NStrikes(); double maxAbs = 1e-30; for(int i = 0; i < nk; i++) maxAbs = MathMax(maxAbs, MathAbs(g_profile.Gex(i))); //--- bar half-height from the on-screen spacing of adjacent strikes int yA, yB; int barH = 8; if(PriceY(g_profile.Strike(0), yA) && PriceY(g_profile.Strike(1), yB)) barH = MathMax(3, (int)(MathAbs(yB - yA) * 0.42)); //--- per-strike bars at real price levels g_canvas.FontSet("Segoe UI", 14); for(int i = 0; i < nk; i++) { int y; if(!PriceY(g_profile.Strike(i), y)) continue; double gex = g_profile.Gex(i); int len = (int)MathRound(maxLen * (MathAbs(gex) / maxAbs)); uint clr = (gex >= 0.0) ? g_pos : g_neg; g_canvas.FillRectangle(baseX - len, y - barH, baseX, y + barH, clr); //--- strike label just left of the profile g_canvas.TextOut(labelX, y - 10, StringFormat("%.0f", g_profile.Strike(i)), g_textDim, TA_RIGHT); } //--- reference lines drawn across the profile band at their true prices //--- (unlabelled: the colour key in the title card identifies them) int lineX0 = labelX - 40; DrawLevel(g_profile.Spot(), lineX0, baseX, g_spot); if(g_profile.Flip() > 0.0) DrawLevel(g_profile.Flip(), lineX0, baseX, g_flip); DrawLevel(g_profile.CallWall(), lineX0, baseX, g_wall); DrawLevel(g_profile.PutWall(), lineX0, baseX, g_wall); //--- title / regime card, top-left double net = g_profile.NetGex(); string underName = (InpSource == GEX_SOURCE_NATIVE && InpUnderlying != "") ? InpUnderlying : _Symbol; string line1 = StringFormat("Gamma Exposure - %s - exp %s", underName, TimeToString(g_profile.Expiry(), TIME_DATE)); string regime = (net >= 0.0) ? "NET LONG GAMMA (mean-reverting)" : "NET SHORT GAMMA (trending)"; string line2 = StringFormat("%s net GEX %.3g flip %s", regime, net, (g_profile.Flip() > 0.0 ? StringFormat("%.2f", g_profile.Flip()) : "none")); DrawTitleTwo(line1, line2, (net >= 0.0) ? g_pos : g_neg); g_canvas.Update(true); } //+------------------------------------------------------------------+ //| Draw one reference level (spot / flip / wall) as a plain line | //| across the profile band. No caption: the lines are kept clean | //| and the colour key in the title card names them, so they do not | //| clutter the profile. | //+------------------------------------------------------------------+ void DrawLevel(const double price, const int x0, const int x1, const uint clr) { int y; if(!PriceY(price, y)) return; g_canvas.LineHorizontal(x0, x1, y, clr); } //+------------------------------------------------------------------+ //| Single-line title card (used only for the "no data" message). | //+------------------------------------------------------------------+ void DrawTitle(const int W, const string text) { g_canvas.FontSet("Segoe UI", 15, FW_BOLD); int w = g_canvas.TextWidth(text) + 36; g_canvas.FillRectangle(8, 8, 8 + w, 8 + 38, g_panel); g_canvas.Rectangle(8, 8, 8 + w, 8 + 38, g_grid); g_canvas.TextOut(22, 16, text, g_text, TA_LEFT); } //+------------------------------------------------------------------+ //| Small colour swatch + label, used to build the reference-line | //| key. Returns the x just past the label so keys chain left to | //| right. | //+------------------------------------------------------------------+ int KeyItem(const int x, const int y, const uint clr, const string label) { g_canvas.FillRectangle(x, y + 3, x + 16, y + 17, clr); g_canvas.FontSet("Segoe UI", 13); g_canvas.TextOut(x + 22, y, label, g_textDim, TA_LEFT); return(x + 22 + g_canvas.TextWidth(label) + 20); } //+------------------------------------------------------------------+ //| Title / regime card, top-left, on an opaque panel so it stays | //| readable over any candles behind it. Three rows: the heading, | //| the net-GEX regime verdict, and a colour key that names the | //| reference lines so they can stay unlabelled on the profile. | //+------------------------------------------------------------------+ void DrawTitleTwo(const string line1, const string line2, const uint regimeClr) { g_canvas.FontSet("Segoe UI", 18, FW_BOLD); int w1 = g_canvas.TextWidth(line1); g_canvas.FontSet("Segoe UI", 15, FW_BOLD); int w2 = g_canvas.TextWidth(line2); int cardW = MathMax(w1, w2) + 36; cardW = MathMax(cardW, 360); int cardH = 104; g_canvas.FillRectangle(8, 8, 8 + cardW, 8 + cardH, g_panel); g_canvas.Rectangle(8, 8, 8 + cardW, 8 + cardH, g_grid); g_canvas.FontSet("Segoe UI", 18, FW_BOLD); g_canvas.TextOut(22, 16, line1, g_text, TA_LEFT); g_canvas.FontSet("Segoe UI", 15, FW_BOLD); g_canvas.TextOut(22, 46, line2, regimeClr, TA_LEFT); //--- reference-line colour key int kx = 22, ky = 76; kx = KeyItem(kx, ky, g_spot, "spot"); kx = KeyItem(kx, ky, g_flip, "flip"); kx = KeyItem(kx, ky, g_wall, "walls"); } //+------------------------------------------------------------------+ //| Create (or recreate) a full-chart bitmap the size of the chart. | //+------------------------------------------------------------------+ bool BuildCanvas(void) { int w = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0); int h = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, 0); if(w <= 0) w = 1200; if(h <= 0) h = 600; g_canvas.Destroy(); if(g_objName != "") ObjectDelete(0, g_objName); g_objName = "GexMapCanvas_" + IntegerToString(ChartID()); if(!g_canvas.CreateBitmapLabel(0, 0, g_objName, 0, 0, w, h, COLOR_FORMAT_ARGB_NORMALIZE)) { PrintFormat("GexMap: CreateBitmapLabel failed (err %d)", GetLastError()); return(false); } ObjectSetInteger(0, g_objName, OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(0, g_objName, OBJPROP_XDISTANCE, 0); ObjectSetInteger(0, g_objName, OBJPROP_YDISTANCE, 0); ObjectSetInteger(0, g_objName, OBJPROP_BACK, false); // draw over the candles g_lastW = w; g_lastH = h; return(true); } //+------------------------------------------------------------------+ //| Rebuild the bitmap only when the chart pixel size actually | //| changed. A scroll or a scale change keeps the same canvas and | //| only needs a redraw, which is far cheaper than a rebuild. | //+------------------------------------------------------------------+ bool RebuildIfResized(void) { int w = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0); int h = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, 0); if(w <= 0 || h <= 0) return(false); if(w == g_lastW && h == g_lastH) return(false); return(BuildCanvas()); } //+------------------------------------------------------------------+ //| Initialization | //+------------------------------------------------------------------+ int OnInit(void) { IndicatorSetString(INDICATOR_SHORTNAME, "GEX Map"); ResolvePalette(); if(!BuildCanvas()) return(INIT_FAILED); Reload(); DrawMap(); EventSetTimer(1); g_lastRefresh = TimeCurrent(); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Timer: re-poll the native source on its interval and redraw when | //| fresh data arrived. The profile is static between reloads, so | //| the timer alone does not force redraws. | //+------------------------------------------------------------------+ void OnTimer(void) { if(InpSource == GEX_SOURCE_NATIVE && InpRefreshSec > 0 && TimeCurrent() - g_lastRefresh >= InpRefreshSec) { Reload(); g_lastRefresh = TimeCurrent(); DrawMap(); } } //+------------------------------------------------------------------+ //| Indicator calculation: this tool draws from the option chain, | //| not the price series, so it does no per-bar work. We keep the | //| required return contract only. | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) { return(rates_total); } //+------------------------------------------------------------------+ //| Chart events: R reloads the data. Because the profile is pinned | //| to the real price axis, any chart change (scroll, zoom, scale) | //| shifts the price-to-pixel mapping, so we rebuild-if-resized and | //| always redraw to keep the bars locked to their strikes. | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id == CHARTEVENT_KEYDOWN) { if((int)lparam == 'R' || (int)lparam == 'r') { Reload(); DrawMap(); } } else if(id == CHARTEVENT_CHART_CHANGE) { RebuildIfResized(); // only rebuilds the bitmap on a real size change DrawMap(); // always redraw: the price axis may have moved } } //+------------------------------------------------------------------+ //| Cleanup | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); g_canvas.Destroy(); if(g_objName != "") ObjectDelete(0, g_objName); ChartRedraw(0); } //+------------------------------------------------------------------+