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