//+------------------------------------------------------------------+ //| 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 //+------------------------------------------------------------------+ //| 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 //+------------------------------------------------------------------+