Implied-Volatility-MQL5/Indicators/IVSurface/IVSurface3D.mq5
2026-07-06 03:02:41 +00:00

756 lines
29 KiB
MQL5

//+------------------------------------------------------------------+
//| 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 <Canvas/Canvas3D.mqh>
#include <Canvas/DX/DXMesh.mqh>
#include <Canvas/DX/DXBox.mqh>
#include <IVSurface/IVSurfaceData.mqh>
#include <IVSurface/IVProviderNative.mqh>
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;
}
}
//+------------------------------------------------------------------+