Implied-Volatility-MQL5/Include/IVSurface/IVSurfaceData.mqh
2026-07-06 02:21:33 +00:00

302 lines
11 KiB
MQL5

//+------------------------------------------------------------------+
//| IVSurfaceData.mqh |
//| MMQ — Muhammad Minhas Qamar |
//| www.mql5.com/en/articles/23385 |
//+------------------------------------------------------------------+
#property copyright "MMQ — Muhammad Minhas Qamar"
#property link "https://www.mql5.com/en/articles/23385"
#property version "1.00"
#ifndef IVSURFACE_IVSURFACEDATA_MQH
#define IVSURFACE_IVSURFACEDATA_MQH
#include <IVSurface/BlackScholes.mqh>
//+------------------------------------------------------------------+
//| One raw option quote, before it is placed on the grid. |
//+------------------------------------------------------------------+
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 iv; // filled in by the provider (-1 if invalid)
};
//+------------------------------------------------------------------+
//| The implied-volatility surface as a regular grid. Axes are the |
//| sorted unique strikes (x) and times to expiry in years (y); each |
//| cell holds implied vol, or a negative sentinel where no valid |
//| quote existed. A regular grid is what lets the renderer hand it |
//| straight to a triangle mesh. |
//+------------------------------------------------------------------+
class CIVSurface
{
private:
double m_strikes[]; // x axis, ascending
double m_times[]; // y axis (years to expiry), ascending
double m_iv[]; // row-major grid[t*nx + k], <0 = empty
double m_spot;
double m_ivMin, m_ivMax; // for colour/height scaling
public:
CIVSurface(void) : m_spot(0), m_ivMin(0), m_ivMax(0) {}
int NStrikes(void) const { return(ArraySize(m_strikes)); }
int NTimes(void) const { return(ArraySize(m_times)); }
double Strike(const int i) const { return(m_strikes[i]); }
double TimeYears(const int j) const { return(m_times[j]); }
double Spot(void) const { return(m_spot); }
double IVMin(void) const { return(m_ivMin); }
double IVMax(void) const { return(m_ivMax); }
double IV(const int j, const int k) const { return(m_iv[j * ArraySize(m_strikes) + k]); }
bool Build(OptionQuote &quotes[]);
void FillGaps(void);
private:
int IndexOf(const double &arr[], const double v) const;
};
//+------------------------------------------------------------------+
//| Build the grid from a flat list of quotes. Collects the unique |
//| strikes and expiries, places each valid IV in its cell, then |
//| fills the holes left by missing or invalid quotes. |
//+------------------------------------------------------------------+
bool CIVSurface::Build(OptionQuote &quotes[])
{
int n = ArraySize(quotes);
if(n == 0)
return(false);
datetime now = TimeCurrent();
if(now == 0)
now = TimeLocal();
m_spot = quotes[0].spot;
//--- collect unique sorted strikes and times
ArrayResize(m_strikes, 0);
ArrayResize(m_times, 0);
for(int i = 0; i < n; i++)
{
if(quotes[i].iv <= 0.0)
continue; // skip invalid quotes for axes
double t = (double)(quotes[i].expiry - now) / (365.0 * 24 * 3600);
if(t <= 0.0)
continue; // expired
if(IndexOf(m_strikes, quotes[i].strike) < 0)
{ int s = ArraySize(m_strikes); ArrayResize(m_strikes, s + 1); m_strikes[s] = quotes[i].strike; }
if(IndexOf(m_times, t) < 0)
{ int s = ArraySize(m_times); ArrayResize(m_times, s + 1); m_times[s] = t; }
}
ArraySort(m_strikes);
ArraySort(m_times);
int nx = ArraySize(m_strikes), ny = ArraySize(m_times);
if(nx < 2 || ny < 2)
return(false);
//--- allocate grid, mark all empty
ArrayResize(m_iv, nx * ny);
ArrayInitialize(m_iv, -1.0);
//--- drop each valid quote into its cell
m_ivMin = 1e18;
m_ivMax = -1e18;
for(int i = 0; i < n; i++)
{
if(quotes[i].iv <= 0.0)
continue;
double t = (double)(quotes[i].expiry - now) / (365.0 * 24 * 3600);
if(t <= 0.0)
continue;
int k = IndexOf(m_strikes, quotes[i].strike);
int j = IndexOf(m_times, t);
if(k < 0 || j < 0)
continue;
m_iv[j * nx + k] = quotes[i].iv;
if(quotes[i].iv < m_ivMin)
m_ivMin = quotes[i].iv;
if(quotes[i].iv > m_ivMax)
m_ivMax = quotes[i].iv;
}
FillGaps();
return(true);
}
//+------------------------------------------------------------------+
//| Fill empty cells so the mesh has no holes. For each gap, take the|
//| nearest filled neighbours along the strike row (linear |
//| interpolation / edge hold). Deliberately simple: a real desk |
//| would fit a model (SABR, SVI), but nearest-neighbour keeps the |
//| surface continuous without inventing structure. |
//+------------------------------------------------------------------+
void CIVSurface::FillGaps(void)
{
int nx = ArraySize(m_strikes), ny = ArraySize(m_times);
for(int j = 0; j < ny; j++)
{
//--- forward pass: carry the last known value across gaps in this row
double last = -1.0;
for(int k = 0; k < nx; k++)
{
int idx = j * nx + k;
if(m_iv[idx] > 0.0)
last = m_iv[idx];
else
if(last > 0.0)
m_iv[idx] = last;
}
//--- backward pass: fill any leading gaps from the first known value
double next = -1.0;
for(int k = nx - 1; k >= 0; k--)
{
int idx = j * nx + k;
if(m_iv[idx] > 0.0)
next = m_iv[idx];
else
if(next > 0.0)
m_iv[idx] = next;
}
}
//--- if a whole row is still empty, copy the nearest non-empty row
for(int j = 0; j < ny; j++)
{
bool empty = true;
for(int k = 0; k < nx; k++)
if(m_iv[j * nx + k] > 0.0)
{
empty = false;
break;
}
if(!empty)
continue;
for(int d = 1; d < ny; d++)
{
int src = -1;
if(j - d >= 0)
{
bool ok = true;
for(int k = 0; k < nx; k++)
if(m_iv[(j - d) * nx + k] <= 0.0)
{
ok = false;
break;
}
if(ok)
src = j - d;
}
if(src < 0 && j + d < ny)
{
bool ok = true;
for(int k = 0; k < nx; k++)
if(m_iv[(j + d) * nx + k] <= 0.0)
{
ok = false;
break;
}
if(ok)
src = j + d;
}
if(src >= 0)
{
for(int k = 0; k < nx; k++)
m_iv[j * nx + k] = m_iv[src * nx + k];
break;
}
}
}
if(m_ivMin > m_ivMax)
{
m_ivMin = 0.1; // guard for empty build
m_ivMax = 0.3;
}
}
//+------------------------------------------------------------------+
//| Linear search for a value in the array. Uses a small tolerance |
//| so float round-trips still match. |
//+------------------------------------------------------------------+
int CIVSurface::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 |
//| where right is C/P and expiry is YYYY.MM.DD. Computes IV for |
//| each row via Black-Scholes inversion. |
//+------------------------------------------------------------------+
class CIVProviderCSV
{
public:
bool Load(const string filename, OptionQuote &out[]);
};
//+------------------------------------------------------------------+
//| Parse the CSV chain into a flat quote list, computing IV for |
//| every row via Black-Scholes inversion as it is read. |
//+------------------------------------------------------------------+
bool CIVProviderCSV::Load(const string filename, OptionQuote &out[])
{
int h = FileOpen(filename, FILE_READ | FILE_CSV | FILE_ANSI, ',');
if(h == INVALID_HANDLE)
{
PrintFormat("CIVProviderCSV: 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);
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);
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("CIVProviderCSV: loaded %d rows from %s", ArraySize(out), filename);
return(ArraySize(out) > 0);
}
#endif // IVSURFACE_IVSURFACEDATA_MQH
//+------------------------------------------------------------------+