436 lines
16 KiB
MQL5
436 lines
16 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 ERR_OFF_QUOTES 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>
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Assertion counters - global state scoped to this script. |
|
|
//+------------------------------------------------------------------+
|
|
int g_tests_passed = 0;
|
|
int g_tests_failed = 0;
|
|
string g_report_text = "";
|
|
const double EPSILON_PRICE = 0.00001; // 1 point on a 5-digit broker
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Log: helper to print and update the chart comment |
|
|
//+------------------------------------------------------------------+
|
|
void Log(const string msg)
|
|
{
|
|
if(msg != "")
|
|
Print(msg);
|
|
g_report_text += msg + "\n";
|
|
Comment(g_report_text);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| AssertTrue: self-validating assertion (FIRST principle). |
|
|
//| Prints [PASS] or [FAIL] with the test name and increments the |
|
|
//| appropriate counter. No manual inspection required. |
|
|
//+------------------------------------------------------------------+
|
|
void AssertTrue(const string test_name, const bool condition)
|
|
{
|
|
if(condition)
|
|
{
|
|
g_tests_passed++;
|
|
PrintFormat(" [PASS] %s", test_name);
|
|
}
|
|
else
|
|
{
|
|
g_tests_failed++;
|
|
PrintFormat(" [FAIL] %s", test_name);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| AssertFalse: inverted assertion for negative test cases. |
|
|
//+------------------------------------------------------------------+
|
|
void AssertFalse(const string test_name, const bool condition)
|
|
{
|
|
AssertTrue(test_name, !condition);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| AssertEqual: floating-point equality with epsilon tolerance. |
|
|
//| Uses EPSILON_PRICE to avoid IEEE 754 rounding false negatives. |
|
|
//+------------------------------------------------------------------+
|
|
void AssertEqual(const string test_name,
|
|
const double expected,
|
|
const double actual)
|
|
{
|
|
bool match = MathAbs(expected - actual) < EPSILON_PRICE;
|
|
if(!match)
|
|
Log(StringFormat(" Expected=%.5f Actual=%.5f", expected, actual));
|
|
AssertTrue(test_name, match);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| AssertIntEqual: integer equality assertion. |
|
|
//+------------------------------------------------------------------+
|
|
void AssertIntEqual(const string test_name,
|
|
const int expected,
|
|
const int actual)
|
|
{
|
|
bool match = (expected == actual);
|
|
if(!match)
|
|
Log(StringFormat(" Expected=%d Actual=%d", expected, actual));
|
|
AssertTrue(test_name, match);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| AssertUintEqual: unsigned integer equality (for retcode checks). |
|
|
//+------------------------------------------------------------------+
|
|
void AssertUintEqual(const string test_name,
|
|
const uint expected,
|
|
const uint actual)
|
|
{
|
|
bool match = (expected == actual);
|
|
if(!match)
|
|
Log(StringFormat(" Expected=%u Actual=%u", expected, actual));
|
|
AssertTrue(test_name, match);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| 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, ERR_OFF_QUOTES 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_OFF_QUOTES;
|
|
|
|
//--- 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: TryExecuteOrder 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 ERR_OFF_QUOTES
|
|
engine.ProcessNextTick(); // tick 0
|
|
engine.ProcessNextTick(); // tick 1
|
|
engine.ProcessNextTick(); // tick 2
|
|
|
|
MqlTradeRequest request;
|
|
MqlTradeResult result;
|
|
BuildTestBuyRequest(request, 0.01);
|
|
|
|
bool success = engine.TryExecuteOrder(request, result);
|
|
|
|
AssertFalse("Order execution fails on spike tick", success);
|
|
AssertUintEqual("Retcode is ERR_OFF_QUOTES",
|
|
SYNTH_ERR_OFF_QUOTES, result.retcode);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| TEST 4: TryExecuteOrder 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.TryExecuteOrder(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.TryExecuteOrder(request, result);
|
|
|
|
AssertTrue("Order succeeds after full recovery", success);
|
|
AssertUintEqual("Retcode is TRADE_RETCODE_DONE after recovery",
|
|
TRADE_RETCODE_DONE, result.retcode);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| TEST 6: TryExecuteOrder 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.TryExecuteOrder(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()
|
|
{
|
|
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)
|
|
Log("");
|
|
Log("==========================================================");
|
|
|
|
string summary = StringFormat(" RESULTS: %d passed, %d failed, %d total",
|
|
g_tests_passed, g_tests_failed,
|
|
g_tests_passed + g_tests_failed);
|
|
Log(summary);
|
|
|
|
string status;
|
|
uint icon;
|
|
if(g_tests_failed == 0)
|
|
{
|
|
status = " STATUS: ALL TESTS PASSED";
|
|
icon = MB_ICONINFORMATION;
|
|
}
|
|
else
|
|
{
|
|
status = " STATUS: SOME TESTS FAILED";
|
|
icon = MB_ICONERROR;
|
|
}
|
|
Log(status);
|
|
Log("==========================================================");
|
|
|
|
// Show MessageBox
|
|
MessageBox(summary + "\n\n" + status, "Test Results", icon | MB_OK);
|
|
|
|
//--- 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("");
|
|
}
|
|
//+------------------------------------------------------------------+
|