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

259 lines
12 KiB
MQL5
Raw Permalink Normal View History

2026-08-01 05:36:33 +00:00
//+------------------------------------------------------------------+
//| GKData.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_GKDATA_MQH
#define GK_GKDATA_MQH
#include <GK\GarmanKohlhagen.mqh>
//+------------------------------------------------------------------+
//| One reconstructed point on a tenor's smile: the strike a quoted |
//| delta pillar maps to, the volatility that lives there, and the |
//| realised delta at that strike (which pins the pillar it came |
//| from). Ordered left to right by strike, these five points are |
//| the smile the FX market only ever quotes in delta space. |
//+------------------------------------------------------------------+
struct SmilePoint
{
string label; // "10P","25P","ATM","25C","10C"
double strike; // reconstructed strike
double vol; // volatility at this strike (decimal)
double delta; // realised delta at the strike (signed)
};
//+------------------------------------------------------------------+
//| One tenor of the surface: the raw ATM / risk-reversal / butter- |
//| fly quotes the desk publishes, the market data they price |
//| against, and — once Build has run — the strike-space smile they |
//| reconstruct into. Vols are held as decimals (0.08, not 8.0); the |
//| provider divides the percent quotes on the way in. |
//+------------------------------------------------------------------+
struct SmileTenor
{
string label; // "1W","1M","3M"...
double days; // calendar days to expiry
double spot; // FX spot for the pair
double rd, rf; // domestic / foreign continuous rates
double atm; // ATM vol
double rr25, bf25; // 25-delta risk reversal / butterfly
double rr10, bf10; // 10-delta risk reversal / butterfly
bool has10; // are the 10-delta wings supplied?
//--- filled by CGKSmile::Build
double T; // days / 365
double F; // forward
SmilePoint pts[]; // reconstructed smile, ascending strike
};
//+------------------------------------------------------------------+
//| The reconstructed volatility smile across tenors. Build takes |
//| the raw delta-space quotes and, under a chosen delta and ATM |
//| convention, turns each tenor's ATM/RR/BF into strike-space |
//| pillars: it derives the pillar vols, then solves each pillar's |
//| delta back to the strike it sits on. |
//| |
//| The pillar-vol step uses the simplified "smile strangle" reading |
//| — sigma(25c)=ATM+BF+RR/2, sigma(25p)=ATM+BF-RR/2 — where the |
//| quoted butterfly is taken directly as the smile's wing offset. |
//| A desk's "market strangle" quote would need an extra calibration |
//| step; we keep the simplified reading, which is exact for the |
//| pillars themselves and is what most reconstruction tools use. |
//+------------------------------------------------------------------+
class CGKSmile
{
private:
SmileTenor m_tenors[];
ENUM_GK_DELTA m_dconv;
ENUM_GK_ATM m_aconv;
void BuildTenor(SmileTenor &t);
void AddPoint(SmileTenor &t, const string label, const double strike,
const double vol, const double delta);
public:
CGKSmile(void) : m_dconv(GK_DELTA_SPOT), m_aconv(GK_ATM_FORWARD) {}
bool Build(SmileTenor &raw[], const ENUM_GK_DELTA dconv, const ENUM_GK_ATM aconv);
//--- tenor-level accessors
int NTenors(void) const { return(ArraySize(m_tenors)); }
string Label(const int i) const { return(m_tenors[i].label); }
double Days(const int i) const { return(m_tenors[i].days); }
double T(const int i) const { return(m_tenors[i].T); }
double Spot(const int i) const { return(m_tenors[i].spot); }
double Rd(const int i) const { return(m_tenors[i].rd); }
double Rf(const int i) const { return(m_tenors[i].rf); }
double Forward(const int i) const { return(m_tenors[i].F); }
double Atm(const int i) const { return(m_tenors[i].atm); }
double RR25(const int i) const { return(m_tenors[i].rr25); }
double BF25(const int i) const { return(m_tenors[i].bf25); }
//--- point-level accessors
int NPoints(const int i) const { return(ArraySize(m_tenors[i].pts)); }
string PtLabel(const int i, const int j) const { return(m_tenors[i].pts[j].label); }
double PtStrike(const int i, const int j) const { return(m_tenors[i].pts[j].strike); }
double PtVol(const int i, const int j) const { return(m_tenors[i].pts[j].vol); }
double PtDelta(const int i, const int j) const { return(m_tenors[i].pts[j].delta); }
};
//+------------------------------------------------------------------+
//| Append one reconstructed pillar to a tenor, recording the strike |
//| the solver returned, its vol, and the delta actually realised |
//| there (so a caller can confirm a 25-delta pillar really sits at |
//| 0.25). Skips a pillar whose strike solve failed. |
//+------------------------------------------------------------------+
void CGKSmile::AddPoint(SmileTenor &t, const string label, const double strike,
const double vol, const double delta)
{
if(strike <= 0.0)
return;
int n = ArraySize(t.pts);
ArrayResize(t.pts, n + 1);
t.pts[n].label = label;
t.pts[n].strike = strike;
t.pts[n].vol = vol;
t.pts[n].delta = delta;
}
//+------------------------------------------------------------------+
//| Reconstruct one tenor's smile. Derive the pillar vols from the |
//| ATM/RR/BF quotes, solve each pillar delta back to a strike using |
//| that pillar's own vol, and record the realised delta. The put |
//| pillars are solved to negative deltas, the calls to positive; a |
//| correct convention returns strikes that stay in ascending order |
//| 10P < 25P < ATM < 25C < 10C. |
//+------------------------------------------------------------------+
void CGKSmile::BuildTenor(SmileTenor &t)
{
t.T = t.days / 365.0;
t.F = GKForward(t.spot, t.rd, t.rf, t.T);
ArrayResize(t.pts, 0);
//--- pillar vols from the delta-space quotes (simplified strangle)
double s25c = t.atm + t.bf25 + 0.5 * t.rr25;
double s25p = t.atm + t.bf25 - 0.5 * t.rr25;
//--- pillar strikes (put pillars first so points stay ascending)
if(t.has10)
{
double s10c = t.atm + t.bf10 + 0.5 * t.rr10;
double s10p = t.atm + t.bf10 - 0.5 * t.rr10;
double k10p = GKStrikeFromDelta(OPT_PUT, m_dconv, 0.10, t.spot, t.rd, t.rf, s10p, t.T);
AddPoint(t, "10P", k10p, s10p, GKDelta(OPT_PUT, m_dconv, t.spot, k10p, t.rd, t.rf, s10p, t.T));
}
double k25p = GKStrikeFromDelta(OPT_PUT, m_dconv, 0.25, t.spot, t.rd, t.rf, s25p, t.T);
AddPoint(t, "25P", k25p, s25p, GKDelta(OPT_PUT, m_dconv, t.spot, k25p, t.rd, t.rf, s25p, t.T));
double kAtm = GKATMStrike(m_aconv, m_dconv, t.F, t.atm, t.T);
AddPoint(t, "ATM", kAtm, t.atm, GKDelta(OPT_CALL, m_dconv, t.spot, kAtm, t.rd, t.rf, t.atm, t.T));
double k25c = GKStrikeFromDelta(OPT_CALL, m_dconv, 0.25, t.spot, t.rd, t.rf, s25c, t.T);
AddPoint(t, "25C", k25c, s25c, GKDelta(OPT_CALL, m_dconv, t.spot, k25c, t.rd, t.rf, s25c, t.T));
if(t.has10)
{
double s10c = t.atm + t.bf10 + 0.5 * t.rr10;
double k10c = GKStrikeFromDelta(OPT_CALL, m_dconv, 0.10, t.spot, t.rd, t.rf, s10c, t.T);
AddPoint(t, "10C", k10c, s10c, GKDelta(OPT_CALL, m_dconv, t.spot, k10c, t.rd, t.rf, s10c, t.T));
}
}
//+------------------------------------------------------------------+
//| Reconstruct every tenor under the given conventions and take |
//| ownership of the result. Returns false only if there is nothing |
//| to build. |
//+------------------------------------------------------------------+
bool CGKSmile::Build(SmileTenor &raw[], const ENUM_GK_DELTA dconv, const ENUM_GK_ATM aconv)
{
int n = ArraySize(raw);
if(n == 0)
return(false);
m_dconv = dconv;
m_aconv = aconv;
ArrayResize(m_tenors, n);
for(int i = 0; i < n; i++)
{
m_tenors[i] = raw[i];
BuildTenor(m_tenors[i]);
}
return(true);
}
//+------------------------------------------------------------------+
//| CSV provider. Reads a delta-space vol sheet from MQL5\Files with |
//| one row per tenor and the fixed column order: |
//| tenor,days,spot,atm,rr25,bf25,rr10,bf10,r_dom,r_for |
//| Vols are in percent (8.0 = 8%), rates in decimals. Set rr10 and |
//| bf10 to a blank/zero when the 10-delta wings are not quoted; the |
//| row then carries three pillars instead of five. |
//+------------------------------------------------------------------+
class CGKProviderCSV
{
public:
bool Load(const string filename, SmileTenor &out[]);
};
//+------------------------------------------------------------------+
//| Parse the vol sheet into raw tenors. Skips the header line and |
//| any blank line, converts the percent vols to decimals, and |
//| flags whether the 10-delta wings were supplied. |
//+------------------------------------------------------------------+
bool CGKProviderCSV::Load(const string filename, SmileTenor &out[])
{
int h = FileOpen(filename, FILE_READ | FILE_CSV | FILE_ANSI, ',');
if(h == INVALID_HANDLE)
{
PrintFormat("CGKProviderCSV: cannot open %s (err %d)", filename, GetLastError());
return(false);
}
ArrayResize(out, 0);
bool header = true;
while(!FileIsEnding(h))
{
string sTenor = FileReadString(h);
if(FileIsLineEnding(h) && StringLen(sTenor) == 0)
continue;
string sDays = FileReadString(h);
string sSpot = FileReadString(h);
string sAtm = FileReadString(h);
string sRr25 = FileReadString(h);
string sBf25 = FileReadString(h);
string sRr10 = FileReadString(h);
string sBf10 = FileReadString(h);
string sRd = FileReadString(h);
string sRf = FileReadString(h);
if(header)
{
header = false; // skip the column titles
continue;
}
if(StringLen(sDays) == 0)
continue;
int n = ArraySize(out);
ArrayResize(out, n + 1);
out[n].label = sTenor;
out[n].days = StringToDouble(sDays);
out[n].spot = StringToDouble(sSpot);
out[n].atm = StringToDouble(sAtm) / 100.0;
out[n].rr25 = StringToDouble(sRr25) / 100.0;
out[n].bf25 = StringToDouble(sBf25) / 100.0;
out[n].rr10 = StringToDouble(sRr10) / 100.0;
out[n].bf10 = StringToDouble(sBf10) / 100.0;
out[n].rd = StringToDouble(sRd);
out[n].rf = StringToDouble(sRf);
out[n].has10 = (StringLen(sBf10) > 0 && MathAbs(out[n].bf10) + MathAbs(out[n].rr10) > 0.0);
}
FileClose(h);
PrintFormat("CGKProviderCSV: loaded %d tenors from %s", ArraySize(out), filename);
return(ArraySize(out) > 0);
}
#endif // GK_GKDATA_MQH
//+------------------------------------------------------------------+