gk-fx-smile/Include/GK/GKProviderNative.mqh

356 lines
13 KiB
MQL5
Raw Permalink Normal View History

2026-08-01 05:36:33 +00:00
//+------------------------------------------------------------------+
//| 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 <GK\GKData.mqh>
//+------------------------------------------------------------------+
//| 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
//+------------------------------------------------------------------+