forked from animatedread/Warrior_EA
CTradeJournalManager::GenerateReport() mixed four jobs in one 211-line method: DB fetch, per-hour/day/confidence aggregation, suggestion-derivation, and CSV formatting. Split into FetchClosedTrades/AggregateJournalStats/DeriveSuggestions/ WriteJournalReportCsv, each independently testable/replaceable; GenerateReport is now a 12-line orchestrator. AggregateJournalStats touches no class member so it stays a free function alongside the existing JournalBucket* helpers (moved next to SJournalStats, ahead of the class, since the new method signatures reference it); Fetch/Derive/Write stay private methods since Derive needs the already-private AddSuggestion. Pure relocation - every quoted string literal and if/for/return count verified identical (net of the intentional new step-boundary guards/returns) against the pre-edit file.
291 lines
16 KiB
MQL5
291 lines
16 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| 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);
|
|
}
|