外汇掉期套利:构建合成投资组合,创造持续稳定的掉期收益流·进阶篇
(2/3)· 多数零售 trader 连 5% 的掉期红利都未碰,本篇拆解组合优化的隐藏杠杆
掉期策略的评分公式怎么搭
正向掉期累积玩法的数学内核,是把市场盈亏、掉期收入和波动风险揉成一个综合分,再按分数挑货币对。思路不复杂:每个品种都先算三样东西——顺着方向的市场平均盈利、平均掉期、以及被杠杆放大的波动率,最后用配置权重捏合。 这里的标准化是关键动作。三个原始量纲不同,直接相加没意义,所以都做了 min-max 归一化,分母里加 1e-10 是为了防除零。归一化后分别乘 swap_weight、return_weight,波动率那项前面带负号乘 volatility_weight,得出 combined_score,分数越高品种越值得碰。 外汇和贵金属带高杠杆,掉期虽能补收益,但单边行情吃掉的分可能远超掉期,策略只是倾向降低波动暴露,不保证正期望。下面这段是评分循环的核心实现,建议直接丢进 MT5 的 Python 环境或自建回测框架跑一遍。
# 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,保证权重绝对值乘平均掉期值为正,组合才不只靠赌波动,还能每天产生正向现金流。外汇与贵金属杠杆高,这类协同效应只是概率上的优势,实盘仍属高风险。 |
|---|
<span class="keyword">def</span> _optimize_portfolio(self): <span class="comment"># ... (pre-processing of data)</span> 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>] <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> <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>] <span class="keyword">for</span> pair <span class="keyword">in</span> eligible_pairs} returns_df = pd.DataFrame(returns_data) cov_matrix = returns_df.cov() <span class="comment"># Covariance matrix is the key to understanding correlations.</span> <span class="comment"># ... (forming the objective function and limitations)</span> <span class="comment"># Optimization objective function</span> <span class="keyword">def</span> objective(weights, expected_returns, cov_matrix, risk_free_rate): returns, std, sharpe = portfolio_performance(weights, expected_returns, cov_matrix, risk_free_rate) <span class="keyword">class="kw">return</span> -sharpe <span class="comment"># Maximize the Sharpe ratio</span> <span class="keyword">def</span> _simulate_portfolio_performance(self): <span class="comment"># ... (initializing variables)</span> <span class="keyword">for</span> date <span class="keyword">in</span> all_dates: daily_return = <span class="number">class="num">0</span> daily_swap = <span class="number">class="num">0</span> <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(): <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: 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> 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>] <span class="keyword">if</span> weight > <span class="number">class="num">0</span>: daily_return += pair_return * weight * self.config[<span class="class="type">class="kw">string">&class="macro">#x27;leverage&class="macro">#x27;</span>] daily_swap += pair_swap * <span class="built_in">abs</span>(weight) <span class="keyword">else</span>: 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>] daily_swap += pair_swap * <span class="built_in">abs</span>(weight) is_weekend = date.weekday() >= <span class="number">class="num">5</span> 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% 就该重新评估持仓周期。
# Calculation of profitability with and without swaps # ...
把掉期塞进马科维茨里跑优化
掉期套利要是只靠直觉挑高息货币对,大概率跑不赢系统优化。把掉期收益直接并进收益率函数,让算法在组合层同时看市场回报、持仓方向、杠杆和平均掉期,才能挖出分步优化会漏掉的结构性组合。 具体落地时,每种货币对先单独算综合收益率:多头方向取历史收益乘杠杆加平均掉期,空头则取负号加平均掉期。方向不是拍脑袋定的,而是比较多空两边掉期大小,谁大就站哪边。 夏普比率在这里是核心筛选标准,分子里硬塞进 Rs(掉期收益)才是关键改动。掉期近似确定性流入,能把单位风险超额收益比拉起来。回测图上紫色线(掉期+定存+再投资)明显厚于蓝色纯市场线,夏普从 0.95 提到 1.68,风险收益轮廓改善显著,但外汇和贵金属本身杠杆波动大,实盘仍可能回撤。 优化用 SLSQP 解非线性约束,硬约束是组合累计掉期必须为正,这就剔掉了那些市场回报好看但天天扣掉期的坑货。波动率、市场方向、掉期在模型里不是分开的,而是用标准化到 [0,1] 后加权:swap_weight 调掉期偏好,volatility_weight 前面带负号就是压波动。高掉期货币往往按利率平价趋贬,Rm 和 Rs 互相打架,协方差矩阵负责在非显性分散里找平衡点。
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 跑同一组品种,组合偏好可能明显转向低波动货币对。
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, # ... }