//+------------------------------------------------------------------+ //| TradeJournalReport.mqh | //| AnimateDread | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "AnimateDread" #property link "https://www.mql5.com" //--- Included from TradeJournalManager.mqh, after CTradeJournalManager's own declaration - this file //--- only implements that class's GenerateReport() (out-of-line, same pattern the AI\Network.mqh //--- classes use), kept in its own file so the always-running per-tick tracking code in //--- TradeJournalManager.mqh doesn't have to carry this on-demand reporting/insights logic with it. //--- Sample-size floor before a bucket is trusted enough to base a suggestion on. #define JOURNAL_MIN_INSIGHT_SAMPLES 10 //--- A bucket is "underperforming" once its win rate trails the comparison bucket by this many //--- percentage points. #define JOURNAL_UNDERPERFORM_DELTA_PP 15.0 //--- A non-TP close counts as a "near miss" if price got at least this fraction of the way to the //--- take-profit (in R-multiples) before reversing. #define JOURNAL_NEARMISS_MFE_FRACTION 0.8 //--- Near-miss suggestion fires once this fraction of non-TP closes qualify as near misses. #define JOURNAL_NEARMISS_FLAG_FRACTION 0.30 //--- Stop-loss hits that overshoot the stop by less than this many R are "tight" (average, across //--- all SL-stopped trades). #define JOURNAL_SLTIGHT_OVERSHOOT_R 0.15 //--- SL-tight suggestion fires once stop-outs make up at least this fraction of ALL closed trades. #define JOURNAL_SLTIGHT_FLAG_FRACTION 0.30 struct SJournalBucket { int n; int wins; double sumR; }; void JournalBucketZero(SJournalBucket &b) { b.n = 0; b.wins = 0; b.sumR = 0.0; } //--- win/loss is decided on real profit, not the derived R-multiple (which is forced to 0 whenever //--- riskDistance is 0 - practically never with this EA's SL modes, but real money P&L is the more //--- correct signal regardless); rMultiple is only ever used for the magnitude (avg R) stat. void JournalBucketAdd(SJournalBucket &b, double profit, double rMultiple) { b.n++; if(profit > 0.0) b.wins++; b.sumR += rMultiple; } double JournalBucketWinRate(const SJournalBucket &b) { return (b.n > 0) ? 100.0 * b.wins / b.n : 0.0; } double JournalBucketAvgR(const SJournalBucket &b) { return (b.n > 0) ? b.sumR / b.n : 0.0; } //+------------------------------------------------------------------+ //| Reads every closed trade back out of TradeJournal, aggregates it | //| by hour/day-of-week/AI-confidence bucket, derives a short list of| //| plain-language settings suggestions from repeating weak spots, | //| and writes all of it to a CSV file. resultPath comes back as the | //| absolute path so the caller can tell the trader exactly where to | //| find it; errorMsg is set (and false returned) on any failure. | //+------------------------------------------------------------------+ bool CTradeJournalManager::GenerateReport(string &resultPath, string &errorMsg) { resultPath = ""; errorMsg = ""; if(CheckPointer(m_dbm) == POINTER_INVALID) { errorMsg = "database not initialized"; return false; } if(!m_dbm.BeginTransaction()) { errorMsg = "could not open database"; return false; } STradeJournalRecord rec, records[]; bool fetched = m_dbm.FetchTradeRecords(m_tableName, rec, records); m_dbm.CommitTransaction(); if(!fetched) { errorMsg = "failed to read the trade journal table"; return false; } int total = ArraySize(records); if(total == 0) { errorMsg = "no closed trades recorded yet - nothing to report"; return false; } SJournalBucket overall; JournalBucketZero(overall); SJournalBucket perHour[24]; for(int h = 0; h < 24; h++) JournalBucketZero(perHour[h]); SJournalBucket perDow[7]; for(int d = 0; d < 7; d++) JournalBucketZero(perDow[d]); //--- confidence buckets: 50-60/60-70/70-80/80-90/90-100 % - only AI-driven trades (aiConfidence>0) //--- fall into these; classic-signal-only trades leave every bucket untouched, which is correct. SJournalBucket perConf[5]; for(int c = 0; c < 5; c++) JournalBucketZero(perConf[c]); int nearMissCount = 0, nonTPCloses = 0; double slOvershootSum = 0.0; int slCount = 0; for(int i = 0; i < total; i++) { STradeJournalRecord r = records[i]; JournalBucketAdd(overall, r.profit, r.rMultiple); if(r.openHour >= 0 && r.openHour < 24) JournalBucketAdd(perHour[r.openHour], r.profit, r.rMultiple); if(r.openDayOfWeek >= 0 && r.openDayOfWeek < 7) JournalBucketAdd(perDow[r.openDayOfWeek], r.profit, r.rMultiple); if(r.aiConfidence > 0.0) { int bucket = (int)MathFloor((r.aiConfidence * 100.0 - 50.0) / 10.0); if(bucket >= 0 && bucket < 5) JournalBucketAdd(perConf[bucket], r.profit, r.rMultiple); } //--- TP-miss: a real TP was set, this trade didn't close via TP, but price still got most of //--- the way there (in R-multiples) before reversing. bool hasTP = (r.tpPrice > 0.0 && r.riskDistance > 0.0); if(hasTP && r.exitReason != "TP") { double tpDistance = MathAbs(r.tpPrice - r.entryPrice); double tpR = tpDistance / r.riskDistance; nonTPCloses++; if(tpR > 0.0 && r.mfeR >= JOURNAL_NEARMISS_MFE_FRACTION * tpR) nearMissCount++; } //--- SL-tight: how far PAST the exact 1R stop level price actually traveled before the stop //--- executed (spread/slippage means this is rarely exactly 0). if(r.exitReason == "SL" && r.riskDistance > 0.0) { slCount++; slOvershootSum += (r.maeR - 1.0); } } double overallWR = JournalBucketWinRate(overall); double overallAvgR = JournalBucketAvgR(overall); string dowNames[7] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; string confLabels[5] = {"50-60%", "60-70%", "70-80%", "80-90%", "90-100%"}; string suggestions[]; int sc = 0; for(int h = 0; h < 24; h++) { if(perHour[h].n < JOURNAL_MIN_INSIGHT_SAMPLES) continue; double wr = JournalBucketWinRate(perHour[h]); if(overallWR - wr >= JOURNAL_UNDERPERFORM_DELTA_PP) { ArrayResize(suggestions, sc + 1); suggestions[sc++] = StringFormat( "Trades opened around %02d:00 win %.0f%% of the time vs %.0f%% overall (n=%d) - consider adding hour %d to the Intraday Time Filter's Bad Hours, or narrowing the Session Filter.", h, wr, overallWR, perHour[h].n, h); } } for(int d = 0; d < 7; d++) { if(perDow[d].n < JOURNAL_MIN_INSIGHT_SAMPLES) continue; double wr = JournalBucketWinRate(perDow[d]); if(overallWR - wr >= JOURNAL_UNDERPERFORM_DELTA_PP) { ArrayResize(suggestions, sc + 1); suggestions[sc++] = StringFormat( "%s trades win %.0f%% of the time vs %.0f%% overall (n=%d) - consider adding %s to the Intraday Time Filter's Bad Days.", dowNames[d], wr, overallWR, perDow[d].n, dowNames[d]); } } if(nonTPCloses >= JOURNAL_MIN_INSIGHT_SAMPLES) { double frac = (double)nearMissCount / nonTPCloses; if(frac >= JOURNAL_NEARMISS_FLAG_FRACTION) { ArrayResize(suggestions, sc + 1); suggestions[sc++] = StringFormat( "%.0f%% of trades that didn't hit their take-profit (n=%d) still came within %.0f%% of the target before reversing - consider a nearer take-profit (lower ATR multiple) or the Intelligent TP mode.", frac * 100.0, nonTPCloses, JOURNAL_NEARMISS_MFE_FRACTION * 100.0); } } if(slCount >= JOURNAL_MIN_INSIGHT_SAMPLES) { double avgOvershoot = slOvershootSum / slCount; double slFractionOfAll = (double)slCount / total; if(avgOvershoot <= JOURNAL_SLTIGHT_OVERSHOOT_R && slFractionOfAll >= JOURNAL_SLTIGHT_FLAG_FRACTION) { ArrayResize(suggestions, sc + 1); suggestions[sc++] = StringFormat( "%.0f%% of all trades (n=%d) were stopped out, typically only around %.0f%% beyond the stop level - consider widening the stop-loss (higher ATR multiple) or the Intelligent SL mode.", slFractionOfAll * 100.0, slCount, avgOvershoot * 100.0); } } int highestReliableBucket = -1; for(int c = 4; c >= 0; c--) if(perConf[c].n >= JOURNAL_MIN_INSIGHT_SAMPLES) { highestReliableBucket = c; break; } if(highestReliableBucket > 0) { double topWR = JournalBucketWinRate(perConf[highestReliableBucket]); for(int c = 0; c < highestReliableBucket; c++) { if(perConf[c].n >= JOURNAL_MIN_INSIGHT_SAMPLES && topWR - JournalBucketWinRate(perConf[c]) >= JOURNAL_UNDERPERFORM_DELTA_PP) { ArrayResize(suggestions, sc + 1); suggestions[sc++] = StringFormat( "AI confidence %s wins %.0f%% of the time (n=%d) vs %.0f%% at %s (n=%d) - consider raising Min vote to open toward %s.", confLabels[c], JournalBucketWinRate(perConf[c]), perConf[c].n, topWR, confLabels[highestReliableBucket], perConf[highestReliableBucket].n, confLabels[highestReliableBucket]); break; // one clear suggestion here is more useful than a wall of overlapping ones } } } if(sc == 0) { ArrayResize(suggestions, 1); suggestions[0] = "No statistically-repeating weak spot found yet (or not enough trades per bucket - need at least " + IntegerToString(JOURNAL_MIN_INSIGHT_SAMPLES) + ") - keep trading and re-run this report periodically."; sc = 1; } string relativeDir = eaName + "\\Reports\\"; MqlDateTime now; TimeToStruct(TimeCurrent(), now); string stamp = StringFormat("%04d%02d%02d_%02d%02d%02d", now.year, now.mon, now.day, now.hour, now.min, now.sec); string relativeFile = relativeDir + _Symbol + "_" + IntegerToString((int)_Period) + "_" + stamp + ".csv"; int handle = FileOpen(relativeFile, FILE_WRITE | FILE_ANSI | FILE_COMMON); if(handle == INVALID_HANDLE) { errorMsg = "failed to create report file, error " + IntegerToString(GetLastError()); return false; } FileWriteString(handle, "Warrior EA Trade Journal Report\r\n"); FileWriteString(handle, "Generated," + TimeToString(TimeCurrent(), TIME_DATE | TIME_MINUTES) + "\r\n"); FileWriteString(handle, "Symbol," + _Symbol + ",Timeframe," + EnumToString((ENUM_TIMEFRAMES)_Period) + "\r\n"); FileWriteString(handle, "Total closed trades," + IntegerToString(total) + "\r\n"); FileWriteString(handle, "Overall win rate (%)," + DoubleToString(overallWR, 1) + "\r\n"); FileWriteString(handle, "Overall avg R-multiple," + DoubleToString(overallAvgR, 3) + "\r\n\r\n"); FileWriteString(handle, "SUGGESTIONS\r\n"); for(int i = 0; i < sc; i++) FileWriteString(handle, "\"" + suggestions[i] + "\"\r\n"); FileWriteString(handle, "\r\nBY HOUR OF DAY\r\n"); FileWriteString(handle, "Hour,Trades,Win rate %,Avg R\r\n"); for(int h = 0; h < 24; h++) if(perHour[h].n > 0) FileWriteString(handle, IntegerToString(h) + "," + IntegerToString(perHour[h].n) + "," + DoubleToString(JournalBucketWinRate(perHour[h]), 1) + "," + DoubleToString(JournalBucketAvgR(perHour[h]), 3) + "\r\n"); FileWriteString(handle, "\r\nBY DAY OF WEEK\r\n"); FileWriteString(handle, "Day,Trades,Win rate %,Avg R\r\n"); for(int d = 0; d < 7; d++) if(perDow[d].n > 0) FileWriteString(handle, dowNames[d] + "," + IntegerToString(perDow[d].n) + "," + DoubleToString(JournalBucketWinRate(perDow[d]), 1) + "," + DoubleToString(JournalBucketAvgR(perDow[d]), 3) + "\r\n"); FileWriteString(handle, "\r\nBY AI CONFIDENCE AT ENTRY\r\n"); FileWriteString(handle, "Confidence,Trades,Win rate %,Avg R\r\n"); for(int c = 0; c < 5; c++) if(perConf[c].n > 0) FileWriteString(handle, confLabels[c] + "," + IntegerToString(perConf[c].n) + "," + DoubleToString(JournalBucketWinRate(perConf[c]), 1) + "," + DoubleToString(JournalBucketAvgR(perConf[c]), 3) + "\r\n"); FileWriteString(handle, "\r\nRAW TRADES\r\n"); FileWriteString(handle, "Ticket,OpenTime,CloseTime,Symbol,Direction,Entry,Exit,SL,TP,Lots,Profit,R-Multiple,MAE(R),MFE(R),AIConf,DBConf,ExitReason,Filter\r\n"); for(int i = 0; i < total; i++) { STradeJournalRecord r = records[i]; string openStr = StringFormat("%04d.%02d.%02d %02d:%02d", r.openYear, r.openMonth, r.openDay, r.openHour, r.openMinute); string closeStr = StringFormat("%04d.%02d.%02d %02d:%02d", r.closeYear, r.closeMonth, r.closeDay, r.closeHour, r.closeMinute); FileWriteString(handle, IntegerToString(r.ticket) + "," + openStr + "," + closeStr + "," + r.symbol + "," + r.direction + "," + DoubleToString(r.entryPrice, _Digits) + "," + DoubleToString(r.exitPrice, _Digits) + "," + DoubleToString(r.slPrice, _Digits) + "," + DoubleToString(r.tpPrice, _Digits) + "," + DoubleToString(r.lots, 2) + "," + DoubleToString(r.profit, 2) + "," + DoubleToString(r.rMultiple, 3) + "," + DoubleToString(r.maeR, 3) + "," + DoubleToString(r.mfeR, 3) + "," + DoubleToString(r.aiConfidence, 3) + "," + DoubleToString(r.dbConfidence, 3) + "," + r.exitReason + "," + r.filterID + "\r\n"); } FileClose(handle); resultPath = TerminalInfoString(TERMINAL_COMMONDATA_PATH) + "\\Files\\" + relativeFile; return true; }