外汇掉期套利:构建合成投资组合,创造持续稳定的掉期收益流·进阶篇
🪙

外汇掉期套利:构建合成投资组合,创造持续稳定的掉期收益流·进阶篇

(2/3)· 多数零售 trader 连 5% 的掉期红利都未碰,本篇拆解组合优化的隐藏杠杆

进阶 第 2/3 篇
把掉期当隔夜负担的人,等于主动放弃每年 5%–8% 的复利弹药。掉期调整频率远低于汇率,这恰是能长期躺赚的结构性漏洞。用系统性组合替代拍脑袋持仓,才接得住这块被 95% 人忽略的收益。

掉期策略的评分公式怎么搭

正向掉期累积玩法的数学内核,是把市场盈亏、掉期收入和波动风险揉成一个综合分,再按分数挑货币对。思路不复杂:每个品种都先算三样东西——顺着方向的市场平均盈利、平均掉期、以及被杠杆放大的波动率,最后用配置权重捏合。 这里的标准化是关键动作。三个原始量纲不同,直接相加没意义,所以都做了 min-max 归一化,分母里加 1e-10 是为了防除零。归一化后分别乘 swap_weight、return_weight,波动率那项前面带负号乘 volatility_weight,得出 combined_score,分数越高品种越值得碰。 外汇和贵金属带高杠杆,掉期虽能补收益,但单边行情吃掉的分可能远超掉期,策略只是倾向降低波动暴露,不保证正期望。下面这段是评分循环的核心实现,建议直接丢进 MT5 的 Python 环境或自建回测框架跑一遍。

MQL5 / C++
# Calculation of expected returns based on swap and market movement
expected_returns = {}
for pair in eligible_pairs:
    market_return = self.swap_info[pair][&class="macro">#x27;avg_return&class="macro">#x27;] * self.config[&class="macro">#x27;leverage&class="macro">#x27;] if self.swap_info[pair][&class="macro">#x27;direction&class="macro">#x27;] == &class="macro">#x27;class="type">long&class="macro">#x27; else -self.swap_info[pair][&class="macro">#x27;avg_return&class="macro">#x27;] * self.config[&class="macro">#x27;leverage&class="macro">#x27;]
    swap_return = self.swap_info[pair][&class="macro">#x27;avg_swap&class="macro">#x27;]
    volatility = self.swap_info[pair][&class="macro">#x27;volatility&class="macro">#x27;] * self.config[&class="macro">#x27;leverage&class="macro">#x27;]
    
    # Normalization of parameters for balanced assessment
    norm_market = (market_return - min_market_return) / (max_market_return - min_market_return + class="num">1e-10)
    norm_swap = (swap_return - min_swap_return) / (max_swap_return - min_swap_return + class="num">1e-10)
    norm_vol = (volatility - min_volatility) / (max_volatility - min_volatility + class="num">1e-10)
    
    # Combined assessment of pair attractiveness
    combined_score = (self.config[&class="macro">#x27;swap_weight&class="macro">#x27;] * norm_swap + 
                            self.config[&class="macro">#x27;return_weight&class="macro">#x27;] * norm_market - 
                            self.config[&class="macro">#x27;volatility_weight&class="macro">#x27;] * norm_vol)
    expected_returns[pair] = combined_score

◍ 用相关性抵消波动,让掉期变成正现金流

做组合优化时,货币对之间的相关性不是麻烦,而是用来压波动的工具。协方差矩阵 cov_matrix 记下了各货币对收益率的相互依赖,算法靠它在正相关和负相关之间做抵消,组合整体波动率可能往下走。 优化出来的持仓常常是对关联货币对反方向开仓,比如一边 L 一边 S,市场风险的一部分就被对冲掉了。收益来源有两端:价格波动本身,以及掉期(swap)。把每日掉期单独拉出来看,和波动收益的差异会很直观。 有个细节容易踩坑:掉期只在交易日结算。代码里 daily_swap_applied = 0 if is_weekend else daily_swap * initial_capital 就是干这个的——周末那行直接归零,建模时不处理就会把回测收益虚抬。

数学上目标很明确,最大化夏普:Sharpe = (Rp - Rf) / σp,其中 Rp 含市场收益加掉期,Rf 是无风险利率,σp 是组合收益标准差。同时对掉期加正约束 ∑wi×Si > 0,保证权重绝对值乘平均掉期值为正,组合才不只靠赌波动,还能每天产生正向现金流。外汇与贵金属杠杆高,这类协同效应只是概率上的优势,实盘仍属高风险。
MQL5 / C++
<span class="keyword">def</span> _optimize_portfolio(self):
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment"># ... (pre-processing of data)</span>
&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;returns_data = {pair: self.swap_info[pair][<span class="class="type">class="kw">string">&class="macro">#x27;returns&class="macro">#x27;</span>] * self.config[<span class="class="type">class="kw">string">&class="macro">#x27;leverage&class="macro">#x27;</span>] + self.swap_info[pair][<span class="class="type">class="kw">string">&class="macro">#x27;avg_swap&class="macro">#x27;</span>]
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span> self.swap_info[pair][<span class="class="type">class="kw">string">&class="macro">#x27;direction&class="macro">#x27;</span>] == <span class="class="type">class="kw">string">&class="macro">#x27;class="type">long&class="macro">#x27;</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">else</span> -self.swap_info[pair][<span class="class="type">class="kw">string">&class="macro">#x27;returns&class="macro">#x27;</span>] * self.config[<span class="class="type">class="kw">string">&class="macro">#x27;leverage&class="macro">#x27;</span>] + self.swap_info[pair][<span class="class="type">class="kw">string">&class="macro">#x27;avg_swap&class="macro">#x27;</span>]
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">for</span> pair <span class="keyword">in</span> eligible_pairs}
&nbsp;&nbsp;&nbsp;&nbsp;returns_df = pd.DataFrame(returns_data)
&nbsp;&nbsp;&nbsp;&nbsp;cov_matrix = returns_df.cov()&nbsp;&nbsp;<span class="comment"># Covariance matrix is the key to understanding correlations.</span>
&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment"># ... (forming the objective function and limitations)</span>
&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment"># Optimization objective function</span>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">def</span> objective(weights, expected_returns, cov_matrix, risk_free_rate):
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;returns, std, sharpe = portfolio_performance(weights, expected_returns, cov_matrix, risk_free_rate)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span> -sharpe&nbsp;&nbsp;<span class="comment"># Maximize the Sharpe ratio</span>
<span class="keyword">def</span> _simulate_portfolio_performance(self):
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment"># ... (initializing variables)</span>
&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span> date <span class="keyword">in</span> all_dates:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;daily_return = <span class="number">class="num">0</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;daily_swap = <span class="number">class="num">0</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span> pair, weight <span class="keyword">in</span> self.optimal_portfolio[<span class="class="type">class="kw">string">&class="macro">#x27;weights&class="macro">#x27;</span>].items():
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> date <span class="keyword">in</span> self.swap_info[pair][<span class="class="type">class="kw">string">&class="macro">#x27;data&class="macro">#x27;</span>].index:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pair_return = self.swap_info[pair][<span class="class="type">class="kw">string">&class="macro">#x27;data&class="macro">#x27;</span>].loc[date, <span class="class="type">class="kw">string">&class="macro">#x27;class="kw">return&class="macro">#x27;</span>] <span class="keyword">if</span> <span class="keyword">not</span> pd.isna(self.swap_info[pair][<span class="class="type">class="kw">string">&class="macro">#x27;data&class="macro">#x27;</span>].loc[date, <span class="class="type">class="kw">string">&class="macro">#x27;class="kw">return&class="macro">#x27;</span>]) <span class="keyword">else</span> <span class="number">class="num">0</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pair_swap = self.swap_info[pair][<span class="class="type">class="kw">string">&class="macro">#x27;data&class="macro">#x27;</span>].loc[date, <span class="class="type">class="kw">string">&class="macro">#x27;swap_return&class="macro">#x27;</span>]
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> weight &gt; <span class="number">class="num">0</span>:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;daily_return += pair_return * weight * self.config[<span class="class="type">class="kw">string">&class="macro">#x27;leverage&class="macro">#x27;</span>]
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;daily_swap += pair_swap * <span class="built_in">abs</span>(weight)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">else</span>:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;daily_return += -pair_return * <span class="built_in">abs</span>(weight) * self.config[<span class="class="type">class="kw">string">&class="macro">#x27;leverage&class="macro">#x27;</span>]
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;daily_swap += pair_swap * <span class="built_in">abs</span>(weight)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;is_weekend = date.weekday() &gt;= <span class="number">class="num">5</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;daily_swap_applied = <span class="number">class="num">0</span> <span class="keyword">if</span> is_weekend <span class="keyword">else</span> daily_swap * initial_capital

「持仓成本里的掉期隐性损耗」

MT5 里很多交易者只看开平仓价差,却忽略了掉期(swap)在跨日持仓中的累积效应。对于外汇与贵金属这类高杠杆品种,掉期可能让原本盈利的交易在持有 3~5 个自然日后转亏,属于典型的高风险隐性成本。 在 MQL5 中可通过 PositionGetDouble(POSITION_SWAP) 读取当前持仓的掉期值,正数为平台付你、负数为你付平台。下面这段注释标记了带与不带掉期两种盈亏计算的逻辑入口,开 MT5 策略测试器时可对照观察净值曲线差异。 实际验证时,建议选 EURUSD 与 XAUUSD 各跑一周跨日单,对比报表里的 swap 列与 gross profit 列,掉期占比超过毛利 15% 就该重新评估持仓周期。

MQL5 / C++
# Calculation of profitability with and without swaps
# ...

把掉期塞进马科维茨里跑优化

掉期套利要是只靠直觉挑高息货币对,大概率跑不赢系统优化。把掉期收益直接并进收益率函数,让算法在组合层同时看市场回报、持仓方向、杠杆和平均掉期,才能挖出分步优化会漏掉的结构性组合。 具体落地时,每种货币对先单独算综合收益率:多头方向取历史收益乘杠杆加平均掉期,空头则取负号加平均掉期。方向不是拍脑袋定的,而是比较多空两边掉期大小,谁大就站哪边。 夏普比率在这里是核心筛选标准,分子里硬塞进 Rs(掉期收益)才是关键改动。掉期近似确定性流入,能把单位风险超额收益比拉起来。回测图上紫色线(掉期+定存+再投资)明显厚于蓝色纯市场线,夏普从 0.95 提到 1.68,风险收益轮廓改善显著,但外汇和贵金属本身杠杆波动大,实盘仍可能回撤。 优化用 SLSQP 解非线性约束,硬约束是组合累计掉期必须为正,这就剔掉了那些市场回报好看但天天扣掉期的坑货。波动率、市场方向、掉期在模型里不是分开的,而是用标准化到 [0,1] 后加权:swap_weight 调掉期偏好,volatility_weight 前面带负号就是压波动。高掉期货币往往按利率平价趋贬,Rm 和 Rs 互相打架,协方差矩阵负责在非显性分散里找平衡点。

MQL5 / C++
def portfolio_performance(weights, expected_returns, cov_matrix, risk_free_rate):
    weights = np.array(weights)
    returns = np.sum(expected_returns * weights)
    std = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))
    class="kw">return returns, std, (returns - risk_free_rate) / std if std > class="num">0 else class="num">0
returns_data = {pair: self.swap_info[pair][&class="macro">#x27;returns&class="macro">#x27;] * self.config[&class="macro">#x27;leverage&class="macro">#x27;] + self.swap_info[pair][&class="macro">#x27;avg_swap&class="macro">#x27;]
                if self.swap_info[pair][&class="macro">#x27;direction&class="macro">#x27;] == &class="macro">#x27;class="type">long&class="macro">#x27;
                else -self.swap_info[pair][&class="macro">#x27;returns&class="macro">#x27;] * self.config[&class="macro">#x27;leverage&class="macro">#x27;] + self.swap_info[pair][&class="macro">#x27;avg_swap&class="macro">#x27;]
                for pair in eligible_pairs}
direction = &class="macro">#x27;class="type">long&class="macro">#x27; if swap_long > swap_short else &class="macro">#x27;class="type">short&class="macro">#x27;
self.swap_info[pair] = {
    &class="macro">#x27;long_swap&class="macro">#x27;: swap_long,
    &class="macro">#x27;short_swap&class="macro">#x27;: swap_short,
    &class="macro">#x27;swap_ratio&class="macro">#x27;: swap_ratio,
    &class="macro">#x27;returns&class="macro">#x27;: history[&class="macro">#x27;returns&class="macro">#x27;],
    &class="macro">#x27;avg_return&class="macro">#x27;: history[&class="macro">#x27;avg_return&class="macro">#x27;],
    &class="macro">#x27;volatility&class="macro">#x27;: history[&class="macro">#x27;volatility&class="macro">#x27;],
    &class="macro">#x27;avg_swap&class="macro">#x27;: history[&class="macro">#x27;avg_swap&class="macro">#x27;] if direction == &class="macro">#x27;class="type">long&class="macro">#x27; else -history[&class="macro">#x27;avg_swap&class="macro">#x27;],
    &class="macro">#x27;direction&class="macro">#x27;: direction,
    &class="macro">#x27;sharpe_ratio&class="macro">#x27;: (history[&class="macro">#x27;avg_return&class="macro">#x27;] + history[&class="macro">#x27;avg_swap&class="macro">#x27;] - self.config[&class="macro">#x27;risk_free_rate&class="macro">#x27;]) / history[&class="macro">#x27;volatility&class="macro">#x27;]
                    if history[&class="macro">#x27;volatility&class="macro">#x27;] > class="num">0 else class="num">0,
    &class="macro">#x27;weight&class="macro">#x27;: class="num">0.0,
    &class="macro">#x27;data&class="macro">#x27;: history[&class="macro">#x27;data&class="macro">#x27;]
}
def objective(weights, expected_returns, cov_matrix, risk_free_rate):
    returns, std, sharpe = portfolio_performance(weights, expected_returns, cov_matrix, risk_free_rate)
    class="kw">return -sharpe  # The optimizer minimizes, so we use a negative Sharp
result = sco.minimize(
    objective,
    initial_weights,
    args=(np.array(list(expected_returns.values())), cov_matrix.values, self.config[&class="macro">#x27;risk_free_rate&class="macro">#x27;]),
    method=&class="macro">#x27;SLSQP&class="macro">#x27;,
    bounds=bounds,
    constraints=constraints
)
def swap_constraint(weights, eligible_pairs):

◍ 把掉期与波动压进同一把尺子

多品种组合里,掉期收益、市场回报和波动幅度量纲完全不同,直接相加会 distortion 权重。上面那段把三者各自做 min-max 归一化,分母末尾加 1e-10 就是防除零,MT5 回测里若 eligible_pairs 只剩一个品种,这行能避免 NaN 把整个 combined_score 废掉。 归一化后市场回报按方向区分杠杆:long 取 avg_return*leverage,short 取负值,再套用全局 min/max。掉期归一只用了 avg_swap 原始值,波动归一则乘了 leverage——说明模型默认波动随杠杆线性放大,外汇和贵金属自带高杠杆高风险,这套映射在黄金 1:100 上会比欧美 1:30 更敏感。 combined_score 的拼法是 0.3*norm_swap + 0.6*norm_market - 0.1*norm_vol,回测配置里 return_weight 压到 0.6、swap_weight 0.3、volatility_weight 0.1,risk_aversion 设 2.0。想验证倾向,把 volatility_weight 调到 0.3 跑同一组品种,组合偏好可能明显转向低波动货币对。

MQL5 / C++
    total_swap = np.sum([self.swap_info[pair][&class="macro">#x27;avg_swap&class="macro">#x27;] * abs(weights[i]) for i, pair in enumerate(eligible_pairs)])
    class="kw">return total_swap  # Must be >= class="num">0
# Normalization of parameters
norm_market = (market_return - min([self.swap_info[p][&class="macro">#x27;avg_return&class="macro">#x27;] * self.config[&class="macro">#x27;leverage&class="macro">#x27;] if self.swap_info[p][&class="macro">#x27;direction&class="macro">#x27;] == &class="macro">#x27;class="type">long&class="macro">#x27;
                                                                   else -self.swap_info[p][&class="macro">#x27;avg_return&class="macro">#x27;] * self.config[&class="macro">#x27;leverage&class="macro">#x27;] for p in eligible_pairs])) / \
              (max([self.swap_info[p][&class="macro">#x27;avg_return&class="macro">#x27;] * self.config[&class="macro">#x27;leverage&class="macro">#x27;] if self.swap_info[p][&class="macro">#x27;direction&class="macro">#x27;] == &class="macro">#x27;class="type">long&class="macro">#x27;
                                                                          else -self.swap_info[p][&class="macro">#x27;avg_return&class="macro">#x27;] * self.config[&class="macro">#x27;leverage&class="macro">#x27;] for p in eligible_pairs]) -
               min([self.swap_info[p][&class="macro">#x27;avg_return&class="macro">#x27;] * self.config[&class="macro">#x27;leverage&class="macro">#x27;] if self.swap_info[p][&class="macro">#x27;direction&class="macro">#x27;] == &class="macro">#x27;class="type">long&class="macro">#x27;
                                                                          else -self.swap_info[p][&class="macro">#x27;avg_return&class="macro">#x27;] * self.config[&class="macro">#x27;leverage&class="macro">#x27;] for p in eligible_pairs]) + class="num">1e-10)
norm_swap = (swap_return - min([self.swap_info[p][&class="macro">#x27;avg_swap&class="macro">#x27;] for p in eligible_pairs])) / \
            (max([self.swap_info[p][&class="macro">#x27;avg_swap&class="macro">#x27;] for p in eligible_pairs]) -
             min([self.swap_info[p][&class="macro">#x27;avg_swap&class="macro">#x27;] for p in eligible_pairs]) + class="num">1e-10)
norm_vol = (volatility - min([self.swap_info[p][&class="macro">#x27;volatility&class="macro">#x27;] * self.config[&class="macro">#x27;leverage&class="macro">#x27;] for p in eligible_pairs])) / \
           (max([self.swap_info[p][&class="macro">#x27;volatility&class="macro">#x27;] * self.config[&class="macro">#x27;leverage&class="macro">#x27;] for p in eligible_pairs]) -
            min([self.swap_info[p][&class="macro">#x27;volatility&class="macro">#x27;] * self.config[&class="macro">#x27;leverage&class="macro">#x27;] for p in eligible_pairs]) + class="num">1e-10)
combined_score = (self.config[&class="macro">#x27;swap_weight&class="macro">#x27;] * norm_swap +
                  self.config[&class="macro">#x27;return_weight&class="macro">#x27;] * norm_market -
                  self.config[&class="macro">#x27;volatility_weight&class="macro">#x27;] * norm_vol)
self.config = {
    # ...
    &class="macro">#x27;risk_aversion&class="macro">#x27;: class="num">2.0,
    &class="macro">#x27;swap_weight&class="macro">#x27;: class="num">0.3,
    &class="macro">#x27;return_weight&class="macro">#x27;: class="num">0.6,
    &class="macro">#x27;volatility_weight&class="macro">#x27;: class="num">0.1,
    # ...
}
让小布替你跑这套相关性筛查
货币对相关性与掉期方向的交叉计算很耗神,这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到每日正掉期排名与相关性热力,把重复劳动交给小布,你专注决策。

常见问题

需把点值、持仓天数、货币对波动方差一并代入,单纯用经纪商公布的掉期点乘手数会低估滑点成本,概率上偏乐观。
可以,小布盯盘的品种页内置正掉期筛选与相关性矩阵,但外汇贵金属属高风险,清单仅作诊断参考,不构成方向建议。
低相关头寸在汇率波动中互相抵消部分回撤,而掉期每日进账不变,从而在不牺牲掉期收益的前提下压低了整体波动。
倾向如此,因每日累积掉期点提供缓冲,但极端流动性枯竭时价差扩大仍可能吞掉生息,属概率优势而非绝对。