forked from animatedread/Warrior_EA
Removes standalone AI confidence parameters (MinAIConfidence, MinAIExitConfidence) and replaces them with unified Min_Vote_Open and Min_Vote_Close thresholds that apply to both AI and classic engines. Updates all code comments, report suggestions, and market descriptions accordingly, simplifying configuration and ensuring consistent vote requirements across entry and exit logic.
276 lines
14 KiB
MQL5
276 lines
14 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() (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;
|
|
}
|