2026-07-22 22:51:04 -04:00
//+------------------------------------------------------------------+
//| TradeJournalManager.mqh |
//| AnimateDread |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
# property copyright " AnimateDread "
# property link " https://www.mql5.com "
# include "DatabaseManager.mqh"
# include "..\Variables\ConfidenceBridge.mqh"
//+------------------------------------------------------------------+
//| One closed trade, as persisted to/read from the TradeJournal |
//| table. Field ORDER matters - DatabaseReadBind()/InsertTradeRecord |
//| match it positionally against the table's own column order |
//| (TradeJournalSchema below), not by name. |
//+------------------------------------------------------------------+
struct STradeJournalRecord
{
long ticket ;
int openYear , openMonth , openDay , openDayOfWeek , openHour , openMinute ;
int closeYear , closeMonth , closeDay , closeHour , closeMinute ;
string symbol ;
string direction ;
double entryPrice , exitPrice , slPrice , tpPrice , lots ;
double profit ; // real net P&L: deal profit + swap + commission
double riskDistance ; // |entryPrice-slPrice| at open; 0 if no SL was set
double rMultiple ; // realized price move / riskDistance; 0 if riskDistance is 0
double maePoints , mfePoints ; // worst adverse / best favorable excursion, price units, >=0
double maeR , mfeR ; // same, normalized by riskDistance; 0 if riskDistance is 0
double aiConfidence , dbConfidence ; // 0..1, snapshotted at entry
string exitReason ; // SL/TP/Expert/Manual/StopOut/Other - from the closing deal's DEAL_REASON
string filterID ; // which engine was driving trades this run (AIType name, or "Classic")
} ;
//--- column order matches STradeJournalRecord's field order exactly (see struct comment above)
const string TradeJournalSchema =
" ticket INTEGER, "
" openYear INTEGER, openMonth INTEGER, openDay INTEGER, openDayOfWeek INTEGER, openHour INTEGER, openMinute INTEGER, "
" closeYear INTEGER, closeMonth INTEGER, closeDay INTEGER, closeHour INTEGER, closeMinute INTEGER, "
" symbol TEXT, direction TEXT, "
" entryPrice REAL, exitPrice REAL, slPrice REAL, tpPrice REAL, lots REAL, "
" profit REAL, riskDistance REAL, rMultiple REAL, "
" maePoints REAL, mfePoints REAL, maeR REAL, mfeR REAL, "
" aiConfidence REAL, dbConfidence REAL, "
" exitReason TEXT, filterID TEXT " ;
//+------------------------------------------------------------------+
//| In-memory tracking for a still-open position - MAE/MFE can only |
//| be measured live, tick by tick, while the position exists; there |
//| is no post-hoc MQL5 API to recover it once the position is gone. |
//+------------------------------------------------------------------+
struct SJournalOpenTrack
{
ulong ticket ;
datetime openTime ;
string symbol ;
string direction ;
double entryPrice , slPrice , tpPrice , lots ;
double riskDistance ;
double aiConfidence , dbConfidence ;
string filterID ;
double maePoints ;
double mfePoints ;
} ;
//+------------------------------------------------------------------+
//| Owns the TradeJournal table: detects this EA's own positions |
//| opening/closing (by polling PositionsTotal() every tick rather |
//| than hooking OnTradeTransaction - simpler and robust against |
//| partial fills/multiple deals per position), tracks MAE/MFE live |
//| while a position is open, and resolves the real closing P&L/ |
//| reason from deal history (HistoryDealGetInteger(DEAL_REASON) - |
//| broker-confirmed, not a heuristic) once it closes. |
//+------------------------------------------------------------------+
class CTradeJournalManager
{
private :
CDatabaseManager * m_dbm ;
ulong m_magic ;
string m_tableName ;
SJournalOpenTrack m_tracked [ ] ;
int FindTracked ( ulong ticket )
{
for ( int i = 0 ; i < ArraySize ( m_tracked ) ; i + + )
if ( m_tracked [ i ] . ticket = = ticket )
return i ;
return -1 ;
}
void RemoveTracked ( int idx )
{
int last = ArraySize ( m_tracked ) - 1 ;
if ( idx < 0 | | idx > last )
return ;
if ( idx ! = last )
m_tracked [ idx ] = m_tracked [ last ] ;
ArrayResize ( m_tracked , last ) ;
}
string CurrentFilterID ( void )
{
if ( AIType = = AI_NONE )
return " Classic " ;
return EnumToString ( ( AI_CHOICE ) AIType ) ;
}
string ExitReasonFromDealReason ( long reason )
{
switch ( ( ENUM_DEAL_REASON ) reason )
{
case DEAL_REASON_SL : return " SL " ;
case DEAL_REASON_TP : return " TP " ;
case DEAL_REASON_EXPERT : return " Expert " ;
case DEAL_REASON_CLIENT :
case DEAL_REASON_MOBILE :
case DEAL_REASON_WEB : return " Manual " ;
case DEAL_REASON_SO : return " StopOut " ;
default : return " Other " ;
}
}
//--- resolves the closing deal for a position no longer in PositionsTotal() - returns false if
//--- history hasn't caught up yet (rare timing edge case); caller keeps tracking it and retries
//--- next tick rather than dropping the trade unrecorded.
//--- sums profit across every OUT/INOUT deal for this position (covers a partial close followed by
//--- a final close, however rare) rather than trusting a single deal to represent the whole
//--- position; exitPrice/exitReason are taken from the LAST (most recent) such deal, representing
//--- how the position ultimately finished.
bool ResolveClose ( ulong ticket , double & exitPrice , double & profit , string & exitReason )
{
if ( ! HistorySelectByPosition ( ( long ) ticket ) )
return false ;
int deals = HistoryDealsTotal ( ) ;
bool found = false ;
profit = 0.0 ;
for ( int d = 0 ; d < deals ; d + + )
{
ulong dealTicket = HistoryDealGetTicket ( d ) ;
if ( dealTicket = = 0 )
continue ;
long entry = HistoryDealGetInteger ( dealTicket , DEAL_ENTRY ) ;
if ( entry ! = DEAL_ENTRY_OUT & & entry ! = DEAL_ENTRY_INOUT )
continue ;
profit + = HistoryDealGetDouble ( dealTicket , DEAL_PROFIT ) +
HistoryDealGetDouble ( dealTicket , DEAL_SWAP ) +
HistoryDealGetDouble ( dealTicket , DEAL_COMMISSION ) ;
exitPrice = HistoryDealGetDouble ( dealTicket , DEAL_PRICE ) ;
exitReason = ExitReasonFromDealReason ( HistoryDealGetInteger ( dealTicket , DEAL_REASON ) ) ;
found = true ;
}
return found ;
}
bool InsertClosedTrade ( const SJournalOpenTrack & t , double exitPrice , double profit , string exitReason )
{
MqlDateTime openT , closeT ;
TimeToStruct ( t . openTime , openT ) ;
TimeToStruct ( TimeCurrent ( ) , closeT ) ;
double move = ( t . direction = = " Buy " ) ? ( exitPrice - t . entryPrice ) : ( t . entryPrice - exitPrice ) ;
double riskDistance = t . riskDistance ;
double rMultiple = ( riskDistance > 0.0 ) ? move / riskDistance : 0.0 ;
double maeR = ( riskDistance > 0.0 ) ? t . maePoints / riskDistance : 0.0 ;
double mfeR = ( riskDistance > 0.0 ) ? t . mfePoints / riskDistance : 0.0 ;
string cols [ ] = { " ticket " , " openYear " , " openMonth " , " openDay " , " openDayOfWeek " , " openHour " , " openMinute " ,
" closeYear " , " closeMonth " , " closeDay " , " closeHour " , " closeMinute " ,
" symbol " , " direction " , " entryPrice " , " exitPrice " , " slPrice " , " tpPrice " , " lots " ,
" profit " , " riskDistance " , " rMultiple " , " maePoints " , " mfePoints " , " maeR " , " mfeR " ,
" aiConfidence " , " dbConfidence " , " exitReason " , " filterID "
} ;
string vals [ ] ;
ArrayResize ( vals , ArraySize ( cols ) ) ;
int i = 0 ;
vals [ i + + ] = IntegerToString ( ( long ) t . ticket ) ;
vals [ i + + ] = IntegerToString ( openT . year ) ;
vals [ i + + ] = IntegerToString ( openT . mon ) ;
vals [ i + + ] = IntegerToString ( openT . day ) ;
vals [ i + + ] = IntegerToString ( openT . day_of_week ) ;
vals [ i + + ] = IntegerToString ( openT . hour ) ;
vals [ i + + ] = IntegerToString ( openT . min ) ;
vals [ i + + ] = IntegerToString ( closeT . year ) ;
vals [ i + + ] = IntegerToString ( closeT . mon ) ;
vals [ i + + ] = IntegerToString ( closeT . day ) ;
vals [ i + + ] = IntegerToString ( closeT . hour ) ;
vals [ i + + ] = IntegerToString ( closeT . min ) ;
vals [ i + + ] = t . symbol ;
vals [ i + + ] = t . direction ;
vals [ i + + ] = DoubleToString ( t . entryPrice , 8 ) ;
vals [ i + + ] = DoubleToString ( exitPrice , 8 ) ;
vals [ i + + ] = DoubleToString ( t . slPrice , 8 ) ;
vals [ i + + ] = DoubleToString ( t . tpPrice , 8 ) ;
vals [ i + + ] = DoubleToString ( t . lots , 2 ) ;
vals [ i + + ] = DoubleToString ( profit , 2 ) ;
vals [ i + + ] = DoubleToString ( riskDistance , 8 ) ;
vals [ i + + ] = DoubleToString ( rMultiple , 4 ) ;
vals [ i + + ] = DoubleToString ( t . maePoints , 8 ) ;
vals [ i + + ] = DoubleToString ( t . mfePoints , 8 ) ;
vals [ i + + ] = DoubleToString ( maeR , 4 ) ;
vals [ i + + ] = DoubleToString ( mfeR , 4 ) ;
vals [ i + + ] = DoubleToString ( t . aiConfidence , 4 ) ;
vals [ i + + ] = DoubleToString ( t . dbConfidence , 4 ) ;
vals [ i + + ] = exitReason ;
vals [ i + + ] = t . filterID ;
if ( ! m_dbm . BeginTransaction ( ) )
return false ;
bool ok = m_dbm . InsertTradeRecord ( m_tableName , cols , vals ) ;
if ( ! m_dbm . CommitTransaction ( ) )
ok = false ;
return ok ;
}
public :
CTradeJournalManager ( void ) : m_dbm ( NULL ) , m_magic ( 0 ) , m_tableName ( " TradeJournal " ) { }
bool Init ( CDatabaseManager * dbmPtr , ulong magic )
{
m_dbm = dbmPtr ;
m_magic = magic ;
if ( CheckPointer ( m_dbm ) = = POINTER_INVALID )
return false ;
//--- CreateTable() needs a genuinely open handle - dbm.Init() only stores the path, it doesn't
//--- open it (OpenDatabase() does that, and Warrior_EA.mq5 doesn't call it until well after
//--- filters/patterns are registered). Opening explicitly here means this also has to run
//--- before AddFilterToSignal() - see the call site in Warrior_EA.mq5's OnInit().
if ( ! m_dbm . OpenDatabase ( ) )
return false ;
return m_dbm . CreateTable ( m_tableName , TradeJournalSchema ) ;
}
//--- call once per tick: detects this EA's own positions opening/closing on the current symbol and
//--- updates MAE/MFE for every one still open. Cheap - PositionsTotal() is typically single digits.
void Update ( void )
{
if ( CheckPointer ( m_dbm ) = = POINTER_INVALID )
return ;
int preExistingCount = ArraySize ( m_tracked ) ;
bool seen [ ] ;
ArrayResize ( seen , preExistingCount ) ;
for ( int i = 0 ; i < preExistingCount ; i + + )
seen [ i ] = false ;
int total = PositionsTotal ( ) ;
for ( int p = 0 ; p < total ; p + + )
{
ulong ticket = PositionGetTicket ( p ) ;
if ( ticket = = 0 )
continue ;
if ( ! PositionSelectByTicket ( ticket ) )
continue ;
if ( PositionGetString ( POSITION_SYMBOL ) ! = _Symbol )
continue ;
if ( ( ulong ) PositionGetInteger ( POSITION_MAGIC ) ! = m_magic )
continue ;
double currentPrice = PositionGetDouble ( POSITION_PRICE_CURRENT ) ;
int idx = FindTracked ( ticket ) ;
if ( idx < 0 )
{
SJournalOpenTrack t ;
t . ticket = ticket ;
t . openTime = ( datetime ) PositionGetInteger ( POSITION_TIME ) ;
t . symbol = _Symbol ;
t . direction = ( PositionGetInteger ( POSITION_TYPE ) = = POSITION_TYPE_BUY ) ? " Buy " : " Sell " ;
t . entryPrice = PositionGetDouble ( POSITION_PRICE_OPEN ) ;
t . slPrice = PositionGetDouble ( POSITION_SL ) ;
t . tpPrice = PositionGetDouble ( POSITION_TP ) ;
t . lots = PositionGetDouble ( POSITION_VOLUME ) ;
t . riskDistance = ( t . slPrice > 0.0 ) ? MathAbs ( t . entryPrice - t . slPrice ) : 0.0 ;
//--- same-tick snapshot OpenParams() populated right before this trade was sent - see
//--- ConfidenceBridge.mqh's declaration comments.
t . aiConfidence = MathAbs ( g_AISignedConfidence ) ;
t . dbConfidence = g_DBConfidence ;
t . filterID = CurrentFilterID ( ) ;
t . maePoints = 0.0 ;
t . mfePoints = 0.0 ;
int newIdx = ArraySize ( m_tracked ) ;
ArrayResize ( m_tracked , newIdx + 1 ) ;
m_tracked [ newIdx ] = t ;
}
else
{
if ( idx < preExistingCount )
seen [ idx ] = true ;
double excursion = ( m_tracked [ idx ] . direction = = " Buy " ) ?
( currentPrice - m_tracked [ idx ] . entryPrice ) :
( m_tracked [ idx ] . entryPrice - currentPrice ) ;
if ( excursion > m_tracked [ idx ] . mfePoints )
m_tracked [ idx ] . mfePoints = excursion ;
if ( - excursion > m_tracked [ idx ] . maePoints )
m_tracked [ idx ] . maePoints = - excursion ;
}
}
//--- anything tracked before this pass but not seen in it closed since the last tick - resolve
//--- and record it. Walk backwards since RemoveTracked() swap-removes (changes indices >= idx).
for ( int i = preExistingCount - 1 ; i > = 0 ; i - - )
{
if ( seen [ i ] )
continue ;
double exitPrice = 0.0 , profit = 0.0 ;
string exitReason = " Other " ;
if ( ResolveClose ( m_tracked [ i ] . ticket , exitPrice , profit , exitReason ) )
{
2026-07-26 12:12:14 -04:00
// MAE/MFE and the rest of this closed trade's record cannot be reconstructed after the
// fact once m_tracked[i] is removed below - if the DB insert fails (lock contention on
// the shared COMMON db, disk issue, retry exhaustion), log every field so the record is
// at least manually recoverable from the Experts journal instead of silently vanishing.
if ( ! InsertClosedTrade ( m_tracked [ i ] , exitPrice , profit , exitReason ) )
PrintFormat ( " %s: ERROR - failed to insert closed trade into journal DB, record LOST from DB (recoverable from this log only): ticket=%I64u direction=%s entry=%.5f exit=%.5f profit=%.2f mae=%.5f mfe=%.5f reason=%s " ,
__FUNCTION__ , m_tracked [ i ] . ticket , m_tracked [ i ] . direction , m_tracked [ i ] . entryPrice , exitPrice , profit ,
m_tracked [ i ] . maePoints , m_tracked [ i ] . mfePoints , exitReason ) ;
2026-07-22 22:51:04 -04:00
RemoveTracked ( i ) ;
}
//--- else: history hasn't caught up yet this tick - leave it tracked and retry next tick
}
}
//--- forward-declared here, implemented in TradeJournalReport.mqh (kept separate - this file is
//--- the live tracking path, that one is the offline reporting/insights path; no reason for the
//--- per-tick code to pull in report-building logic it never calls).
bool GenerateReport ( string & resultPath , string & errorMsg ) ;
} ;
//--- CTradeJournalManager::GenerateReport() - split out, see that file's own header comment.
# include "TradeJournalReport.mqh"