#include "Backtester.mqh" #include "SummaryFunctions.mqh" #include "ExposureController.mqh" #include "ExtendedSummary.mqh" //Debug handles #define DEBUG_TRADE_FUNCTION false #define DEBUG_TRADE_SIZES false //Porcetaje decimal a exposure en entero (trade percent = /2) #define PERCENT_TO_EXPOSURE(amount) amount * 200.0 #define EXPOSURE_TO_PERCENT(exposure) exposure / 200.0 #define EVZ_INDICATOR "::Indicators\\EuroFXVix" #define NEWS_INDICATOR "::Indicators\\NewsIndicator" #define INVISIBLE_ATR "::Indicators\\ATR" class CompleteNNFXTester : public Backtester { protected: bool useEvz; int evzHandle; double evzMinimum; double evzHalfRisk; bool scaleOut_halfRisk; double tradePercent; double tradeValueInitial; bool scaleOutInitial; double accountInitial; bool printAdvancedStats; bool writeJournal; bool saveEquityCurve; bool compoundInterest; double currentMaxPairExposure; bool useExposure; ExposureController* exposureControl; TradeSummary tradesToExecute[]; bool useNews; NewsController* newsControl; double pairTops[], maxAbsDrawdown[]; double maxGeneralTop, maxAbsGeneralDrawdown; //Statistical bool recordStatistics; double statsStep; int statsArray[]; int maxIndex, minIndex; //Currency indexes (news/exposure) bool currenciesProcessed; CurrencyIndexObject* currencyIndexesInfo; //ROI datetime firstTradingDay; //Advanced TS bool use_advanced_ts; int adv_ts_buyBuffer, adv_ts_sellBuffer; int trailingHandles[]; //Extended Summary bool use_extended_summary; CExtendedSummary extended_summary; #ifdef __MQL4__ string trailingIndName; double trailingParameters[]; #endif void UpdateEvzValue(); virtual void AddProfits(double amount, double trade_elapsed); virtual void WaitMissedTrade(); virtual void ExecuteBuy(string where, int signal_place); virtual void ExecuteSell(string where, int signal_place); void ExecuteTradesAfterExposureCheck(); void RecalculateTradeValue(); void SetStateBeforeExposure(int procedence); void ResetStateAfterExposure(int procedence, int symbol); void ChangeStateAtSymbol(TradeStates newState, int symbol); void ProcessCurrencies(); virtual void UpdateTrailingStops(); virtual void CheckForNewsExit(); virtual void CloseEvent(datetime time, double profit, CloseTradeProcedence procedence, TradeCloseLevel level); void DetectPossibleGaps(); public: CompleteNNFXTester(double trade_percent, string &symbolsArray[], bool pullback, bool one_candle, bool bridge_tf, bool scale_out, bool result_in_pips=false, bool use_main_exit=true, double minimalTrade=0.5, double sl_atr=1.5, double tp_atr=1.0, double start_move_sl=2.0, bool advancedStats=false, bool useDistributions=false); ~CompleteNNFXTester(); virtual double TesterResult(int optimization_mode, int write_to_file, string custom_str); virtual void BacktesterTick(); void SetEVZHandle(int handle, double minimum, double halfRisk, bool scaleAtHalfRisk); void RecordEquityCurve() { if (!MQLInfoInteger(MQL_OPTIMIZATION)){ saveEquityCurve = true; ClearEquityFile(); } } void RecordTradeJournal() { if (!MQLInfoInteger(MQL_OPTIMIZATION)){ writeJournal = true; ClearJournalFile(); } } #ifdef __MQL5__ void SetRealTradeMode(); #endif void UseCompoundInterest() { compoundInterest = true; } void UseCurrencyExposure(); void UseNewsFiltering(string news_EUR, string news_GBP, string news_AUD, string news_NZD, string news_USD, string news_CAD, string news_CHF, string news_JPY); void SetDistanceBaseline(double atrMult) {atrDistanceMultiplier = atrMult; } void SetBridgeTooFarCount(int btfCount) { bridgeTooFarCount = btfCount; } void SetAdvancedTrailingStops(int buy_buffer, int sell_buffer); void SetTrailingHandle(int handle, int symbolIndex); void UseExtendedSummary() { if (!MQLInfoInteger(MQL_OPTIMIZATION)) use_extended_summary = true; } #ifdef __MQL4__ void SetTrailingIndicatorProperties(string indicator_name, double ¶meters[]); #endif }; CompleteNNFXTester::CompleteNNFXTester(double trade_percent,string &symbolsArray[], bool pullback, bool one_candle, bool bridge_tf, bool scale_out,bool result_in_pips=false,bool use_main_exit=true,double minimalTrade=0.5,double sl_atr=1.5,double tp_atr=1.0,double start_move_sl=2.0, bool advancedStats=false, bool useDistributions=false) :Backtester(trade_percent*AccountInfoDouble(ACCOUNT_BALANCE), symbolsArray, pullback, one_candle, bridge_tf, scale_out, MQLInfoInteger(MQL_VISUAL_MODE), DEBUG_TRADE_FUNCTION, DEBUG_TRADE_SIZES, result_in_pips, use_main_exit) { minimalTradePercent = minimalTrade; currenciesProcessed = false; tradePercent = trade_percent; accountInitial = AccountInfoDouble(ACCOUNT_BALANCE); tradeValueInitial = accountInitial * tradePercent; currentMaxPairExposure = PERCENT_TO_EXPOSURE(tradePercent); // En entero scaleOutInitial = scaleOut; useEvz = false; saveEquityCurve = false; compoundInterest = false; useExposure = false; firstTradingDay = TimeCurrent(); use_extended_summary = false; recordStatistics = useDistributions; if (recordStatistics) { statsStep = tradeValueInitial==0.0 ? 1.0 : tradeValueInitial*2.0 / 10.0; //Dividir en 10 pasos minIndex = -10; maxIndex = 0; ArrayResize(statsArray, maxIndex - minIndex + 1); ArrayInitialize(statsArray, 0); } printAdvancedStats = advancedStats; ArrayResize(pairTops, totalSymbols); ArrayFill(pairTops, 0, totalSymbols, 0.0); ArrayResize(maxAbsDrawdown, totalSymbols); ArrayFill(maxAbsDrawdown, 0, totalSymbols, 0.0); maxGeneralTop = 0.0; maxAbsGeneralDrawdown = 0.0; use_advanced_ts = false; ArrayResize(trailingHandles, totalSymbols); for (int i=0; i < totalSymbols; i++) { buyArray[i].AdvancedSettings(sl_atr, tp_atr, start_move_sl, true); sellArray[i].AdvancedSettings(sl_atr, tp_atr, start_move_sl, true); } } void CompleteNNFXTester::SetTrailingHandle(int handle, int symbolIndex) { trailingHandles[symbolIndex] = handle; } void CompleteNNFXTester::SetAdvancedTrailingStops(int buy_buffer, int sell_buffer) { use_advanced_ts = true; adv_ts_buyBuffer = buy_buffer; adv_ts_sellBuffer = sell_buffer; } #ifdef __MQL4__ void CompleteNNFXTester::SetTrailingIndicatorProperties(string indicator_name,double ¶meters[]) { trailingIndName = indicator_name; ArrayCopy(trailingParameters, parameters); } #endif void CompleteNNFXTester::UseCurrencyExposure(void) { useExposure = true; ProcessCurrencies(); exposureControl = new ExposureController(currencyIndexesInfo); } #ifdef __MQL5__ void CompleteNNFXTester::SetRealTradeMode(void) { for (int i=0; i < totalSymbols; i++) { buyArray[i].SetRealTrades(); sellArray[i].SetRealTrades(); } } #endif void CompleteNNFXTester::UseNewsFiltering(string news_EUR, string news_GBP, string news_AUD, string news_NZD, string news_USD, string news_CAD, string news_CHF, string news_JPY) { useNews = true; ProcessCurrencies(); newsControl = new NewsController(currencyIndexesInfo); newsControl.ProcessNewsArrays(news_EUR, news_GBP, news_AUD, news_NZD, news_USD, news_CAD, news_CHF, news_JPY); } void CompleteNNFXTester::ProcessCurrencies(void) { if (currenciesProcessed) return; currenciesProcessed = true; currencyIndexesInfo = new CurrencyIndexObject(symbolsToTrade); } CompleteNNFXTester::~CompleteNNFXTester() { if (useExposure) delete exposureControl; if (useNews) delete newsControl; if (currenciesProcessed) delete currencyIndexesInfo; } double CompleteNNFXTester::TesterResult(int optimization_mode, int write_to_file, string custom_str) { #ifdef __MQL5__ if (optimization_mode == N_REAL_TRADES) return 0.0; //Don't do anything #endif CloseTradesAtEnd(); int numberOfLoses=0, numberOfWins=0; bool printStatistics = optimization_mode == N_DIST_SHAPE || optimization_mode == N_DIST_VALUE; for (int i = 0; i < totalSymbols; i++) { numberOfLoses += totalLosesArray[i]; numberOfWins += totalWinsArray[i]; if (!printStatistics) SymbolPrint(optimization_mode, symbolsToTrade[i], totalWinsArray[i], totalLosesArray[i], grossWinArray[i], grossLossArray[i], maxAbsDrawdown[i]); } //Statistics double statsShape=0.0, statsValue=0.0; if (recordStatistics) { CalculateStatistics(statsStep, maxIndex, minIndex, statsArray, statsShape, statsValue); if (printStatistics) { PrintStatsSummary(statsStep, maxIndex, minIndex, statsArray, statsShape, statsValue); if (write_to_file != NO_WRITE) WriteDistributionFile(statsStep, maxIndex, minIndex, statsArray); } } double g_winRate, g_profitFactor, g_expectedPayoff, g_ROI; CalculateGeneralStats(numberOfWins, numberOfLoses, grossWin, grossLoss, g_winRate, g_profitFactor, g_expectedPayoff); double years = ((TimeCurrent()-firstTradingDay)/(ONE_DAY * 365.0)); CalculateReturnOnInvestment((grossWin+grossLoss)/accountInitial, years, compoundInterest, g_ROI); // Final Summary Prints FinalPrints(optimization_mode, numberOfWins, numberOfLoses, grossWin, grossLoss, g_winRate); if (use_extended_summary) extended_summary.PrintExtendedSummary(); if (printAdvancedStats) { PrintAdvancedStats(g_profitFactor, maxAbsGeneralDrawdown, g_expectedPayoff, g_ROI); } if (write_to_file == SUMMARY) { WriteSummaryFile(optimization_mode, symbolsToTrade, totalWinsArray, totalLosesArray, grossWinArray, grossLossArray, maxAbsDrawdown, totalSymbols, years, accountInitial, compoundInterest); if (printAdvancedStats) { WriteMetricsInSummary(g_winRate, numberOfLoses+numberOfWins, g_profitFactor, g_expectedPayoff, -maxAbsGeneralDrawdown, g_ROI, grossWin+grossLoss, statsValue, statsShape); } } if (write_to_file == OPTIMIZE) { WriteOptimizeFile(optimization_mode, g_winRate, g_profitFactor, g_expectedPayoff, -maxAbsGeneralDrawdown, g_ROI, grossWin+grossLoss, statsValue, statsShape, numberOfLoses+numberOfWins); } DetectPossibleGaps(); if (optimization_mode == N_WIN_RATE) return g_winRate; if (optimization_mode == N_TOTAL_PIPS || optimization_mode == N_TOTAL_PROFIT) return grossWin+grossLoss; if (optimization_mode == N_DRAWDOWN) return -maxAbsGeneralDrawdown; if (optimization_mode == N_PROFIT_FACTOR) return g_profitFactor; if (optimization_mode == N_EXP_PAYOFF) return g_expectedPayoff; if (optimization_mode == N_DIST_VALUE) return statsValue; if (optimization_mode == N_DIST_SHAPE) return statsShape; if (optimization_mode == N_EQUITY_COMP || optimization_mode == N_EQUITY_CURVE) return grossWin + grossLoss; if (optimization_mode == N_ROI) return g_ROI; if (optimization_mode == N_CUSTOM_OPTIMIZE) { //Create Dictionary CDictionary* var_dict = new CDictionary(); var_dict.Set("#WR", g_winRate); var_dict.Set("#DD", maxAbsGeneralDrawdown); //POSITIVE var_dict.Set("#PF", g_profitFactor); var_dict.Set("#EP", g_expectedPayoff); var_dict.Set("#SV", statsValue); var_dict.Set("#SS", statsShape); var_dict.Set("#ROI", g_ROI); var_dict.Set("#NW", double(numberOfWins)); var_dict.Set("#NL", double(numberOfLoses)); var_dict.Set("#NT", double(numberOfWins+numberOfLoses)); //Number of trades var_dict.Set("#GW", grossWin); var_dict.Set("#GL", grossLoss); var_dict.Set("#FP", grossWin+grossLoss); //Final Profit double result = EvaluateCustomOptimization(custom_str, var_dict); delete var_dict; return result; } return 0.0; } void CompleteNNFXTester::BacktesterTick() { //Interes compuesto if (compoundInterest) RecalculateTradeValue(); //Evz, News y tal UpdateEvzValue(); if (useExposure || useNews) ArrayFree(tradesToExecute); if (useNews) newsControl.GetNewsOfCandle(TimeCurrent()); ///############################### GenericBacktester::BacktesterTick(); if (useNews) newsControl.DiscardTradesWithNews(tradesToExecute); if (useExposure) { //Calcular exposure (puede haber cerrado trades, pero no abierto nuevos) exposureControl.ResetExposure(); for (int i=0; i= evzHalfRisk { tradeValue = tradeValueInitial; currentMaxPairExposure = PERCENT_TO_EXPOSURE(tradePercent); } if (!scaleOut_halfRisk) //Sino, se mantiene como este al principio { if (currentEvz < evzHalfRisk) scaleOut = false; else scaleOut = scaleOutInitial; } } void CompleteNNFXTester::WaitMissedTrade(void) { //Print("Miss"); if (!useBaseline) { ChangeState(MAIN_SIGNAL); return; } int currentSignal, reverseSignal; string out_info = ""; if (CheckMainConfirmationSignal(out_info, BUY_SIGNAL, false)) { currentSignal = BUY_SIGNAL; reverseSignal = SELL_SIGNAL; } else if (CheckMainConfirmationSignal(out_info, SELL_SIGNAL, false)) { currentSignal = SELL_SIGNAL; reverseSignal = BUY_SIGNAL; } else { return; } if (CheckExitIndicator(out_info, reverseSignal)) //Si son opuestos, uno da salida { ChangeState(CONTINUATION); return; } // Se da la vuelta, Continuation if (useMainForExit && !CheckBaseline(out_info, currentSignal)) { ChangeState(CONTINUATION); return; } } void CompleteNNFXTester::ExecuteBuy(string where, int signal_place) { if (tradeValue == 0.0) { DebugFilteredTrade(BUY_SIGNAL, where, "EVZ", ""); if (useBaseline) ChangeState(MISSED_TRADE); else ChangeState(MAIN_SIGNAL); return; } if (!buyArray[activeSymbol].CheckIfOpen()) { //Guardar trades en arrays para procesar al final, y comprobar exposure double price = currentRates[DATA_RECENT].close; datetime time = currentRates[DATA_RECENT].time; #ifdef __MQL5__ double atr = atrCurrentValues[DATA_RECENT]; #else double atr = GetIndicatorValue(ATR_IND, DISPLACEMENT); #endif if (useExposure || useNews) { int tradeAmount = ArraySize(tradesToExecute); ArrayResize(tradesToExecute, tradeAmount+1); tradesToExecute[tradeAmount].open_price = price; tradesToExecute[tradeAmount].atr = atr; tradesToExecute[tradeAmount].open_time = time; tradesToExecute[tradeAmount].order_type = ORDER_TYPE_BUY; tradesToExecute[tradeAmount].symbol_index = activeSymbol; tradesToExecute[tradeAmount].result_exposure = useExposure ? 0.0 : currentMaxPairExposure; tradesToExecute[tradeAmount].trade_procedence = signal_place; SetStateBeforeExposure(signal_place); } else { buyArray[activeSymbol].OpenTrade(price, atr, ORDER_TYPE_BUY, time, tradeValue, scaleOut, currentMaxPairExposure); if (writeJournal) WriteInJournal(time, symbolsToTrade[activeSymbol], 0.0, "BUY OPEN"); DebugCompleteTrade(ORDER_TYPE_BUY, where); } color arrowColor; string arrowString; GetTradeIconSets(signal_place, arrowColor, arrowString); CreateNewIcon(TRADE_SYMBOL, arrowString, arrowColor); } else { DebugFilteredTrade(ORDER_TYPE_BUY, where, "ALREADY OPEN", ""); } } void CompleteNNFXTester::ExecuteSell(string where, int signal_place) { if (tradeValue == 0.0) { DebugFilteredTrade(SELL_SIGNAL, where, "EVZ", ""); if (useBaseline) ChangeState(MISSED_TRADE); else ChangeState(MAIN_SIGNAL); return; } if (!sellArray[activeSymbol].CheckIfOpen()) { //Guardar trades en arrays para procesar al final, y comprobar exposure double price = currentRates[DATA_RECENT].close; datetime time = currentRates[DATA_RECENT].time; #ifdef __MQL5__ double atr = atrCurrentValues[DATA_RECENT]; #else double atr = GetIndicatorValue(ATR_IND, DISPLACEMENT); #endif if (useExposure || useNews) { int tradeAmount = ArraySize(tradesToExecute); ArrayResize(tradesToExecute, tradeAmount+1); tradesToExecute[tradeAmount].open_price = price; tradesToExecute[tradeAmount].atr = atr; tradesToExecute[tradeAmount].open_time = time; tradesToExecute[tradeAmount].order_type = ORDER_TYPE_SELL; tradesToExecute[tradeAmount].symbol_index = activeSymbol; tradesToExecute[tradeAmount].result_exposure = useExposure ? 0.0 : currentMaxPairExposure; tradesToExecute[tradeAmount].trade_procedence = signal_place; SetStateBeforeExposure(signal_place); } else { sellArray[activeSymbol].OpenTrade(price, atr, ORDER_TYPE_SELL, time, tradeValue, scaleOut, currentMaxPairExposure); if (writeJournal) WriteInJournal(time, symbolsToTrade[activeSymbol], 0.0, "SELL OPEN"); DebugCompleteTrade(ORDER_TYPE_SELL, where); } color arrowColor; string arrowString; GetTradeIconSets(signal_place, arrowColor, arrowString); CreateNewIcon(TRADE_SYMBOL, arrowString, arrowColor); } else { DebugFilteredTrade(ORDER_TYPE_SELL, where, "ALREADY OPEN", ""); } } void CompleteNNFXTester::AddProfits(double amount, double trade_elapsed) { GenericBacktester::AddProfits(amount, trade_elapsed); if (amount == 0.0) return; //drawdown pair y general if (maxGeneralTop < (grossWin + grossLoss)) maxGeneralTop = grossWin + grossLoss; if (pairTops[activeSymbol] < (grossWinArray[activeSymbol] + grossLossArray[activeSymbol])) pairTops[activeSymbol] = grossWinArray[activeSymbol] + grossLossArray[activeSymbol]; double currentDD, currentSymbolDD; if (compoundInterest) { currentDD = (maxGeneralTop - (grossWin + grossLoss)) / (accountInitial + grossWin + grossLoss)*100.0; currentSymbolDD = (pairTops[activeSymbol] - (grossWinArray[activeSymbol] + grossLossArray[activeSymbol])) / (accountInitial + grossWinArray[activeSymbol] + grossLossArray[activeSymbol])*100.0; } else { currentDD = (maxGeneralTop - (grossWin + grossLoss)) / accountInitial*100.0; currentSymbolDD = (pairTops[activeSymbol] - (grossWinArray[activeSymbol] + grossLossArray[activeSymbol])) / accountInitial*100.0; } if (maxAbsGeneralDrawdown < currentDD) maxAbsGeneralDrawdown = currentDD; if (maxAbsDrawdown[activeSymbol] < currentSymbolDD) maxAbsDrawdown[activeSymbol] = currentSymbolDD; if (recordStatistics) { int current = (int)MathRound(amount / statsStep); if (current > maxIndex) { ArrayResize(statsArray, current - minIndex + 1); ArrayFill(statsArray, maxIndex - minIndex, current - maxIndex+1, 0); maxIndex = current; } if (current < minIndex) { current = minIndex; Print("STAT INDEX OUT OF RANGE"); } statsArray[current - minIndex] += 1; } } void CompleteNNFXTester::RecalculateTradeValue(void) { //Solo para interes compuesto tradeValueInitial = tradePercent * (accountInitial + grossLoss + grossWin); } void CompleteNNFXTester::ExecuteTradesAfterExposureCheck() { for (int i=0; i Gap en la siguiente vela (no actual) y sin displaceCorrection, se ignora el desplazamiento } else { finalDisplace = displaceCorrection[activeSymbol] + DISPLACEMENT; } double trailingValue[]; #endif double tValue; if (buyArray[activeSymbol].HasTouchedTP()) { if (use_advanced_ts) { #ifdef __MQL5__ CopyBuffer(trailingHandles[activeSymbol], adv_ts_buyBuffer, finalDisplace, 1, trailingValue); tValue = trailingValue[0]; #else tValue = GetICustom(trailingIndName, trailingParameters, adv_ts_buyBuffer, DISPLACEMENT); #endif buyArray[activeSymbol].UpdateTrailingStopToValue(currentRates[DATA_RECENT].close, tValue, currentRates[DATA_RECENT].time); } else buyArray[activeSymbol].UpdateTrailingStop(currentRates[DATA_RECENT].close, currentRates[DATA_RECENT].time); } if (sellArray[activeSymbol].HasTouchedTP()) { if (use_advanced_ts) { #ifdef __MQL5__ CopyBuffer(trailingHandles[activeSymbol], adv_ts_sellBuffer, finalDisplace, 1, trailingValue); tValue = trailingValue[0]; #else tValue = GetICustom(trailingIndName, trailingParameters, adv_ts_sellBuffer, DISPLACEMENT); #endif sellArray[activeSymbol].UpdateTrailingStopToValue(currentRates[DATA_RECENT].close, tValue, currentRates[DATA_RECENT].time); } else sellArray[activeSymbol].UpdateTrailingStop(currentRates[DATA_RECENT].close, currentRates[DATA_RECENT].time); } } void CompleteNNFXTester::CheckForNewsExit() //En Generic Backtester { if (!useNews) return; int longId, shortId; currencyIndexesInfo.GetLongShortIds(activeSymbol, ORDER_TYPE_BUY, longId, shortId); //No importa Order Type if (newsControl.CheckNewsOfPair(longId, shortId)) { if (buyArray[activeSymbol].GetExposure()>0.0) { if (CloseTrade(ORDER_TYPE_BUY, CTP_NEWS)) CreateNewIcon(174, "EXIT BY NEWS", NEWS_COLOR); } if (sellArray[activeSymbol].GetExposure()>0.0) { if (CloseTrade(ORDER_TYPE_SELL, CTP_NEWS)) CreateNewIcon(174, "EXIT BY NEWS", NEWS_COLOR); } } } void CompleteNNFXTester::DetectPossibleGaps(void) { int symbolCandles[]; ArrayResize(symbolCandles, totalSymbols); int maxCandles = iBarShift(Symbol(), PERIOD_CURRENT, firstTradingDay); bool detected = false; int i; for (i=0; i