//+------------------------------------------------------------------+
//| DailyTradeStats.mq5 |
//| 复刻MTCommander日统计,首行持仓汇总,导出CSV,本地持久化存储 |
//| 【无外部mqh依赖,原生图表按钮,直接编译】 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026"
#property link ""
#property version "1.03"
#property strict
//---- 配置参数
input string PersistFileName = "DailyStatsData.bin"; //持久化数据文件名称
input int ButtonX = 20; //导出按钮X坐标
input int ButtonY = 20; //导出按钮Y坐标
//---- 单日统计结构体
struct DayStat
{
datetime dayStart; //服务器日期零点
double totalLots; //当日总手数
double maxSingleLot; //当日单笔最大手数
int tradeCount; //当日平仓次数
double netProfit; //当日净盈亏
double depositWithdraw; //当日出入金
double endBalance; //当日收盘账户余额
double maxFloatLoss; //当日最大浮亏金额
double maxFloatLossPct; //当日最大浮亏比例
double maxFloatProfit; //当日最大浮盈金额
double maxFloatProfitPct; //当日最大浮盈比例
};
//----全局变量
DayStat g_days[];
int g_dayCount = 0;
datetime g_lastSaveTime = 0;
const string BUTTON_NAME = "BTN_EXPORT_CSV";
//+------------------------------------------------------------------+
//| 初始化 |
//+------------------------------------------------------------------+
int OnInit()
{
// 创建原生图表按钮(不需要mqh库)
if(!ObjectCreate(0,BUTTON_NAME,OBJ_BUTTON,0,0,0))
{
Print("按钮创建失败,err=",GetLastError());
return(INIT_FAILED);
}
ObjectSetInteger(0,BUTTON_NAME,OBJPROP_XDISTANCE,ButtonX);
ObjectSetInteger(0,BUTTON_NAME,OBJPROP_YDISTANCE,ButtonY);
ObjectSetInteger(0,BUTTON_NAME,OBJPROP_XSIZE,140);
ObjectSetInteger(0,BUTTON_NAME,OBJPROP_YSIZE,30);
ObjectSetString(0,BUTTON_NAME,OBJPROP_TEXT,"导出CSV报表");
ObjectSetInteger(0,BUTTON_NAME,OBJPROP_STATE,0); //初始弹起
//加载本地持久化文件
LoadDataFromFile();
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| 反初始化 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
SaveDataToFile();
ObjectDelete(0,BUTTON_NAME);
}
//+------------------------------------------------------------------+
//| 图表事件:捕获按钮点击 |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
if(id == CHARTEVENT_OBJECT_CLICK && sparam == BUTTON_NAME)
{
ExportToCSV();
Print("✅ CSV报表导出完成,文件路径:"+TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Files\\DailyReport.csv");
ObjectSetInteger(0,BUTTON_NAME,OBJPROP_STATE,0); //强制弹起
}
}
//+------------------------------------------------------------------+
//| Tick主循环:实时更新当日浮盈浮亏峰值,自动保存数据 |
//+------------------------------------------------------------------+
void OnTick()
{
UpdateCurrentDayFloatPeak();
//每60秒自动持久化一次
if(TimeCurrent() - g_lastSaveTime > 60)
{
SaveDataToFile();
g_lastSaveTime = TimeCurrent();
}
}
//====================================================================
// 【持久化 读写本地数据文件】
//====================================================================
bool SaveDataToFile()
{
int hFile = FileOpen(PersistFileName,FILE_WRITE|FILE_BIN);
if(hFile == INVALID_HANDLE)
{
Print("保存文件失败,err=",GetLastError());
return false;
}
FileWriteInteger(hFile,g_dayCount);
for(int i=0; i<g_dayCount; i++)
{
FileWriteLong(hFile,g_days[i].dayStart);
FileWriteDouble(hFile,g_days[i].totalLots);
FileWriteDouble(hFile,g_days[i].maxSingleLot);
FileWriteInteger(hFile,g_days[i].tradeCount);
FileWriteDouble(hFile,g_days[i].netProfit);
FileWriteDouble(hFile,g_days[i].depositWithdraw);
FileWriteDouble(hFile,g_days[i].endBalance);
FileWriteDouble(hFile,g_days[i].maxFloatLoss);
FileWriteDouble(hFile,g_days[i].maxFloatLossPct);
FileWriteDouble(hFile,g_days[i].maxFloatProfit);
FileWriteDouble(hFile,g_days[i].maxFloatProfitPct);
}
FileClose(hFile);
return true;
}
bool LoadDataFromFile()
{
if(!FileIsExist(PersistFileName)) return true;
int hFile = FileOpen(PersistFileName,FILE_READ|FILE_BIN);
if(hFile == INVALID_HANDLE)
{
Print("读取文件失败");
return false;
}
g_dayCount = FileReadInteger(hFile);
ArrayResize(g_days,g_dayCount);
for(int i=0; i<g_dayCount; i++)
{
g_days[i].dayStart = (datetime)FileReadLong(hFile);
g_days[i].totalLots = FileReadDouble(hFile);
g_days[i].maxSingleLot = FileReadDouble(hFile);
g_days[i].tradeCount = FileReadInteger(hFile);
g_days[i].netProfit = FileReadDouble(hFile);
g_days[i].depositWithdraw = FileReadDouble(hFile);
g_days[i].endBalance = FileReadDouble(hFile);
g_days[i].maxFloatLoss = FileReadDouble(hFile);
g_days[i].maxFloatLossPct = FileReadDouble(hFile);
g_days[i].maxFloatProfit = FileReadDouble(hFile);
g_days[i].maxFloatProfitPct = FileReadDouble(hFile);
}
FileClose(hFile);
return true;
}
//====================================================================
// 【辅助函数:获取服务器当日零点时间 替代TimeStartOfDay】
//====================================================================
datetime GetDayStart(datetime t)
{
MqlDateTime dt;
TimeToStruct(t,dt);
dt.hour=0; dt.min=0; dt.sec=0;
return StructToTime(dt);
}
//====================================================================
// 【实时更新当日持仓浮盈浮亏峰值】
//====================================================================
void UpdateCurrentDayFloatPeak()
{
datetime today = GetDayStart(TimeCurrent());
int idx = FindDayIndex(today);
if(idx == -1)
{
//新建当日记录
AddNewDayRecord(today);
idx = g_dayCount-1;
}
//遍历全部持仓,计算当前总浮盈
double totalFloat = 0;
double bal = AccountInfoDouble(ACCOUNT_BALANCE);
for(int p=0; p<PositionsTotal(); p++)
{
ulong posTicket = PositionGetTicket(p);
if(PositionSelectByTicket(posTicket))
{
totalFloat += PositionGetDouble(POSITION_PROFIT);
}
}
double equity = bal + totalFloat;
double floatPct = (bal>0) ? (equity - bal)/bal *100 : 0;
//更新最大浮亏 / 浮盈
if(totalFloat < g_days[idx].maxFloatLoss)
{
g_days[idx].maxFloatLoss = totalFloat;
g_days[idx].maxFloatLossPct = floatPct;
}
if(totalFloat > g_days[idx].maxFloatProfit)
{
g_days[idx].maxFloatProfit = totalFloat;
g_days[idx].maxFloatProfitPct = floatPct;
}
g_days[idx].endBalance = bal;
}
//====================================================================
// 【历史成交汇总:读取全部已平仓Deal,按服务器日期分组】
//====================================================================
void CalcHistoryDeals()
{
//重置所有历史交易统计(浮盈峰值保留,因为是实时记录)
for(int i=0; i<g_dayCount; i++)
{
g_days[i].totalLots=0;
g_days[i].maxSingleLot=0;
g_days[i].tradeCount=0;
g_days[i].netProfit=0;
g_days[i].depositWithdraw=0;
}
//读取全部历史成交
if(!HistorySelect(0,TimeCurrent())) return;
long dealTotal = HistoryDealsTotal();
for(long i=0; i<dealTotal; i++)
{
//强制转换消除long→uint警告
ulong ticket = HistoryDealGetTicket((uint)i);
if(ticket == 0) continue;
MqlDateTime dtDeal;
datetime dTime = (datetime)HistoryDealGetInteger(ticket,DEAL_TIME);
TimeToStruct(dTime,dtDeal);
datetime day = GetDayStart(dTime);
long entryType = HistoryDealGetInteger(ticket,DEAL_ENTRY);
long dealType = HistoryDealGetInteger(ticket,DEAL_TYPE);
double lot = HistoryDealGetDouble(ticket,DEAL_VOLUME);
double profit = HistoryDealGetDouble(ticket,DEAL_PROFIT);
int idx = FindDayIndex(day);
if(idx == -1)
{
AddNewDayRecord(day);
idx = g_dayCount-1;
}
//开仓记录:统计手数,更新单笔最大手数
if(entryType == DEAL_ENTRY_IN)
{
g_days[idx].totalLots += lot;
if(lot>g_days[idx].maxSingleLot) g_days[idx].maxSingleLot = lot;
}
//平仓记录:统计次数 + 净盈亏
if(entryType == DEAL_ENTRY_OUT)
{
g_days[idx].tradeCount++;
g_days[idx].netProfit += profit;
}
//出入金:MQL5用 DEAL_TYPE_BALANCE
if(dealType == DEAL_TYPE_BALANCE)
{
g_days[idx].depositWithdraw += profit;
}
}
}
//====================================================================
// 【导出CSV主函数】
//====================================================================
void ExportToCSV()
{
CalcHistoryDeals(); //每次导出前重新计算历史成交
int hFile = FileOpen("DailyReport.csv",FILE_WRITE|FILE_CSV|FILE_ANSI);
if(hFile == INVALID_HANDLE)
{
Print("CSV创建失败,err=",GetLastError());
return;
}
//CSV表头
FileWriteString(hFile,"日期,手数,最大手数,次数,盈亏金额,百分比%,出入金,余额,最大浮亏金额,最大浮亏比例,最大浮盈金额,最大浮盈比例\n");
//===== 第一行:当前持仓汇总(和截图第一行【持仓】保持一致)=====
double posTotalLots=0;
double posMaxLot=0;
int posCount=0;
double posFloatTotal=0;
double bal = AccountInfoDouble(ACCOUNT_BALANCE);
for(int p=0;p<PositionsTotal();p++)
{
ulong t = PositionGetTicket(p);
if(PositionSelectByTicket(t))
{
double l = PositionGetDouble(POSITION_VOLUME);
posTotalLots += l;
if(l>posMaxLot) posMaxLot = l;
posCount++;
posFloatTotal += PositionGetDouble(POSITION_PROFIT);
}
}
double posPct = bal>0 ? posFloatTotal / bal * 100 : 0;
FileWriteString(hFile,
StringFormat("持仓,%.2f,%.2f,%d,%.2f,%.2f%%,%.2f,%.2f,%.2f,%.2f%%,%.2f,%.2f%%\n",
posTotalLots,posMaxLot,posCount,posFloatTotal,posPct,0.0,bal,posFloatTotal,posPct,0,0));
//===== 遍历每日历史记录,按时间倒序输出(新日期在前,和截图顺序一致)=====
for(int i = g_dayCount-1; i>=0; i--)
{
DayStat ds = g_days[i];
if(ds.dayStart == GetDayStart(TimeCurrent())) continue; //当日实时记录跳过
double pct = ds.endBalance>0 ? ds.netProfit / ds.endBalance *100 :0;
FileWriteString(hFile,
StringFormat("%s,%.2f,%.2f,%d,%.2f,%.2f%%,%.2f,%.2f,%.2f,%.2f%%,%.2f,%.2f%%\n",
TimeToString(ds.dayStart,TIME_DATE),
ds.totalLots,
ds.maxSingleLot,
ds.tradeCount,
ds.netProfit,
pct,
ds.depositWithdraw,
ds.endBalance,
ds.maxFloatLoss,
ds.maxFloatLossPct,
ds.maxFloatProfit,
ds.maxFloatProfitPct
));
}
FileClose(hFile);
}
//====================================================================
// 辅助函数:查找日期索引
//====================================================================
int FindDayIndex(datetime dayTime)
{
for(int i=0; i<g_dayCount; i++)
{
if(g_days[i].dayStart == dayTime) return i;
}
return -1;
}
//新增一条日期记录
void AddNewDayRecord(datetime dayTime)
{
ArrayResize(g_days,g_dayCount+1);
g_days[g_dayCount].dayStart = dayTime;
g_days[g_dayCount].totalLots=0;
g_days[g_dayCount].maxSingleLot=0;
g_days[g_dayCount].tradeCount=0;
g_days[g_dayCount].netProfit=0;
g_days[g_dayCount].depositWithdraw=0;
g_days[g_dayCount].endBalance=0;
g_days[g_dayCount].maxFloatLoss=0;
g_days[g_dayCount].maxFloatLossPct=0;
g_days[g_dayCount].maxFloatProfit=0;
g_days[g_dayCount].maxFloatProfitPct=0;
g_dayCount++;
}
//+------------------------------------------------------------------+
|