Gamma-Exposure-MQL5/Indicators/GEX/GexMap.mq5
2026-07-07 19:24:31 +00:00

462 lines
20 KiB
MQL5

//+------------------------------------------------------------------+
//| 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 <Canvas/Canvas.mqh>
#include <GEX/GexData.mqh>
#include <GEX/GexProviderNative.mqh>
//+------------------------------------------------------------------+
//| 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);
}
//+------------------------------------------------------------------+