Warrior_EA/Database/TradeJournalManager.mqh

309 lines
15 KiB
MQL5
Raw Permalink Normal View History

//+------------------------------------------------------------------+
//| 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))
{
// 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);
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"