302 lines
11 KiB
MQL5
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 "es[]);
|
|
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 "es[])
|
|
{
|
|
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
|
|
//+------------------------------------------------------------------+
|