Warrior_EA/Database/TradeJournalReport.mqh

276 lines
14 KiB
MQL5
Raw Permalink Normal View History

//+------------------------------------------------------------------+
//| 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;
}