356 lines
13 KiB
MQL5
356 lines
13 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| 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
|
|
//+------------------------------------------------------------------+
|