WiseTrader Toolbox

Tutorial 2: Classifying the Next Direction

Predicting the exact size of the next move is hard, as Tutorial 1 showed. This tutorial asks an easier and more trade-relevant question: will the next bar close up or down? That is a classification problem, and it changes three things — the target becomes a yes/no label, the output neuron becomes a sigmoid, and the loss function changes to match. We also take the first real pass at controlling overfitting, which is where most of the actual work in trading networks lives.

Note

A ready-to-run copy of the worked example below is installed with the toolbox at WiseTraderToolbox\Neural Networks\Tutorial 2 - Direction Classifier.afl.

Regression versus classification

In Tutorial 1 the network produced a number — a predicted return. That is regression. Here we want a yes/no answer, so we set up classification instead. The recipe is small but you must get all three pieces right together:

  • The target is Ref(C, 1) > C, which is 1 when the next bar closes higher and 0 when it does not.
  • The output activation is a sigmoid (activation code 0), so the network's output is squeezed into the 0–1 range and reads naturally as "how strongly it leans up".
  • The loss is SetErrorAlgorithm(1), the classification loss that pairs with a sigmoid output.

Several inputs, the easy way

Rather than many lags of one indicator, we give the network a small, varied panel of stationary inputs — a return, two oscillators, a trend gauge and a volatility measure. The idea is that each describes a different facet of the bar, and the network learns how to weigh them together. We keep each one in a sensible range (dividing RSI and the stochastic by 100, and so on); the network scales the data internally, but feeding it tidy, stationary values still helps.

The worked example

//  WiseTrader Toolbox - Neural Network Tutorial 2: Direction Classifier
//  --------------------------------------------------------------------------
//  Classifies whether the NEXT bar closes up or down using a handful of
//  indicators. The output neuron is a sigmoid, so the network returns a score
//  between 0 and 1 that you can read as "lean up" (above 0.5) or "lean down"
//  (below 0.5).
//
//  Training runs ONLY when you click the Train button, so this is happy left on
//  a chart. Drag it into its own pane below the price. Once a network is saved
//  it plots the up/down score automatically. The button technique is explained
//  in full under "Running a Network on a Chart".
//  --------------------------------------------------------------------------

SetBarsRequired(99999, 99999);

// 1. Settings
SetLearningAlgorithm(8);                     // AdamW : modern optimizer with weight decay
SetLearningRate(0.002);                      // the Adam family wants a small learning rate
SetAdamWeightDecay(0.01);                    // gently shrink weights (regularization)
SetNetworkWithActivationLayer1(6, 1, 0);     // 6 tanh hidden neurons, SIGMOID output (0..1)
SetErrorAlgorithm(1);                        // classification loss (pairs with a sigmoid output)
SetScalingAlgorithm(0);                      // mean / standard-deviation scaling
SetPercentTestingData(25);                   // hold back the most recent 25% as unseen test data
SetEarlyStoppingPatience(30);
SetDropoutRate(0.2);                         // randomly ignore 20% of hidden units while training
SetMaximumEpochs(400);
SetSeed(1);

netName = Name() + "_T2_Direction";
netPath = "WiseTraderToolbox\\NeuralNetwork\\" + netName;

ClearNeuralNetworkInputs();

// 2. Has this symbol's network been trained yet? Ask the disk.
hasNet = False;
fh = fopen(netPath, "r");
if(fh)
{
    fclose(fh);                                  // close every handle fopen returns (or Error 53)
    hasNet = True;
}

// 3. The Train / Retrain button (a chart click triggers a recalc).
function DrawButton(Text, x1, y1, minW, h, colorFrom, colorTo)
{
    GfxSetOverlayMode(0);
    GfxSelectFont("Segoe UI", 9, 700);           // pick the font BEFORE measuring the caption
    GfxSetBkMode(1);
    padX = 12;                                    // space to leave on each side of the caption
    w    = Max(minW, GfxGetTextWidth(Text) + 2 * padX);   // widen the button if the label is long
    GfxGradientRect(x1, y1, x1 + w, y1 + h, colorFrom, colorTo);
    GfxSetTextColor(colorWhite);                 // white caption on the green box - reads on any theme
    GfxDrawText(Text, x1, y1, x1 + w, y1 + h, 32|4|1);    // single line, centred horizontally + vertically
    return w;
}

BtnX = 5;
BtnY = 40;                                   // pushed down so it clears the chart title
BtnW = 170;
BtnH = 30;
BtnTop = ColorRGB(59, 130, 246);             // button gradient - lighter blue on top ...
BtnBot = ColorRGB(37, 99, 235);              // ... deepening to a darker blue at the bottom
LBClick = GetCursorMouseButtons() == 9;      // 9 = left button down AND cursor over this pane
MouseX  = Nz(GetCursorXPosition(1));          // pixel coordinates
MouseY  = Nz(GetCursorYPosition(1));

if(hasNet)
    BtnLabel = "Retrain network";
else
    BtnLabel = "Train network";

BtnW = DrawButton(BtnLabel, BtnX, BtnY, BtnW, BtnH, BtnTop, BtnBot);   // BtnW becomes the width actually drawn
CursorInBtn = MouseX >= BtnX AND MouseX <= BtnX + BtnW AND MouseY >= BtnY AND MouseY <= BtnY + BtnH;
BtnClicked  = CursorInBtn AND LBClick;

// 4. TRAIN - only on a click. A small, varied panel of stationary inputs; the
//    target is 1 if the next bar closes higher, otherwise 0.
if(BtnClicked)
{
    ClearNeuralNetworkInputs();
    AddNeuralNetworkInput(ROC(C, 1), 0);            // one-bar return
    AddNeuralNetworkInput(RSI(14) / 100, 0);        // momentum, 0..1
    AddNeuralNetworkInput(StochD() / 100, 0);       // stochastic, 0..1
    AddNeuralNetworkInput(CCI(20) / 200, 0);        // roughly centred and scaled
    AddNeuralNetworkInput((ATR(10) / C) * 100, 0);  // volatility as a percent of price
    AddNeuralNetworkOutput(Ref(C, 1) > C, 0);       // 1 if the next bar closes higher

    fdelete(netPath);                            // drop any stale network so we retrain fresh
    TrainMultiInputNeuralNetwork(netName);
    StaticVarSetText("WTT_" + netName, "Training MSE = " + NeuralNetworkMSE +
                     "   Test MSE = " + TestingDataNeuralNetworkMSE);
    ClearNeuralNetworkInputs();
}

// 5. Re-read the disk after training: hasNet is true only if the save worked.
hasNet = False;
fh = fopen(netPath, "r");
if(fh)
{
    fclose(fh);
    hasNet = True;
}

// 6. Display. Running a trained network is cheap, so it runs on every recalc.
//    upScore is a 0..1 lean: above 0.5 the network expects an up bar.
if(hasNet)
{
    ClearNeuralNetworkInputs();                  // run set: same inputs, index 0, NO output
    AddNeuralNetworkInput(ROC(C, 1), 0);
    AddNeuralNetworkInput(RSI(14) / 100, 0);
    AddNeuralNetworkInput(StochD() / 100, 0);
    AddNeuralNetworkInput(CCI(20) / 200, 0);
    AddNeuralNetworkInput((ATR(10) / C) * 100, 0);

    upScore = RunMultiInputNeuralNetwork(netName);
    Plot(upScore, "P(up next bar)", colorBlue, styleLine);
    Plot(0.5, "", colorGrey50, styleLine | styleNoLabel);

    Title = "Tutorial 2 - P(up next bar) = " + upScore + "      " +
            StaticVarGetText("WTT_" + netName);
}
else
{
    // White text over a black outline, so it stays readable whether the chart
    // background is white or black.
    msg = "The neural network has not been trained yet.";
    pw  = Status("pxwidth");
    ph  = Status("pxheight");
    GfxSetOverlayMode(0);
    GfxSelectFont("Segoe UI", 12, 700);
    GfxSetBkMode(1);
    GfxSetTextColor(colorBlack);
    GfxDrawText(msg,  1, 0, pw + 1, ph,     32|1|4|16);
    GfxDrawText(msg, -1, 0, pw - 1, ph,     32|1|4|16);
    GfxDrawText(msg, 0,  1, pw,     ph + 1, 32|1|4|16);
    GfxDrawText(msg, 0, -1, pw,     ph - 1, 32|1|4|16);
    GfxSetTextColor(colorWhite);
    GfxDrawText(msg, 0,  0, pw,     ph,     32|1|4|16);

    Title = "Tutorial 2 - click Train to train the network";
}

// 7. Cleanup
EnableProgress();
RestoreDefaults();
ClearNeuralNetworkInputs();
Tip

The Train button is what lets this formula live on a chart: a chart indicator recalculates on every scroll, zoom and click, so training inline would retrain endlessly and freeze the pane. Gating training behind a click trains once and then just plots the saved network. The mechanism — the disk check, the click handling and the input-set sequencing — is explained step by step under Running a Network on a Chart.

Reading the output

Because the output is a sigmoid, upScore comes back between 0 and 1. Read it as the network's lean: above 0.5 it expects an up bar, below 0.5 a down bar, and values near 0.5 mean it is genuinely unsure — which, on next-bar direction, is most of the time. Treat it as a soft lean rather than a promise. You might only act when the score is decisively away from the middle, say above 0.6 or below 0.4, and ignore the noisy band in between.

Tip

The 0–1 score is not a calibrated probability — a 0.7 does not mean "70% of the time price rose". It is just an ordering: higher means more confident. If you want to turn it into trade signals, compare it to a threshold you choose, and judge that threshold on the held-out test data, not on the training data.

The first overfitting controls

This formula does several small things to keep the network honest. None of them is dramatic on its own, but together they make a real difference to how the model behaves on bars it has never seen — see Accuracy & Overfitting for the full story and the measured impact.

ControlWhat it does
SetPercentTestingData(25)Reserves the most recent quarter of the data as a test set and keeps the network that scores best on it. The most important line in the formula.
SetEarlyStoppingPatience(30)Stops training when the test error stops improving, before the network starts memorising.
SetDropoutRate(0.2)Randomly ignores a fifth of the hidden units on each training step, so the network can't lean on any single neuron. It is automatically switched off when predicting.
SetNetworkWithActivationLayer1(6, …)A deliberately small hidden layer. Smaller networks have less room to memorise noise. Start small and only grow if the network genuinely can't fit the data.
SetLearningAlgorithm(8) + decayAdamW, a modern optimizer with built-in weight decay (SetAdamWeightDecay) that nudges weights toward zero. Remember the Adam family wants a small learning rate, here 0.002.

After a run, look at the two MSE numbers in the title. If the training MSE is much lower than the test MSE, dial the controls up: raise the dropout a little, shrink the network, or lower the epoch budget. If both numbers are high and close together, the network is underfitting — it can't find a pattern. Then you do the opposite: a slightly bigger network, more epochs, or better inputs. You are steering between those two failure modes, and the test number is your steering wheel.

Where to take it next

A natural extension is to grow the network with AmiBroker's optimizer — for example, let the number of hidden neurons or the choice of inputs be Optimize() parameters, and search for the smallest network that still does the job. You can also train one network across several symbols at once by giving each symbol its own data-set index in the multi-input API (see Neural Network Functions) — but only with inputs whose scale is comparable between symbols. An oscillator like RSI lives in 0–100 on every symbol; a raw moving average does not, so it would not pool cleanly.

So far both tutorials have used feed-forward networks that judge each bar more or less on its own. Tutorial 3 steps up to a recurrent LSTM that reads a whole sequence of bars in order, which is a better fit for anticipating a developing swing.