462 lines
20 KiB
MQL5
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);
| |||
}
| |||
//+------------------------------------------------------------------+
|