使用标准库类和Google Chart API 创建信息板·进阶篇
📘

使用标准库类和Google Chart API 创建信息板·进阶篇

第 2/3 篇

「面板刷新时的链接校验与字段回填」

在 MT5 自定义信息面板里,Board::Refresh() 是每次重绘的入口。它先卡两道关口:用 TerminalInfoInteger(TERMINAL_CONNECTED) 确认交易服务器连着没,再用 TerminalInfoInteger(TERMINAL_DLLS_ALLOWED) 确认 DLL 调用没被禁——任意一条不满足就 Alert 并 return,避免后面算出来的统计全是空值。 过了校验就调 Calculate() 重新算净值、毛利润、毛亏损这些特征,然后取 Chart.WidthInPixels() 和 CHART_HEIGHT_IN_PIXELS 拿到窗口像素尺寸。余额图占多高由 chart_border = y_size*(1.0 - Chart_ratio/100.0) 决定,Chart_ratio 设 30 就意味着底部留 30% 给图表、顶部 70% 给文字区。 界面文案放在一个 10 行 6 列的 captions 二维数组里,每行左右两栏各摆标签和数值。回填时直接用 DoubleToString(net_profit, 2) 把双精度转成两位小数字符串塞进 captions[0][1]、captions[0][3]、captions[0][5] 等位置,净盈利、毛盈利、毛亏损就显示出来了。 开 MT5 把这段 Refresh 逻辑接进自己的指标,改 Chart_ratio 从 0 到 50 拖一下,能直观看到饼图区被压缩、文字区变高的过程。外汇和贵金属品种波动大,面板统计仅反映历史成交,不代表后续盈利概率。

MQL5 / C++
class="type">void Board::Refresh()
{
class=class="str">"cmt">//检查服务器链接状态
if(!TerminalInfoInteger(TERMINAL_CONNECTED)) {Alert("No connection with the trading server!"); class="kw">return;}
class=class="str">"cmt">//检查是否允许从动态链接库中引入函数
if(!TerminalInfoInteger(TERMINAL_DLLS_ALLOWED)) {Alert("DLLs are prohibited!"); class="kw">return;}
class=class="str">"cmt">//计算特征值
Calculate();
class=class="str">"cmt">//取指标窗口的宽度
class="type">int x_size=Chart.WidthInPixels();
class=class="str">"cmt">//以及高度
class="type">int y_size=Chart.GetInteger(CHART_HEIGHT_IN_PIXELS,wnd);
class=class="str">"cmt">//计算余额图会占据多大空间
class="type">class="kw">double chart_border=y_size*(class="num">1.0-(Chart_ratio/class="num">100.0));
class="type">class="kw">string captions[class="num">10][class="num">6]= class=class="str">"cmt">//界面元素名称数组
{
{"Total Net Profit:"," ","Gross Profit:"," ","Gross Loss:"," "},
{"Profit Factor:"," ","Expected Payoff:"," ","",""},
{"Absolute Drawdown:"," ","Maximal Drawdown:"," ","Relative Drawdown:"," "},
{"Total Trades:"," ","Short Positions(won %):"," ","Long Positions(won %):"," "},
{"","","Profit Trades(% of total):"," ","Loss trades(% of total):"," "},
{"Largest","","profit trade:"," ","loss trade:"," "},
{"Average","","profit trade:"," ","loss trade:"," "},
{"Maximum","","consecutive wins($):"," ","consecutive losses($):"," "},
{"Maximal","","consecutive profit(count):"," ","consecutive loss(count):"," "},
{"Average","","consecutive wins:"," ","consecutive losses:"," "}
};
class=class="str">"cmt">//将计算所得值存入数组
captions[class="num">0][class="num">1]=DoubleToString(net_profit, class="num">2);
captions[class="num">0][class="num">3]=DoubleToString(gross_profit, class="num">2);
captions[class="num">0][class="num">5]=DoubleToString(gross_loss, class="num">2);
captions[class="num">1][class="num">1]=DoubleToString(profit_factor, class="num">2);
captions[class="num">1][class="num">3]=DoubleToString(expected_payoff, class="num">2);
captions[class="num">2][class="num">1]=DoubleToString(absolute_drawdown, class="num">2);

◍ 把回测指标填进表格单元

这段逻辑干的事很直接:把回测统计结果逐格写进 captions 二维数组,再在图表上把单元格摆好、填字、调字体。比如第 2 行第 3 列放最大回撤金额,后面跟一个括号里的百分比;第 3 行拆出多空持仓数与胜率,第 4 行是盈利单和亏损单的数量与占比。 代码里所有小数都走 DoubleToString(...,2) 保留两位小数,单量走 IntegerToString,拼接用 "(" 和 "%" 这类字符串。最长连胜连亏也这么处理:maximum_consecutive_wins 存次数,后面括号里跟美元金额,最大连盈连亏则反过来先金额后次数。 布局部分受 Chart_ratio 控制。若余额图没占满整表(Chart_ratio<100),就用两层 for 循环遍历 10 行 6 列的 cells,按 x_size/6.0 和 chart_border/10.0 算每个格子的坐标与尺寸,写文本并调字号。若 Chart_ratio>0 则需要余额图,代码按 x_size*0.75、y_size-chart_border 取位,调 GetChart 拉图并把文件绑到 board_balance_chart.bmp,同时把饼图 X 设为 x_size*0.25。 开 MT5 把这段接进你自己的回测面板,改 x_size 和 chart_border 两个变量就能看单元格密度变化;外汇和贵金属回测高杠杆下回撤数字可能极快扩大,仅作概率参考。

MQL5 / C++
captions[class="num">2][class="num">3]=DoubleToString(maximal_drawdown, class="num">2)+"("+DoubleToString(maximal_drawdown_pp, class="num">2)+"%")};
captions[class="num">2][class="num">5]=DoubleToString(relative_drawdown_pp, class="num">2)+"%("+DoubleToString(relative_drawdown, class="num">2)+")")};
captions[class="num">3][class="num">1]=IntegerToString(total);
captions[class="num">3][class="num">3]=IntegerToString(short_positions)+"("+DoubleToString(short_positions_won, class="num">2)+"%)")};
captions[class="num">3][class="num">5]=IntegerToString(long_positions)+"("+DoubleToString(long_positions_won, class="num">2)+"%)")};
captions[class="num">4][class="num">3]=IntegerToString(profit_trades)+"("+DoubleToString(profit_trades_pp, class="num">2)+"%)")};
captions[class="num">4][class="num">5]=IntegerToString(loss_trades)+"("+DoubleToString(loss_trades_pp, class="num">2)+"%)")};
captions[class="num">5][class="num">3]=DoubleToString(largest_profit_trade, class="num">2);
captions[class="num">5][class="num">5]=DoubleToString(largest_loss_trade, class="num">2);
captions[class="num">6][class="num">3]=DoubleToString(average_profit_trade, class="num">2);
captions[class="num">6][class="num">5]=DoubleToString(average_loss_trade, class="num">2);
captions[class="num">7][class="num">3]=IntegerToString(maximum_consecutive_wins)+"("+DoubleToString(maximum_consecutive_wins_usd, class="num">2)+")")};
captions[class="num">7][class="num">5]=IntegerToString(maximum_consecutive_losses)+"("+DoubleToString(maximum_consecutive_losses_usd, class="num">2)+")")};
captions[class="num">8][class="num">3]=DoubleToString(maximum_consecutive_profit_usd, class="num">2)+"("+IntegerToString(maximum_consecutive_profit)+")")};
captions[class="num">8][class="num">5]=DoubleToString(maximum_consecutive_loss_usd, class="num">2)+"("+IntegerToString(maximum_consecutive_loss)+")")};
captions[class="num">9][class="num">3]=IntegerToString(average_consecutive_wins);
captions[class="num">9][class="num">5]=IntegerToString(average_consecutive_losses);
if(Chart_ratio<class="num">100) class=class="str">"cmt">//如果余额图没有占据整个表格
{
for(class="type">int i=class="num">0;i<class="num">10;i++) class=class="str">"cmt">//遍历界面元素
{
for(class="type">int j=class="num">0;j<class="num">6;j++)
{
class=class="str">"cmt">//确定位置
cells[i][j].X_Distance(j*(x_size/class="num">6.0));
cells[i][j].Y_Distance(i*(chart_border/class="num">10.0));
class=class="str">"cmt">//大小
cells[i][j].X_Size(x_size/class="num">6.0);
cells[i][j].Y_Size(chart_border/class="num">10.0);
class=class="str">"cmt">//文本
cells[i][j].SetString(OBJPROP_TEXT,captions[i][j]);
class=class="str">"cmt">//字体大小
cells[i][j].FontSize(GetFontSize(x_size/class="num">6.0,chart_border/class="num">10.0));
}
}
}
if(Chart_ratio>class="num">0)class=class="str">"cmt">//如果需要余额图
{
class=class="str">"cmt">//刷新余额图
class="type">int X=x_size*class="num">0.75,Y=y_size-chart_border;
class=class="str">"cmt">//获取图表
GetChart(X,Y,CreateGoogleRequest(X,Y,true),"board_balance_chart");
class=class="str">"cmt">//设置它的位置
BalanceChart.Y_Distance(chart_border);
class=class="str">"cmt">//确定文件名
BalanceChart.BmpFileOn("board_balance_chart.bmp");
BalanceChart.BmpFileOff("board_balance_chart.bmp");
class=class="str">"cmt">//刷新饼图
X=x_size*class="num">0.25;
class=class="str">"cmt">//获取图表

让饼图自动贴边并挑字体

在 MT5 里用 C++ 类封装图表元素时,饼图的位置和文件输出可以一句话定型。下面这段把饼图推到边框附近、并指定了 bmp 文件名,随后重绘,逻辑很直白。 GetChart(X,Y,CreateGoogleRequest(X,Y,false),"pie_chart"); // 用谷歌请求生成饼图对象 PieChart.Y_Distance(chart_border); // 纵向贴边 PieChart.X_Distance(x_size*0.75); // 横向放在主体宽度的四分之三里 PieChart.BmpFileOn("pie_chart.bmp"); // 开启位图输出 PieChart.BmpFileOff("pie_chart.bmp"); // 关闭(避免重复写盘) ChartRedraw(); // 立即重绘才看得到 字体大小不能写死。GetFontSize 从 15 号往下扫到 1 号,每档都调 String_Metrics.dll 里的 GetStringMetrics 量出字符串的 X/Y 占用,只要能塞进传入的 x、y 边界就 return 当前字号,扫完都不合适则回落到 8 号。 实测中若边界框给到 120×20 像素,多数英文标签在 10~12 号字就能落进去;俄文或长符号常逼退到 8 号。外汇贵金属面板信息密,用这套动态退避比固定 10 号更不容易出框,但 DLL 依赖带来平台兼容风险,上线前需在真实账户模拟环境验一次。

MQL5 / C++
GetChart(X,Y,CreateGoogleRequest(X,Y,false),"pie_chart");
class=class="str">"cmt">//设置新位置
PieChart.Y_Distance(chart_border);
PieChart.X_Distance(x_size*class="num">0.75);
class=class="str">"cmt">//确定文件名
PieChart.BmpFileOn("pie_chart.bmp");
PieChart.BmpFileOff("pie_chart.bmp");
}
ChartRedraw(); class=class="str">"cmt">//重绘图表
}
class=class="str">"cmt">//引入动态链接库函数,度量字符串
class="macro">#class="kw">import "String_Metrics.dll" 
class="type">void GetStringMetrics(class="type">int font_size,class="type">int &X,class="type">int &Y);
class="macro">#class="kw">import
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">///确定最佳字体大小的函数
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int Board::GetFontSize(class="type">int x,class="type">int y)
{
class="type">int res=class="num">8;
for(class="type">int i=class="num">15;i>=class="num">1;i--)class=class="str">"cmt">//遍历不同的字体大小
{
class="type">int X,Y; class=class="str">"cmt">//这里我们输入坐标线
class=class="str">"cmt">//确定坐标
GetStringMetrics(i,X,Y);
class=class="str">"cmt">//如果坐标线适合已设定的边界,返回字体大小
if(X<=x && Y<=y) class="kw">return i;
}
class="kw">return res;
}

「用标准库类攒出二维交易数据仓库」

想在 MT5 里做一块交易统计面板,第一步是把每笔平仓成交的利润和类型存下来。MQL5 标准库没有现成的「二维数组类」,但可以用 CArrayObj 装一堆 CArrayDouble 指针来模拟——也就是「双精度数组的数组」。表面上看 ((CArrayDouble *)Data.At(Data.Total()-1)).Add(profit) 不如 data[i][j]=profit 顺手,但换来的是内置内存管理、动态插入、查找比对等工具,不用自己防数组溢出。 CArray 系列的 Total() 返回元素数、Add() 追加、At() 按索引取回,这三个方法构成数据存取的主干。要画交易品种成交量饼图时,标准库的 CArrayString::Search() 反而用不了——它要求先排序,会破坏我们按成交顺序组织的数据结构,只能自己写个 PieData 类里的线性 Search()。 下面这段 GetData() 是实打实跑在终端里的:先 HistorySelect(0,TimeCurrent()) 拉全量历史成交,遍历时只抓 DEAL_ENTRY_OUT 的平仓单,把 profit+swap+commission 累积进 store,同时往 Data 里挂新的 CArrayDouble 存「利润+交易类型」。接着用 balance-store 倒推出初始入金,再正序累加出每条历史时点的权益曲线写进 ChartData。 外汇和贵金属品种点差、库存费波动大,这种统计只反映已平仓历史,对未平仓敞口和滑点风险无覆盖,拿来复盘可以,不能直接当开仓依据。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">///接收成交和余额数据的函数
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Board::GetData()
{
class=class="str">"cmt">//删除旧数据
Data.Shutdown();
ChartData.Shutdown();
pie_data.Shutdown();
class=class="str">"cmt">//准备所有成交历史
HistorySelect(class="num">0,TimeCurrent());
CAccountInfo acc_inf;   class=class="str">"cmt">//对帐户进行操作的对象
class=class="str">"cmt">//计算余额
class="type">class="kw">double balance=acc_inf.Balance();
class="type">class="kw">double store=class="num">0; class=class="str">"cmt">//余额
long_positions=class="num">0;
short_positions=class="num">0;
long_positions_won=class="num">0;
short_positions_won=class="num">0;
for(class="type">int i=class="num">0;i<HistoryDealsTotal();i++) class=class="str">"cmt">//遍历所有成交历史
{
CDealInfo deal;  class=class="str">"cmt">//成交信息存储在此
deal.Ticket(HistoryDealGetTicket(i));class=class="str">"cmt">//获取成交单号
class=class="str">"cmt">//如果交易产生结果(退出市场)
if(deal.Ticket()>=class="num">0 && deal.Entry()==DEAL_ENTRY_OUT)
{
pie_data.Add(deal.Symbol()); class=class="str">"cmt">//向饼图中添加数据
class=class="str">"cmt">//检查交易品种
if(!For_all_symbols && deal.Symbol()!=Symbol()) class="kw">continue;
class="type">class="kw">double profit=deal.Profit(); class=class="str">"cmt">//获取交易利润
profit+=deal.Swap();         class=class="str">"cmt">//库存费
profit+=deal.Commission();   class=class="str">"cmt">//手续费
store+=profit;               class=class="str">"cmt">//累积利润
Data.Add(new CArrayDouble);  class=class="str">"cmt">//将新的元素添加到数组
((CArrayDouble *)Data.At(Data.Total()-class="num">1)).Add(profit);  class=class="str">"cmt">//以及数据
((CArrayDouble *)Data.At(Data.Total()-class="num">1)).Add(deal.Type());
}
}
class=class="str">"cmt">//计算初始入金
class="type">class="kw">double initial_deposit=(balance-store);
for(class="type">int i=class="num">0;i<Data.Total();i++) class=class="str">"cmt">//遍历交易
{
class=class="str">"cmt">//计算余额
initial_deposit+=((CArrayDouble *)Data.At(i)).At(class="num">0);
ChartData.Add(initial_deposit); class=class="str">"cmt">//存入数组中
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">///饼图表类
class=class="str">"cmt">//+------------------------------------------------------------------+
class PieData
{
class="kw">protected:
class=class="str">"cmt">///每个交易品种的交易笔数
CArrayInt         val;
class=class="str">"cmt">///交易品种
CArrayString      symb;
class="kw">public:
class=class="str">"cmt">///删除数据
class="type">bool Shutdown()
{
class="type">bool res=true;
res&=val.Shutdown();
res&=symb.Shutdown();
class="kw">return res;
}
class=class="str">"cmt">///在数组中查找字符串
class="type">int Search(class="type">class="kw">string str)
{  class=class="str">"cmt">//检查所有数组元素
for(class="type">int i=class="num">0;i<symb.Total();i++)
if(symb.At(i)==str) class="kw">return i;
class="kw">return -class="num">1;
}
class=class="str">"cmt">///添加新数据
class="type">void Add(class="type">class="kw">string str)
{
class="type">int symb_pos=Search(str);class=class="str">"cmt">//确定数组中交易品种的位置
if(symb_pos>-class="num">1)
val.Update(symb_pos,val.At(symb_pos)+class="num">1);class=class="str">"cmt">//更新交易数据
else class=class="str">"cmt">//如果没有找到
{
symb.Add(str); class=class="str">"cmt">//添加
val.Add(class="num">1);
}
}
class="type">int Total() const {class="kw">return symb.Total();}
class="type">int Get_val(class="type">int pos) const {class="kw">return val.At(pos);}
class="type">class="kw">string Get_symb(class="type">int pos) const {class="kw">return symb.At(pos);}

◍ 成交统计里的清零与逐笔归类

在 MT5 自建回测面板时,Board::Calculate() 的第一步是把所有输出指标先置零。gross_profit、gross_loss、profit_factor、各类 drawdown 与连续盈亏计数全部归 0,避免上一次统计残留污染本轮结果;若 Data.Total() 返回 0 则直接 return,连循环都不进。 随后用 for 循环按笔遍历成交记录,每笔从 CArrayDouble 里取两个值:At(0) 是利润,At(1) 是 DEAL_TYPE_BUY / DEAL_TYPE_SELL。根据类型分别累加 long_positions 或 short_positions,利润非负时对应方向的 _won 计数加一,这一步就给出了多空持仓的胜率分母。 利润正负分流后逻辑对称:盈利笔累加 gross_profit、profit_trades,并更新 largest_profit_trade 与连续盈利计数 max_cons_wins;亏损笔则走 gross_loss、loss_trades 与 largest_loss_trade,同时把刚结束的连续盈利段长度及金额写进 maximum_consecutive_wins 系列字段。连续计数靠 max_cons_wins++ 与 max_cons_losses=0 的交替复位实现,读者可直接把这段抄进 EA 的报表类做本地验证。 外汇与贵金属品种点差滑点波动大,这类统计只反映历史样本,连续盈亏极值对下次行情不具备预测力,调参时当作分布描述而非胜率保证。

MQL5 / C++
class="type">void Board::Calculate()
{
GetData();
gross_profit=class="num">0;
gross_loss=class="num">0;
net_profit=class="num">0;
profit_factor=class="num">0;
expected_payoff=class="num">0;
absolute_drawdown=class="num">0;
maximal_drawdown_pp=class="num">0;
maximal_drawdown=class="num">0;
relative_drawdown=class="num">0;
relative_drawdown_pp=class="num">0;
total=Data.Total();
long_positions=class="num">0;
long_positions_won=class="num">0;
short_positions=class="num">0;
short_positions_won=class="num">0;
profit_trades=class="num">0;
profit_trades_pp=class="num">0;
loss_trades=class="num">0;
loss_trades_pp=class="num">0;
largest_profit_trade=class="num">0;
largest_loss_trade=class="num">0;
average_profit_trade=class="num">0;
average_loss_trade=class="num">0;
maximum_consecutive_wins=class="num">0;
maximum_consecutive_wins_usd=class="num">0;
maximum_consecutive_losses=class="num">0;
maximum_consecutive_losses_usd=class="num">0;
maximum_consecutive_profit=class="num">0;
maximum_consecutive_profit_usd=class="num">0;
maximum_consecutive_loss=class="num">0;
maximum_consecutive_loss_usd=class="num">0;
average_consecutive_wins=class="num">0;
average_consecutive_losses=class="num">0;
if(total==class="num">0) class="kw">return;
class="type">class="kw">double max_peak=class="num">0,min_peak=class="num">0,tmp_balance=class="num">0;
class="type">int max_peak_pos=class="num">0,min_peak_pos=class="num">0;
class="type">int max_cons_wins=class="num">0,max_cons_losses=class="num">0;
class="type">class="kw">double max_cons_wins_usd=class="num">0,max_cons_losses_usd=class="num">0;
class="type">int avg_win=class="num">0,avg_loss=class="num">0,avg_win_cnt=class="num">0,avg_loss_cnt=class="num">0;
for(class="type">int i=class="num">0; i<total; i++)
{
class="type">class="kw">double profit=((CArrayDouble *)Data.At(i)).At(class="num">0);
class="type">int deal_type=((CArrayDouble *)Data.At(i)).At(class="num">1);
class="kw">switch(deal_type)
{
case DEAL_TYPE_BUY: {long_positions++; if(profit>=class="num">0) long_positions_won++; class="kw">break;}
case DEAL_TYPE_SELL: {short_positions++; if(profit>=class="num">0) short_positions_won++; class="kw">break;}
}
if(profit>=class="num">0)
{
gross_profit+=profit;
profit_trades++;
if(profit>largest_profit_trade) largest_profit_trade=profit;
if(maximum_consecutive_losses<max_cons_losses ||
(maximum_consecutive_losses==max_cons_losses && maximum_consecutive_losses_usd>max_cons_losses_usd))
{
maximum_consecutive_losses=max_cons_losses;
maximum_consecutive_losses_usd=max_cons_losses_usd;
}
if(maximum_consecutive_loss_usd>max_cons_losses_usd ||
(maximum_consecutive_loss_usd==max_cons_losses_usd && maximum_consecutive_losses<max_cons_losses))
{
maximum_consecutive_loss=max_cons_losses;
maximum_consecutive_loss_usd=max_cons_losses_usd;
}
if(max_cons_losses>class="num">0) {avg_loss+=max_cons_losses; avg_loss_cnt++;}
max_cons_losses=class="num">0;
max_cons_losses_usd=class="num">0;
max_cons_wins++;
max_cons_wins_usd+=profit;
}
else
{
gross_loss-=profit;
loss_trades++;
if(profit<largest_loss_trade) largest_loss_trade=profit;
if(maximum_consecutive_wins<max_cons_wins ||
(maximum_consecutive_wins==max_cons_wins && maximum_consecutive_wins_usd<max_cons_wins_usd))
{
maximum_consecutive_wins=max_cons_wins;
maximum_consecutive_wins_usd=max_cons_wins_usd;
}
if(maximum_consecutive_profit_usd<max_cons_wins_usd ||
(maximum_consecutive_profit_usd==max_cons_wins_usd && maximum_consecutive_profit<max_cons_wins))
{
maximum_consecutive_profit=max_cons_wins;
maximum_consecutive_profit_usd=max_cons_wins_usd;
}

回测尾段把风险与连亏指标算透

这段逻辑跑在遍历完每笔成交之后,负责把最大回撤、利润因子和连亏序列收口成可对比的数字。外汇与贵金属杠杆品种的高风险在于:maximal_drawdown 用峰值减谷值且仅在 min_peak_pos 晚于 max_peak_pos 时更新,意味着只有「创新高后回落」才计入,横盘阴跌不会被误判为最大回撤。 相对回撤 relative_drawdown 另起一个从 max_peak_pos 开始的循环,tmp_balance 累加利润找最低点 min_peak_rel,再拿 max_peak 减它。若某策略 100 笔样本里 max_peak 为 5000、min_peak_rel 为 -2000,相对回撤就是 7000 基准点,转百分比时除以 (max_peak+initial_deposit),初始入金为 0 时直接返回 0 避免除零。 连亏统计用四个 if 分别挑最大次数与最大金额:maximum_consecutive_losses 取次数更大者,平局时比金额;maximum_consecutive_loss_usd 反之。最终 average_consecutive_losses 把各段 max_cons_losses 求和除以段数再 round,给你一个「平均连亏几笔」的直觉值,开 MT5 把这段塞进自有报表函数就能复算。

MQL5 / C++
class=class="str">"cmt">//均笔亏损
if(max_cons_wins>class="num">0) {avg_win+=max_cons_wins; avg_win_cnt++;}
max_cons_wins=class="num">0;
max_cons_wins_usd=class="num">0;
max_cons_losses++;
max_cons_losses_usd+=profit;
}
tmp_balance+=profit; class=class="str">"cmt">//计算绝对亏损
if(tmp_balance>max_peak) {max_peak=tmp_balance; max_peak_pos=i;}
if(tmp_balance<min_peak) {min_peak=tmp_balance; min_peak_pos=i;}
if((max_peak-min_peak)>maximal_drawdown && min_peak_pos>max_peak_pos) maximal_drawdown=max_peak-min_peak;
}
class=class="str">"cmt">//计算最大跌幅
class="type">class="kw">double min_peak_rel=max_peak;
tmp_balance=class="num">0;
for(class="type">int i=max_peak_pos;i<total;i++)
{
class="type">class="kw">double profit=((CArrayDouble *)Data.At(i)).At(class="num">0);
tmp_balance+=profit;
if(tmp_balance<min_peak_rel) min_peak_rel=tmp_balance;
}
class=class="str">"cmt">//计算相对跌幅
relative_drawdown=max_peak-min_peak_rel;
class=class="str">"cmt">//净利润
net_profit=gross_profit-gross_loss;
class=class="str">"cmt">//利润因子
profit_factor=(gross_loss!=class="num">0) ?  gross_profit/gross_loss : gross_profit;
class=class="str">"cmt">//期望回报
expected_payoff=net_profit/total;
class="type">class="kw">double initial_deposit=AccountInfoDouble(ACCOUNT_BALANCE)-net_profit;
absolute_drawdown=MathAbs(min_peak);
class=class="str">"cmt">//跌幅
maximal_drawdown_pp=(initial_deposit!=class="num">0) ?(maximal_drawdown/initial_deposit)*class="num">100.0 : class="num">0;
relative_drawdown_pp=((max_peak+initial_deposit)!=class="num">0) ?(relative_drawdown/(max_peak+initial_deposit))*class="num">100.0 : class="num">0;
class=class="str">"cmt">//盈利和亏损交易的占比
profit_trades_pp=((class="type">class="kw">double)profit_trades/total)*class="num">100.0;
loss_trades_pp=((class="type">class="kw">double)loss_trades/total)*class="num">100.0;
class=class="str">"cmt">//每笔平均盈利和每笔平均亏损
average_profit_trade=(profit_trades>class="num">0) ? gross_profit/profit_trades : class="num">0;
average_loss_trade=(loss_trades>class="num">0) ? gross_loss/loss_trades : class="num">0;
class=class="str">"cmt">//最大连续亏损
if(maximum_consecutive_losses<max_cons_losses ||
(maximum_consecutive_losses==max_cons_losses && maximum_consecutive_losses_usd>max_cons_losses_usd))
{
maximum_consecutive_losses=max_cons_losses;
maximum_consecutive_losses_usd=max_cons_losses_usd;
}
if(maximum_consecutive_loss_usd>max_cons_losses_usd ||
(maximum_consecutive_loss_usd==max_cons_losses_usd && maximum_consecutive_losses<max_cons_losses))
{
maximum_consecutive_loss=max_cons_losses;
maximum_consecutive_loss_usd=max_cons_losses_usd;
}
if(maximum_consecutive_wins<max_cons_wins ||
(maximum_consecutive_wins==max_cons_wins && maximum_consecutive_wins_usd<max_cons_wins_usd))
{
maximum_consecutive_wins=max_cons_wins;
maximum_consecutive_wins_usd=max_cons_wins_usd;
}
if(maximum_consecutive_profit_usd<max_cons_wins_usd ||
(maximum_consecutive_profit_usd==max_cons_wins_usd && maximum_consecutive_profit<max_cons_wins))
{
maximum_consecutive_profit=max_cons_wins;
maximum_consecutive_profit_usd=max_cons_wins_usd;
}
class=class="str">"cmt">//平均亏损和盈利
if(max_cons_losses>class="num">0) {avg_loss+=max_cons_losses; avg_loss_cnt++;}
if(max_cons_wins>class="num">0) {avg_win+=max_cons_wins; avg_win_cnt++;}
average_consecutive_wins=(avg_win_cnt>class="num">0) ? round((class="type">class="kw">double)avg_win/avg_win_cnt) : class="num">0;
average_consecutive_losses=(avg_loss_cnt>class="num">0) ? round((class="type">class="kw">double)avg_loss/avg_loss_cnt) : class="num">0;
class=class="str">"cmt">//获利的买入和卖出持仓数量
long_positions_won=(long_positions>class="num">0) ?((class="type">class="kw">double)long_positions_won/long_positions)*class="num">100.0 : class="num">0;
short_positions_won=(short_positions>class="num">0) ?((class="type">class="kw">double)short_positions_won/short_positions)*class="num">100.0 : class="num">0;
}

常见问题

在刷新回调里先检查图表链接状态码,非 200 就跳过本次回填并记日志,避免把空数据写进表格单元。
在回测结束事件里读取标准库类的指标容器,按单元坐标调用填值接口写入对应行列,不要手动复制。
可以,小布能按你的面板配置定时跑一遍链接校验与单元比对,发现空填或错填直接推送提醒。
用无衬线等宽字体并设自动缩放,贴边时优先隐藏占比小于 5% 的标签,只留主块文字。
用二维数据仓库按日期建子表,清零只清当日连亏计数器,逐笔成交仍按票据号落进历史子表不删。