Neural Networks Without Magic: Occam’s Razor and a Programmer’s View

Artificial intelligence doesn’t have to be esoteric. High school math, a few derivatives, and clean code in C++/MQL5 are enough. No magic—just numbers in matrices, functions, and discipline.


A Problem Without Fog: What We Really Need

Texts about neural networks often contain a lot of “smoke and mirrors.” I used Occam’s razor and kept only what is necessary—the mathematical minimum everyone remembers from high school:

  • multiplication and addition of numbers,
  • simple nonlinear functions (tanh, sigmoid, ReLU),
  • basic work with vectors and matrices,
  • and derivatives for learning (backpropagation).

That’s it. Everything else is just comfort, not necessity.


Derivatives: The Only “Optional Mandatory” Chapter

Without derivatives, we only get the result of the forward pass. But learning requires knowing how to adjust each weight. This is where the chain rule comes in: compute the error, send it back through the layers, and adjust weights and biases according to the derivatives of activations and linear parts.

  • Error: difference between prediction and reality (MSE).
  • Gradient: derivative of the composite function (chain rule).
  • Update: a small step in the direction of error reduction (SGD).

If you can handle the derivative of sigmoid, tanh, and “ReLU derivative,” you’ve covered 99% of what’s needed.


C++ and MQL5: Math Translated into Practice

In C++ I have a lightweight DLL: dense layers, activations, forward pass, backpropagation, single-sample and mini-batch training. Everything runs through a simple C API (handle, no global singletons). In MQL5, the EA takes data directly from the chart, normalizes it, feeds it into the network, and draws outputs.

  • y = f(Wx + b) — forward pass.
  • dL/dW, dL/db — gradients via activation derivatives.
  • SGD, MSE, gradient clipping, He/Xavier init.
  • Batch wrappers for throughput (NN_ForwardBatch, NN_TrainBatch).
  • Weights access for tooling (NN_GetWeights, NN_SetWeights).

Result: AI stops looking magical. What remains is programming craft with understandable behavior.


Occam’s Razor in Bullet Points

  • Forward pass: matrix–vector multiplication.
  • Activation: tanh/sigmoid/ReLU as ordinary functions.
  • Loss (MSE): mean squared error.
  • Backpropagation: pure chain rule.

Complexity arises from stacking simple things, not from mystery.


What This Path Gave Me

I see the network as a system of calculation, not a prophecy. I understand both its limits and possibilities. And I have a tool I can expand: save/load weights (via app code), add Adam, more outputs, connect it to trading logic.

High school math, patience, and the will to remove ballast are enough.


Conclusion

Neural networks are not a black box. They are numbers in matrices and a few derivatives—a mathematical mechanism translated into the languages I like: C++ and MQL5.


Download (EA / sources)

NNMQL5 Documentation:


A lightweight DLL exposing a C API for a simple MLP (stack of dense layers) with:

  • forward inference (NN_Forward, NN_ForwardBatch)
  • training: single-sample and mini-batch (NN_TrainOne, NN_TrainBatch)
  • multi-network instance management via integer handles
  • weights access for tooling/debug (NN_GetWeights, NN_SetWeights)

The network is stateful (weights in RAM); persistence is handled by the host (e.g., save/load via NN_Get/SetWeights from MQL).

Features

  • Dense layers: W[out x in], bias b[out]
  • Activations: SIGMOID, RELU, TANH, LINEAR, SYM_SIG (symmetric sigmoid)
  • Weight init: He for ReLU, Xavier-like for others
  • Gradient clipping (per-neuron, ±5)
  • Double precision, x64 build (MSVC)
  • C ABI (no name mangling) → easy to call from MQL5
  • Batch wrappers for throughput
  • Weights read/write for reproducible experiments

Limitations (by design)

  • No built-in optimizer beyond SGD (Adam/Nesterov not included yet)
  • No built-in serialization API (persist via NN_Get/SetWeights in host code)
  • Thread safety: per-handle calls must be serialized by the host

Binary / Build Info

  • Platform: Windows x64, MSVC (Visual Studio)
  • Exports: extern "C" __declspec(dllexport)
  • Exceptions: never cross the C boundary; API returns bool/int
  • Threading: instance table guarded by std::mutex. Per-network ops are not re-entrant.

Suggested VS settings

  • Configuration: Release | x64
  • C++: /std:c++17 (or newer), /O2, /EHsc
  • Runtime: /MD (shared CRT)

API (C interface)

All functions use C ABI. Signatures:

// Create / free
int  NN_Create(void);
void NN_Free(int h);

// Topology (act: 0=SIGMOID, 1=RELU, 2=TANH, 3=LINEAR, 4=SYM_SIG)
bool NN_AddDense(int h, int inSz, int outSz, int act);

// Introspection
int  NN_InputSize(int h);
int  NN_OutputSize(int h);

// Inference
bool NN_Forward(int h, const double* in, int in_len,
                double* out, int out_len);
bool NN_ForwardBatch(int h, const double* in, int batch, int in_len,
                     double* out, int out_len);

// Training (SGD + MSE)
bool NN_TrainOne(int h, const double* in, int in_len,
                 const double* tgt, int tgt_len,
                 double lr, double* mse);
bool NN_TrainBatch(int h, const double* in, int batch, int in_len,
                   const double* tgt, int tgt_len,
                   double lr, double* mean_mse);

// Weights access
bool NN_GetWeights(int h, int i, double* W, int Wlen, double* b, int blen);
bool NN_SetWeights(int h, int i, const double* W, int Wlen, const double* b, int blen);

Activation codes (act)

CodeActivationNotes
0SIGMOID1/(1+e^{-x})
1RELUmax(0,x), He init
2TANH\tanh(x)
3LINEARIdentity
4SYM_SIG2·sigmoid(x)-1 ∈ (-1,1)

Lifecycle (recommended)

  1. h = NN_Create()
  2. Topology via NN_AddDense(...) in order
    • First layer sets input size
    • Last layer sets output size
    • Sizes must match: out(k-1) == in(k) → returns false otherwise
  3. Optionally check NN_InputSize(h), NN_OutputSize(h)
  4. Training: loop NN_TrainOne or NN_TrainBatch
  5. Inference: NN_Forward or NN_ForwardBatch
  6. NN_Free(h)

Semantics

  • Validation: functions return false for invalid handle, empty network, or size mismatch.
  • Memory safety: caller allocates and owns buffers; DLL only reads/writes.
  • Numerics: double I/O; gradient clipping ±5 per neuron; He/Xavier-like init.
  • Instances: any number of independent networks (separate handles).
  • Re-entrancy: don’t call into the same handle concurrently.

MQL5 usage

Import block

#import "NNMQL5.dll"
int  NN_Create();
void NN_Free(int h);
bool NN_AddDense(int h,int inSz,int outSz,int act);
int  NN_InputSize(int h);
int  NN_OutputSize(int h);
bool NN_Forward(int h,const double &in[],int in_len,double &out[],int out_len);
bool NN_ForwardBatch(int h,const double &in[],int batch,int in_len,double &out[],int out_len);
bool NN_TrainOne(int h,const double &in[],int in_len,const double &tgt[],int tgt_len,double lr,double &mse);
bool NN_TrainBatch(int h,const double &in[],int batch,int in_len,const double &tgt[],int tgt_len,double lr,double &mean_mse);
bool NN_GetWeights(int h,int i,double &W[],int Wlen,double &b[],int blen);
bool NN_SetWeights(int h,int i,const double &W[],int Wlen,const double &b[],int blen);
#import

Note: Arrays are passed as const double &arr[] in MQL5. Place the DLL in %APPDATA%\MetaQuotes\Terminal\<id>\MQL5\Libraries\ and allow DLL imports in MT5 settings.

Minimal network + inference (e.g., 4→8→1)

int h = NN_Create();
if(h <= 0){ Print("Create failed"); return; }

bool ok = true;
ok &= NN_AddDense(h, 4, 8, 1); // RELU
ok &= NN_AddDense(h, 8, 1, 3); // LINEAR
if(!ok){ Print("Topology failed"); NN_Free(h); return; }

double x[4] = {1,2,3,4};
double y[1];
if(!NN_Forward(h, x, 4, y, 1)) Print("Forward failed");
Print("y=", y[0]);

NN_Free(h);

One SGD step

double x[4] = {/* features */};
double t[1] = {/* target   */};
double mse=0.0, lr=0.01;

bool ok = NN_TrainOne(h, x, 4, t, 1, lr, mse);
if(!ok) Print("TrainOne failed");
else    Print("MSE=", DoubleToString(mse,6));

Mini-batch training

const int BATCH=32, IN=240, OUT=15;
double X[BATCH*IN], T[BATCH*OUT], mean_mse;

if(!NN_TrainBatch(h, X, BATCH, IN, T, OUT, 0.001, mean_mse))
  Print("TrainBatch failed");

Quick reference (return values)

FunctionSuccess (true/>0)False/0 means
NN_CreatePositive handleAllocation failed (rare)
NN_Free— (idempotent)
NN_AddDenseLayer addedInvalid handle / size mismatch
NN_InputSizeDeclared input size0 (invalid handle / empty network)
NN_OutputSizeDeclared output size0 (invalid handle / empty network)
NN_Forwardout[] filledInvalid handle/sizes/empty network
NN_TrainOneUpdate applied; *mse setInvalid handle/sizes/empty network
NN_ForwardBatchAll rows processedInvalid handle/sizes/empty network
NN_TrainBatchMean MSE setInvalid handle/sizes/empty network

Implementation notes (for audit)

  • Forward: per layer z = W·x + b; caches last_in, last_z, last_out
  • Backward: dL/dz = dL/dy ⊙ f'(z) (ReLU uses x>0); clip ±5; update b -= lr·dL/dz, W -= lr·(dL/dz)·last_in^T
  • Loss: MSE = mean((y - t)^2), gradient dL/dy = 2(y - t)/n
  • Init: w = U[-1,1]·scale, scale = sqrt(2/in) for ReLU, else sqrt(1/in)

Performance notes

  • Allocate in OnInit; train via timer; avoid per-tick create/destroy
  • Reuse MQL arrays to reduce allocations
  • Balance lookback vs. network width/depth

MQL5 compatibility

  • double ABI matches (8B)
  • Arrays passed by reference; DLL reads/writes via raw pointers
  • bool mapping is standard (0/1)

Troubleshooting

  • NN_AddDense returns false: size mismatch (e.g., 8→16 then 32→1). Fix to 8→16→1 or adjust layer sizes.
  • NN_Forward/Train* false: wrong lengths or invalid handle / empty network.
  • No learning: tune lr, normalize data, use LINEAR output for unbounded regression.
NNMQL5_Predictor — Examples (Dark)

NNMQL5_Predictor — Examples (Dark)

Code snippets based on indicator NNMQL5_Predictor.mq5 (MetaTrader 5 • NNMQL5.dll).

Hist +1 Hist +2 Hist +3 Hist +4 Hist +5 Future

License note

“MIT-like spirit”: use freely, please keep attribution.


Author: Tomáš Bělák — Remind