第四章 ETF套利实战:搭建实时监控系统

ETF套利,说白了就是吃价差。但价差转瞬即逝,你得有套趁手的工具。今天我就带你亲手搭一套实时监控系统,把IOPV和实时价差算得明明白白。

4.1 系统架构设计

先看整体结构。我习惯把系统拆成三层:数据层、计算层、展示层。

数据层 实时行情推送 → ETF净值计算 → 成分股价格 计算层 IOPV计算 → 实时价差计算 → 套利信号生成 展示层 实时监控面板 → 套利机会提醒 → 模拟交易记录

三层之间用消息队列串起来。我个人偏好用Redis的pub/sub,轻量又好用。你想想看,行情数据每秒几十笔,用HTTP轮询肯定扛不住。

4.2 IOPV计算原理

IOPV,全称是实时估算净值。交易所每5秒算一次,但咱们自己算可以更快。

核心公式:

IOPV = (成分股1价格 × 权重1 + 成分股2价格 × 权重2 + ...) / 基金份额

嗯,这里要注意。权重数据每天更新一次,你得在开盘前拉取最新的ETF申赎清单。

我曾经踩过一个坑:某天成分股停牌了,但权重没变。结果算出来的IOPV和实际净值差了0.3%。后来我加了个停牌处理逻辑——停牌股用上一交易日收盘价。

4.3 实时价差计算

价差 = ETF市价 - IOPV。正价差说明ETF贵了,负价差说明便宜了。

价差方向 套利方向 操作
正价差 > 阈值 溢价套利 买入成分股 → 申购ETF → 卖出ETF
负价差 < -阈值 折价套利 买入ETF → 赎回成分股 → 卖出成分股

阈值怎么设?我一般用万三。低于这个数,扣除手续费基本不赚钱。

4.4 代码实现:实时监控核心

直接上代码。这是监控系统的核心模块:

import redis
import json
import time
from typing import Dict, List

class ETFMonitor:
    def __init__(self, etf_code: str):
        self.etf_code = etf_code
        self.redis_client = redis.Redis(host='localhost', port=6379, db=0)
        self.components = self._load_components()
        self.threshold = 0.0003  # 万三阈值
        
    def _load_components(self) -> Dict:
        """加载成分股权重"""
        # 实际项目中从数据库或文件读取
        return {
            '600519': {'weight': 0.15, 'name': '贵州茅台'},
            '000858': {'weight': 0.12, 'name': '五粮液'},
            # ... 更多成分股
        }
    
    def calculate_iopv(self, prices: Dict[str, float]) -> float:
        """计算实时IOPV"""
        iopv = 0.0
        for code, info in self.components.items():
            price = prices.get(code, 0)
            iopv += price * info['weight']
        return iopv
    
    def check_arbitrage(self, etf_price: float, iopv: float):
        """检查套利机会"""
        spread = (etf_price - iopv) / iopv
        
        if spread > self.threshold:
            self._send_signal('溢价套利', spread)
        elif spread < -self.threshold:
            self._send_signal('折价套利', spread)
    
    def _send_signal(self, signal_type: str, spread: float):
        """发送套利信号"""
        message = {
            'time': time.time(),
            'etf': self.etf_code,
            'type': signal_type,
            'spread': round(spread * 10000, 2)  # 转为BP
        }
        self.redis_client.publish('arbitrage_signals', json.dumps(message))

小技巧:实际生产环境中,我会用asyncio做异步处理。行情来了直接回调,不用轮询。性能能提升3-5倍。

4.5 模拟交易引擎

模拟交易,说白了就是纸上谈兵。但别小看它,我见过太多实盘亏钱的策略,模拟跑得飞起。

模拟引擎的核心逻辑:

class SimulatedTrader:
    def __init__(self, initial_capital: float = 1000000):
        self.capital = initial_capital
        self.positions = {}  # 持仓
        self.trades = []     # 交易记录
        
    def execute_arbitrage(self, signal: dict):
        """执行套利操作"""
        if signal['type'] == '溢价套利':
            # 买入成分股
            cost = self._buy_components(signal['amount'])
            # 申购ETF
            etf_shares = self._subscribe_etf(signal['amount'])
            # 卖出ETF
            revenue = self._sell_etf(etf_shares, signal['etf_price'])
            
            profit = revenue - cost
            self._record_trade(signal, profit)
            
        elif signal['type'] == '折价套利':
            # 反向操作
            pass
    
    def _record_trade(self, signal: dict, profit: float):
        """记录交易"""
        self.trades.append({
            'time': signal['time'],
            'type': signal['type'],
            'profit': profit,
            'capital_after': self.capital + profit
        })
        self.capital += profit

注意:模拟交易和实盘最大的区别是滑点和冲击成本。我建议模拟时主动加0.1%的滑点,这样结果更接近真实情况。

4.6 监控面板展示

数据算出来了,得让人看得懂。我习惯用表格展示实时数据:

时间 ETF代码 ETF市价 IOPV 价差(BP) 信号
09:30:05 510050 3.456 3.452 +1.16
09:30:10 510050 3.468 3.453 +4.34 溢价套利
09:30:15 510050 3.445 3.454 -2.61 折价套利

你看,09:30:10那笔溢价4.34个BP,超过万三阈值了。系统立刻发出信号。这时候你就要判断:是手动跟,还是让程序自动执行?

我的建议:刚开始做套利,先手动跟一个月。等摸清了市场的脾气,再考虑自动化。我当年就是太心急,一上来就全自动,结果某天行情剧烈波动,程序来回刷单,一天亏了2万多。

4.7 避坑指南

做ETF套利监控,有几个坑你一定要注意:

  • 数据延迟:行情推送有延迟,我建议用Level-2行情。普通行情延迟3-5秒,套利机会早没了。
  • 成分股变更:ETF每半年调一次成分股。我吃过这个亏,调仓后没更新权重,算出来的IOPV全是错的。
  • 停牌处理:成分股停牌时,用收盘价还是用估值?我建议用估值模型算,但保守点就用收盘价。
  • 手续费计算:ETF申购赎回有固定费用,别漏算。我见过有人模拟跑得欢,实盘一算手续费直接亏。

好了,这套监控系统搭起来后,你就能实时看到ETF的套利机会了。下一节我们聊聊怎么优化交易执行,把滑点降到最低。


公众号:蓝海资料掘金营,微信deep3321