//+------------------------------------------------------------------+ //| 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 #include #include //+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+