277 行
12 KiB
MQL5
277 行
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");
|
|
}
|
|
//+------------------------------------------------------------------+
|