629 lines
26 KiB
MQL5
629 lines
26 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| GKSmile.mq5 |
|
|
//| 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 description "Garman-Kohlhagen FX smile: reconstruct the strike-space volatility smile from delta-space ATM/RR/BF quotes, with the strike ladder printed on the curve."
|
|
#property indicator_chart_window
|
|
#property indicator_plots 0
|
|
#property indicator_buffers 0
|
|
|
|
#include <Canvas\Canvas.mqh>
|
|
#include <GK\GKData.mqh>
|
|
#include <GK\GKProviderNative.mqh>
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Inputs |
|
|
//+------------------------------------------------------------------+
|
|
enum ENUM_GK_SOURCE
|
|
{
|
|
GK_SOURCE_CSV = 0, // read a delta-space vol sheet from MQL5\Files
|
|
GK_SOURCE_NATIVE = 1 // read the broker's native FX-option symbols
|
|
};
|
|
|
|
enum ENUM_GK_XAXIS
|
|
{
|
|
GK_XAXIS_STRIKE = 0, // strike on the x axis (natural)
|
|
GK_XAXIS_DELTA = 1 // delta pillars evenly spaced (FX-native)
|
|
};
|
|
|
|
enum ENUM_GK_THEME
|
|
{
|
|
GK_THEME_DARK = 0, // light curve on a dark card
|
|
GK_THEME_LIGHT = 1 // dark curve on a light card
|
|
};
|
|
|
|
input ENUM_GK_SOURCE InpSource = GK_SOURCE_CSV; // data source
|
|
input string InpCsvFile = "GK\\EURUSD.csv"; // CSV vol sheet (in MQL5\Files)
|
|
input string InpUnderlying = ""; // native: base symbol (empty = chart symbol)
|
|
input ENUM_GK_DELTA InpDeltaConv = GK_DELTA_SPOT; // delta convention (per pair!)
|
|
input ENUM_GK_ATM InpAtmConv = GK_ATM_DNS; // ATM convention
|
|
input double InpRateDom = 0.0430; // native: domestic rate
|
|
input double InpRateFor = 0.0250; // native: foreign rate
|
|
input ENUM_GK_XAXIS InpXAxis = GK_XAXIS_DELTA; // x axis: delta pillars or strike
|
|
input int InpTenor = 1; // active tenor index (0-based)
|
|
input bool InpShowAll = true; // draw the other tenors faintly
|
|
input int InpRefreshSec = 0; // native re-poll seconds (0 = manual, press R)
|
|
input ENUM_GK_THEME InpTheme = GK_THEME_DARK; // card / curve theme
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Globals |
|
|
//+------------------------------------------------------------------+
|
|
CCanvas g_canvas;
|
|
CGKSmile g_smile;
|
|
string g_objName = "";
|
|
datetime g_lastRefresh = 0;
|
|
bool g_haveData = false;
|
|
int g_lastW = -1, g_lastH = -1;
|
|
int g_active = 0; // active tenor index
|
|
ENUM_GK_XAXIS g_xaxis = GK_XAXIS_DELTA; // live axis mode, seeded from the input and toggled by X
|
|
double g_fs = 1.0; // font / spacing scale, from the canvas height
|
|
|
|
//--- plot geometry, recomputed each draw
|
|
int g_plotX0, g_plotX1, g_plotY0, g_plotY1;
|
|
double g_xMin, g_xMax, g_yMin, g_yMax;
|
|
|
|
//--- resolved palette (set in OnInit from the theme)
|
|
uint g_panel, g_card2, g_grid, g_text, g_textDim, g_line, g_fill, g_put, g_call, g_atm, g_faint;
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Resolve the theme into an opaque ARGB palette once. The bitmap is|
|
|
//| drawn in RAW mode, so the card is fully opaque and the area tone |
|
|
//| is pre-mixed against it rather than leaning on alpha blending. |
|
|
//| Put and call wings get their own accent so the skew reads at a |
|
|
//| glance: a rich put wing is the market pricing downside. |
|
|
//+------------------------------------------------------------------+
|
|
void ResolvePalette(void)
|
|
{
|
|
g_line = ColorToARGB(C'46,196,214', 255); // smile curve (cyan)
|
|
g_put = ColorToARGB(C'232,120,120', 255); // put-wing accent
|
|
g_call = ColorToARGB(C'120,200,140', 255); // call-wing accent
|
|
g_atm = ColorToARGB(C'220,200,90', 255); // ATM marker (amber)
|
|
if(InpTheme == GK_THEME_LIGHT)
|
|
{
|
|
g_panel = ColorToARGB(clrWhiteSmoke, 255);
|
|
g_card2 = ColorToARGB(C'235,237,242', 255);
|
|
g_grid = ColorToARGB(clrSilver);
|
|
g_text = ColorToARGB(clrBlack);
|
|
g_textDim = ColorToARGB(clrDimGray);
|
|
g_fill = ColorToARGB(C'170,215,222', 255);
|
|
g_faint = ColorToARGB(C'190,192,198', 255);
|
|
}
|
|
else
|
|
{
|
|
g_panel = ColorToARGB(C'22,24,30', 255);
|
|
g_card2 = ColorToARGB(C'28,31,39', 255);
|
|
g_grid = ColorToARGB(C'70,74,86');
|
|
g_text = ColorToARGB(clrWhiteSmoke);
|
|
g_textDim = ColorToARGB(clrSilver);
|
|
g_fill = ColorToARGB(C'28,60,70', 255);
|
|
g_faint = ColorToARGB(C'60,64,76', 255);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| (Re)load the chosen data source and reconstruct the smile under |
|
|
//| the selected delta and ATM conventions. Clamps the active tenor |
|
|
//| to the range the data actually carries. |
|
|
//+------------------------------------------------------------------+
|
|
bool Reload(void)
|
|
{
|
|
SmileTenor raw[];
|
|
bool ok = false;
|
|
if(InpSource == GK_SOURCE_CSV)
|
|
{
|
|
CGKProviderCSV csv;
|
|
ok = csv.Load(InpCsvFile, raw);
|
|
}
|
|
else
|
|
{
|
|
string under = (InpUnderlying == "") ? _Symbol : InpUnderlying;
|
|
CGKProviderNative nat;
|
|
ok = nat.Load(under, InpRateDom, InpRateFor, InpDeltaConv, raw);
|
|
}
|
|
if(ok)
|
|
ok = g_smile.Build(raw, InpDeltaConv, InpAtmConv);
|
|
g_haveData = ok;
|
|
if(g_haveData)
|
|
{
|
|
g_active = InpTenor;
|
|
if(g_active < 0)
|
|
g_active = 0;
|
|
if(g_active >= g_smile.NTenors())
|
|
g_active = g_smile.NTenors() - 1;
|
|
}
|
|
return(g_haveData);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Font sizes and row spacings are authored against a 580-pixel-tall|
|
|
//| card and scaled from there. Pixel-constant fonts are the classic |
|
|
//| canvas mistake: they look right on the chart you developed on |
|
|
//| and shrink into illegibility on a large one, since the card |
|
|
//| grows with the chart while the type does not. The clamp keeps |
|
|
//| both extremes sane. |
|
|
//+------------------------------------------------------------------+
|
|
int SC(const int base)
|
|
{
|
|
return((int)MathRound(base * g_fs));
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Right (call/put) implied by a pillar label. The put pillars |
|
|
//| carry "P"; ATM is treated as a call for its ~0.5 delta. |
|
|
//+------------------------------------------------------------------+
|
|
ENUM_OPT_RIGHT PillarRight(const string label)
|
|
{
|
|
return((StringFind(label, "P") >= 0) ? OPT_PUT : OPT_CALL);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Plot x-coordinate of a pillar. In delta mode the pillars are |
|
|
//| spaced evenly by index (the classic FX smile look); in strike |
|
|
//| mode they sit at their reconstructed strike. |
|
|
//+------------------------------------------------------------------+
|
|
int PxOf(const int tenor, const int j)
|
|
{
|
|
int np = g_smile.NPoints(tenor);
|
|
double f;
|
|
if(g_xaxis == GK_XAXIS_DELTA)
|
|
f = (np <= 1) ? 0.5 : (double)j / (np - 1);
|
|
else
|
|
f = (g_smile.PtStrike(tenor, j) - g_xMin) / (g_xMax - g_xMin);
|
|
return(g_plotX0 + (int)MathRound(f * (g_plotX1 - g_plotX0)));
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Map a volatility to a y pixel inside the plot area. |
|
|
//+------------------------------------------------------------------+
|
|
int YOf(const double vol)
|
|
{
|
|
double f = (vol - g_yMin) / (g_yMax - g_yMin);
|
|
if(f < 0.0)
|
|
f = 0.0;
|
|
if(f > 1.0)
|
|
f = 1.0;
|
|
return(g_plotY1 - (int)MathRound(f * (g_plotY1 - g_plotY0)));
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| The reconstruction result, printed at the pillar it belongs to: |
|
|
//| the strike we solved for, its vol and its delta. Reading a |
|
|
//| strike off a curve whose x axis is delta is impossible, so these |
|
|
//| three numbers are what turns the picture into a quotable ladder. |
|
|
//| The block sits above the marker unless that would leave the plot,|
|
|
//| in which case it flips underneath. In delta mode the x axis |
|
|
//| already names the pillar, so the block carries numbers only and |
|
|
//| the accent colour does the naming; in strike mode it labels |
|
|
//| itself, because there the axis is spelling out strikes instead. |
|
|
//+------------------------------------------------------------------+
|
|
void DrawPillarBox(const int tenor, const int j, const int px, const int py, const uint accent)
|
|
{
|
|
bool needLabel = (g_xaxis == GK_XAXIS_STRIKE);
|
|
int nLines = needLabel ? 3 : 2;
|
|
string kTx = StringFormat("%.4f", g_smile.PtStrike(tenor, j));
|
|
string vTx = StringFormat("%.2f%% %+.3f", 100.0 * g_smile.PtVol(tenor, j), g_smile.PtDelta(tenor, j));
|
|
|
|
//--- The outer pillars sit on the frame itself, so a block centred on the
|
|
//--- marker spills over the axis labels beside it. Measure the widest line
|
|
//--- and pull the block back inside the plot; the marker still anchors it.
|
|
g_canvas.FontSet("Consolas", SC(11));
|
|
int wide = g_canvas.TextWidth(vTx);
|
|
g_canvas.FontSet("Consolas", SC(13), FW_BOLD);
|
|
wide = MathMax(wide, g_canvas.TextWidth(kTx));
|
|
int half = wide / 2 + SC(4);
|
|
int cx = px;
|
|
if(cx - half < g_plotX0)
|
|
cx = g_plotX0 + half;
|
|
if(cx + half > g_plotX1)
|
|
cx = g_plotX1 - half;
|
|
|
|
bool above = (py - SC(14 + 18 * nLines) >= g_plotY0);
|
|
int y = above ? py - SC(12) : py + SC(12);
|
|
int step = above ? -SC(18) : SC(18);
|
|
uint va = above ? TA_BOTTOM : TA_TOP;
|
|
|
|
if(needLabel)
|
|
{
|
|
g_canvas.FontSet("Segoe UI", SC(12), FW_BOLD);
|
|
g_canvas.TextOut(cx, y, g_smile.PtLabel(tenor, j), accent, TA_CENTER | va);
|
|
y += step;
|
|
}
|
|
g_canvas.FontSet("Consolas", SC(13), FW_BOLD);
|
|
g_canvas.TextOut(cx, y, kTx, accent, TA_CENTER | va);
|
|
g_canvas.FontSet("Consolas", SC(11));
|
|
g_canvas.TextOut(cx, y + step, vTx, g_textDim, TA_CENTER | va);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Draw one tenor's smile as a polyline through its pillars. The |
|
|
//| active tenor is drawn bright with filled wings and labelled dot |
|
|
//| markers; the others (when shown) are faint context lines. |
|
|
//+------------------------------------------------------------------+
|
|
void DrawSmile(const int tenor, const bool activeTenor)
|
|
{
|
|
int np = g_smile.NPoints(tenor);
|
|
if(np < 2)
|
|
return;
|
|
int xs[], ys[];
|
|
ArrayResize(xs, np);
|
|
ArrayResize(ys, np);
|
|
for(int j = 0; j < np; j++)
|
|
{
|
|
xs[j] = PxOf(tenor, j);
|
|
ys[j] = YOf(g_smile.PtVol(tenor, j));
|
|
}
|
|
|
|
//--- the y axis is scaled to the active tenor alone, so a context line
|
|
//--- can run off the box.
|
|
if(!activeTenor)
|
|
{
|
|
for(int j = 0; j < np - 1; j++)
|
|
{
|
|
double v0 = g_smile.PtVol(tenor, j);
|
|
double v1 = g_smile.PtVol(tenor, j + 1);
|
|
if(v0 < g_yMin || v0 > g_yMax || v1 < g_yMin || v1 > g_yMax)
|
|
continue;
|
|
g_canvas.LineAA(xs[j], ys[j], xs[j + 1], ys[j + 1], g_faint);
|
|
}
|
|
return;
|
|
}
|
|
|
|
//--- filled area under the active smile, column by column
|
|
for(int j = 0; j < np - 1; j++)
|
|
{
|
|
for(int x = xs[j]; x <= xs[j + 1]; x++)
|
|
{
|
|
double w = (xs[j + 1] == xs[j]) ? 0.0 : (double)(x - xs[j]) / (xs[j + 1] - xs[j]);
|
|
int y = (int)MathRound(ys[j] + w * (ys[j + 1] - ys[j]));
|
|
if(y < g_plotY1)
|
|
g_canvas.LineVertical(x, y, g_plotY1, g_fill);
|
|
}
|
|
}
|
|
g_canvas.PolylineAA(xs, ys, g_line);
|
|
|
|
//--- pillar markers, each carrying its own reconstructed numbers
|
|
for(int j = 0; j < np; j++)
|
|
{
|
|
string lab = g_smile.PtLabel(tenor, j);
|
|
uint c = (lab == "ATM") ? g_atm : (PillarRight(lab) == OPT_PUT ? g_put : g_call);
|
|
g_canvas.FillCircle(xs[j], ys[j], SC(4), c);
|
|
DrawPillarBox(tenor, j, xs[j], ys[j], c);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Plot axes. The y axis carries a few volatility gridlines; the x |
|
|
//| axis shows either the pillar labels (delta mode) or evenly |
|
|
//| spaced strikes (strike mode). |
|
|
//+------------------------------------------------------------------+
|
|
void DrawAxis(void)
|
|
{
|
|
g_canvas.LineHorizontal(g_plotX0, g_plotX1, g_plotY1, g_grid);
|
|
g_canvas.LineVertical(g_plotX0, g_plotY0, g_plotY1, g_grid);
|
|
g_canvas.FontSet("Segoe UI", SC(11));
|
|
|
|
//--- y: volatility ticks. Two decimals, because the axis now hugs the
|
|
//--- active smile and one decimal would print repeated labels.
|
|
int yt = 4;
|
|
for(int i = 0; i <= yt; i++)
|
|
{
|
|
double v = g_yMin + (g_yMax - g_yMin) * i / yt;
|
|
int y = YOf(v);
|
|
g_canvas.LineHorizontal(g_plotX0, g_plotX1, y, g_grid);
|
|
g_canvas.TextOut(g_plotX0 - SC(6), y, StringFormat("%.2f", 100.0 * v), g_textDim, TA_RIGHT | TA_VCENTER);
|
|
}
|
|
|
|
//--- x: pillar labels (delta) or strike ticks
|
|
if(g_xaxis == GK_XAXIS_DELTA)
|
|
{
|
|
int np = g_smile.NPoints(g_active);
|
|
g_canvas.FontSet("Segoe UI", SC(12), FW_BOLD);
|
|
for(int j = 0; j < np; j++)
|
|
{
|
|
string lab = g_smile.PtLabel(g_active, j);
|
|
uint c = (lab == "ATM") ? g_atm : (PillarRight(lab) == OPT_PUT ? g_put : g_call);
|
|
g_canvas.TextOut(PxOf(g_active, j), g_plotY1 + SC(6), lab, c, TA_CENTER | TA_TOP);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
int xt = 6;
|
|
for(int i = 0; i <= xt; i++)
|
|
{
|
|
double k = g_xMin + (g_xMax - g_xMin) * i / xt;
|
|
int x = g_plotX0 + (int)MathRound((double)i / xt * (g_plotX1 - g_plotX0));
|
|
g_canvas.TextOut(x, g_plotY1 + SC(6), StringFormat("%.4f", k), g_textDim, TA_CENTER | TA_TOP);
|
|
}
|
|
}
|
|
g_canvas.FontSet("Segoe UI", SC(11));
|
|
g_canvas.TextOut(g_plotX0 - SC(6), g_plotY0 - SC(16), "vol %", g_textDim, TA_LEFT | TA_TOP);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Compact term-structure strip: ATM, 25d RR and 25d BF across all |
|
|
//| tenors, with the active tenor flagged. It is the cross-tenor |
|
|
//| view the single smile cannot show, kept to one thin row. |
|
|
//+------------------------------------------------------------------+
|
|
void DrawTermStrip(const int sx0, const int sy0, const int sx1, const int sy1)
|
|
{
|
|
int n = g_smile.NTenors();
|
|
if(n < 1)
|
|
return;
|
|
g_canvas.FontSet("Segoe UI", SC(11));
|
|
g_canvas.TextOut(sx0, sy0 - SC(16), "term structure (25-delta, vol pts)", g_textDim, TA_LEFT | TA_TOP);
|
|
|
|
//--- name the three rows down the left margin, so the strip reads without
|
|
//--- having to map it back onto the caption
|
|
g_canvas.FontSet("Consolas", SC(11));
|
|
g_canvas.TextOut(sx0, sy0 + SC(18), "ATM", g_textDim, TA_LEFT | TA_TOP);
|
|
g_canvas.TextOut(sx0, sy0 + SC(34), "RR", g_textDim, TA_LEFT | TA_TOP);
|
|
g_canvas.TextOut(sx0, sy0 + SC(50), "BF", g_textDim, TA_LEFT | TA_TOP);
|
|
|
|
for(int i = 0; i < n; i++)
|
|
{
|
|
int x = sx0 + (int)((double)(i + 0.5) / n * (sx1 - sx0));
|
|
bool act = (i == g_active);
|
|
uint c = act ? g_text : g_textDim;
|
|
g_canvas.FontSet("Segoe UI", SC(act ? 12 : 11), act ? FW_BOLD : FW_NORMAL);
|
|
g_canvas.TextOut(x, sy0, g_smile.Label(i), c, TA_CENTER | TA_TOP);
|
|
g_canvas.FontSet("Consolas", SC(11));
|
|
g_canvas.TextOut(x, sy0 + SC(18), StringFormat("%.2f", 100.0 * g_smile.Atm(i)), c, TA_CENTER | TA_TOP);
|
|
g_canvas.TextOut(x, sy0 + SC(34), StringFormat("%+.2f", 100.0 * g_smile.RR25(i)), (g_smile.RR25(i) < 0 ? g_put : g_call), TA_CENTER | TA_TOP);
|
|
g_canvas.TextOut(x, sy0 + SC(50), StringFormat("%.2f", 100.0 * g_smile.BF25(i)), c, TA_CENTER | TA_TOP);
|
|
if(act)
|
|
g_canvas.Rectangle(x - SC(30), sy0 - SC(2), x + SC(30), sy0 + SC(68), g_grid);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Header block: pair, tenor, spot / forward, the two rates and the |
|
|
//| active convention, then the quotes the smile was built from and |
|
|
//| the pair of rate sensitivities. Both rhos live here rather than |
|
|
//| on a per-pillar row because they are quoted at the ATM strike |
|
|
//| only; keeping them side by side is the point, since they carry |
|
|
//| opposite signs and an FX book has to hedge two curves, not one. |
|
|
//+------------------------------------------------------------------+
|
|
void DrawHeader(const int cardX0, const int cardY0)
|
|
{
|
|
string under = (InpSource == GK_SOURCE_NATIVE && InpUnderlying != "") ? InpUnderlying :
|
|
(InpSource == GK_SOURCE_NATIVE ? _Symbol : InpCsvFile);
|
|
string convName[4] = {"spot", "forward", "spot p-adj", "fwd p-adj"};
|
|
double S = g_smile.Spot(g_active);
|
|
double F = g_smile.Forward(g_active);
|
|
double rd = g_smile.Rd(g_active);
|
|
double rf = g_smile.Rf(g_active);
|
|
double T = g_smile.T(g_active);
|
|
int dd = (int)MathRound(g_smile.Days(g_active));
|
|
|
|
//--- rate Greeks at the ATM strike, in desk units (per 1% of rate)
|
|
int np = g_smile.NPoints(g_active);
|
|
double Katm = g_smile.PtStrike(g_active, (np - 1) / 2);
|
|
double vatm = g_smile.Atm(g_active);
|
|
double rhod = GKRhoDom(OPT_CALL, S, Katm, rd, rf, vatm, T) / 100.0;
|
|
double rhof = GKRhoFor(OPT_CALL, S, Katm, rd, rf, vatm, T) / 100.0;
|
|
|
|
int tx = cardX0 + SC(16);
|
|
g_canvas.FontSet("Segoe UI", SC(17), FW_BOLD);
|
|
g_canvas.TextOut(tx, cardY0 + SC(12), StringFormat("Garman-Kohlhagen FX Smile - %s - %s (%d days)",
|
|
under, g_smile.Label(g_active), dd), g_text, TA_LEFT | TA_TOP);
|
|
g_canvas.FontSet("Segoe UI", SC(13));
|
|
g_canvas.TextOut(tx, cardY0 + SC(42), StringFormat("spot %.5f fwd %.5f r_dom %.2f%% r_for %.2f%% delta: %s",
|
|
S, F, 100.0 * rd, 100.0 * rf, convName[InpDeltaConv]), g_textDim, TA_LEFT | TA_TOP);
|
|
g_canvas.TextOut(tx, cardY0 + SC(64), StringFormat("25d RR %+.2f 25d BF %+.2f (vol pts) ATM rho_dom %+.4f rho_for %+.4f (per 1%%)",
|
|
100.0 * g_smile.RR25(g_active), 100.0 * g_smile.BF25(g_active),
|
|
rhod, rhof), g_textDim, TA_LEFT | TA_TOP);
|
|
g_canvas.FontSet("Segoe UI", SC(11));
|
|
g_canvas.TextOut(tx, cardY0 + SC(88), "keys: R reload , . tenor X axis", g_textDim, TA_LEFT | TA_TOP);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Render the whole tool: an opaque card, the header, the smile |
|
|
//| plot with its axes, the ladder/Greeks panel, and the term strip. |
|
|
//+------------------------------------------------------------------+
|
|
void Draw(void)
|
|
{
|
|
int W = g_canvas.Width();
|
|
int H = g_canvas.Height();
|
|
g_canvas.Erase(0);
|
|
|
|
g_fs = (double)H / 580.0;
|
|
if(g_fs < 1.0)
|
|
g_fs = 1.0;
|
|
if(g_fs > 2.4)
|
|
g_fs = 2.4;
|
|
|
|
int cardX0 = 20, cardY0 = 20, cardX1 = W - 20, cardY1 = H - 20;
|
|
g_canvas.FillRectangle(cardX0, cardY0, cardX1, cardY1, g_panel);
|
|
g_canvas.Rectangle(cardX0, cardY0, cardX1, cardY1, g_grid);
|
|
|
|
if(!g_haveData || g_smile.NTenors() < 1)
|
|
{
|
|
g_canvas.FontSet("Segoe UI", SC(16), FW_BOLD);
|
|
g_canvas.TextOut(cardX0 + SC(20), cardY0 + SC(20), "GK Smile: no data. Check CSV path / source, then press R.", g_text, TA_LEFT | TA_TOP);
|
|
g_canvas.Update(true);
|
|
return;
|
|
}
|
|
|
|
DrawHeader(cardX0, cardY0);
|
|
|
|
//--- the plot owns the full card width; the side margins are only wide
|
|
//--- enough for the outermost pillar's number block to stay on the card
|
|
int bodyY0 = cardY0 + SC(108);
|
|
int stripH = SC(88);
|
|
g_plotX0 = cardX0 + SC(64);
|
|
g_plotX1 = cardX1 - SC(70);
|
|
g_plotY0 = bodyY0 + SC(8);
|
|
g_plotY1 = cardY1 - stripH - SC(44);
|
|
|
|
//--- Axis window from the ACTIVE tenor only.
|
|
g_yMin = 1e9;
|
|
g_yMax = -1e9;
|
|
double kLo = 1e18, kHi = -1e18;
|
|
int npa = g_smile.NPoints(g_active);
|
|
for(int j = 0; j < npa; j++)
|
|
{
|
|
double v = g_smile.PtVol(g_active, j);
|
|
g_yMin = MathMin(g_yMin, v);
|
|
g_yMax = MathMax(g_yMax, v);
|
|
kLo = MathMin(kLo, g_smile.PtStrike(g_active, j));
|
|
kHi = MathMax(kHi, g_smile.PtStrike(g_active, j));
|
|
}
|
|
double pad = 0.18 * (g_yMax - g_yMin) + 1e-6;
|
|
g_yMin -= pad;
|
|
g_yMax += pad;
|
|
double kpad = 0.04 * (kHi - kLo) + 1e-9;
|
|
g_xMin = kLo - kpad;
|
|
g_xMax = kHi + kpad;
|
|
|
|
DrawAxis();
|
|
if(InpShowAll)
|
|
for(int i = 0; i < g_smile.NTenors(); i++)
|
|
if(i != g_active)
|
|
DrawSmile(i, false);
|
|
DrawSmile(g_active, true);
|
|
|
|
//--- term strip, on the same full width as the plot above it
|
|
DrawTermStrip(g_plotX0, cardY1 - stripH + 8, g_plotX1, cardY1 - 12);
|
|
|
|
g_canvas.Update(true);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Create (or recreate) a full-chart bitmap the size of the chart. |
|
|
//+------------------------------------------------------------------+
|
|
bool BuildCanvas(void)
|
|
{
|
|
int w = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0);
|
|
int h = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, 0);
|
|
if(w <= 0)
|
|
w = 1280;
|
|
if(h <= 0)
|
|
h = 720;
|
|
|
|
g_canvas.Destroy();
|
|
if(g_objName != "")
|
|
ObjectDelete(0, g_objName);
|
|
|
|
g_objName = "GKSmileCanvas_" + IntegerToString(ChartID());
|
|
if(!g_canvas.CreateBitmapLabel(0, 0, g_objName, 0, 0, w, h, COLOR_FORMAT_ARGB_RAW))
|
|
{
|
|
PrintFormat("GKSmile: CreateBitmapLabel failed (err %d)", GetLastError());
|
|
return(false);
|
|
}
|
|
ObjectSetInteger(0, g_objName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
|
|
ObjectSetInteger(0, g_objName, OBJPROP_XDISTANCE, 0);
|
|
ObjectSetInteger(0, g_objName, OBJPROP_YDISTANCE, 0);
|
|
ObjectSetInteger(0, g_objName, OBJPROP_BACK, false);
|
|
g_lastW = w;
|
|
g_lastH = h;
|
|
return(true);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Rebuild the bitmap only when the chart pixel size changed. |
|
|
//+------------------------------------------------------------------+
|
|
bool RebuildIfResized(void)
|
|
{
|
|
int w = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0);
|
|
int h = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, 0);
|
|
if(w <= 0 || h <= 0)
|
|
return(false);
|
|
if(w == g_lastW && h == g_lastH)
|
|
return(false);
|
|
return(BuildCanvas());
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Initialization |
|
|
//+------------------------------------------------------------------+
|
|
int OnInit(void)
|
|
{
|
|
IndicatorSetString(INDICATOR_SHORTNAME, "GK Smile");
|
|
ResolvePalette();
|
|
g_xaxis = InpXAxis;
|
|
if(!BuildCanvas())
|
|
return(INIT_FAILED);
|
|
Reload();
|
|
Draw();
|
|
EventSetTimer(1);
|
|
g_lastRefresh = TimeCurrent();
|
|
return(INIT_SUCCEEDED);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Timer: re-poll the native source on its interval and redraw. |
|
|
//+------------------------------------------------------------------+
|
|
void OnTimer(void)
|
|
{
|
|
if(InpSource == GK_SOURCE_NATIVE && InpRefreshSec > 0 &&
|
|
TimeCurrent() - g_lastRefresh >= InpRefreshSec)
|
|
{
|
|
Reload();
|
|
g_lastRefresh = TimeCurrent();
|
|
Draw();
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Indicator calculation: this tool draws from the vol quotes, not |
|
|
//| the price series, so it does no per-bar work. |
|
|
//+------------------------------------------------------------------+
|
|
int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
|
|
{
|
|
return(rates_total);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Chart events: R reloads, comma/period step the active tenor, X |
|
|
//| toggles between the delta and strike x axis, and a size change |
|
|
//| rebuilds the bitmap. None of these touch the reconstruction, so |
|
|
//| they only redraw; the smile itself is static until R. |
|
|
//+------------------------------------------------------------------+
|
|
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
|
|
{
|
|
if(id == CHARTEVENT_KEYDOWN)
|
|
{
|
|
int key = (int)lparam;
|
|
if(key == 'R')
|
|
{
|
|
Reload();
|
|
Draw();
|
|
}
|
|
else if((key == 188 || key == 190) && g_haveData) // ',' and '.'
|
|
{
|
|
g_active += (key == 190) ? 1 : -1;
|
|
if(g_active < 0)
|
|
g_active = g_smile.NTenors() - 1;
|
|
if(g_active >= g_smile.NTenors())
|
|
g_active = 0;
|
|
Draw();
|
|
}
|
|
else if(key == 'X' && g_haveData)
|
|
{
|
|
g_xaxis = (g_xaxis == GK_XAXIS_DELTA) ? GK_XAXIS_STRIKE : GK_XAXIS_DELTA;
|
|
Draw();
|
|
}
|
|
}
|
|
if(id == CHARTEVENT_CHART_CHANGE)
|
|
{
|
|
if(RebuildIfResized())
|
|
Draw();
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Cleanup |
|
|
//+------------------------------------------------------------------+
|
|
void OnDeinit(const int reason)
|
|
{
|
|
EventKillTimer();
|
|
g_canvas.Destroy();
|
|
if(g_objName != "")
|
|
ObjectDelete(0, g_objName);
|
|
ChartRedraw(0);
|
|
}
|
|
//+------------------------------------------------------------------+
|