//+------------------------------------------------------------------+ //| GKProviderNative.mqh | //| MMQ — Muhammad Minhas Qamar | //| www.mql5.com/en/articles/23807 | //+------------------------------------------------------------------+ #property copyright "MMQ — Muhammad Minhas Qamar" #property link "https://www.mql5.com/en/articles/23807" #property version "1.00" #property strict #ifndef GK_GKPROVIDERNATIVE_MQH #define GK_GKPROVIDERNATIVE_MQH #include //+------------------------------------------------------------------+ //| Linear interpolation of the listed IV curve at an arbitrary | //| strike, clamped to the end vols outside the quoted range. The | //| native path only ever has the broker's discrete strikes, so any | //| delta pillar that lands between them needs this to read a vol. | //+------------------------------------------------------------------+ double GKInterpIV(const double &strikes[], const double &ivs[], const int n, const double K) { if(n <= 0) return(-1.0); if(K <= strikes[0]) return(ivs[0]); if(K >= strikes[n - 1]) return(ivs[n - 1]); for(int i = 1; i < n; i++) { if(K <= strikes[i]) { double w = (K - strikes[i - 1]) / (strikes[i] - strikes[i - 1]); return(ivs[i - 1] + w * (ivs[i] - ivs[i - 1])); } } return(ivs[n - 1]); } //+------------------------------------------------------------------+ //| Solve a target delta to a strike on a LISTED curve, where the | //| vol used at each trial strike is itself read off the interpolated| //| chain. This is the strike solver of the math core wrapped around | //| a live smile: the delta is no longer evaluated at a fixed vol | //| but at vol(K), so we scan the quoted strike range high to low | //| and bracket the first sign change, which is the out-of-the-money | //| root even for the non-monotonic premium-adjusted call. | //+------------------------------------------------------------------+ double GKStrikeAtDeltaInterp(const ENUM_OPT_RIGHT right, const ENUM_GK_DELTA conv, const double targetAbs, const double S, const double rd, const double rf, const double T, const double &strikes[], const double &ivs[], const int n) { if(n < 2 || targetAbs <= 0.0) return(-1.0); double target = (right == OPT_CALL) ? targetAbs : -targetAbs; double kHi = strikes[n - 1], kLo = strikes[0]; int steps = 400; double prevK = kHi; double prevF = GKDelta(right, conv, S, prevK, rd, rf, GKInterpIV(strikes, ivs, n, prevK), T) - target; for(int i = 1; i <= steps; i++) { double curK = kHi + (kLo - kHi) * (double)i / steps; double curV = GKInterpIV(strikes, ivs, n, curK); double curF = GKDelta(right, conv, S, curK, rd, rf, curV, T) - target; if(prevF == 0.0) return(prevK); if(curF == 0.0) return(curK); if(prevF * curF < 0.0) { double a = curK, b = prevK, fa = curF; for(int j = 0; j < 100; j++) { double m = 0.5 * (a + b); double mv = GKInterpIV(strikes, ivs, n, m); double fm = GKDelta(right, conv, S, m, rd, rf, mv, T) - target; if(MathAbs(fm) < 1e-12 || (b - a) < 1e-10) return(m); if(fa * fm < 0.0) b = m; else { a = m; fa = fm; } } return(0.5 * (a + b)); } prevK = curK; prevF = curF; } return(-1.0); } //+------------------------------------------------------------------+ //| Summarise a listed IV curve into the desk's delta-space quotes. | //| This is the exact inverse of CGKSmile's reconstruction: where | //| the CSV path turns ATM/RR/BF into strikes, here we turn a chain | //| of strikes back into ATM/RR/BF so both providers can feed the | //| same Build. The ATM vol is found by a short delta-neutral- | //| straddle fixed point (the ATM strike depends on the ATM vol, | //| which is read at that strike), and each wing vol is the chain's | //| vol at its solved 25- or 10-delta strike. Returns false when the | //| chain is too narrow to reach the 25-delta pillars. | //+------------------------------------------------------------------+ bool GKDeriveDeltaQuotes(const double &strikes[], const double &ivs[], const int n, const double S, const double rd, const double rf, const double T, const ENUM_GK_DELTA conv, double &atm, double &rr25, double &bf25, double &rr10, double &bf10, bool &has10) { atm = rr25 = bf25 = rr10 = bf10 = 0.0; has10 = false; if(n < 3 || T <= 0.0) return(false); double F = GKForward(S, rd, rf, T); //--- ATM vol: delta-neutral-straddle fixed point, seeded at vol(F) double atmVol = GKInterpIV(strikes, ivs, n, F); for(int k = 0; k < 12; k++) { double Katm = GKATMStrike(GK_ATM_DNS, conv, F, atmVol, T); double v = GKInterpIV(strikes, ivs, n, Katm); if(MathAbs(v - atmVol) < 1e-12) { atmVol = v; break; } atmVol = v; } atm = atmVol; //--- 25-delta pillars are mandatory double k25c = GKStrikeAtDeltaInterp(OPT_CALL, conv, 0.25, S, rd, rf, T, strikes, ivs, n); double k25p = GKStrikeAtDeltaInterp(OPT_PUT, conv, 0.25, S, rd, rf, T, strikes, ivs, n); if(k25c <= 0.0 || k25p <= 0.0) return(false); double s25c = GKInterpIV(strikes, ivs, n, k25c); double s25p = GKInterpIV(strikes, ivs, n, k25p); rr25 = s25c - s25p; bf25 = 0.5 * (s25c + s25p) - atm; //--- 10-delta wings when the chain is wide enough to carry them double k10c = GKStrikeAtDeltaInterp(OPT_CALL, conv, 0.10, S, rd, rf, T, strikes, ivs, n); double k10p = GKStrikeAtDeltaInterp(OPT_PUT, conv, 0.10, S, rd, rf, T, strikes, ivs, n); if(k10c > 0.0 && k10p > 0.0) { double s10c = GKInterpIV(strikes, ivs, n, k10c); double s10p = GKInterpIV(strikes, ivs, n, k10p); rr10 = s10c - s10p; bf10 = 0.5 * (s10c + s10p) - atm; has10 = true; } return(true); } //+------------------------------------------------------------------+ //| Native provider. Reads the broker's own MetaTrader 5 FX-option | //| symbols on an underlying, inverts each contract to an implied | //| vol, groups the chain by expiry, and summarises every expiry | //| into the same SmileTenor the CSV provider emits. Rates are not | //| carried by the symbol, so the caller supplies them. | //+------------------------------------------------------------------+ class CGKProviderNative { public: //--- underlying: base symbol; rd/rf: domestic/foreign continuous rates bool Load(const string underlying, const double rd, const double rf, const ENUM_GK_DELTA conv, SmileTenor &out[]); private: bool IsOption(const string sym) const; void SortByStrike(double &strikes[], double &ivs[], const int n) const; }; //+------------------------------------------------------------------+ //| A symbol is an option if the server reports an option right for | //| it; brokers without options fail here, so this doubles as the | //| "does this account have options?" test. A valid positive strike | //| is the second confirmation. | //+------------------------------------------------------------------+ bool CGKProviderNative::IsOption(const string sym) const { long right = 0; if(!SymbolInfoInteger(sym, SYMBOL_OPTION_RIGHT, right)) return(false); double strike = SymbolInfoDouble(sym, SYMBOL_OPTION_STRIKE); return(strike > 0.0); } //+------------------------------------------------------------------+ //| Insertion sort of parallel strike/iv arrays by ascending strike. | //| The chains are short (a few dozen strikes per expiry), so a | //| simple in-place sort keeps the derivation's interpolation input | //| monotone without pulling in extra machinery. | //+------------------------------------------------------------------+ void CGKProviderNative::SortByStrike(double &strikes[], double &ivs[], const int n) const { for(int i = 1; i < n; i++) { double ks = strikes[i], vs = ivs[i]; int j = i - 1; while(j >= 0 && strikes[j] > ks) { strikes[j + 1] = strikes[j]; ivs[j + 1] = ivs[j]; j--; } strikes[j + 1] = ks; ivs[j + 1] = vs; } } //+------------------------------------------------------------------+ //| Enumerate the option symbols on the underlying, invert each to | //| an implied vol (using the server's own IV when it publishes | //| one), collect them by expiry, and derive one SmileTenor per | //| expiry. Expiries whose chain cannot reach the 25-delta pillars | //| are skipped rather than reported with holes. | //+------------------------------------------------------------------+ bool CGKProviderNative::Load(const string underlying, const double rd, const double rf, const ENUM_GK_DELTA conv, SmileTenor &out[]) { ArrayResize(out, 0); int total = SymbolsTotal(false); if(total <= 0) { Print("CGKProviderNative: no symbols available"); return(false); } double spot = SymbolInfoDouble(underlying, SYMBOL_BID); if(spot <= 0.0) spot = SymbolInfoDouble(underlying, SYMBOL_LAST); datetime now = TimeCurrent(); if(now == 0) now = TimeLocal(); //--- collect the whole chain: parallel expiry / strike / iv arrays datetime exps[]; double strk[], ivv[]; int found = 0; for(int i = 0; i < total; i++) { string sym = SymbolName(i, false); if(!IsOption(sym)) continue; if(SymbolInfoString(sym, SYMBOL_BASIS) != underlying) continue; SymbolSelect(sym, true); long lright = 0; SymbolInfoInteger(sym, SYMBOL_OPTION_RIGHT, lright); ENUM_OPT_RIGHT right = (lright == (long)SYMBOL_OPTION_RIGHT_CALL) ? OPT_CALL : OPT_PUT; double K = SymbolInfoDouble(sym, SYMBOL_OPTION_STRIKE); datetime exp = (datetime)SymbolInfoInteger(sym, SYMBOL_EXPIRATION_TIME); double T = (double)(exp - now) / (365.0 * 24 * 3600); if(T <= 0.0) continue; double bid = SymbolInfoDouble(sym, SYMBOL_BID); double ask = SymbolInfoDouble(sym, SYMBOL_ASK); double mid = (bid > 0.0 && ask > 0.0) ? 0.5 * (bid + ask) : SymbolInfoDouble(sym, SYMBOL_LAST); double iv = SymbolInfoDouble(sym, SYMBOL_PRICE_VOLATILITY) / 100.0; if(iv <= 0.0) iv = GKImpliedVol(right, mid, spot, K, rd, rf, T); if(iv <= 0.0) continue; int m = ArraySize(strk); ArrayResize(exps, m + 1); ArrayResize(strk, m + 1); ArrayResize(ivv, m + 1); exps[m] = exp; strk[m] = K; ivv[m] = iv; found++; } if(found == 0) { PrintFormat("CGKProviderNative: no option contracts found for %s " "(account may not offer options, or the underlying name differs from SYMBOL_BASIS)", underlying); return(false); } //--- unique expiries, then derive one tenor each datetime uexp[]; for(int i = 0; i < found; i++) { bool seen = false; for(int u = 0; u < ArraySize(uexp); u++) { if(uexp[u] == exps[i]) { seen = true; break; } } if(!seen) { int u = ArraySize(uexp); ArrayResize(uexp, u + 1); uexp[u] = exps[i]; } } ArraySort(uexp); for(int u = 0; u < ArraySize(uexp); u++) { double es[], vs[]; for(int i = 0; i < found; i++) { if(exps[i] == uexp[u]) { int m = ArraySize(es); ArrayResize(es, m + 1); ArrayResize(vs, m + 1); es[m] = strk[i]; vs[m] = ivv[i]; } } int cn = ArraySize(es); if(cn < 3) continue; SortByStrike(es, vs, cn); double T = (double)(uexp[u] - now) / (365.0 * 24 * 3600); double atm, rr25, bf25, rr10, bf10; bool has10; if(!GKDeriveDeltaQuotes(es, vs, cn, spot, rd, rf, T, conv, atm, rr25, bf25, rr10, bf10, has10)) continue; int t = ArraySize(out); ArrayResize(out, t + 1); out[t].label = TimeToString(uexp[u], TIME_DATE); out[t].days = T * 365.0; out[t].spot = spot; out[t].rd = rd; out[t].rf = rf; out[t].atm = atm; out[t].rr25 = rr25; out[t].bf25 = bf25; out[t].rr10 = rr10; out[t].bf10 = bf10; out[t].has10 = has10; } PrintFormat("CGKProviderNative: %s -> %d contracts, %d usable expiries", underlying, found, ArraySize(out)); return(ArraySize(out) > 0); } #endif // GK_GKPROVIDERNATIVE_MQH //+------------------------------------------------------------------+