掉期利率(第一部分):锁定与合成仓位·进阶篇
(2/3)· 半数点差藏在隔夜掉期里,却少有人把它变成可计算的锁仓工具
用结构体和数组给掉期检验备好原始数据
要在 MT5 里自动筛查多边掉期回路,第一步是把市场观察里的品种规整成可计算的容器。货币名必须正好 6 个大写字母,部分经纪商带前缀后缀,字符串处理时要兼容。 作者定义了两个结构,基础版 Pair 存名称、买卖掉期、TickValue、合约大小等;扩展版 PairAdvanced 继承前者,多出 Side(分子或分母位置)、LotK 和 Lot,方便后面常规化手数。基础结构同时被其他用途复用,避免建多余容器。 数组大小靠 SetSizePairsArray 控制,其中 MaxSymbols 是 EA 输入参数,手动限定最大品种数;MaxPairs 限定公式单边最大货币对数。实际分配时 BasicPairsLeft/Right 取 MaxPairs*2,因为每对含两种货币,基准货币上限翻倍。 这套准备是后面随机公式生成和平衡过滤的地基。开 MT5 把下面代码丢进 EA 头文件,改 MaxSymbols 就能先看清自己经纪商观察窗口能喂多少品种进去。
class="kw">struct Pairclass=class="str">"cmt">// required symbol information { class="type">class="kw">string Name;class=class="str">"cmt">// currency pair class="type">class="kw">double SwapBuy;class=class="str">"cmt">// buy swap class="type">class="kw">double SwapSell;class=class="str">"cmt">// swap sell class="type">class="kw">double TickValue;class=class="str">"cmt">// profit from class="num">1 movement tick of a class="num">1-lot position class="type">class="kw">double TickSize;class=class="str">"cmt">// tick size in the price class="type">class="kw">double PointX;class=class="str">"cmt">// point size in the price class="type">class="kw">double ContractSize;class=class="str">"cmt">// contract size in the base deposit currency class="type">class="kw">double Margin;class=class="str">"cmt">// margin for opening class="num">1 lot }; class="kw">struct PairAdvanced : Pairclass=class="str">"cmt">// extended container { class="type">class="kw">string Side;class=class="str">"cmt">// in numerator or denominator class="type">class="kw">double LotK;class=class="str">"cmt">// lot coefficient class="type">class="kw">double Lot;class=class="str">"cmt">// lot }; Pair Pairs[];class=class="str">"cmt">// data of currency pairs class="type">void SetSizePairsArray()class=class="str">"cmt">// set size of the array of pairs { ArrayResize(Pairs,MaxSymbols); ArrayResize(BasicPairsLeft,MaxPairs*class="num">2);class=class="str">"cmt">// since each pair has class="num">2 currencies, there can be a maximum of class="num">2 times more base currencies ArrayResize(BasicPairsRight,MaxPairs*class="num">2);class=class="str">"cmt">// since each pair has class="num">2 currencies, there can be a maximum of class="num">2 times more base currencies }
「用字符掩码筛掉非法品种名」
MT5 里批量扫描可交易品种时,先得确认品种名合规。下面这段逻辑把小写字母和数字列为非法字符,凡是名字里撞上这些字符的就直接判无效,只留全大写字母组成的名称。 对外汇和贵金属来说,这种过滤能避开带后缀的非常规报价(如带 '.m' 或 '.pro' 的变体),但注意市场观察窗口里的品种仍可能含特殊前缀,需配合长度判断。高风险品种过滤不当会导致后续持仓计算偏差。 核心校验函数逐行拆解如下: bool IsValid(string s) // 检查品种名有效性,要求全大写字母 string Mask="abcdefghijklmnopqrstuvwxyz1234567890"; // 定义不支持的字符掩码:小写字母与数字 for(int i=0;i<StringLen(s);i++) // 遍历输入字符串每个字符 for(int j=0;j<StringLen(Mask);j++) // 遍历掩码每个字符 if(s[i]==Mask[j]) return false; // 若命中掩码字符,立即返回无效 return true; // 未命中任何非法字符,返回有效 在 FillPairsArray 中调用时,配合 SymbolName(i,false) 取市场观察窗内第 i 个品种名,长度须等于 6+PrefixE+PostfixE 且 IsValid 通过、交易模式为 SYMBOL_TRADE_MODE_FULL,才纳入Pairs数组。
class="type">bool IsValid(class="type">class="kw">string s)class=class="str">"cmt">// checking the instrument validity(its name must consist of upper-case letters) { class="type">class="kw">string Mask="abcdefghijklmnopqrstuvwxyz1234567890";class=class="str">"cmt">// mask of unsupported characters(lowercase letters and numbers) for ( class="type">int i=class="num">0; i<StringLen(s); i++ )class=class="str">"cmt">// reset symbols { for ( class="type">int j=class="num">0; j<StringLen(Mask); j++ ) { if ( s[i] == Mask[j] ) class="kw">return class="kw">false; } } class="kw">return true; }
◍ 百分比掉期换算到账户币种的细节
当券商把掉期费设成百分比模式(case 5 / case 6),代码用同一套公式把多空过夜成本折算成报价币金额:correction 乘上 SYMBOL_SWAP_LONG/SHORT,再乘当前 BID 与合约规模,最后除以 360.0*100.0。360 这个除数对应一年交易日近似,意味着算出来的是「每元仓位占用的年化百分比对应的单日成本」,直接读 SYMBOL_SWAP_LONG 原值会高估约 360 倍。 CorrectedValue 函数干的是币种对齐:先取账户币种 OurValue,再从交易对名里截出后半段币种 Half2Source。若它和账户币种一致,rez 直接返回 1.0,不再查汇率。 不一致时遍历 MarketWatch 内全部符号,只挑长度等于 6+PrefixE+PostfixE 且 IsValid 通过的去做交叉汇率匹配。这里隐含一个硬约束——目标交叉对必须已加进行情窗口,否则循环扫不到,rez 拿不到值,百分比掉期计算会偏掉。外汇与贵金属杠杆高,掉期虽小但持仓过周末可能吃掉短线盈利,建议在 MT5 里打印几组品种的 SwapBuy 验证除数逻辑。
case class="num">5:class=class="str">"cmt">// in percent Pairs[iterator].SwapBuy=correction*SymbolInfoDouble(Pairs[iterator].Name,SYMBOL_SWAP_LONG)*SymbolInfoDouble(Pairs[iterator].Name,SYMBOL_BID)*Pairs[iterator].ContractSize/(class="num">360.0*class="num">100.0); Pairs[iterator].SwapSell=correction*SymbolInfoDouble(Pairs[iterator].Name,SYMBOL_SWAP_SHORT)*SymbolInfoDouble(Pairs[iterator].Name,SYMBOL_BID)*Pairs[iterator].ContractSize/(class="num">360.0*class="num">100.0); class="kw">break; case class="num">6:class=class="str">"cmt">// in percent Pairs[iterator].SwapBuy=correction*SymbolInfoDouble(Pairs[iterator].Name,SYMBOL_SWAP_LONG)*SymbolInfoDouble(Pairs[iterator].Name,SYMBOL_BID)*Pairs[iterator].ContractSize/(class="num">360.0*class="num">100.0); Pairs[iterator].SwapSell=correction*SymbolInfoDouble(Pairs[iterator].Name,SYMBOL_SWAP_SHORT)*SymbolInfoDouble(Pairs[iterator].Name,SYMBOL_BID)*Pairs[iterator].ContractSize/(class="num">360.0*class="num">100.0); class="kw">break; } Pairs[iterator].Margin=SymbolInfoDouble(Pairs[iterator].Name,SYMBOL_MARGIN_INITIAL); Pairs[iterator].TickValue=SymbolInfoDouble(Pairs[iterator].Name,SYMBOL_TRADE_TICK_VALUE); iterator++; } } class="type">bool CorrectedValue(class="type">class="kw">string Pair0,class="type">class="kw">double &rez)class=class="str">"cmt">// adjustment factor to convert to deposit currency for the percentage swap calculation method { class="type">class="kw">string OurValue=AccountInfoString(ACCOUNT_CURRENCY);class=class="str">"cmt">// deposit currency class="type">class="kw">string Half2Source=StringSubstr(Pair0,PrefixE+class="num">3,class="num">3);class=class="str">"cmt">// lower currency of the pair to be adjusted if ( Half2Source == OurValue ) { rez=class="num">1.0; class="kw">return true; } for ( class="type">int i=class="num">0; i<SymbolsTotal(class="kw">false); i++ )class=class="str">"cmt">// check symbols from the MarketWatch window { if ( StringLen(SymbolName(i,class="kw">false)) == class="num">6+PrefixE+PostfixE && IsValid(SymbolName(i,class="kw">false)) )class=class="str">"cmt">//find the currency rate to convert to the account currency { class="type">class="kw">string Half1=StringSubstr(SymbolName(i,class="kw">false),PrefixE,class="num">3); class="type">class="kw">string Half2=StringSubstr(SymbolName(i,class="kw">false),PrefixE+class="num">3,class="num">3);
等式结构与随机配对生成逻辑
这段代码片段在做两件事:先按货币对构成匹配等式两侧,再随机生成左右两边的货币对数量。前半段 if 判断里,当 Half2 等于目标值且 Half1 来自源侧时,直接取 SymbolName(i,false) 的 SYMBOL_BID 作为 rez 并返回 true;若反过来 Half1 匹配、Half2 来自源侧,则取该 Bid 价的倒数再返回,等价于处理反向报价对。 随后定义的 EquationBasic 结构体,用四个 string 字段分别存等式左侧/右侧的货币对清单与结构描述,为后续组合校验提供容器。 随机数量生成函数里有个硬约束值得注意:GenerateRandomQuantityRightSide 中若左端只有 1 对且右端算出来小于 2,会强制返回 2。注释写得很直白——任一侧至少要有 2 对,否则等式就退化成开两个反向仓位,失去中性组合意义。 GenerateRandomIndex 用 MathRand()/32767.0 映射到 [0, MaxSymbols) 区间,越界时钳到 MaxSymbols-1,保证只从当前 MarketWatch 窗口取符号索引。外汇与贵金属杠杆品种波动剧烈,这类随机组合回测前务必在 MT5 策略测试器用极小手数验证,避免实盘暴露过高风险。
if ( Half2 == OurValue && Half1 == Half2Source ) { rez=SymbolInfoDouble(SymbolName(i,class="kw">false),SYMBOL_BID); class="kw">return true; } if ( Half1 == OurValue && Half2 == Half2Source ) { rez=class="num">1.0/SymbolInfoDouble(SymbolName(i,class="kw">false),SYMBOL_BID); class="kw">return true; } } } class="kw">return class="kw">false; } class="kw">struct EquationBasic class=class="str">"cmt">// structure containing the basic formula { class="type">class="kw">string LeftSide;class=class="str">"cmt">// currency pairs participating in the formula on the left side of the "=" sign class="type">class="kw">string LeftSideStructure;class=class="str">"cmt">// structure of the left side of the formula class="type">class="kw">string RightSide;class=class="str">"cmt">// currency pairs participating in the right side of the formula class="type">class="kw">string RightSideStructure;class=class="str">"cmt">// structure of the right side of the formula }; class="type">int GenerateRandomQuantityLeftSide()class=class="str">"cmt">// generate a random number of pairs on the left side of the formula { class="type">int RandomQuantityLeftSide=class="num">1+class="type">int(MathFloor((class="type">class="kw">double(MathRand())/class="num">32767.0)*(MaxPairs-class="num">1))); if ( RandomQuantityLeftSide >= MaxPairs ) class="kw">return MaxPairs-class="num">1; class="kw">return RandomQuantityLeftSide; } class="type">int GenerateRandomQuantityRightSide(class="type">int LeftLenght)class=class="str">"cmt">// generate a random number of pairs on the right side of the formula(taking into account the number of pairs on the left side) { class="type">int RandomQuantityRightSide=class="num">1+class="type">int(MathFloor((class="type">class="kw">double(MathRand())/class="num">32767.0)*(MaxPairs-LeftLenght))); if ( RandomQuantityRightSide < class="num">2 && LeftLenght == class="num">1 ) class="kw">return class="num">2;class=class="str">"cmt">// there must be at least class="num">2 pairs in one of the sides, otherwise it will be equivalent to opening two opposite positions if ( RandomQuantityRightSide > (MaxPairs-LeftLenght) ) class="kw">return (MaxPairs-LeftLenght); class="kw">return RandomQuantityRightSide; } class="type">int GenerateRandomIndex()class=class="str">"cmt">// generate a random index of a symbol from the MarketWatch window { class="type">int RandomIndex=class="num">0; class="kw">while(true) { RandomIndex=class="type">int(MathFloor((class="type">class="kw">double(MathRand())/class="num">32767.0) * class="type">class="kw">double(MaxSymbols)) ); if ( RandomIndex >= MaxSymbols ) RandomIndex=MaxSymbols-class="num">1;
「随机方程的左右侧拼装与平衡判定」
生成随机方程时,左侧和右侧的品种数量分别由 GenerateRandomQuantityLeftSide 与 GenerateRandomQuantityRightSide 决定,两者独立抽取,因此左右侧长度可能不等。循环中用 ^ 连接多个品种名,仅最后一个不加分隔符,最终 LeftSide 形如 EURUSD^GBPUSD 这类字符串。
方向标记靠 MathRand()/32767.0 > 0.5 抛掷决定,大于 0.5 记 u(上行),否则记 d(下行),左右侧各自累计成 TempLeftStructure / TempRightStructure。这套 0/1 概率 split 意味着单品种方向约 50% 倾向,不预设任何趋势优劣。
bBalanced 函数接收 EquationBasic 引用,尝试把公式归整进 EquationCorrected。它逐段切出 SubPair、Half1、Half2,用 Divider 区分分子分母位置,ReadStartIterator 与 quantityiterator 控制读取游标。若当前组合可配平,就把修正结果写回 r 并返回 true,否则 false。
外汇与贵金属品种代入该类随机方程做回测时波动剧烈、杠杆风险高,实盘前务必在 MT5 策略测试器用历史数据验证配平逻辑是否如预期触发。
if( StringLen(Pairs[RandomIndex].Name) > class="num">0 ) class="kw">return RandomIndex; } class="kw">return RandomIndex; } EquationBasic GenerateBasicEquation()class=class="str">"cmt">// generate both parts of the random equation { class="type">int RandomQuantityLeft=GenerateRandomQuantityLeftSide(); class="type">int RandomQuantityRight=GenerateRandomQuantityRightSide(RandomQuantityLeft); class="type">class="kw">string TempLeft=""; class="type">class="kw">string TempRight=""; class="type">class="kw">string TempLeftStructure=""; class="type">class="kw">string TempRightStructure=""; for( class="type">int i=class="num">0; i<RandomQuantityLeft; i++ ) { class="type">int RandomIndex=GenerateRandomIndex(); if( i == class="num">0 && RandomQuantityLeft > class="num">1 ) TempLeft+=Pairs[RandomIndex].Name+"^"; if( i != class="num">0 && (RandomQuantityLeft-i) > class="num">1 ) TempLeft+=Pairs[RandomIndex].Name+"^"; if( i == RandomQuantityLeft-class="num">1 ) TempLeft+=Pairs[RandomIndex].Name; if( class="type">class="kw">double(MathRand())/class="num">32767.0 > class="num">0.5 ) TempLeftStructure+="u"; else TempLeftStructure+="d"; } for( class="type">int i=RandomQuantityLeft; i<RandomQuantityLeft+RandomQuantityRight; i++ ) { class="type">int RandomIndex=GenerateRandomIndex(); if( i == RandomQuantityLeft && RandomQuantityRight > class="num">1 ) TempRight+=Pairs[RandomIndex].Name+"^"; if( i != RandomQuantityLeft && (RandomQuantityLeft+RandomQuantityRight-i) > class="num">1 ) TempRight+=Pairs[RandomIndex].Name+"^"; if( i == RandomQuantityLeft+RandomQuantityRight-class="num">1 ) TempRight+=Pairs[RandomIndex].Name; if( class="type">class="kw">double(MathRand())/class="num">32767.0 > class="num">0.5 ) TempRightStructure+="u"; else TempRightStructure+="d"; } EquationBasic result; result.LeftSide=TempLeft; result.LeftSideStructure=TempLeftStructure; result.RightSide=TempRight; result.RightSideStructure=TempRightStructure; class="kw">return result; } BasicValue BasicPairsLeft[];class=class="str">"cmt">// array of base pairs to the left BasicValue BasicPairsRight[];class=class="str">"cmt">// array of base pairs to the right class="type">bool bBalanced(EquationBasic &CheckedPair,EquationCorrected &r)class=class="str">"cmt">// if the current formula is balanced(if yes, class="kw">return the corrected version to the "r" variable) { class="type">bool bEnd=class="kw">false; class="type">class="kw">string SubPair;class=class="str">"cmt">// the full name of the currency pair class="type">class="kw">string Half1;class=class="str">"cmt">// the first currency of the pair class="type">class="kw">string Half2;class=class="str">"cmt">// the second currency of the pair class="type">class="kw">string SubSide;class=class="str">"cmt">// the currency pair in the numerator or denominator class="type">class="kw">string Divider;class=class="str">"cmt">// separator class="type">int ReadStartIterator=class="num">0;class=class="str">"cmt">// reading start index class="type">int quantityiterator=class="num">0;class=class="str">"cmt">// quantity class="type">bool bNew; BasicValue b0;
◍ 等式左侧基础币种的拆解与计数
做三角套利或跨式价差扫描时,先把左右两侧的基础货币数组清零是避免上根 K 线残留污染的硬动作。下面这段逻辑在每次重算前把 BasicPairsLeft 和 BasicPairsRight 的 Value 置空、Quantity 归零,保证旧标的不会混进新等式。 左侧解析靠 '^' 分隔符切分字符串,从 CheckedPair.LeftSide 里逐段抽出 3 字符的 Half1(基础币)与 Half2(报价币)。若遇到分隔符或抵达末尾,就按 LeftSideStructure 里的 'u'(向上)/ 'd'(向下)标记给对应币种 Quantity 加一或减一。 数组里没有该币种就向后找第一个空槽写入,新插入同样遵循 u/d 的增减规则。实盘里EURUSD^GBPUSD 这类 6 字符子串会被切成 EUR 与 USD 分别计权,跑一遍你能在 Watch 窗口看到左侧数组里 EUR 的 Quantity 随等式结构在 -2 到 +2 之间摆动,外汇与贵金属品种在此类扫描中滑点敏感,高风险。
for ( class="type">int i=class="num">0; i<ArraySize(BasicPairsLeft); i++ )class=class="str">"cmt">//reset the array of base pairs { BasicPairsLeft[i].Value = ""; BasicPairsLeft[i].Quantity = class="num">0; } for ( class="type">int i=class="num">0; i<ArraySize(BasicPairsRight); i++ )class=class="str">"cmt">// resetting the array of base pairs { BasicPairsRight[i].Value = ""; BasicPairsRight[i].Quantity = class="num">0; } class=class="str">"cmt">//// Calculate balance values for the left side quantityiterator=class="num">0; ReadStartIterator=class="num">0; for ( class="type">int i=ReadStartIterator; i<StringLen(CheckedPair.LeftSide); i++ )class=class="str">"cmt">// extract base currencies from the left side of the equation { Divider=StringSubstr(CheckedPair.LeftSide,i,class="num">1); if ( Divider == "^" || i == StringLen(CheckedPair.LeftSide) - class="num">1 ) { SubPair=StringSubstr(CheckedPair.LeftSide,ReadStartIterator+PrefixE,class="num">6); SubSide=StringSubstr(CheckedPair.LeftSideStructure,quantityiterator,class="num">1); Half1=StringSubstr(CheckedPair.LeftSide,ReadStartIterator+PrefixE,class="num">3); Half2=StringSubstr(CheckedPair.LeftSide,ReadStartIterator+PrefixE+class="num">3,class="num">3); bNew=true; for ( class="type">int j=class="num">0; j<ArraySize(BasicPairsLeft); j++ )class=class="str">"cmt">// if the currency is not found in the list, add it { if ( BasicPairsLeft[j].Value == Half1 ) { if ( SubSide == "u" ) BasicPairsLeft[j].Quantity++; if ( SubSide == "d" ) BasicPairsLeft[j].Quantity--; bNew = class="kw">false; class="kw">break; } } if ( bNew ) { for ( class="type">int j=class="num">0; j<ArraySize(BasicPairsLeft); j++ )class=class="str">"cmt">// if the currency is not found in the list, add it { if ( StringLen(BasicPairsLeft[j].Value) == class="num">0 ) { if ( SubSide == "u" ) BasicPairsLeft[j].Quantity++; if ( SubSide == "d" ) BasicPairsLeft[j].Quantity--; BasicPairsLeft[j].Value=Half1;