412 lines
13 KiB
Plaintext
412 lines
13 KiB
Plaintext
//+------------------------------------------------------------------+
|
|
//| TradeJournal.mqh — CSV lệnh đóng + tổng hợp tháng (RsiMomentumEA)|
|
|
//+------------------------------------------------------------------+
|
|
#ifndef RSI_MOM_TRADE_JOURNAL_MQH
|
|
#define RSI_MOM_TRADE_JOURNAL_MQH
|
|
|
|
struct RsiMomMonthRow
|
|
{
|
|
string ym;
|
|
int trades;
|
|
int wins;
|
|
int losses;
|
|
double netProfit;
|
|
double grossProfit;
|
|
double grossLoss;
|
|
int slCount;
|
|
int tpCount;
|
|
int otherCount;
|
|
};
|
|
|
|
int g_rsiMomJournalHandle = INVALID_HANDLE;
|
|
RsiMomMonthRow g_rsiMomMonths[];
|
|
int g_rsiMomMonthCount = 0;
|
|
|
|
string RsiMomJournal_TradesPath(const string sym, const ENUM_TIMEFRAMES tf)
|
|
{
|
|
return StringFormat("RsiMomEA\\journal_%s_%s.csv", sym, EnumToString(tf));
|
|
}
|
|
|
|
string RsiMomJournal_SummaryPath(const string sym, const ENUM_TIMEFRAMES tf)
|
|
{
|
|
return StringFormat("RsiMomEA\\summary_%s_%s.csv", sym, EnumToString(tf));
|
|
}
|
|
|
|
int RsiMomJournal_FindMonth(const string ym)
|
|
{
|
|
for(int i = 0; i < g_rsiMomMonthCount; i++)
|
|
{
|
|
if(g_rsiMomMonths[i].ym == ym)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void RsiMomJournal_AddMonth(const string ym, const double profit, const string exitTag)
|
|
{
|
|
int idx = RsiMomJournal_FindMonth(ym);
|
|
if(idx < 0)
|
|
{
|
|
idx = g_rsiMomMonthCount;
|
|
g_rsiMomMonthCount++;
|
|
ArrayResize(g_rsiMomMonths, g_rsiMomMonthCount);
|
|
g_rsiMomMonths[idx].ym = ym;
|
|
g_rsiMomMonths[idx].trades = 0;
|
|
g_rsiMomMonths[idx].wins = 0;
|
|
g_rsiMomMonths[idx].losses = 0;
|
|
g_rsiMomMonths[idx].netProfit = 0.0;
|
|
g_rsiMomMonths[idx].grossProfit = 0.0;
|
|
g_rsiMomMonths[idx].grossLoss = 0.0;
|
|
g_rsiMomMonths[idx].slCount = 0;
|
|
g_rsiMomMonths[idx].tpCount = 0;
|
|
g_rsiMomMonths[idx].otherCount = 0;
|
|
}
|
|
|
|
RsiMomMonthRow r = g_rsiMomMonths[idx];
|
|
r.trades++;
|
|
r.netProfit += profit;
|
|
if(profit > 0.0)
|
|
{
|
|
r.wins++;
|
|
r.grossProfit += profit;
|
|
}
|
|
else if(profit < 0.0)
|
|
{
|
|
r.losses++;
|
|
r.grossLoss += profit;
|
|
}
|
|
|
|
if(exitTag == "SL")
|
|
r.slCount++;
|
|
else if(exitTag == "TP")
|
|
r.tpCount++;
|
|
else
|
|
r.otherCount++;
|
|
|
|
g_rsiMomMonths[idx] = r;
|
|
}
|
|
|
|
bool RsiMomJournal_DeleteIfExists(const string path)
|
|
{
|
|
if(!FileIsExist(path, FILE_COMMON))
|
|
return false;
|
|
return FileDelete(path, FILE_COMMON);
|
|
}
|
|
|
|
bool RsiMomJournal_ResetFiles(const string sym, const ENUM_TIMEFRAMES tf)
|
|
{
|
|
RsiMomJournal_CloseFiles();
|
|
RsiMomJournal_DeleteIfExists(RsiMomJournal_TradesPath(sym, tf));
|
|
RsiMomJournal_DeleteIfExists(RsiMomJournal_SummaryPath(sym, tf));
|
|
return true;
|
|
}
|
|
|
|
bool RsiMomJournal_OpenTrades(const string sym, const ENUM_TIMEFRAMES tf)
|
|
{
|
|
if(g_rsiMomJournalHandle != INVALID_HANDLE)
|
|
return true;
|
|
|
|
FolderCreate("RsiMomEA", FILE_COMMON);
|
|
const string path = RsiMomJournal_TradesPath(sym, tf);
|
|
const bool exists = FileIsExist(path, FILE_COMMON);
|
|
|
|
g_rsiMomJournalHandle = FileOpen(path, FILE_READ | FILE_WRITE | FILE_CSV | FILE_COMMON | FILE_ANSI, ',');
|
|
if(g_rsiMomJournalHandle == INVALID_HANDLE)
|
|
{
|
|
Print("[RsiMomEA] Journal: không mở file ", path, " err=", GetLastError());
|
|
return false;
|
|
}
|
|
|
|
if(!exists || FileSize(g_rsiMomJournalHandle) == 0)
|
|
{
|
|
FileWrite(g_rsiMomJournalHandle,
|
|
"close_time", "year_month", "profit", "dir", "exit",
|
|
"entry_time", "entry_price", "sl", "tp", "volume",
|
|
"risk_pts", "profit_r",
|
|
"rsi", "wma45", "ema200", "close_entry", "above_ema200",
|
|
"atr", "atr_ratio", "atr_exp_ok",
|
|
"session_ok", "spread_pts",
|
|
"symbol", "timeframe", "magic");
|
|
}
|
|
else
|
|
{
|
|
FileSeek(g_rsiMomJournalHandle, 0, SEEK_END);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void RsiMomJournal_CloseFiles()
|
|
{
|
|
if(g_rsiMomJournalHandle != INVALID_HANDLE)
|
|
{
|
|
FileClose(g_rsiMomJournalHandle);
|
|
g_rsiMomJournalHandle = INVALID_HANDLE;
|
|
}
|
|
}
|
|
|
|
bool RsiMomJournal_Copy1(const int handle, const int shift, double &v)
|
|
{
|
|
v = 0.0;
|
|
if(handle == INVALID_HANDLE || shift < 0)
|
|
return false;
|
|
double buf[];
|
|
ArraySetAsSeries(buf, true);
|
|
if(CopyBuffer(handle, 0, shift, 1, buf) < 1)
|
|
return false;
|
|
v = buf[0];
|
|
return true;
|
|
}
|
|
|
|
bool RsiMomJournal_AtrRatioAt(const int hAtr, const int shift,
|
|
const int cmpBars, const double minRatio,
|
|
double &atrNow, double &ratio, bool &expOk)
|
|
{
|
|
atrNow = 0.0;
|
|
ratio = 0.0;
|
|
expOk = false;
|
|
double atrRef = 0.0;
|
|
if(!RsiMomJournal_Copy1(hAtr, shift, atrNow) || !RsiMomJournal_Copy1(hAtr, shift + cmpBars, atrRef))
|
|
return false;
|
|
if(atrRef <= 0.0)
|
|
return false;
|
|
ratio = atrNow / atrRef;
|
|
expOk = (ratio >= MathMax(1.0, minRatio) - 1e-8);
|
|
return true;
|
|
}
|
|
|
|
string RsiMomJournal_ExitTag(const long dealReason, const double profit)
|
|
{
|
|
if(dealReason == DEAL_REASON_SL)
|
|
return "SL";
|
|
if(dealReason == DEAL_REASON_TP)
|
|
return "TP";
|
|
if(profit >= 0.0)
|
|
return "WIN";
|
|
return "LOSS";
|
|
}
|
|
|
|
bool RsiMomJournal_FindEntryDeal(const ulong positionId,
|
|
ulong &entryDeal,
|
|
datetime &entryTime,
|
|
long &posType,
|
|
double &entryPrice,
|
|
double &sl,
|
|
double &tp,
|
|
double &volume)
|
|
{
|
|
entryDeal = 0;
|
|
entryTime = 0;
|
|
posType = 0;
|
|
entryPrice = sl = tp = volume = 0.0;
|
|
|
|
if(positionId == 0 || !HistorySelectByPosition(positionId))
|
|
return false;
|
|
|
|
const int total = HistoryDealsTotal();
|
|
for(int i = 0; i < total; i++)
|
|
{
|
|
const ulong d = HistoryDealGetTicket(i);
|
|
if(d == 0 || !HistoryDealSelect(d))
|
|
continue;
|
|
if(HistoryDealGetInteger(d, DEAL_ENTRY) != DEAL_ENTRY_IN)
|
|
continue;
|
|
|
|
entryDeal = d;
|
|
entryTime = (datetime)HistoryDealGetInteger(d, DEAL_TIME);
|
|
posType = HistoryDealGetInteger(d, DEAL_TYPE);
|
|
entryPrice = HistoryDealGetDouble(d, DEAL_PRICE);
|
|
volume = HistoryDealGetDouble(d, DEAL_VOLUME);
|
|
|
|
const ulong orderTicket = (ulong)HistoryDealGetInteger(d, DEAL_ORDER);
|
|
if(orderTicket > 0 && HistoryOrderSelect(orderTicket))
|
|
{
|
|
sl = HistoryOrderGetDouble(orderTicket, ORDER_SL);
|
|
tp = HistoryOrderGetDouble(orderTicket, ORDER_TP);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void RsiMomJournal_WriteSummary(const string sym, const ENUM_TIMEFRAMES tf)
|
|
{
|
|
if(g_rsiMomMonthCount <= 0)
|
|
return;
|
|
|
|
const string path = RsiMomJournal_SummaryPath(sym, tf);
|
|
const int h = FileOpen(path, FILE_WRITE | FILE_CSV | FILE_COMMON | FILE_ANSI, ',');
|
|
if(h == INVALID_HANDLE)
|
|
{
|
|
Print("[RsiMomEA] Summary CSV lỗi ", path, " err=", GetLastError());
|
|
return;
|
|
}
|
|
|
|
FileWrite(h,
|
|
"year_month", "trades", "wins", "losses", "winrate_pct",
|
|
"net_profit", "gross_profit", "gross_loss", "avg_profit",
|
|
"sl_count", "tp_count", "other_count");
|
|
|
|
for(int i = 0; i < g_rsiMomMonthCount; i++)
|
|
{
|
|
const RsiMomMonthRow r = g_rsiMomMonths[i];
|
|
double winrate = 0.0;
|
|
if(r.trades > 0)
|
|
winrate = 100.0 * (double)r.wins / (double)r.trades;
|
|
const double avg = (r.trades > 0) ? r.netProfit / (double)r.trades : 0.0;
|
|
|
|
FileWrite(h,
|
|
r.ym,
|
|
IntegerToString(r.trades),
|
|
IntegerToString(r.wins),
|
|
IntegerToString(r.losses),
|
|
DoubleToString(winrate, 2),
|
|
DoubleToString(r.netProfit, 2),
|
|
DoubleToString(r.grossProfit, 2),
|
|
DoubleToString(r.grossLoss, 2),
|
|
DoubleToString(avg, 2),
|
|
IntegerToString(r.slCount),
|
|
IntegerToString(r.tpCount),
|
|
IntegerToString(r.otherCount));
|
|
}
|
|
|
|
FileClose(h);
|
|
Print("[RsiMomEA] Summary CSV → ", path, " (FILE_COMMON, ", g_rsiMomMonthCount, " tháng)");
|
|
}
|
|
|
|
void RsiMomJournal_ResetMonths()
|
|
{
|
|
ArrayResize(g_rsiMomMonths, 0);
|
|
g_rsiMomMonthCount = 0;
|
|
}
|
|
|
|
void RsiMomJournal_OnDeinit(const string sym, const ENUM_TIMEFRAMES tf)
|
|
{
|
|
RsiMomJournal_WriteSummary(sym, tf);
|
|
RsiMomJournal_CloseFiles();
|
|
}
|
|
|
|
void RsiMomJournal_RecordClosedDeal(const ulong dealOut,
|
|
const string sym,
|
|
const ENUM_TIMEFRAMES tf,
|
|
const ulong magic,
|
|
const int hRsi,
|
|
const int hWma45,
|
|
const int hEma200,
|
|
const int hAtrRegime,
|
|
const int atrCmpBars,
|
|
const double atrMinRatio,
|
|
const int sessionOkAtEntry)
|
|
{
|
|
if(dealOut == 0 || !HistoryDealSelect(dealOut))
|
|
return;
|
|
if(HistoryDealGetString(dealOut, DEAL_SYMBOL) != sym)
|
|
return;
|
|
if((ulong)HistoryDealGetInteger(dealOut, DEAL_MAGIC) != magic)
|
|
return;
|
|
if(HistoryDealGetInteger(dealOut, DEAL_ENTRY) != DEAL_ENTRY_OUT)
|
|
return;
|
|
|
|
if(!RsiMomJournal_OpenTrades(sym, tf))
|
|
return;
|
|
|
|
const ulong posId = (ulong)HistoryDealGetInteger(dealOut, DEAL_POSITION_ID);
|
|
ulong entryDeal = 0;
|
|
datetime entryTime = 0;
|
|
long posType = 0;
|
|
double entryPrice = 0.0, sl = 0.0, tp = 0.0, volume = 0.0;
|
|
if(!RsiMomJournal_FindEntryDeal(posId, entryDeal, entryTime, posType,
|
|
entryPrice, sl, tp, volume))
|
|
{
|
|
Print("[RsiMomEA] Journal: không tìm entry pos=", posId);
|
|
return;
|
|
}
|
|
|
|
const int entryShift = iBarShift(sym, tf, entryTime, true);
|
|
if(entryShift < 1)
|
|
{
|
|
Print("[RsiMomEA] Journal: entry shift invalid ", TimeToString(entryTime));
|
|
return;
|
|
}
|
|
|
|
const int dig = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS);
|
|
double rsi = 0.0, wma = 0.0, ema200 = 0.0, atrNow = 0.0, atrRatio = 0.0;
|
|
bool atrExpOk = false;
|
|
RsiMomJournal_Copy1(hRsi, entryShift, rsi);
|
|
RsiMomJournal_Copy1(hWma45, entryShift, wma);
|
|
RsiMomJournal_Copy1(hEma200, entryShift, ema200);
|
|
RsiMomJournal_AtrRatioAt(hAtrRegime, entryShift, atrCmpBars, atrMinRatio,
|
|
atrNow, atrRatio, atrExpOk);
|
|
|
|
const double closeEntry = iClose(sym, tf, entryShift);
|
|
const int aboveEma = (ema200 > 0.0 && closeEntry > ema200) ? 1 : 0;
|
|
|
|
const int sessionOk = (sessionOkAtEntry != 0) ? 1 : 0;
|
|
const int spreadPts = (int)SymbolInfoInteger(sym, SYMBOL_SPREAD);
|
|
|
|
double riskPts = 0.0;
|
|
if(posType == DEAL_TYPE_BUY && sl > 0.0)
|
|
riskPts = (entryPrice - sl) / _Point;
|
|
else if(posType == DEAL_TYPE_SELL && sl > 0.0)
|
|
riskPts = (sl - entryPrice) / _Point;
|
|
if(riskPts < 0.0)
|
|
riskPts = 0.0;
|
|
|
|
const datetime closeTime = (datetime)HistoryDealGetInteger(dealOut, DEAL_TIME);
|
|
MqlDateTime dt;
|
|
TimeToStruct(closeTime, dt);
|
|
const string yearMonth = StringFormat("%04d-%02d", dt.year, dt.mon);
|
|
|
|
const double profit = HistoryDealGetDouble(dealOut, DEAL_PROFIT)
|
|
+ HistoryDealGetDouble(dealOut, DEAL_SWAP)
|
|
+ HistoryDealGetDouble(dealOut, DEAL_COMMISSION);
|
|
const long dealReason = HistoryDealGetInteger(dealOut, DEAL_REASON);
|
|
const string exitTag = RsiMomJournal_ExitTag(dealReason, profit);
|
|
const string dir = (posType == DEAL_TYPE_BUY) ? "BUY" : "SELL";
|
|
|
|
double profitR = 0.0;
|
|
if(riskPts > 0.0)
|
|
{
|
|
const double tickVal = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_VALUE);
|
|
const double tickSz = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_SIZE);
|
|
if(tickSz > 0.0 && tickVal > 0.0)
|
|
{
|
|
const double riskMoney = (riskPts * _Point / tickSz) * tickVal * volume;
|
|
if(riskMoney > 0.0)
|
|
profitR = profit / riskMoney;
|
|
}
|
|
}
|
|
|
|
RsiMomJournal_AddMonth(yearMonth, profit, exitTag);
|
|
|
|
FileWrite(g_rsiMomJournalHandle,
|
|
TimeToString(closeTime, TIME_DATE | TIME_MINUTES),
|
|
yearMonth,
|
|
DoubleToString(profit, 2),
|
|
dir,
|
|
exitTag,
|
|
TimeToString(entryTime, TIME_DATE | TIME_MINUTES),
|
|
DoubleToString(entryPrice, dig),
|
|
DoubleToString(sl, dig),
|
|
DoubleToString(tp, dig),
|
|
DoubleToString(volume, 2),
|
|
DoubleToString(riskPts, 1),
|
|
DoubleToString(profitR, 3),
|
|
DoubleToString(rsi, 2),
|
|
DoubleToString(wma, dig),
|
|
DoubleToString(ema200, dig),
|
|
DoubleToString(closeEntry, dig),
|
|
IntegerToString(aboveEma),
|
|
DoubleToString(atrNow, dig),
|
|
DoubleToString(atrRatio, 4),
|
|
IntegerToString(atrExpOk ? 1 : 0),
|
|
IntegerToString(sessionOk),
|
|
IntegerToString(spreadPts),
|
|
sym,
|
|
EnumToString(tf),
|
|
IntegerToString((int)magic));
|
|
|
|
FileFlush(g_rsiMomJournalHandle);
|
|
}
|
|
|
|
#endif
|