掉期利率(第一部分):锁定与合成仓位·进阶篇
🔒

掉期利率(第一部分):锁定与合成仓位·进阶篇

(2/3)· 半数点差藏在隔夜掉期里,却少有人把它变成可计算的锁仓工具

含代码示例 第 2/3 篇
很多做日内的人根本不看隔夜掉期,觉得那点点数无关痛痒。实际上近一半点差在日期切换时才结算,忽略它等于把成本盲盒交给经纪商。用两个账户互锁或合成仓位,才能把隐性成本翻成可见的概率优势。

用结构体和数组给掉期检验备好原始数据

要在 MT5 里自动筛查多边掉期回路,第一步是把市场观察里的品种规整成可计算的容器。货币名必须正好 6 个大写字母,部分经纪商带前缀后缀,字符串处理时要兼容。 作者定义了两个结构,基础版 Pair 存名称、买卖掉期、TickValue、合约大小等;扩展版 PairAdvanced 继承前者,多出 Side(分子或分母位置)、LotK 和 Lot,方便后面常规化手数。基础结构同时被其他用途复用,避免建多余容器。 数组大小靠 SetSizePairsArray 控制,其中 MaxSymbols 是 EA 输入参数,手动限定最大品种数;MaxPairs 限定公式单边最大货币对数。实际分配时 BasicPairsLeft/Right 取 MaxPairs*2,因为每对含两种货币,基准货币上限翻倍。 这套准备是后面随机公式生成和平衡过滤的地基。开 MT5 把下面代码丢进 EA 头文件,改 MaxSymbols 就能先看清自己经纪商观察窗口能喂多少品种进去。

MQL5 / C++
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数组。

MQL5 / C++
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 验证除数逻辑。

MQL5 / C++
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 策略测试器用极小手数验证,避免实盘暴露过高风险。

MQL5 / C++
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 策略测试器用历史数据验证配平逻辑是否如预期触发。

MQL5 / C++
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 之间摆动,外汇与贵金属品种在此类扫描中滑点敏感,高风险。

MQL5 / C++
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;
把多边掉期扫描交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到各家经纪商掉期三倍数与正负的横向对比,你只需决定锁仓结构。

常见问题

多数经纪商把周末两天的递延利息合并到周三过夜这一笔收取,所以那一晚显示约为三倍,具体以品种规范为准。
单账户对冲往往两头都付或都收同类掉期,净值为零;双账户可让一边收正、一边付负,构造出净正掉期的锁仓,倾向降低时间成本。
小布在品种页按掉期方向和三倍日标记可锁标的,并给出两个账户互锁的净掉期估算,省去你手动翻规范。
合成仓位依赖跨品种等价暴露,汇率跳空时两边权重可能漂移,概率上需定期再平衡,并非一锁永逸。
正掉期依赖持仓规模与时间,小资金绝对收益偏低,可能仅覆盖部分佣金,需结合更长持仓周期评估。