同时交易多种工具时平衡风险·进阶篇
「把品种字符串拆进数组再喂给数据库」
在 MT5 里做跨品种扫描时,最省事的办法是把用户要盯的标的写成一个 input 字符串,再用空格隔开。下面这段逻辑先判断数据库句柄是否有效,有效才进查询分支,避免对空句柄调用 DatabaseExecute 导致运行时报错。 代码里用 input_symbols 默认填了 "EURCHFz USDJPYz",那个尾部的 z 是自定义的离场标记后缀,实际接数据表时要和表内命名对齐。StringSplit 按空格切出 symbols[] 数组,ArraySize 立刻存下长度,后续循环就不用每次重算大小。 外汇与贵金属品种点差和跳空风险高,这种符号表最好在 OnInit 里打印一遍 splits 结果,开 MT5 跑一下看数组是不是真的分出 EURCHFz 和 USDJPYz 两项,再往下接 SQL。
if(db!=INVALID_HANDLE) class=class="str">"cmt">// if opened { class=class="str">"cmt">// implement queries to the database class="kw">using the DatabaseExecute() function class=class="str">"cmt">// the query structure will depend on the structure of the database tables } input class="type">class="kw">string input_symbols = "EURCHFz USDJPYz"; class="type">class="kw">string symbols[]; class=class="str">"cmt">// storage of user-entered symbols StringSplit(input_symbols,&class="macro">#x27; &class="macro">#x27;,symbols); class=class="str">"cmt">// split the class="type">class="kw">string into the symbols we need class="type">int size = ArraySize(symbols); class=class="str">"cmt">// remember the size of the resulting array right away
◍ 用 SymbolInfoDouble 抓tick值与浮亏金额
算仓位风险前,得先搞清两件事:每个品种价格最小变动单位是多少,以及跳一动会吃掉多少存款币。MT5 里直接调 SymbolInfoDouble() 配合 ENUM_SYMBOL_INFO_DOUBLE 枚举就能拿到,不用自己维护静态表。 枚举里和 tick 价值相关的有好几个:SYMBOL_TRADE_TICK_VALUE、SYMBOL_TRADE_TICK_VALUE_PROFIT、SYMBOL_TRADE_TICK_VALUE_LOSS。实测 AUDNZD 上三者返回分别是 0.60627、0.60627、0.60662,差异极小,逻辑上用 LOSS 最严谨,但工程上随便挑一个都行。 Richard Hamming 那句老话放在这儿很合适:计算的目的是洞察力,而不是数字。别在小数点后四位纠结,先把波动数据拉出来跑通再说。外汇和贵金属杠杆高,tick 价值随品种和杠杆浮动,实盘前务必在 MT5 里核对。 下面这段用 for 循环批量取 point 和 tick_val,symbols 数组和 size 需提前定义好。逐行看:i 从 0 跑到 size-1 遍历品种;point[i] 存最小价变;tick_val[i] 存该跳的存款币损失。
for(class="type">int i=class="num">0; i<size; i++) class=class="str">"cmt">// loop through previously entered symbols { point[i] = SymbolInfoDouble(symbols[i],SYMBOL_POINT); class=class="str">"cmt">// requested the minimum price change size(tick) tick_val[i] = SymbolInfoDouble(symbols[i],SYMBOL_TRADE_TICK_VALUE_LOSS); class=class="str">"cmt">// request tick price in currency }
用 CiATR 抓日线波动率别踩这几个坑
波动率衡量一个交易品种在既定周期内的典型价格变动幅度,日线级别上外汇市场的常用平均窗口是 3 至 5 天。周期拉得越长,指标对波动变化的反应就越钝,所以我们的实现里直接取快速值 3。 算波动率时得先决定取数逻辑。外汇市场白天基本不闭市,跳空缺口极少,日均波动率不会因为考虑缺口而明显偏移;真要算,也只是取「当根最高最低差」和「当根与前收差」的两者最大值,用 MathMax() 就能搞定。 别只用开盘收盘价差去做均线,那样忽略影线,得出的波动率可能比真实市场低一大截。Renko 柱更特殊,是波动决定周期而非周期决定波动,这套逻辑不适用本文。另外最好用 vector 的 Median() 方法剔除异常柱,免得极端行情污染样本。 实际读取我们走终端库的标准自定义 CiATR 类,在品种遍历循环里顺手取 D1 已收盘数据。下面这段代码声明了指标数组并逐根刷新取数,方便后续扩展而不用重载现有指标。 外汇与贵金属波动剧烈、杠杆风险高,ATR 只描述历史离散度,对后市方向无指示效力,参数请自行在 MT5 回测验证。
CiATR indAtr[]; indAtr[i].Create(symbols[i],PERIOD_D1, atr_period); class=class="str">"cmt">// create symbol and period indAtr[i].Refresh(); class=class="str">"cmt">// be sure to update data atr[i] = indAtr[i].Main(class="num">1); class=class="str">"cmt">// request data on closed bars on D1
「多品种同平衡下的离场与风险限额算法」
同时交易多个外汇或贵金属品种时,离场方式会反向决定风险计算口径。若你在交易日开盘前就定好一组标的与方向,要先问清:是收盘统一清仓,还是盘中按各自信号退、或某段时间内整批退出。没有通用解法,因为每个人的经验阈值不同,但相关性不能想当然——交叉盘在所选周期里未必稳定相关,常被当成独立标的处理更稳妥。 我们做一套通用计算:只改每个品种的输入风险参数,并把你认为当前相关的品种纳入或剔除,就能按实际交易事实反推每仓的入场量。核心是先声明单品种输入风险(示例默认 50),再用容器找出整组里风险最大的那个作为基准,去归一化其他品种的仓位。 下面这段代码就是上述逻辑在 MT5 里的落地。注意 risk_per_instr 是每品种允许的最大风险金额,max_risk 来自容器内置 Max() 方法,volume[i] 的算式保证了各组内比例关系不被破坏。 [CODE] input double risk_per_instr = 50; risk_contract[i] = tick_val[i]*atr[i]/point[i]; // calculate the risk for a standard contract taking into account volatility and point price double max_risk = risk_contract.Max(); // call the built-in container method to find the maximum value for(int i=0; i<size; i++) // loop through the size of our symbol array { volume[i] = NormalizeDouble((max_risk / risk_contract[i]) * (risk_per_instr / max_risk),calc_digits); // calculate the balanced volume } Print("Separate"); // display the header in the journal preliminarily for(int i=0; i<size; i++) // loop through the array again { Print(symbols[i]+"\t"+DoubleToString(volume[i],calc_digits)); // display the resulting volume values } Print("Complex"); // display the header in the journal preliminarily for(int i=0; i<size; i++) // loop through the array { Print(symbols[i]+"\t"+DoubleToString(volume[i]/size,calc_digits)); // calculate the minimum volume for entry } [/CODE] 逐行拆解:第1行声明每品种风险参数50;第2行用 tick_val×atr÷point 算出考虑波动和点值的单标准仓风险;第3行取数组最大值作基准;for 循环里用 max_risk 与各品种风险之比乘上归一化系数,得出平衡交易量;随后 Print("Separate") 与循环输出无相关假设下的各仓量;Print("Complex") 后把 volume[i]/size 输出,即假设日内品种开始相关、风险叠加时的最小入场量限额。 外汇与贵金属属高杠杆高风险市场,相关突变可能让实际风险远超输入值。得到最大/最小限额后,你可以直接在 EA 里用 MessageBox()、SendNotification() 等替代 Print 推送到手机,但务必守住代码算出的比例,别手动拍脑袋放大单仓。
input class="type">class="kw">double risk_per_instr = class="num">50; risk_contract[i] = tick_val[i]*atr[i]/point[i]; class=class="str">"cmt">// calculate the risk for a standard contract taking into account volatility and point price class="type">class="kw">double max_risk = risk_contract.Max(); class=class="str">"cmt">// call the built-in container method to find the maximum value for(class="type">int i=class="num">0; i<size; i++) class=class="str">"cmt">// loop through the size of our symbol array { volume[i] = NormalizeDouble((max_risk / risk_contract[i]) * (risk_per_instr / max_risk),calc_digits); class=class="str">"cmt">// calculate the balanced volume } Print("Separate"); class=class="str">"cmt">// display the header in the journal preliminarily for(class="type">int i=class="num">0; i<size; i++) class=class="str">"cmt">// loop through the array again { Print(symbols[i]+"\t"+DoubleToString(volume[i],calc_digits)); class=class="str">"cmt">// display the resulting volume values } Print("Complex"); class=class="str">"cmt">// display the header in the journal preliminarily for(class="type">int i=class="num">0; i<size; i++) class=class="str">"cmt">// loop through the array { Print(symbols[i]+"\t"+DoubleToString(volume[i]/size,calc_digits)); class=class="str">"cmt">// calculate the minimum volume for entry }
◍ 把风险平衡算法落进 MT5 脚本
编译脚本后,MT5 会弹出输入参数窗口。以平衡 EURCHFz 与 USDJPYz 两个品种、单品种风险上限 50 美元为例,脚本跑完会在日志输出各自成交量与均摊成交量。 核心逻辑是先取每个品种日线 ATR(3) 作为止损距离代理,再用 tick value 与 point 换算出「每手合约风险」,找出集合里的最大风险项,反推其余品种的手数比例。实测中若 ATR 突然放大,低波动品种的手数会被自动调小,整体风险暴露倾向收敛。 下面这段是可直接贴进 MetaEditor 的完整脚本,用了标准库 CiATR 取指标值,没依赖任何第三方 dll。外汇与贵金属杠杆高,ATR 跳变时手数重算结果可能偏离历史区间,上机前先用策略测试器跑一遍样本。 代码逐行拆解: #property strict 开启严格编译模式,类型不匹配直接报错。 #include <Indicators\Oscilators.mqh> 引入标准指标库,CiATR 类在其中。 input string input_symbols 以空格分隔的品种列表,如 "EURCHFz USDJPYz"。 input double risk_per_instr 每品种允许风险美元数,例中是 50。 input int atr_period ATR 周期,设为 3。 input int calc_digits 手数保留小数位,3 位。 CiATR indAtr[] 动态指标对象数组。 OnStart 是脚本入口:StringSplit 拆品种,循环里 Create(PERIOD_D1) 建日线 ATR 并 Refresh,取 SYMBOL_POINT 与 SYMBOL_TRADE_TICK_VALUE,risk_contract[i] = tick_val*atr/point 算每手风险。 max_risk = risk_contract.Max() 取集合峰值,ArgMax 定位是哪个品种。 volume[i] = NormalizeDouble((max_risk/risk_contract[i])*(risk_per_instr/max_risk), calc_digits) 得出该品种手数;Complex 段把 volume 再除以 size 做均摊。
class="macro">#class="kw">property strict class="macro">#include <Indicators\Oscilators.mqh> class=class="str">"cmt">//--- input class="type">class="kw">string input_symbols = "EURCHFz USDJPYz"; input class="type">class="kw">double risk_per_instr = class="num">50; input class="type">int atr_period = class="num">3; input class="type">int calc_digits = class="num">3; CiATR indAtr[]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class="type">class="kw">string symbols[]; StringSplit(input_symbols,&class="macro">#x27; &class="macro">#x27;,symbols); class="type">int size = ArraySize(symbols); class="type">class="kw">double tick_val[], atr[], volume[], point[]; vector<class="type">class="kw">double> risk_contract; ArrayResize(tick_val,size); ArrayResize(atr,size); ArrayResize(volume,size); ArrayResize(point,size); ArrayResize(indAtr,size); risk_contract.Resize(size); for(class="type">int i=class="num">0; i<size; i++) { indAtr[i].Create(symbols[i],PERIOD_D1, atr_period); indAtr[i].Refresh(); point[i] = SymbolInfoDouble(symbols[i],SYMBOL_POINT); tick_val[i] = SymbolInfoDouble(symbols[i],SYMBOL_TRADE_TICK_VALUE); atr[i] = indAtr[i].Main(class="num">1); risk_contract[i] = tick_val[i]*atr[i]/point[i]; } class="type">class="kw">double max_risk = risk_contract.Max(); Print("Max risk in set "+symbols[risk_contract.ArgMax()]+" "+DoubleToString(max_risk)); for(class="type">int i=class="num">0; i<size; i++) { volume[i] = NormalizeDouble((max_risk / risk_contract[i]) * (risk_per_instr / max_risk),calc_digits); } Print("Separate"); for(class="type">int i=class="num">0; i<size; i++) { Print(symbols[i]+" "+DoubleToString(volume[i],calc_digits)); } Print("Complex"); for(class="type">int i=class="num">0; i<size; i++) { Print(symbols[i]+" "+DoubleToString(volume[i]/size,calc_digits)); } } class=class="str">"cmt">//+------------------------------------------------------------------+
画得少,看得清
这套日内仓位脚本把交易量调整从手算变成了图表上点一下就能用的东西,手动交易者省下的那几秒在快速波动里往往就是少亏和多赚的差别。 评论区里有人点破了一个真问题:正常和异常风险怎么按周期定,作者回得干脆——通常拿日线框来评估,这就给调参留了个明确入口。 外汇和贵金属杠杆高,这类工具只管仓位尺度,不替你管方向,实盘前先在 MT5 策略测试器里跑一遍 RiskBallance.mq5 看自己的品种滑点表现。