2026-07-22 22:51:04 -04:00
//+------------------------------------------------------------------+
//| 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 (
2026-07-26 17:27:51 -04:00
" AI confidence %s wins %.0f%% of the time (n=%d) vs %.0f%% at %s (n=%d) - consider raising Min vote to open toward %s. " ,
2026-07-22 22:51:04 -04:00
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 \n BY 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 \n BY 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 \n BY 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 \n RAW 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 ;
}