//+------------------------------------------------------------------+ //| 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() pipeline (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. //--- GenerateReport() itself is a thin orchestrator over four independent steps, each below in the //--- order it runs: FetchClosedTrades (DB read), AggregateJournalStats (a free function - it touches //--- no class member), DeriveSuggestions (the plain-language tuning rules), WriteJournalReportCsv //--- (formatting + FileWrite). Splitting them keeps each independently testable/replaceable - e.g. //--- swapping the CSV writer for a different export format never touches the stats math. //--- 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 //--- shared display labels - DeriveSuggestions and WriteJournalReportCsv both index into these. const string JournalDowNames[7] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; const string JournalConfLabels[5] = {"50-60%", "60-70%", "70-80%", "80-90%", "90-100%"}; //+------------------------------------------------------------------+ //| Appends one already-formatted suggestion string to the array and | //| advances the count - the resize+assign+increment DeriveSuggestions| //| below repeats at every one of its suggestion sites. | //+------------------------------------------------------------------+ void CTradeJournalManager::AddSuggestion(string &suggestions[], int &sc, const string text) { ArrayResize(suggestions, sc + 1); suggestions[sc++] = text; } //+------------------------------------------------------------------+ //| Step 1: reads every closed trade back out of TradeJournal. | //| errorMsg is set (and false returned) on any failure, including an | //| empty journal - there is nothing to aggregate/report on that. | //+------------------------------------------------------------------+ bool CTradeJournalManager::FetchClosedTrades(STradeJournalRecord &records[], string &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; bool fetched = m_dbm.FetchTradeRecords(m_tableName, rec, records); m_dbm.CommitTransaction(); if(!fetched) { errorMsg = "failed to read the trade journal table"; return false; } if(ArraySize(records) == 0) { errorMsg = "no closed trades recorded yet - nothing to report"; return false; } return true; } //+------------------------------------------------------------------+ //| Step 2 (free function - touches no class member): aggregates the | //| fetched rows by hour/day-of-week/AI-confidence bucket, plus the | //| near-miss/SL-tight running sums DeriveSuggestions needs. | //+------------------------------------------------------------------+ void AggregateJournalStats(const STradeJournalRecord &records[], SJournalStats &stats) { JournalStatsZero(stats); stats.total = ArraySize(records); for(int i = 0; i < stats.total; i++) { STradeJournalRecord r = records[i]; JournalBucketAdd(stats.overall, r.profit, r.rMultiple); if(r.openHour >= 0 && r.openHour < 24) JournalBucketAdd(stats.perHour[r.openHour], r.profit, r.rMultiple); if(r.openDayOfWeek >= 0 && r.openDayOfWeek < 7) JournalBucketAdd(stats.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(stats.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; stats.nonTPCloses++; if(tpR > 0.0 && r.mfeR >= JOURNAL_NEARMISS_MFE_FRACTION * tpR) stats.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) { stats.slCount++; stats.slOvershootSum += (r.maeR - 1.0); } } } //+------------------------------------------------------------------+ //| Step 3: derives a short list of plain-language settings | //| suggestions from repeating weak spots in the already-aggregated | //| stats. Returns the suggestion count (also ArraySize(suggestions)).| //+------------------------------------------------------------------+ int CTradeJournalManager::DeriveSuggestions(const SJournalStats &stats, string &suggestions[]) { double overallWR = JournalBucketWinRate(stats.overall); int sc = 0; for(int h = 0; h < 24; h++) { if(stats.perHour[h].n < JOURNAL_MIN_INSIGHT_SAMPLES) continue; double wr = JournalBucketWinRate(stats.perHour[h]); if(overallWR - wr >= JOURNAL_UNDERPERFORM_DELTA_PP) { AddSuggestion(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, stats.perHour[h].n, h)); } } for(int d = 0; d < 7; d++) { if(stats.perDow[d].n < JOURNAL_MIN_INSIGHT_SAMPLES) continue; double wr = JournalBucketWinRate(stats.perDow[d]); if(overallWR - wr >= JOURNAL_UNDERPERFORM_DELTA_PP) { AddSuggestion(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.", JournalDowNames[d], wr, overallWR, stats.perDow[d].n, JournalDowNames[d])); } } if(stats.nonTPCloses >= JOURNAL_MIN_INSIGHT_SAMPLES) { double frac = (double)stats.nearMissCount / stats.nonTPCloses; if(frac >= JOURNAL_NEARMISS_FLAG_FRACTION) { AddSuggestion(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, stats.nonTPCloses, JOURNAL_NEARMISS_MFE_FRACTION * 100.0)); } } if(stats.slCount >= JOURNAL_MIN_INSIGHT_SAMPLES) { double avgOvershoot = stats.slOvershootSum / stats.slCount; double slFractionOfAll = (double)stats.slCount / stats.total; if(avgOvershoot <= JOURNAL_SLTIGHT_OVERSHOOT_R && slFractionOfAll >= JOURNAL_SLTIGHT_FLAG_FRACTION) { AddSuggestion(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, stats.slCount, avgOvershoot * 100.0)); } } int highestReliableBucket = -1; for(int c = 4; c >= 0; c--) if(stats.perConf[c].n >= JOURNAL_MIN_INSIGHT_SAMPLES) { highestReliableBucket = c; break; } if(highestReliableBucket > 0) { double topWR = JournalBucketWinRate(stats.perConf[highestReliableBucket]); for(int c = 0; c < highestReliableBucket; c++) { if(stats.perConf[c].n >= JOURNAL_MIN_INSIGHT_SAMPLES && topWR - JournalBucketWinRate(stats.perConf[c]) >= JOURNAL_UNDERPERFORM_DELTA_PP) { AddSuggestion(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.", JournalConfLabels[c], JournalBucketWinRate(stats.perConf[c]), stats.perConf[c].n, topWR, JournalConfLabels[highestReliableBucket], stats.perConf[highestReliableBucket].n, JournalConfLabels[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; } return sc; } //+------------------------------------------------------------------+ //| Step 4: formats the summary header, suggestions, the 3 bucket | //| tables and the full raw-trade dump as CSV and writes it to a 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::WriteJournalReportCsv(const SJournalStats &stats, const string &suggestions[], const int sc, const STradeJournalRecord &records[], string &resultPath, string &errorMsg) { double overallWR = JournalBucketWinRate(stats.overall); double overallAvgR = JournalBucketAvgR(stats.overall); 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 | FILE_SHARE_READ | FILE_SHARE_WRITE); 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(stats.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(stats.perHour[h].n > 0) FileWriteString(handle, IntegerToString(h) + "," + IntegerToString(stats.perHour[h].n) + "," + DoubleToString(JournalBucketWinRate(stats.perHour[h]), 1) + "," + DoubleToString(JournalBucketAvgR(stats.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(stats.perDow[d].n > 0) FileWriteString(handle, JournalDowNames[d] + "," + IntegerToString(stats.perDow[d].n) + "," + DoubleToString(JournalBucketWinRate(stats.perDow[d]), 1) + "," + DoubleToString(JournalBucketAvgR(stats.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(stats.perConf[c].n > 0) FileWriteString(handle, JournalConfLabels[c] + "," + IntegerToString(stats.perConf[c].n) + "," + DoubleToString(JournalBucketWinRate(stats.perConf[c]), 1) + "," + DoubleToString(JournalBucketAvgR(stats.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 < stats.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; } //+------------------------------------------------------------------+ //| Orchestrates the four steps above in order: fetch, aggregate, | //| derive suggestions, write. Each step alone decides failure; this | //| function just wires them together and stops at the first one that| //| returns false. | //+------------------------------------------------------------------+ bool CTradeJournalManager::GenerateReport(string &resultPath, string &errorMsg) { resultPath = ""; errorMsg = ""; STradeJournalRecord records[]; if(!FetchClosedTrades(records, errorMsg)) return false; SJournalStats stats; AggregateJournalStats(records, stats); string suggestions[]; int sc = DeriveSuggestions(stats, suggestions); return WriteJournalReportCsv(stats, suggestions, sc, records, resultPath, errorMsg); }