MQL5-SyntheticTestHarness/MQL5/Scripts/SyntheticTestHarness/TestSpreadSpike.mq5

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("");
}
//+------------------------------------------------------------------+