riskgate-service-realcase/tests/UnitTests.mq5

277 lines
12 KiB
MQL5

//+------------------------------------------------------------------+
//| UnitTests.mq5 |
//| Unit Tests for Package sockets |
//| Organization: douglasrechia |
//+------------------------------------------------------------------+
#include "../knitpkg/build/BuildInfo.mqh"
#property copyright "Douglas Rechia"
#property link "https://knitpkg.dev"
#property description ""
#property description "Version: " + MANIFEST_VERSION
#property description ""
#property description "Description: Unit tests for package sockets"
#property description "Organization: douglasrechia"
#property description "Author: Douglas Rechia"
#property description "License: MIT"
#property description ""
#property description "Powered by KnitPkg for MetaTrader"
#property description "https://knitpkg.dev"
// Include the headers under test
#include "../src/RiskgateHandler.mqh"
//+------------------------------------------------------------------+
//| Shared helpers |
//+------------------------------------------------------------------+
string TEST_SYMBOL = "EURUSD";
double TEST_SL = 50;
long TEST_MAGIC = 1;
double TEST_BALANCE = 10000.0;
double CalcExpectedLot(double balance, double slPoints, bool correlated = false)
{
double tickValue = SymbolInfoDouble(TEST_SYMBOL, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(TEST_SYMBOL, SYMBOL_TRADE_TICK_SIZE);
double point = SymbolInfoDouble(TEST_SYMBOL, SYMBOL_POINT);
double lotStep = SymbolInfoDouble(TEST_SYMBOL, SYMBOL_VOLUME_STEP);
double lotMin = SymbolInfoDouble(TEST_SYMBOL, SYMBOL_VOLUME_MIN);
double lotMax = SymbolInfoDouble(TEST_SYMBOL, SYMBOL_VOLUME_MAX);
double riskMoney = balance * 0.005;
double slMoney = (slPoints * point / tickSize) * tickValue;
double lot = MathFloor(riskMoney / slMoney / lotStep) * lotStep;
if(correlated) lot *= 0.5;
return MathMax(lotMin, MathMin(lotMax, lot));
}
MyHandler* BuildHandler(MockTradeHistory* history, MockOpenPositions* positions)
{
return new MyHandler(history, new MockAccountInfo(TEST_BALANCE), positions, new RealSymbolInfo());
}
void BuildSignal(CJAVal& signal)
{
signal["symbol"] = TEST_SYMBOL;
signal["sl_points"] = TEST_SL;
signal["magic"] = TEST_MAGIC;
}
//+------------------------------------------------------------------+
//| Verifies that MyHandler approves a trade and sizes the lot to |
//| exactly 0.5% of account balance when no prior history or open |
//| positions exist. |
//+------------------------------------------------------------------+
bool Test_ApprovedTrade_LotRisksHalfPercentOfBalance()
{
MyHandler* handler = BuildHandler(new MockTradeHistory(), new MockOpenPositions());
CJAVal signal, response;
BuildSignal(signal);
handler.OnSignal(signal, response);
delete handler;
return response["approved"].ToBool() &&
MathAbs(response["lot"].ToDbl() - CalcExpectedLot(TEST_BALANCE, TEST_SL)) < SymbolInfoDouble(TEST_SYMBOL, SYMBOL_VOLUME_STEP);
}
//+------------------------------------------------------------------+
//| Verifies that MyHandler approves a trade and sizes the lot to |
//| exactly 0.5% of account balance when no prior history exist but |
//| 5 positions are open. |
//+------------------------------------------------------------------+
bool Test_ApprovedTrade_With5OpenPositions()
{
MockOpenPositions* positions = new MockOpenPositions();
for(int i = 0; i < 5; i++) positions.Add("GBPUSD", TEST_MAGIC + 1);
MyHandler* handler = BuildHandler(new MockTradeHistory(), positions);
CJAVal signal, response;
BuildSignal(signal);
handler.OnSignal(signal, response);
delete handler;
return response["approved"].ToBool() &&
MathAbs(response["lot"].ToDbl() - CalcExpectedLot(TEST_BALANCE, TEST_SL)) < SymbolInfoDouble(TEST_SYMBOL, SYMBOL_VOLUME_STEP);
}
//+------------------------------------------------------------------+
//| Verifies that MyHandler rejects a trade when 6 positions are |
//| already open, hitting the daily max positions limit. |
//+------------------------------------------------------------------+
bool Test_RejectedTrade_With6OpenPositions()
{
MockOpenPositions* positions = new MockOpenPositions();
for(int i = 0; i < 6; i++) positions.Add("GBPUSD", TEST_MAGIC + 1);
MyHandler* handler = BuildHandler(new MockTradeHistory(), positions);
CJAVal signal, response;
BuildSignal(signal);
handler.OnSignal(signal, response);
delete handler;
return !response["approved"].ToBool() &&
response["reason"].ToStr() == "daily_max_positions_limit";
}
//+------------------------------------------------------------------+
//| Verifies that MyHandler rejects a trade when 2 positions are |
//| already open on the same symbol as the incoming signal. |
//+------------------------------------------------------------------+
bool Test_RejectedTrade_With2OpenPositionsOnSameSymbol()
{
MockOpenPositions* positions = new MockOpenPositions();
positions.Add(TEST_SYMBOL, TEST_MAGIC);
positions.Add(TEST_SYMBOL, TEST_MAGIC);
MyHandler* handler = BuildHandler(new MockTradeHistory(), positions);
CJAVal signal, response;
BuildSignal(signal);
handler.OnSignal(signal, response);
delete handler;
return !response["approved"].ToBool() &&
response["reason"].ToStr() == "max_positions_reached";
}
//+------------------------------------------------------------------+
//| Verifies that MyHandler approves a trade and halves the lot when |
//| one open position on a different symbol shares the same magic, |
//| indicating correlated exposure. |
//+------------------------------------------------------------------+
bool Test_ApprovedTrade_LotHalvedWithCorrelatedExposure()
{
MockOpenPositions* positions = new MockOpenPositions();
positions.Add("GBPUSD", TEST_MAGIC);
MyHandler* handler = BuildHandler(new MockTradeHistory(), positions);
CJAVal signal, response;
BuildSignal(signal);
handler.OnSignal(signal, response);
delete handler;
return response["approved"].ToBool() &&
MathAbs(response["lot"].ToDbl() - CalcExpectedLot(TEST_BALANCE, TEST_SL, true)) < SymbolInfoDouble(TEST_SYMBOL, SYMBOL_VOLUME_STEP);
}
//+------------------------------------------------------------------+
//| Verifies that MyHandler approves a trade when closed deals sum |
//| to a daily loss below the 2% threshold (-1.77% in this case). |
//+------------------------------------------------------------------+
bool Test_ApprovedTrade_DailyLossUnder2Percent()
{
// 3 x -60 = -180; m_dayPnl = -180/10180*100 ≈ -1.77% → under threshold
MockTradeHistory* history = new MockTradeHistory();
history.AddDeal(1, true, -60.0);
history.AddDeal(2, true, -60.0);
history.AddDeal(3, true, -60.0);
MyHandler* handler = BuildHandler(history, new MockOpenPositions());
CJAVal signal, response;
BuildSignal(signal);
handler.OnSignal(signal, response);
delete handler;
return response["approved"].ToBool() &&
MathAbs(response["lot"].ToDbl() - CalcExpectedLot(TEST_BALANCE, TEST_SL)) < SymbolInfoDouble(TEST_SYMBOL, SYMBOL_VOLUME_STEP);
}
//+------------------------------------------------------------------+
//| Verifies that MyHandler rejects a signal missing the "symbol" |
//| key with reason "invalid_signal". |
//+------------------------------------------------------------------+
bool Test_RejectedTrade_InvalidSignal_MissingSymbol()
{
MyHandler* handler = BuildHandler(new MockTradeHistory(), new MockOpenPositions());
CJAVal signal, response;
signal["sl_points"] = TEST_SL;
signal["magic"] = TEST_MAGIC;
handler.OnSignal(signal, response);
delete handler;
return !response["approved"].ToBool() &&
response["reason"].ToStr() == "invalid_signal";
}
//+------------------------------------------------------------------+
//| Verifies that MyHandler rejects a signal with sl_points = 0 |
//| with reason "invalid_signal". |
//+------------------------------------------------------------------+
bool Test_RejectedTrade_InvalidSignal_SlPointsZero()
{
MyHandler* handler = BuildHandler(new MockTradeHistory(), new MockOpenPositions());
CJAVal signal, response;
signal["symbol"] = TEST_SYMBOL;
signal["sl_points"] = 0.0;
signal["magic"] = TEST_MAGIC;
handler.OnSignal(signal, response);
delete handler;
return !response["approved"].ToBool() &&
response["reason"].ToStr() == "invalid_signal";
}
//+------------------------------------------------------------------+
//| Verifies that MyHandler rejects a trade when closed deals sum |
//| to a daily loss above the 2% threshold (-2.34% in this case). |
//+------------------------------------------------------------------+
bool Test_RejectedTrade_DailyLossOver2Percent()
{
// (5 x -40) - 80 = -240; m_dayPnl = -240/10240*100 ≈ -2.34% → over threshold
MockTradeHistory* history = new MockTradeHistory();
history.AddDeal(1, true, -40.0);
history.AddDeal(2, true, -40.0);
history.AddDeal(3, true, -40.0);
history.AddDeal(4, true, -40.0);
history.AddDeal(5, true, -80.0);
MyHandler* handler = BuildHandler(history, new MockOpenPositions());
CJAVal signal, response;
BuildSignal(signal);
handler.OnSignal(signal, response);
delete handler;
return !response["approved"].ToBool() &&
response["reason"].ToStr() == "daily_loss_limit";
}
//+------------------------------------------------------------------+
//| DoTests |
//+------------------------------------------------------------------+
void DoTests(int &testsPerformed,int &testsPassed)
{
string testName="";
//--- Tests
#define RUN(fn) testsPerformed++; testName=#fn; if(fn()) { testsPassed++; PrintFormat("%s pass",testName); } else PrintFormat("%s failed",testName)
RUN(Test_RejectedTrade_InvalidSignal_MissingSymbol);
RUN(Test_RejectedTrade_InvalidSignal_SlPointsZero);
RUN(Test_ApprovedTrade_LotRisksHalfPercentOfBalance);
RUN(Test_ApprovedTrade_With5OpenPositions);
RUN(Test_RejectedTrade_With6OpenPositions);
RUN(Test_RejectedTrade_With2OpenPositionsOnSameSymbol);
RUN(Test_ApprovedTrade_LotHalvedWithCorrelatedExposure);
RUN(Test_ApprovedTrade_DailyLossUnder2Percent);
RUN(Test_RejectedTrade_DailyLossOver2Percent);
}
//+------------------------------------------------------------------+
//| UnitTests() |
//+------------------------------------------------------------------+
void UnitTests(const string packageName)
{
PrintFormat("Unit tests for Package %s\n",packageName);
//--- initial values
int testsPerformed=0;
int testsPassed=0;
//--- test distributions
DoTests(testsPerformed,testsPassed);
//--- print statistics
PrintFormat("\n%d of %d passed",testsPassed,testsPerformed);
}
//+------------------------------------------------------------------+
//| Script program start function |
//+------------------------------------------------------------------+
void OnStart()
{
UnitTests("sockets");
}
//+------------------------------------------------------------------+