329 lines
12 KiB
MQL5
329 lines
12 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| TestSpreadSpike.mq5 |
|
|
//| Synthetic Test Harness for MetaTrader 5 |
|
|
//| Institutional Architecture in MQL5 - Part 1 Series |
|
|
//+------------------------------------------------------------------+
|
|
#property copyright "Institutional Architecture in MQL5 Series"
|
|
#property version "1.00"
|
|
#property description "Synthetic test: spread spike with TRADE_RETCODE_PRICE_OFF injection"
|
|
#property description "Validates the Chaos Engineering Engine against a 5-tick"
|
|
#property description "scenario covering normal market, spread spike, and recovery."
|
|
#property script_show_inputs
|
|
|
|
#include <SyntheticTestHarness\CSyntheticEngine.mqh>
|
|
#include <SyntheticTestHarness\CArrayProvider.mqh>
|
|
#include <SyntheticTestHarness\CCanvasVisualizer.mqh>
|
|
#include <SyntheticTestHarness\CAssert.mqh>
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| BuildTestBuyRequest: helper to create a minimal BUY order. |
|
|
//| Avoids repeating the same MqlTradeRequest setup in every test |
|
|
//| (DRY principle). |
|
|
//+------------------------------------------------------------------+
|
|
void BuildTestBuyRequest(MqlTradeRequest &request, const double volume)
|
|
{
|
|
ZeroMemory(request);
|
|
request.action = TRADE_ACTION_DEAL;
|
|
request.type = ORDER_TYPE_BUY;
|
|
request.volume = volume;
|
|
request.symbol = "SYNTHETIC";
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| BuildSpreadSpikeScenario: creates the 5-tick test dataset. |
|
|
//| |
|
|
//| Timeline: |
|
|
//| Tick 0 - Normal market (spread=20pt, no error) |
|
|
//| Tick 1 - Normal market (spread=20pt, no error) |
|
|
//| Tick 2 - SPREAD SPIKE (spread=1500pt, TRADE_RETCODE_PRICE_OFF injected) |
|
|
//| Tick 3 - Recovery phase (spread=100pt, no error) |
|
|
//| Tick 4 - Full recovery (spread=20pt, no error) |
|
|
//+------------------------------------------------------------------+
|
|
void BuildSpreadSpikeScenario(MockTick &ticks[])
|
|
{
|
|
ArrayResize(ticks, 5);
|
|
|
|
//--- Tick 0: Normal market conditions
|
|
ticks[0].time = D'2026.01.15 10:00:00';
|
|
ticks[0].bid = 1.10000;
|
|
ticks[0].ask = 1.10020;
|
|
ticks[0].spread_points = 20.0;
|
|
ticks[0].injected_error = SYNTH_ERR_NONE;
|
|
|
|
//--- Tick 1: Normal market continues
|
|
ticks[1].time = D'2026.01.15 10:00:01';
|
|
ticks[1].bid = 1.10005;
|
|
ticks[1].ask = 1.10025;
|
|
ticks[1].spread_points = 20.0;
|
|
ticks[1].injected_error = SYNTH_ERR_NONE;
|
|
|
|
//--- Tick 2: SPREAD SPIKE + server rejection
|
|
ticks[2].time = D'2026.01.15 10:00:02';
|
|
ticks[2].bid = 1.09800;
|
|
ticks[2].ask = 1.11300;
|
|
ticks[2].spread_points = 1500.0;
|
|
ticks[2].injected_error = SYNTH_ERR_PRICE_OFF;
|
|
|
|
//--- Tick 3: Recovery begins (spread narrowing)
|
|
ticks[3].time = D'2026.01.15 10:00:03';
|
|
ticks[3].bid = 1.09950;
|
|
ticks[3].ask = 1.10050;
|
|
ticks[3].spread_points = 100.0;
|
|
ticks[3].injected_error = SYNTH_ERR_NONE;
|
|
|
|
//--- Tick 4: Full recovery to normal conditions
|
|
ticks[4].time = D'2026.01.15 10:00:04';
|
|
ticks[4].bid = 1.10010;
|
|
ticks[4].ask = 1.10030;
|
|
ticks[4].spread_points = 20.0;
|
|
ticks[4].injected_error = SYNTH_ERR_NONE;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| TEST 1: Engine processes every tick from the provider. |
|
|
//| Verifies: CSyntheticEngine + ITickProvider integration. |
|
|
//+------------------------------------------------------------------+
|
|
void TestEngineProcessesAllTicks()
|
|
{
|
|
Log("--- Test: Engine Processes All Ticks ---");
|
|
|
|
MockTick ticks[];
|
|
BuildSpreadSpikeScenario(ticks);
|
|
|
|
CArrayProvider provider(ticks);
|
|
CSyntheticEngine engine(&provider);
|
|
|
|
int processed = 0;
|
|
while(engine.ProcessNextTick())
|
|
processed++;
|
|
|
|
AssertIntEqual("Processed tick count matches scenario size", 5, processed);
|
|
AssertIntEqual("Engine internal counter agrees",
|
|
5, engine.GetTicksProcessed());
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| TEST 2: Spread spike values are correctly surfaced at tick 2. |
|
|
//| Verifies: CSyntheticEngine delegates to current MockTick. |
|
|
//+------------------------------------------------------------------+
|
|
void TestSpreadSpikeDetected()
|
|
{
|
|
Log("--- Test: Spread Spike Detected ---");
|
|
|
|
MockTick ticks[];
|
|
BuildSpreadSpikeScenario(ticks);
|
|
|
|
CArrayProvider provider(ticks);
|
|
CSyntheticEngine engine(&provider);
|
|
|
|
//--- Advance to the spike tick (index 2)
|
|
engine.ProcessNextTick(); // tick 0
|
|
engine.ProcessNextTick(); // tick 1
|
|
engine.ProcessNextTick(); // tick 2 - spike
|
|
|
|
AssertEqual("Spread at spike is 1500 points", 1500.0, engine.GetSpread());
|
|
AssertEqual("Bid reflects flash crash", 1.09800, engine.GetBid());
|
|
AssertEqual("Ask reflects flash widening", 1.11300, engine.GetAsk());
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| TEST 3: OrderSend fails with injected error code. |
|
|
//| Verifies: error injection path in CSyntheticEngine. |
|
|
//+------------------------------------------------------------------+
|
|
void TestOrderFailsOnInjectedError()
|
|
{
|
|
Log("--- Test: Order Fails On Injected Error ---");
|
|
|
|
MockTick ticks[];
|
|
BuildSpreadSpikeScenario(ticks);
|
|
|
|
CArrayProvider provider(ticks);
|
|
CSyntheticEngine engine(&provider);
|
|
|
|
//--- Advance to the spike tick (index 2) which carries TRADE_RETCODE_PRICE_OFF
|
|
engine.ProcessNextTick(); // tick 0
|
|
engine.ProcessNextTick(); // tick 1
|
|
engine.ProcessNextTick(); // tick 2
|
|
|
|
MqlTradeRequest request;
|
|
MqlTradeResult result;
|
|
BuildTestBuyRequest(request, 0.01);
|
|
|
|
bool success = engine.OrderSend(request, result);
|
|
|
|
AssertFalse("Order execution fails on spike tick", success);
|
|
AssertUintEqual("Retcode is TRADE_RETCODE_PRICE_OFF",
|
|
SYNTH_ERR_PRICE_OFF, result.retcode);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| TEST 4: OrderSend succeeds on normal ticks. |
|
|
//| Verifies: happy path through CSyntheticEngine. |
|
|
//+------------------------------------------------------------------+
|
|
void TestOrderSucceedsOnNormalTick()
|
|
{
|
|
Log("--- Test: Order Succeeds On Normal Tick ---");
|
|
|
|
MockTick ticks[];
|
|
BuildSpreadSpikeScenario(ticks);
|
|
|
|
CArrayProvider provider(ticks);
|
|
CSyntheticEngine engine(&provider);
|
|
|
|
//--- Advance to tick 0 (normal conditions)
|
|
engine.ProcessNextTick();
|
|
|
|
MqlTradeRequest request;
|
|
MqlTradeResult result;
|
|
BuildTestBuyRequest(request, 0.01);
|
|
|
|
bool success = engine.OrderSend(request, result);
|
|
|
|
AssertTrue("Order execution succeeds on normal tick", success);
|
|
AssertUintEqual("Retcode is TRADE_RETCODE_DONE",
|
|
TRADE_RETCODE_DONE, result.retcode);
|
|
AssertEqual("Fill volume matches requested volume",
|
|
0.01, result.volume);
|
|
AssertEqual("Fill price is the ask (BUY order)",
|
|
1.10020, result.price);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| TEST 5: Market recovers after the spike - orders work again. |
|
|
//| Verifies: engine correctly transitions through spike -> recovery. |
|
|
//+------------------------------------------------------------------+
|
|
void TestMarketRecoveryAfterSpike()
|
|
{
|
|
Log("--- Test: Market Recovery After Spike ---");
|
|
|
|
MockTick ticks[];
|
|
BuildSpreadSpikeScenario(ticks);
|
|
|
|
CArrayProvider provider(ticks);
|
|
CSyntheticEngine engine(&provider);
|
|
|
|
//--- Advance through all 5 ticks to the final recovery tick
|
|
for(int i = 0; i < 5; i++)
|
|
engine.ProcessNextTick();
|
|
|
|
AssertEqual("Spread returns to normal after recovery",
|
|
20.0, engine.GetSpread());
|
|
|
|
MqlTradeRequest request;
|
|
MqlTradeResult result;
|
|
BuildTestBuyRequest(request, 0.01);
|
|
|
|
bool success = engine.OrderSend(request, result);
|
|
|
|
AssertTrue("Order succeeds after full recovery", success);
|
|
AssertUintEqual("Retcode is TRADE_RETCODE_DONE after recovery",
|
|
TRADE_RETCODE_DONE, result.retcode);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| TEST 6: OrderSend fails before any tick is loaded. |
|
|
//| Verifies: guard clause for uninitialized engine state. |
|
|
//+------------------------------------------------------------------+
|
|
void TestOrderFailsBeforeFirstTick()
|
|
{
|
|
Log("--- Test: Order Fails Before First Tick ---");
|
|
|
|
MockTick ticks[];
|
|
BuildSpreadSpikeScenario(ticks);
|
|
|
|
CArrayProvider provider(ticks);
|
|
CSyntheticEngine engine(&provider);
|
|
|
|
//--- Attempt execution WITHOUT calling ProcessNextTick()
|
|
MqlTradeRequest request;
|
|
MqlTradeResult result;
|
|
BuildTestBuyRequest(request, 0.01);
|
|
|
|
bool success = engine.OrderSend(request, result);
|
|
|
|
AssertFalse("Order fails when no tick is loaded", success);
|
|
AssertTrue("Retcode is not TRADE_RETCODE_DONE before first tick",
|
|
result.retcode != TRADE_RETCODE_DONE);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| TEST 7: CArrayProvider Reset() allows scenario re-execution. |
|
|
//| Verifies: provider rewind for repeated test runs. |
|
|
//+------------------------------------------------------------------+
|
|
void TestProviderResetReplaysTicks()
|
|
{
|
|
Log("--- Test: Provider Reset Replays Ticks ---");
|
|
|
|
MockTick ticks[];
|
|
BuildSpreadSpikeScenario(ticks);
|
|
|
|
CArrayProvider provider(ticks);
|
|
|
|
//--- First pass: consume all ticks
|
|
MockTick temp;
|
|
int first_pass_count = 0;
|
|
while(provider.HasNext())
|
|
{
|
|
provider.GetNextTick(temp);
|
|
first_pass_count++;
|
|
}
|
|
|
|
AssertFalse("Provider is exhausted after first pass",
|
|
provider.HasNext());
|
|
|
|
//--- Reset and replay
|
|
provider.Reset();
|
|
|
|
int second_pass_count = 0;
|
|
while(provider.HasNext())
|
|
{
|
|
provider.GetNextTick(temp);
|
|
second_pass_count++;
|
|
}
|
|
|
|
AssertIntEqual("Second pass yields same tick count",
|
|
first_pass_count, second_pass_count);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| OnStart: test runner entry point. |
|
|
//| Executes all independent test functions and prints a summary. |
|
|
//+------------------------------------------------------------------+
|
|
void OnStart()
|
|
{
|
|
CAssert::Reset();
|
|
|
|
Log("==========================================================");
|
|
Log(" SYNTHETIC TEST HARNESS - Spread Spike Scenario");
|
|
Log(" Institutional Architecture in MQL5 - Part 1");
|
|
Log("==========================================================");
|
|
Log("");
|
|
|
|
//--- Run each test independently (FIRST: Independent)
|
|
TestEngineProcessesAllTicks();
|
|
Log("");
|
|
TestSpreadSpikeDetected();
|
|
Log("");
|
|
TestOrderFailsOnInjectedError();
|
|
Log("");
|
|
TestOrderSucceedsOnNormalTick();
|
|
Log("");
|
|
TestMarketRecoveryAfterSpike();
|
|
Log("");
|
|
TestOrderFailsBeforeFirstTick();
|
|
Log("");
|
|
TestProviderResetReplaysTicks();
|
|
|
|
//--- Summary (FIRST: Self-validating)
|
|
CAssert::Summary("Spread Spike Test Results", true);
|
|
|
|
//--- Visual overlay: render the scenario candles on the chart
|
|
MockTick visual_ticks[];
|
|
BuildSpreadSpikeScenario(visual_ticks);
|
|
CCanvasVisualizer visualizer;
|
|
visualizer.ShowScenario(visual_ticks, 5);
|
|
|
|
// Clear comment after closing MessageBox
|
|
Comment("");
|
|
}
|
|
//+------------------------------------------------------------------+
|