376 lines
16 KiB
MQL5
376 lines
16 KiB
MQL5
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| GexData.mqh |
|
||
|
|
//| 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.00"
|
||
|
|
|
||
|
|
#ifndef GEX_GEXDATA_MQH
|
||
|
|
#define GEX_GEXDATA_MQH
|
||
|
|
|
||
|
|
#include <GEX/BlackScholes.mqh>
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| One raw option quote, before it is aggregated into the profile. |
|
||
|
|
//| It carries everything the gamma math needs: the strike, the |
|
||
|
|
//| right, the time to expiry, and the open interest that scales a |
|
||
|
|
//| contract's gamma into a real dealer exposure. The iv field is |
|
||
|
|
//| filled by the provider (inverted from the price, or read from |
|
||
|
|
//| the server) because gamma is a function of volatility. |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
struct OptionQuote
|
||
|
|
{
|
||
|
|
ENUM_OPT_RIGHT right;
|
||
|
|
double strike;
|
||
|
|
datetime expiry;
|
||
|
|
double price; // market mid price
|
||
|
|
double spot; // underlying price
|
||
|
|
double rate; // risk-free rate
|
||
|
|
double open_interest; // contracts outstanding at this strike
|
||
|
|
double iv; // filled in by the provider (-1 if invalid)
|
||
|
|
};
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| The gamma-exposure profile for a single expiry. It reduces a |
|
||
|
|
//| flat option chain to one number per strike: the net dealer gamma |
|
||
|
|
//| exposure (GEX) at that strike, positive where dealers are long |
|
||
|
|
//| gamma and negative where they are short. From that profile it |
|
||
|
|
//| derives the three levels traders actually read: the call wall |
|
||
|
|
//| (largest positive GEX), the put wall (largest negative GEX), and |
|
||
|
|
//| the zero-gamma "flip" price where total exposure crosses zero. |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
class CGexProfile
|
||
|
|
{
|
||
|
|
private:
|
||
|
|
double m_strikes[]; // sorted unique strikes for the chosen expiry
|
||
|
|
double m_gex[]; // signed dealer GEX per strike (aligned to m_strikes)
|
||
|
|
OptionQuote m_quotes[]; // the quotes that survived onto the chosen expiry
|
||
|
|
double m_spot;
|
||
|
|
double m_multiplier; // contract size (100 for US equity options)
|
||
|
|
datetime m_expiry; // the single expiry this profile represents
|
||
|
|
|
||
|
|
double m_netGex; // sum of m_gex, the headline regime number
|
||
|
|
double m_gexMin, m_gexMax; // most-negative / most-positive per-strike GEX
|
||
|
|
double m_callWall; // strike of the largest positive GEX
|
||
|
|
double m_putWall; // strike of the most-negative GEX
|
||
|
|
double m_flip; // zero-gamma price (<0 if none found)
|
||
|
|
|
||
|
|
public:
|
||
|
|
CGexProfile(void) : m_spot(0), m_multiplier(100.0), m_expiry(0),
|
||
|
|
m_netGex(0), m_gexMin(0), m_gexMax(0),
|
||
|
|
m_callWall(0), m_putWall(0), m_flip(-1.0) {}
|
||
|
|
|
||
|
|
//--- accessors the renderer reads
|
||
|
|
int NStrikes(void) const { return(ArraySize(m_strikes)); }
|
||
|
|
double Strike(const int i) const { return(m_strikes[i]); }
|
||
|
|
double Gex(const int i) const { return(m_gex[i]); }
|
||
|
|
double Spot(void) const { return(m_spot); }
|
||
|
|
datetime Expiry(void) const { return(m_expiry); }
|
||
|
|
double NetGex(void) const { return(m_netGex); }
|
||
|
|
double GexMin(void) const { return(m_gexMin); }
|
||
|
|
double GexMax(void) const { return(m_gexMax); }
|
||
|
|
double CallWall(void) const { return(m_callWall); }
|
||
|
|
double PutWall(void) const { return(m_putWall); }
|
||
|
|
double Flip(void) const { return(m_flip); }
|
||
|
|
|
||
|
|
void Multiplier(const double m) { m_multiplier = (m > 0.0) ? m : 100.0; }
|
||
|
|
|
||
|
|
//--- build the profile; target is the expiry to use, or 0 for "nearest future"
|
||
|
|
bool Build(OptionQuote "es[], const datetime target = 0);
|
||
|
|
|
||
|
|
private:
|
||
|
|
datetime PickExpiry(OptionQuote "es[], const datetime target) const;
|
||
|
|
double DealerGammaAtSpot(const double S) const;
|
||
|
|
double SolveFlip(void) const;
|
||
|
|
int IndexOf(const double &arr[], const double v) const;
|
||
|
|
};
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Choose which expiry to profile. A GEX map is always a single |
|
||
|
|
//| expiry (near-dated positioning is what pins index price), so we |
|
||
|
|
//| either honour the caller's requested expiry or, when none is |
|
||
|
|
//| given, take the nearest one still in the future. Expired |
|
||
|
|
//| contracts never win here. |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
datetime CGexProfile::PickExpiry(OptionQuote "es[], const datetime target) const
|
||
|
|
{
|
||
|
|
datetime now = TimeCurrent();
|
||
|
|
if(now == 0)
|
||
|
|
now = TimeLocal();
|
||
|
|
|
||
|
|
datetime best = 0;
|
||
|
|
double bestDist = 1e18;
|
||
|
|
int n = ArraySize(quotes);
|
||
|
|
for(int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
datetime e = quotes[i].expiry;
|
||
|
|
if(e <= now)
|
||
|
|
continue; // never profile an expired contract
|
||
|
|
//--- distance is "closeness to the requested expiry", or "closeness to now"
|
||
|
|
double dist = (target > 0) ? MathAbs((double)(e - target)) : (double)(e - now);
|
||
|
|
if(dist < bestDist)
|
||
|
|
{
|
||
|
|
bestDist = dist;
|
||
|
|
best = e;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return(best);
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Build the single-expiry profile. Pick the expiry, keep only the |
|
||
|
|
//| quotes on it, collect the sorted unique strikes, then sum the |
|
||
|
|
//| signed dealer GEX at each strike. The dealer-sign convention is |
|
||
|
|
//| the standard one: dealers are assumed short calls and long puts |
|
||
|
|
//| against the customer, so a call's gamma adds to exposure and a |
|
||
|
|
//| put's subtracts. Per contract the exposure is |
|
||
|
|
//| gamma * OI * multiplier * spot^2 * 0.01 |
|
||
|
|
//| i.e. dollar gamma per 1% move in the underlying. |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
bool CGexProfile::Build(OptionQuote "es[], const datetime target)
|
||
|
|
{
|
||
|
|
int n = ArraySize(quotes);
|
||
|
|
if(n == 0)
|
||
|
|
return(false);
|
||
|
|
|
||
|
|
m_spot = quotes[0].spot;
|
||
|
|
m_expiry = PickExpiry(quotes, target);
|
||
|
|
if(m_expiry == 0)
|
||
|
|
{
|
||
|
|
Print("CGexProfile: no future expiry to profile");
|
||
|
|
return(false);
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- keep only this expiry's usable quotes (valid IV, positive OI)
|
||
|
|
ArrayResize(m_quotes, 0);
|
||
|
|
ArrayResize(m_strikes, 0);
|
||
|
|
for(int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
if(quotes[i].expiry != m_expiry)
|
||
|
|
continue;
|
||
|
|
if(quotes[i].iv <= 0.0 || quotes[i].open_interest <= 0.0)
|
||
|
|
continue; // no gamma or no size means no exposure
|
||
|
|
int s = ArraySize(m_quotes);
|
||
|
|
ArrayResize(m_quotes, s + 1);
|
||
|
|
m_quotes[s] = quotes[i];
|
||
|
|
if(IndexOf(m_strikes, quotes[i].strike) < 0)
|
||
|
|
{ int k = ArraySize(m_strikes); ArrayResize(m_strikes, k + 1); m_strikes[k] = quotes[i].strike; }
|
||
|
|
}
|
||
|
|
ArraySort(m_strikes);
|
||
|
|
int nk = ArraySize(m_strikes);
|
||
|
|
if(nk < 2)
|
||
|
|
{
|
||
|
|
Print("CGexProfile: fewer than 2 usable strikes on the chosen expiry");
|
||
|
|
return(false);
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- sum signed dealer GEX per strike, evaluated at the live spot
|
||
|
|
datetime now = TimeCurrent();
|
||
|
|
if(now == 0)
|
||
|
|
now = TimeLocal();
|
||
|
|
ArrayResize(m_gex, nk);
|
||
|
|
ArrayInitialize(m_gex, 0.0);
|
||
|
|
|
||
|
|
int nq = ArraySize(m_quotes);
|
||
|
|
for(int i = 0; i < nq; i++)
|
||
|
|
{
|
||
|
|
double T = (double)(m_quotes[i].expiry - now) / (365.0 * 24 * 3600);
|
||
|
|
if(T <= 0.0)
|
||
|
|
continue;
|
||
|
|
double g = BSGamma(m_spot, m_quotes[i].strike, m_quotes[i].rate, 0.0, m_quotes[i].iv, T);
|
||
|
|
double contractGex = g * m_quotes[i].open_interest * m_multiplier * m_spot * m_spot * 0.01;
|
||
|
|
double signed_ = (m_quotes[i].right == OPT_CALL) ? contractGex : -contractGex;
|
||
|
|
int k = IndexOf(m_strikes, m_quotes[i].strike);
|
||
|
|
if(k >= 0)
|
||
|
|
m_gex[k] += signed_;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- derive the headline numbers and the walls from the per-strike profile
|
||
|
|
m_netGex = 0.0;
|
||
|
|
m_gexMin = 1e18;
|
||
|
|
m_gexMax = -1e18;
|
||
|
|
m_callWall = m_strikes[0];
|
||
|
|
m_putWall = m_strikes[0];
|
||
|
|
double maxPos = -1e18, maxNeg = 1e18;
|
||
|
|
for(int k = 0; k < nk; k++)
|
||
|
|
{
|
||
|
|
m_netGex += m_gex[k];
|
||
|
|
if(m_gex[k] < m_gexMin)
|
||
|
|
m_gexMin = m_gex[k];
|
||
|
|
if(m_gex[k] > m_gexMax)
|
||
|
|
m_gexMax = m_gex[k];
|
||
|
|
if(m_gex[k] > maxPos)
|
||
|
|
{ maxPos = m_gex[k]; m_callWall = m_strikes[k]; }
|
||
|
|
if(m_gex[k] < maxNeg)
|
||
|
|
{ maxNeg = m_gex[k]; m_putWall = m_strikes[k]; }
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- the zero-gamma flip: the spot at which total dealer gamma is zero
|
||
|
|
m_flip = SolveFlip();
|
||
|
|
PrintFormat("CGexProfile: expiry=%s strikes=%d netGEX=%.3g flip=%.2f callWall=%.2f putWall=%.2f",
|
||
|
|
TimeToString(m_expiry, TIME_DATE), nk, m_netGex, m_flip, m_callWall, m_putWall);
|
||
|
|
return(true);
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Total signed dealer gamma if the underlying were trading at S. |
|
||
|
|
//| This is the same aggregation as Build, but with every contract's |
|
||
|
|
//| gamma re-evaluated at the hypothetical spot S (each keeps its |
|
||
|
|
//| own implied volatility). Sweeping S through this function traces |
|
||
|
|
//| the exposure curve whose zero crossing is the flip level. We |
|
||
|
|
//| return raw dealer gamma (not the spot^2-scaled dollar figure), |
|
||
|
|
//| because only its sign and zero crossing matter here. |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
double CGexProfile::DealerGammaAtSpot(const double S) const
|
||
|
|
{
|
||
|
|
datetime now = TimeCurrent();
|
||
|
|
if(now == 0)
|
||
|
|
now = TimeLocal();
|
||
|
|
|
||
|
|
double total = 0.0;
|
||
|
|
int nq = ArraySize(m_quotes);
|
||
|
|
for(int i = 0; i < nq; i++)
|
||
|
|
{
|
||
|
|
double T = (double)(m_quotes[i].expiry - now) / (365.0 * 24 * 3600);
|
||
|
|
if(T <= 0.0)
|
||
|
|
continue;
|
||
|
|
double g = BSGamma(S, m_quotes[i].strike, m_quotes[i].rate, 0.0, m_quotes[i].iv, T);
|
||
|
|
double contrib = g * m_quotes[i].open_interest * m_multiplier;
|
||
|
|
total += (m_quotes[i].right == OPT_CALL) ? contrib : -contrib;
|
||
|
|
}
|
||
|
|
return(total);
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Find the zero-gamma flip by scanning dealer gamma across a price |
|
||
|
|
//| band around spot and interpolating the first sign change. The |
|
||
|
|
//| band spans the strikes we hold, widened a little so a flip just |
|
||
|
|
//| outside the quoted strikes is still caught. If dealer gamma |
|
||
|
|
//| never changes sign across the band there is no flip (a wholly |
|
||
|
|
//| long- or short-gamma book), and we return a negative sentinel. |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
double CGexProfile::SolveFlip(void) const
|
||
|
|
{
|
||
|
|
int nk = ArraySize(m_strikes);
|
||
|
|
if(nk < 2)
|
||
|
|
return(-1.0);
|
||
|
|
|
||
|
|
double lo = m_strikes[0];
|
||
|
|
double hi = m_strikes[nk - 1];
|
||
|
|
double pad = 0.15 * (hi - lo); // widen the band by 15% each way
|
||
|
|
lo -= pad;
|
||
|
|
hi += pad;
|
||
|
|
if(lo <= 0.0)
|
||
|
|
lo = 0.01 * m_strikes[0];
|
||
|
|
|
||
|
|
int steps = 400;
|
||
|
|
double dx = (hi - lo) / steps;
|
||
|
|
double prevS = lo;
|
||
|
|
double prevG = DealerGammaAtSpot(lo);
|
||
|
|
for(int i = 1; i <= steps; i++)
|
||
|
|
{
|
||
|
|
double s = lo + i * dx;
|
||
|
|
double g = DealerGammaAtSpot(s);
|
||
|
|
if((prevG <= 0.0 && g > 0.0) || (prevG >= 0.0 && g < 0.0))
|
||
|
|
{
|
||
|
|
//--- linear interpolation of the crossing between prevS and s
|
||
|
|
double denom = (g - prevG);
|
||
|
|
if(MathAbs(denom) < 1e-30)
|
||
|
|
return(0.5 * (prevS + s));
|
||
|
|
return(prevS - prevG * (s - prevS) / denom);
|
||
|
|
}
|
||
|
|
prevS = s;
|
||
|
|
prevG = g;
|
||
|
|
}
|
||
|
|
return(-1.0); // no sign change: no flip in this band
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Linear search for a strike in the array. Uses a small tolerance |
|
||
|
|
//| so float round-trips still match. |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
int CGexProfile::IndexOf(const double &arr[], const double v) const
|
||
|
|
{
|
||
|
|
int n = ArraySize(arr);
|
||
|
|
for(int i = 0; i < n; i++)
|
||
|
|
if(MathAbs(arr[i] - v) < 1e-6)
|
||
|
|
return(i);
|
||
|
|
return(-1);
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| CSV provider. Reads a chain file from MQL5\Files with columns: |
|
||
|
|
//| right,strike,expiry,mid,spot,rate,oi |
|
||
|
|
//| where right is C/P and expiry is YYYY.MM.DD. Computes the |
|
||
|
|
//| implied volatility for each row via Black-Scholes inversion so |
|
||
|
|
//| the gamma aggregation downstream has a sigma to work with. |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
class CGexProviderCSV
|
||
|
|
{
|
||
|
|
public:
|
||
|
|
bool Load(const string filename, OptionQuote &out[]);
|
||
|
|
};
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Parse the CSV chain into a flat quote list, inverting each row's |
|
||
|
|
//| price to an implied volatility as it is read. |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
bool CGexProviderCSV::Load(const string filename, OptionQuote &out[])
|
||
|
|
{
|
||
|
|
int h = FileOpen(filename, FILE_READ | FILE_CSV | FILE_ANSI, ',');
|
||
|
|
if(h == INVALID_HANDLE)
|
||
|
|
{
|
||
|
|
PrintFormat("CGexProviderCSV: cannot open %s (err %d)", filename, GetLastError());
|
||
|
|
return(false);
|
||
|
|
}
|
||
|
|
ArrayResize(out, 0);
|
||
|
|
bool header = true;
|
||
|
|
while(!FileIsEnding(h))
|
||
|
|
{
|
||
|
|
string sRight = FileReadString(h);
|
||
|
|
if(FileIsLineEnding(h) && StringLen(sRight) == 0)
|
||
|
|
continue;
|
||
|
|
string sStrike = FileReadString(h);
|
||
|
|
string sExpiry = FileReadString(h);
|
||
|
|
string sMid = FileReadString(h);
|
||
|
|
string sSpot = FileReadString(h);
|
||
|
|
string sRate = FileReadString(h);
|
||
|
|
string sOi = FileReadString(h);
|
||
|
|
if(header)
|
||
|
|
{
|
||
|
|
header = false; // skip the column titles
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if(StringLen(sStrike) == 0)
|
||
|
|
continue;
|
||
|
|
|
||
|
|
OptionQuote q;
|
||
|
|
string rr = sRight;
|
||
|
|
StringToUpper(rr);
|
||
|
|
q.right = (StringFind(rr, "P") >= 0) ? OPT_PUT : OPT_CALL;
|
||
|
|
q.strike = StringToDouble(sStrike);
|
||
|
|
q.expiry = StringToTime(sExpiry);
|
||
|
|
q.price = StringToDouble(sMid);
|
||
|
|
q.spot = StringToDouble(sSpot);
|
||
|
|
q.rate = StringToDouble(sRate);
|
||
|
|
q.open_interest = StringToDouble(sOi);
|
||
|
|
|
||
|
|
datetime now = TimeCurrent();
|
||
|
|
if(now == 0)
|
||
|
|
now = TimeLocal();
|
||
|
|
double T = (double)(q.expiry - now) / (365.0 * 24 * 3600);
|
||
|
|
q.iv = ImpliedVol(q.right, q.price, q.spot, q.strike, q.rate, 0.0, T);
|
||
|
|
|
||
|
|
int s = ArraySize(out);
|
||
|
|
ArrayResize(out, s + 1);
|
||
|
|
out[s] = q;
|
||
|
|
}
|
||
|
|
FileClose(h);
|
||
|
|
PrintFormat("CGexProviderCSV: loaded %d rows from %s", ArraySize(out), filename);
|
||
|
|
return(ArraySize(out) > 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
#endif // GEX_GEXDATA_MQH
|
||
|
|
//+------------------------------------------------------------------+
|