MQL5仓位历史属性函数库实战
用交易历史获取首仓价与持仓时长
为什么需要交易历史函数库
在MetaTrader 5中,若交易系统在整个持仓周期保持固定手数,标准仓位标识符已足够。但当你计划实现资金管理系统,在策略运行中途动态加仓或减仓时,仅靠POSITION_PRICE_OPEN等实时标识无法追溯仓位的建仓原点。此时必须访问账户交易历史,提取第一笔成交价、初始手数、最后一笔加仓价等信息。本文函数库正是为这类需求而写。
举例来说,一个EA在EURUSD上首仓0.1手,行情顺着趋势后再加0.1手,此时POSITION_VOLUME返回0.2,但策略可能需要知道首仓成本是1.1000还是1.1020,以计算均价距离。这些信息只能从历史成交记录中按仓位开启时间过滤获得。
核心数据结构设计
原文用枚举ENUM_POSITION_PROPERTIES统一描述仓位属性,并新增P_PRICE_FIRST_DEAL(6)、P_PRICE_LAST_DEAL(9)、P_INITIAL_VOLUME(12)、P_DURATION(16)等标识。同时用struct position_properties替代零散全局变量,使代码更清晰,编辑器可自动提示字段注释。
类似地,symbol_properties结构保存点差、止损位限制等品种属性。创建结构变量pos与symb后,访问方式形如pos.exists、pos.first_deal_price,与类对象用法一致。将旧全局变量如pos_open用查找替换(Ctrl+H)批量改为pos.exists,可大幅提升重构效率。
enum ENUM_POSITION_PROPERTIES
{
P_TOTAL_DEALS = 0,
P_SYMBOL = 1,
P_MAGIC = 2,
P_COMMENT = 3,
P_SWAP = 4,
P_COMMISSION = 5,
P_PRICE_FIRST_DEAL= 6,
P_PRICE_OPEN = 7,
P_PRICE_CURRENT = 8,
P_PRICE_LAST_DEAL = 9,
P_PROFIT = 10,
P_VOLUME = 11,
P_INITIAL_VOLUME = 12,
P_SL = 13,
P_TP = 14,
P_TIME = 15,
P_DURATION = 16,
P_ID = 17,
P_TYPE = 18,
P_ALL = 19
};
struct position_properties
{
uint total_deals;
bool exists;
string symbol;
long magic;
string comment;
double swap;
double commission;
double first_deal_price;
double price;
double current_price;
double last_deal_price;
double profit;
double volume;
double initial_volume;
double sl;
double tp;
datetime time;
ulong duration;
long id;
ENUM_POSITION_TYPE type;
};
外部参数与下单辅助函数
EA新增VolumeIncrease等input参数,并用sinput修饰MagicNumber与ShowInfoPanel,使它们在策略测试器中灰显不可优化。OpenPosition()函数封装CTrade的PositionOpen,提前用trade.SetExpertMagicNumber与SetDeviationInPoints配置幻数和滑点。
在TradingBlock()中,若同方向信号且VolumeIncrease>0,则计算加仓手数并调用OpenPosition增量下单;若反向且Reverse=true,则平掉原仓并反手。这样开启、反转、加仓仅需一行调用。
sinput long MagicNumber=777; input double VolumeIncrease=0.1; sinput bool ShowInfoPanel=true;
void OpenPosition(double lot, ENUM_ORDER_TYPE order_type, double price, double sl, double tp, string comment)
{
trade.SetExpertMagicNumber(MagicNumber);
trade.SetDeviationInPoints(CorrectValueBySymbolDigits(Deviation));
if(!trade.PositionOpen(_Symbol,order_type,lot,price,sl,tp,comment))
{ Print("开启仓位错误: ",GetLastError()); }
}
从历史提取仓位交易数量
CurrentPositionTotalDeals()以pos.time为起点调用HistorySelect(pos.time,TimeCurrent()),再用HistoryDealsTotal()遍历,统计与_Symbol同品种成交数。注意反转仓位时仓位ID不变,但开启时间不变,因此用时间选历史比用HistorySelectByPosition更直观。
uint CurrentPositionTotalDeals()
{
int total=0, count=0;
string deal_symbol="";
if(HistorySelect(pos.time,TimeCurrent()))
{
total=HistoryDealsTotal();
for(int i=0; i<total; i++)
{
deal_symbol=HistoryDealGetString(HistoryDealGetTicket(i),DEAL_SYMBOL);
if(deal_symbol==_Symbol) count++;
}
}
return(count);
}
获取首笔与末笔成交价
CurrentPositionFirstDealPrice()正向遍历,当交易时间等于pos.time且品种匹配时退出,返回该笔DEAL_PRICE。CurrentPositionLastDealPrice()反向遍历,通常首轮即命中,但多品种同跑时需继续至品种匹配。两者均依赖HistorySelect覆盖的时间窗。
double CurrentPositionFirstDealPrice()
{
int total=0; string deal_symbol=""; double deal_price=0.0; datetime deal_time=NULL;
if(HistorySelect(pos.time,TimeCurrent()))
{
total=HistoryDealsTotal();
for(int i=0; i<total; i++)
{
deal_price=HistoryDealGetDouble(HistoryDealGetTicket(i),DEAL_PRICE);
deal_symbol=HistoryDealGetString(HistoryDealGetTicket(i),DEAL_SYMBOL);
deal_time=(datetime)HistoryDealGetInteger(HistoryDealGetTicket(i),DEAL_TIME);
if(deal_time==pos.time && deal_symbol==_Symbol) break;
}
}
return(deal_price);
}
计算初始仓位交易量
CurrentPositionInitialVolume()最复杂:反向遍历历史,累加入场后的加仓量sum_volume,若DEAL_ENTRY为INOUT(反转)则用pos.volume减sum_volume得首仓量。需处理无加仓、减仓等边界,代码注释详尽,实测无误。
double CurrentPositionInitialVolume()
{
int total=0; ulong ticket=0; ENUM_DEAL_ENTRY deal_entry=WRONG_VALUE; bool inout=false;
double sum_volume=0.0, deal_volume=0.0; string deal_symbol=""; datetime deal_time=NULL;
if(HistorySelect(pos.time,TimeCurrent()))
{
total=HistoryDealsTotal();
for(int i=total-1; i>=0; i--)
{
if((ticket=HistoryDealGetTicket(i))>0)
{
deal_volume=HistoryDealGetDouble(ticket,DEAL_VOLUME);
deal_entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket,DEAL_ENTRY);
deal_time=(datetime)HistoryDealGetInteger(ticket,DEAL_TIME);
deal_symbol=HistoryDealGetString(ticket,DEAL_SYMBOL);
if(deal_time<=pos.time) break;
if(deal_symbol==_Symbol) sum_volume+=deal_volume;
}
}
}
if(deal_entry==DEAL_ENTRY_INOUT)
{
if(fabs(sum_volume)>0) { double result=pos.volume-sum_volume; deal_volume=result>0?result:pos.volume; }
if(sum_volume==0) deal_volume=pos.volume;
}
return(NormalizeDouble(deal_volume,2));
}
持仓时间函数与格式化
枚举ENUM_POSITION_DURATION提供天/时/分/秒模式。CurrentPositionDuration()用TimeCurrent()-pos.time算秒,再除相应系数。CurrentPositionDurationToString()将秒拆为DD:HH:MM:SS字符串,便于信息面板展示。
ulong CurrentPositionDuration(ENUM_POSITION_DURATION mode)
{
ulong seconds=TimeCurrent()-pos.time;
switch(mode)
{
case DAYS: return seconds/(60*60*24);
case HOURS: return seconds/(60*60);
case MINUTES: return seconds/60;
case SECONDS: return seconds;
default: return 0;
}
}
回测优化与多品种验证
原文在策略测试器对参数优化,按最大采收率排序后取4.07组,在EURUSD、AUDUSD、NZDUSD均正面。说明该函数库不影响EA逻辑,仅丰富属性输出,可安全用于实盘资金管理。
优化时sinput参数灰显排除,避免无意义维度。使用者应自测经纪商历史质量,再决定是否用于生产。