//+------------------------------------------------------------------+ //| IVSurface3D.mq5 | //| MMQ — Muhammad Minhas Qamar | //| www.mql5.com/en/articles/23385 | //+------------------------------------------------------------------+ #property copyright "MMQ — Muhammad Minhas Qamar" #property link "https://www.mql5.com/en/articles/23385" #property version "1.00" #property indicator_chart_window #property indicator_plots 0 #property indicator_buffers 0 #include #include #include #include #include enum ENUM_IV_SOURCE { IV_SOURCE_CSV = 0, // read a chain CSV from MQL5\Files IV_SOURCE_NATIVE = 1 // enumerate the broker's MT5 option symbols }; input ENUM_IV_SOURCE InpSource = IV_SOURCE_CSV; input string InpCsvFile = "IVSurface\iv_chain_sample.csv"; input string InpUnderlying = ""; // native: base symbol, empty = current chart symbol input double InpRiskFree = 0.045; // native: risk-free rate for IV inversion input int InpRefreshSec = 0; // native: auto-refresh seconds (0 = manual R only) input bool InpLightTheme = true; // light background (vs dark) #define FRAME_MS 16 //--- geometry scale of the surface in world units #define SURF_W 3.2f // strike axis span #define SURF_D 3.2f // time axis span #define SURF_H 1.6f // vol height span //--- theme colours, resolved from InpLightTheme in ResolveTheme() uint g_bgArgb; // 3D clear colour color g_textColor; // overlay text color g_panelColor; // overlay panel fill color g_axisColor; // axis boxes / labels //--- the 3D canvas, the surface mesh, the data, the camera CCanvas3D g_canvas; CDXMesh *g_mesh = NULL; // the solid shaded surface CDXMesh *g_wire = NULL; // a line-list mesh overlaying the quote grid CDXBox *g_rodX = NULL; // coloured spine along the strike edge CDXBox *g_rodY = NULL; // coloured spine along the implied-vol edge CDXBox *g_rodZ = NULL; // coloured spine along the time edge CIVSurface g_surf; string g_objName = "IVSurface3D_canvas"; //--- wireframe vertices (share the surface geometry) + edge index list DXVertex g_wireVerts[]; uint g_wireIdx[]; DXVertex g_verts[]; uint g_idx[]; double g_camYaw = 0.7; double g_camPitch = 0.5; double g_camDist = 6.5; bool g_dragging = false; int g_dragX = 0, g_dragY = 0; datetime g_lastRefresh = 0; //+------------------------------------------------------------------+ //| MQL5 colour -> DXColor (normalized RGBA). | //+------------------------------------------------------------------+ DXColor ToDXColor(const color clr, const float a = 1.0f) { DXColor c; c.r = ((clr) & 0xFF) / 255.0f; c.g = ((clr >> 8) & 0xFF) / 255.0f; c.b = ((clr >> 16) & 0xFF) / 255.0f; c.a = a; return(c); } //+------------------------------------------------------------------+ //| Map a normalized value t in [0,1] to a blue->green->red ramp, | //| the classic "cool low vol, hot high vol" heat scale. | //+------------------------------------------------------------------+ DXColor HeatColor(double t) { if(t < 0.0) t = 0.0; if(t > 1.0) t = 1.0; double r, g, b; if(t < 0.5) { double u = t / 0.5; r = 0.1; g = 0.2 + 0.7 * u; b = 0.9 - 0.5 * u; } else { double u = (t - 0.5) / 0.5; r = 0.1 + 0.9 * u; g = 0.9 - 0.7 * u; b = 0.4 - 0.4 * u; } DXColor c; c.r = (float)r; c.g = (float)g; c.b = (float)b; c.a = 1.0f; return(c); } //+------------------------------------------------------------------+ //| Resolve the theme palette from the light/dark input. Kept in one | //| place so the 3D clear colour and the 2D overlay stay consistent. | //+------------------------------------------------------------------+ void ResolveTheme(void) { if(InpLightTheme) { g_bgArgb = ColorToARGB((color)0xEDEFF2); g_textColor = (color)0x2A2E33; g_panelColor = (color)0xFFFFFF; g_axisColor = (color)0x606060; } else { g_bgArgb = ColorToARGB((color)0x0E1116); g_textColor = (color)0xD8DEE6; g_panelColor = (color)0x1A1E24; g_axisColor = (color)0x9098A0; } } //+------------------------------------------------------------------+ //| Map a data point (strike, timeYears, iv) to its world position, | //| using the same normalization the surface mesh uses so labels and | //| gridlines line up exactly with the surface. | //+------------------------------------------------------------------+ DXVector3 DataToWorld(const double strike, const double t, const double iv) { int nx = g_surf.NStrikes(), ny = g_surf.NTimes(); double kMin = g_surf.Strike(0), kMax = g_surf.Strike(nx - 1); double tMin = g_surf.TimeYears(0), tMax = g_surf.TimeYears(ny - 1); double vMin = g_surf.IVMin(), vMax = g_surf.IVMax(); double x = ((strike - kMin) / MathMax(kMax - kMin, 1e-9) - 0.5) * SURF_W; double z = ((t - tMin) / MathMax(tMax - tMin, 1e-9) - 0.5) * SURF_D; double y = ((iv - vMin) / MathMax(vMax - vMin, 1e-9) - 0.5) * SURF_H; return(DXVector3((float)x, (float)y, (float)z)); } //+------------------------------------------------------------------+ //| Project a world position to 2D screen pixels using the canvas's | //| current view and projection matrices. Returns false when the | //| point is behind the camera. This is the workhorse that lets us | //| draw axis tick numbers and titles that track the surface as the | //| camera orbits (CCanvas3D has no native 3D text). | //+------------------------------------------------------------------+ bool WorldToScreen(const DXVector3 &world, int &sx, int &sy) { DXMatrix view, proj, vp; g_canvas.ViewMatrixGet(view); g_canvas.ProjectionMatrixGet(proj); DXMatrixMultiply(vp, view, proj); DXVector4 p = DXVector4(world.x, world.y, world.z, 1.0f), clip; DXVec4Transform(clip, p, vp); if(clip.w <= 1e-5) return(false); double ndcX = clip.x / clip.w; double ndcY = clip.y / clip.w; sx = (int)((ndcX * 0.5 + 0.5) * g_canvas.Width()); sy = (int)((1.0 - (ndcY * 0.5 + 0.5)) * g_canvas.Height()); return(true); } //+------------------------------------------------------------------+ //| Build a line-list wireframe over the surface. Every strike | //| gridline and every expiry gridline becomes a pair of vertices, | //| so DirectX draws the quote lattice as thin lines on top of the | //| solid surface (the classic vol-surface look). | //+------------------------------------------------------------------+ void RebuildWireData(void) { int nx = g_surf.NStrikes(), ny = g_surf.NTimes(); int count = nx * ny; ArrayResize(g_wireVerts, count); //--- lift the wire a hair above the surface so it does not z-fight DXColor line = ToDXColor(InpLightTheme ? (color)0x303438 : (color)0xC8CED6); for(int i = 0; i < count; i++) { g_wireVerts[i] = g_verts[i]; g_wireVerts[i].position.y += 0.004f; g_wireVerts[i].vcolor = line; } //--- horizontal edges (along strike) + vertical edges (along expiry) int nEdges = ny * (nx - 1) + nx * (ny - 1); ArrayResize(g_wireIdx, nEdges * 2); int c = 0; for(int j = 0; j < ny; j++) for(int k = 0; k < nx - 1; k++) { g_wireIdx[c++] = j * nx + k; g_wireIdx[c++] = j * nx + k + 1; } for(int k = 0; k < nx; k++) for(int j = 0; j < ny - 1; j++) { g_wireIdx[c++] = j * nx + k; g_wireIdx[c++] = (j + 1) * nx + k; } } //+------------------------------------------------------------------+ //| Create the wireframe mesh from the current grid and register it | //| with the canvas. | //+------------------------------------------------------------------+ bool BuildWireframe(void) { RebuildWireData(); g_wire = new CDXMesh; if(!g_wire.Create(g_canvas.DXDispatcher(), g_canvas.InputScene(), g_wireVerts, g_wireIdx, DX_PRIMITIVE_TOPOLOGY_LINELIST)) return(false); g_canvas.ObjectAdd(g_wire); return(true); } //+------------------------------------------------------------------+ //| Three thin coloured CDXBox rods running along the same three | //| edges the tick labels annotate, so each axis has a colour spine: | //| blue = strike, green = time, red = implied vol. They double as a | //| colour key next to the numeric labels. | //+------------------------------------------------------------------+ bool BuildAxisRods(void) { float t = 0.014f; // rod half-thickness float ox = -SURF_W * 0.5f, oy = -SURF_H * 0.5f, oz = -SURF_D * 0.5f; // the labelled corner CDXDispatcher *d = g_canvas.DXDispatcher(); CDXInput *s = g_canvas.InputScene(); g_rodX = new CDXBox; g_rodY = new CDXBox; g_rodZ = new CDXBox; //--- strike spine: along X at (min time, min vol) edge if(!g_rodX.Create(d, s, DXVector3(ox, oy - t, oz - t), DXVector3(ox + SURF_W, oy + t, oz + t))) return(false); //--- implied-vol spine: up Y at (min strike, min time) edge if(!g_rodY.Create(d, s, DXVector3(ox - t, oy, oz - t), DXVector3(ox + t, oy + SURF_H, oz + t))) return(false); //--- time spine: along Z at (max strike, min vol) edge (matches the labels) if(!g_rodZ.Create(d, s, DXVector3(ox + SURF_W - t, oy - t, oz), DXVector3(ox + SURF_W + t, oy + t, oz + SURF_D))) return(false); g_rodX.DiffuseColorSet(ToDXColor((color)0x1E6FBF)); // blue strike g_rodY.DiffuseColorSet(ToDXColor((color)0xC23A5A)); // red vol g_rodZ.DiffuseColorSet(ToDXColor((color)0x1FA36A)); // green time g_rodX.EmissionColorSet(ToDXColor((color)0x1E6FBF, 0.4f)); // glow a little so they read on any angle g_rodY.EmissionColorSet(ToDXColor((color)0xC23A5A, 0.4f)); g_rodZ.EmissionColorSet(ToDXColor((color)0x1FA36A, 0.4f)); g_canvas.ObjectAdd(g_rodX); g_canvas.ObjectAdd(g_rodY); g_canvas.ObjectAdd(g_rodZ); return(true); } //+------------------------------------------------------------------+ //| Load the chosen data source into g_surf. Returns false if no | //| usable surface could be built. | //+------------------------------------------------------------------+ bool LoadData(void) { OptionQuote quotes[]; bool ok = false; if(InpSource == IV_SOURCE_CSV) { CIVProviderCSV csv; ok = csv.Load(InpCsvFile, quotes); } else { string under = (InpUnderlying == "") ? _Symbol : InpUnderlying; CIVProviderNative nat; ok = nat.Load(under, InpRiskFree, quotes); } if(!ok) { Print("IVSurface3D: data load failed"); return(false); } if(!g_surf.Build(quotes)) { Print("IVSurface3D: surface build failed (need >=2 strikes and >=2 expiries)"); return(false); } PrintFormat("IVSurface3D: %d x %d grid, IV %.1f%%..%.1f%%", g_surf.NStrikes(), g_surf.NTimes(), g_surf.IVMin() * 100, g_surf.IVMax() * 100); return(true); } //+------------------------------------------------------------------+ //| Build the surface mesh vertices from the current grid. x maps to | //| strike, z to time-to-expiry, y (height) and colour to IV, each | //| normalized to the world-unit box so any chain fits the view. | //+------------------------------------------------------------------+ void BuildMeshVertices(void) { int nx = g_surf.NStrikes(), ny = g_surf.NTimes(); int count = nx * ny; if(ArraySize(g_verts) != count) ArrayResize(g_verts, count); double kMin = g_surf.Strike(0), kMax = g_surf.Strike(nx - 1); double tMin = g_surf.TimeYears(0), tMax = g_surf.TimeYears(ny - 1); double vMin = g_surf.IVMin(), vMax = g_surf.IVMax(); double kSpan = MathMax(kMax - kMin, 1e-9); double tSpan = MathMax(tMax - tMin, 1e-9); double vSpan = MathMax(vMax - vMin, 1e-9); for(int j = 0; j < ny; j++) for(int k = 0; k < nx; k++) { int i = j * nx + k; double iv = g_surf.IV(j, k); double vt = (iv - vMin) / vSpan; double x = ((g_surf.Strike(k) - kMin) / kSpan - 0.5) * SURF_W; double z = ((g_surf.TimeYears(j) - tMin) / tSpan - 0.5) * SURF_D; double y = (vt - 0.5) * SURF_H; g_verts[i].position = DXVector4((float)x, (float)y, (float)z, 1.0f); g_verts[i].normal = DXVector4(0, 1, 0, 0); g_verts[i].tcoord = DXVector2((float)k / (nx - 1), (float)j / (ny - 1)); g_verts[i].vcolor = HeatColor(vt); } RecomputeNormals(); } //+------------------------------------------------------------------+ //| Two triangles per grid cell; the index list is static for a | //| given grid size, so we only rebuild it when the grid changes. | //+------------------------------------------------------------------+ void BuildMeshIndices(void) { int nx = g_surf.NStrikes(), ny = g_surf.NTimes(); int cells = (nx - 1) * (ny - 1); ArrayResize(g_idx, cells * 6); int c = 0; for(int j = 0; j < ny - 1; j++) for(int k = 0; k < nx - 1; k++) { int i0 = j * nx + k, i1 = j * nx + k + 1, i2 = (j + 1) * nx + k, i3 = (j + 1) * nx + k + 1; g_idx[c++] = i0; g_idx[c++] = i2; g_idx[c++] = i1; g_idx[c++] = i1; g_idx[c++] = i2; g_idx[c++] = i3; } } //+------------------------------------------------------------------+ //| Smooth per-vertex normals accumulated from adjacent faces, so | //| the lighting follows the ridges and valleys of the surface. | //+------------------------------------------------------------------+ void RecomputeNormals(void) { int count = ArraySize(g_verts); for(int i = 0; i < count; i++) g_verts[i].normal = DXVector4(0, 0, 0, 0); int tris = ArraySize(g_idx) / 3; for(int t = 0; t < tris; t++) { uint ia = g_idx[t * 3 + 0], ib = g_idx[t * 3 + 1], ic = g_idx[t * 3 + 2]; DXVector3 a = DXVector3(g_verts[ia].position.x, g_verts[ia].position.y, g_verts[ia].position.z); DXVector3 b = DXVector3(g_verts[ib].position.x, g_verts[ib].position.y, g_verts[ib].position.z); DXVector3 cc = DXVector3(g_verts[ic].position.x, g_verts[ic].position.y, g_verts[ic].position.z); DXVector3 ab, ac, fn; DXVec3Subtract(ab, b, a); DXVec3Subtract(ac, cc, a); DXVec3Cross(fn, ab, ac); g_verts[ia].normal.x += fn.x; g_verts[ia].normal.y += fn.y; g_verts[ia].normal.z += fn.z; g_verts[ib].normal.x += fn.x; g_verts[ib].normal.y += fn.y; g_verts[ib].normal.z += fn.z; g_verts[ic].normal.x += fn.x; g_verts[ic].normal.y += fn.y; g_verts[ic].normal.z += fn.z; } for(int i = 0; i < count; i++) { DXVector3 n = DXVector3(g_verts[i].normal.x, g_verts[i].normal.y, g_verts[i].normal.z); DXVec3Normalize(n, n); g_verts[i].normal = DXVector4(n.x, n.y, n.z, 0.0f); } } //+------------------------------------------------------------------+ //| Place the camera from yaw/pitch/distance. | //+------------------------------------------------------------------+ void UpdateCamera(void) { double cp = MathCos(g_camPitch), sp = MathSin(g_camPitch); double cy = MathCos(g_camYaw), sy = MathSin(g_camYaw); DXVector3 eye; eye.x = (float)(g_camDist * cp * sy); eye.y = (float)(g_camDist * sp); eye.z = (float)(g_camDist * cp * cy); g_canvas.ViewPositionSet(eye); g_canvas.ViewTargetSet(DXVector3(0.0f, 0.0f, 0.0f)); g_canvas.ViewUpDirectionSet(DXVector3(0.0f, 1.0f, 0.0f)); } //+------------------------------------------------------------------+ //| "Nice number" helper: round a raw step to 1/2/5 x 10^n so tick | //| labels land on human-friendly values. | //+------------------------------------------------------------------+ double NiceStep(const double range, const int target) { double raw = range / MathMax(target, 1); double mag = MathPow(10, MathFloor(MathLog10(raw))); double n = raw / mag; double nice = (n < 1.5) ? 1 : (n < 3) ? 2 : (n < 7) ? 5 : 10; return(nice * mag); } //+------------------------------------------------------------------+ //| Draw numeric tick labels along the three axes and an axis title | //| beside each, by projecting world positions to screen pixels. This| //| gives the surface the labelled coordinate frame that turns it | //| from a floating blob into a readable chart. | //+------------------------------------------------------------------+ void DrawAxes3D(void) { int nx = g_surf.NStrikes(), ny = g_surf.NTimes(); double kMin = g_surf.Strike(0), kMax = g_surf.Strike(nx - 1); double tMin = g_surf.TimeYears(0), tMax = g_surf.TimeYears(ny - 1); double vMin = g_surf.IVMin(), vMax = g_surf.IVMax(); uint axisClr = ColorToARGB(g_axisColor); uint titleClr = ColorToARGB(g_textColor); int sx, sy; double ex = g_camDist * MathCos(g_camPitch) * MathSin(g_camYaw); double ez = g_camDist * MathCos(g_camPitch) * MathCos(g_camYaw); bool strikeVisible = (ez < 0.0); // strike edge sits on the -Z face bool timeVisible = (ex > 0.0); // time edge sits on the +X face bool volVisible = (ex < 0.0 || ez < 0.0); // vertical spine at the (-X,-Z) corner g_canvas.FontSet("Segoe UI", 12, FW_BOLD); if(strikeVisible) { double kStep = NiceStep(kMax - kMin, 6); for(double k = MathCeil(kMin / kStep) * kStep; k <= kMax + 1e-6; k += kStep) { DXVector3 w = DataToWorld(k, tMin, vMin); if(WorldToScreen(w, sx, sy)) g_canvas.TextOut(sx, sy + 6, StringFormat("%.0f", k), axisClr, TA_CENTER); } } if(timeVisible) { double tStep = NiceStep(tMax - tMin, 5); for(double t = MathCeil(tMin / tStep) * tStep; t <= tMax + 1e-6; t += tStep) { DXVector3 w = DataToWorld(kMax, t, vMin); if(WorldToScreen(w, sx, sy)) g_canvas.TextOut(sx + 8, sy, StringFormat("%.2fy", t), axisClr, TA_LEFT); } } if(volVisible) { double vStep = NiceStep(vMax - vMin, 5); for(double v = MathCeil(vMin / vStep) * vStep; v <= vMax + 1e-6; v += vStep) { DXVector3 w = DataToWorld(kMin, tMin, v); if(WorldToScreen(w, sx, sy)) g_canvas.TextOut(sx - 8, sy - 7, StringFormat("%.0f%%", v * 100), axisClr, TA_RIGHT); } } //--- axis titles at the midpoints, gated by the same visibility g_canvas.FontSet("Segoe UI", 14, FW_BOLD); DXVector3 wt; if(strikeVisible) { wt = DataToWorld((kMin + kMax) * 0.5, tMin, vMin); if(WorldToScreen(wt, sx, sy)) g_canvas.TextOut(sx, sy + 26, "Strike", titleClr, TA_CENTER); } if(timeVisible) { wt = DataToWorld(kMax, (tMin + tMax) * 0.5, vMin); if(WorldToScreen(wt, sx, sy)) g_canvas.TextOut(sx + 40, sy + 8, "Time to Expiry", titleClr, TA_LEFT); } if(volVisible) { wt = DataToWorld(kMin, tMin, (vMin + vMax) * 0.5); if(WorldToScreen(wt, sx, sy)) g_canvas.TextOut(sx - 44, sy - 8, "Implied Vol", titleClr, TA_RIGHT); } } //+------------------------------------------------------------------+ //| 2D overlay drawn on top of the finished 3D frame. Because | //| CCanvas3D inherits the 2D CCanvas drawing calls, we can paint a | //| title, an axis key, a colour legend for the implied-vol scale, | //| and a spot / range readout straight onto the same bitmap after | //| Render() but before Update() flushes it to the chart. | //+------------------------------------------------------------------+ void DrawOverlay(void) { DrawAxes3D(); // projected axis ticks + titles, under the panels int W = g_canvas.Width(); uint txt = ColorToARGB(g_textColor); uint dim = ColorToARGB(g_textColor, 200); //--- title card (top-left) int px = 12, py = 12, pw = 330, ph = 70; uint card = ColorToARGB(g_panelColor, 235); // near-opaque card uint border = ColorToARGB(InpLightTheme ? (color)0xC4C8CE : (color)0x2E343C); uint header = ColorToARGB(InpLightTheme ? (color)0x2A2E33 : (color)0x3A4048); g_canvas.FillRectangle(px, py, px + pw, py + ph, card); g_canvas.Rectangle(px, py, px + pw, py + ph, border); // crisp border g_canvas.FillRectangle(px, py, px + pw, py + 34, header); // title bar g_canvas.FontSet("Segoe UI", 18, FW_BOLD); g_canvas.TextOut(px + 14, py + 7, "Implied Volatility Surface", ColorToARGB(clrWhite)); g_canvas.FontSet("Segoe UI", 14); g_canvas.TextOut(px + 14, py + 44, StringFormat("spot %.2f IV %.1f%% - %.1f%%", g_surf.Spot(), g_surf.IVMin() * 100.0, g_surf.IVMax() * 100.0), txt); //--- vertical colour legend (right edge) int lx = W - 52, lw = 22; int barTop = 70, barBot = 300; for(int y = barTop; y < barBot; y++) { double t = 1.0 - (double)(y - barTop) / (barBot - barTop); // top = high vol DXColor c = HeatColor(t); color mc = (color)(((int)(c.b * 255) << 16) | ((int)(c.g * 255) << 8) | (int)(c.r * 255)); g_canvas.LineHorizontal(lx, lx + lw, y, ColorToARGB(mc)); } g_canvas.Rectangle(lx, barTop, lx + lw, barBot, ColorToARGB(InpLightTheme ? (color)0x808890 : (color)0x505860)); g_canvas.FontSet("Segoe UI", 13, FW_BOLD); g_canvas.TextOut(lx + lw - 2, barTop - 20, StringFormat("%.0f%%", g_surf.IVMax() * 100.0), txt, TA_RIGHT); g_canvas.TextOut(lx + lw - 2, barBot + 4, StringFormat("%.0f%%", g_surf.IVMin() * 100.0), txt, TA_RIGHT); g_canvas.TextOut(lx + lw - 2, (barTop + barBot) / 2 - 8, "IV", txt, TA_RIGHT); //--- controls hint (bottom-left) g_canvas.FontSet("Segoe UI", 13); g_canvas.TextOut(20, g_canvas.Height() - 26, "drag: orbit wheel: zoom R: reload", dim); } //+------------------------------------------------------------------+ //| Create the 3D canvas + bind a chart object, build the mesh from | //| the loaded surface, and set camera / projection / lighting. | //+------------------------------------------------------------------+ bool CreateScene(void) { int w = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); int h = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); if(w < 50 || h < 50) return(false); //--- create the 3D canvas (builds DX context + scene input) then bind an //--- OBJ_BITMAP_LABEL to its resource so the frames are actually shown. if(!g_canvas.Create(g_objName, w, h, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("IVSurface3D: 3D canvas Create failed - is DirectX 11 available?"); return(false); } if(ObjectFind(0, g_objName) < 0) ObjectCreate(0, g_objName, OBJ_BITMAP_LABEL, 0, 0, 0); ObjectSetInteger(0, g_objName, OBJPROP_XDISTANCE, 0); ObjectSetInteger(0, g_objName, OBJPROP_YDISTANCE, 0); ObjectSetInteger(0, g_objName, OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(0, g_objName, OBJPROP_BACK, false); ObjectSetInteger(0, g_objName, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, g_objName, OBJPROP_HIDDEN, true); ObjectSetString(0, g_objName, OBJPROP_BMPFILE, g_canvas.ResourceName()); g_canvas.ProjectionMatrixSet((float)(M_PI / 5.0), (float)w / h, 0.1f, 100.0f); DXVector3 light = DXVector3(0.3f, -0.8f, 0.5f); DXVec3Normalize(light, light); g_canvas.LightDirectionSet(light); g_canvas.LightColorSet(ToDXColor(clrWhite)); //--- brighter ambient on a light theme so shaded faces do not go muddy g_canvas.AmbientColorSet(ToDXColor(InpLightTheme ? (color)0x8A8E94 : (color)0x505560)); UpdateCamera(); BuildMeshIndices(); BuildMeshVertices(); g_mesh = new CDXMesh; if(!g_mesh.Create(g_canvas.DXDispatcher(), g_canvas.InputScene(), g_verts, g_idx)) { Print("IVSurface3D: surface mesh Create failed"); return(false); } g_mesh.SpecularColorSet(ToDXColor(clrWhite)); g_mesh.SpecularPowerSet(24.0f); g_canvas.ObjectAdd(g_mesh); if(!BuildWireframe()) Print("IVSurface3D: wireframe creation failed (surface still renders)"); if(!BuildAxisRods()) Print("IVSurface3D: axis rods creation failed (surface still renders)"); return(true); } //+------------------------------------------------------------------+ //| Expert initialization: load data, build the scene, start timer. | //+------------------------------------------------------------------+ int OnInit(void) { ResolveTheme(); if(!LoadData()) return(INIT_FAILED); if(!CreateScene()) return(INIT_FAILED); ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); ChartSetInteger(0, CHART_MOUSE_SCROLL, false); EventSetMillisecondTimer(FRAME_MS); g_lastRefresh = TimeCurrent(); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization: tear down the scene in the right order. | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false); ChartSetInteger(0, CHART_MOUSE_SCROLL, true); ObjectDelete(0, g_objName); ChartRedraw(0); g_canvas.Destroy(); g_mesh = NULL; g_wire = NULL; g_rodX = NULL; g_rodY = NULL; g_rodZ = NULL; } //+------------------------------------------------------------------+ //| Custom indicator iteration function (unused, no plots/buffers). | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) { return(rates_total); } //+------------------------------------------------------------------+ //| Rebuild the surface mesh from freshly loaded data (native source | //| auto-refresh, or a manual R press). | //+------------------------------------------------------------------+ void Reload(void) { if(!LoadData() || g_mesh == NULL) return; BuildMeshIndices(); BuildMeshVertices(); //--- grid dimensions may have changed, so rebuild both meshes in place //--- (they stay registered with the canvas, so we do not ObjectAdd again) g_mesh.Create(g_canvas.DXDispatcher(), g_canvas.InputScene(), g_verts, g_idx); g_mesh.SpecularColorSet(ToDXColor(clrWhite)); g_mesh.SpecularPowerSet(24.0f); if(g_wire != NULL) { RebuildWireData(); g_wire.Create(g_canvas.DXDispatcher(), g_canvas.InputScene(), g_wireVerts, g_wireIdx, DX_PRIMITIVE_TOPOLOGY_LINELIST); } } //+------------------------------------------------------------------+ //| Timer tick: auto-refresh the native source, then render + flush | //| a fresh frame with the 2D overlay drawn on top. | //+------------------------------------------------------------------+ void OnTimer(void) { if(InpSource == IV_SOURCE_NATIVE && InpRefreshSec > 0 && TimeCurrent() - g_lastRefresh >= InpRefreshSec) { Reload(); g_lastRefresh = TimeCurrent(); } g_canvas.Render(DX_CLEAR_COLOR | DX_CLEAR_DEPTH, g_bgArgb); DrawOverlay(); // 2D title / legend / axis key on top of the 3D frame g_canvas.Update(true); } //+------------------------------------------------------------------+ //| Mouse drag orbits the camera, wheel zooms, R reloads the data, | //| and a chart resize rebuilds the DX scene at the new pixel size. | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id == CHARTEVENT_MOUSE_MOVE) { int mx = (int)lparam, my = (int)dparam; bool leftDown = (((uint)sparam & 1) != 0); if(leftDown && !g_dragging) { g_dragging = true; g_dragX = mx; g_dragY = my; } else if(leftDown && g_dragging) { g_camYaw -= (mx - g_dragX) * 0.01; g_camPitch += (my - g_dragY) * 0.01; if(g_camPitch > 1.45) g_camPitch = 1.45; if(g_camPitch < -0.2) g_camPitch = -0.2; g_dragX = mx; g_dragY = my; UpdateCamera(); } else if(!leftDown) g_dragging = false; return; } if(id == CHARTEVENT_MOUSE_WHEEL) { g_camDist *= (dparam > 0) ? 0.92 : 1.08; if(g_camDist < 2.5) g_camDist = 2.5; if(g_camDist > 18.0) g_camDist = 18.0; UpdateCamera(); return; } if(id == CHARTEVENT_KEYDOWN) { if((int)lparam == 'R') Reload(); return; } if(id == CHARTEVENT_CHART_CHANGE) { int w = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); int h = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); if(w > 50 && h > 50 && (w != g_canvas.Width() || h != g_canvas.Height())) { g_canvas.Destroy(); // frees the meshes it owns g_mesh = NULL; g_wire = NULL; g_rodX = NULL; g_rodY = NULL; g_rodZ = NULL; CreateScene(); // re-new's them at the new size } return; } } //+------------------------------------------------------------------+