找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2206

积分

0

好友

312

主题
发表于 3 天前 | 查看: 16| 回复: 0

引言

你有没有思考过,为什么俄罗斯卢布、挪威克朗、加拿大元这些货币的汇率波动,总是和国际油价走势紧密相连?

这类货币在金融领域有一个专业术语,叫做“石油货币”(Petrocurrencies),特指那些国民经济严重依赖石油出口的国家的货币。其核心逻辑在于,当国际油价上涨时,这些国家的贸易顺差扩大,外汇储备增加,本国货币往往随之升值;反之,当油价下跌时,其货币则倾向于贬值。

今天,我们将通过一个完整的 Python 项目,深入探索如何利用数据分析与机器学习技术,挖掘石油价格与对应石油货币之间的统计套利机会。该项目贯穿了数据获取、可视化、模型构建、策略回测与参数优化的全流程,非常适合希望系统学习量化交易的开发者。

项目概览

本项目重点研究了四个主要石油出口国的货币与其对应原油基准价格之间的联动关系:

  • 加拿大元(CAD) ↔ 西加拿大精选原油(WCS)
  • 挪威克朗(NOK) ↔ 布伦特原油(Brent)
  • 哥伦比亚比索(COP) ↔ 瓦斯科尼亚原油(Vasconia)
  • 俄罗斯卢布(RUB) ↔ 乌拉尔原油(Urals)

整个研究的核心假设是:石油货币的汇率受到油价的强烈影响,二者之间存在长期稳定的统计关系。当市场价格暂时偏离这一历史关系时,便可能产生基于均值回归的统计套利机会。

环境搭建

开始之前,我们需要搭建一个独立的 Python 环境并安装必要的依赖库。

# 使用 conda 创建环境
conda create -n oilmoney python=3.9
conda activate oilmoney

# 或者使用 venv
python -m venv oilmoney-env
source oilmoney-env/bin/activate  # macOS/Linux

# 安装依赖包
pip install pandas numpy matplotlib statsmodels scikit-learn seaborn folium

核心技术详解

1. 双轴图可视化函数

由于货币汇率和原油价格的单位与量纲不同,直接绘制在同一坐标轴下难以观察其关联。因此,我们需要一个双轴图函数来清晰展示两者的走势关系。

import matplotlib.pyplot as plt

def dual_axis_plot(xaxis, data1, data2, fst_color='r',
                   sec_color='b', fig_size=(10, 5),
                   x_label='', y_label1='', y_label2='',
                   legend1='', legend2='', grid=False, title=''):
    """
    创建双轴图,用于比较两个不同量纲的数据序列

    参数:
        xaxis: x 轴数据(通常是日期)
        data1: 第一个数据序列(使用左 y 轴)
        data2: 第二个数据序列(使用右 y 轴)
        fst_color: 第一条线的颜色
        sec_color: 第二条线的颜色
    """
    fig = plt.figure(figsize=fig_size)
    ax = fig.add_subplot(111)

    # 绑定第一条线到左侧 y 轴
    ax.set_xlabel(x_label)
    ax.set_ylabel(y_label1, color=fst_color)
    ax.plot(xaxis, data1, color=fst_color, label=legend1)
    ax.tick_params(axis='y', labelcolor=fst_color)
    plt.legend(loc=3)

    # 创建共享 x 轴的第二个 y 轴
    ax2 = ax.twinx()
    ax2.set_ylabel(y_label2, color=sec_color, rotation=270)
    ax2.plot(xaxis, data2, color=sec_color, label=legend2)
    ax2.tick_params(axis='y', labelcolor=sec_color)

    fig.tight_layout()
    plt.legend(loc=4)
    plt.title(title)
    plt.show()

2. K-Means 聚类检测结构性变化

在研究加拿大元与WCS原油关系时,我们引入 K-Means 聚类算法来科学地检测时间序列中的结构性断点,这比人为选定划分日期更具客观性。

import pandas as pd
import numpy as np
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score

# 加载数据
df = pd.read_csv('wcs crude cadaud.csv', encoding='utf-8')
df.set_index('date', inplace=True)
df.index = pd.to_datetime(df.index, format='%m/%d/%Y')

# 添加日期序号作为特征(用于检测时间上的结构性断点)
df['date'] = [i for i in range(len(df.index))]

# 准备聚类数据:加拿大元、WCS 原油价格、日期序号
x = df[['cad', 'wcs', 'date']].reset_index(drop=True)

# 使用肘部法则确定最佳聚类数
sse = []
for i in range(1, 8):
    kmeans = KMeans(n_clusters=i)
    kmeans.fit(x)
    sse.append(kmeans.inertia_ / 10000)  # 缩放以便可视化

# 使用轮廓系数验证
sil = []
for n in range(2, 8):
    clf = KMeans(n).fit(x)
    projection = clf.predict(x)
    sil.append(silhouette_score(x, projection))

# 应用 2 聚类(分析显示最优)
clf = KMeans(n_clusters=2).fit(x)
df['class'] = clf.predict(x)

# 获取结构性断点日期
threshold = df[df['class'] == 0].index[-1]
print(f"检测到的结构性断点:{threshold.strftime('%Y-%m-%d')}")

3. 交易信号生成策略

这是整个项目的核心引擎,它基于滚动窗口的回归分析来生成具体的交易信号。

import statsmodels.api as sm
import copy

def signal_generation(dataset, x, y, method,
                      holding_threshold=10,
                      stop=0.5, rsquared_threshold=0.7,
                      train_len=50):
    """
    基于回归分析生成交易信号

    参数:
        dataset: 输入数据集
        x: 自变量列名(如原油价格)
        y: 因变量列名(如货币汇率)
        holding_threshold: 最大持仓天数
        stop: 止损/止盈阈值(点数)
        rsquared_threshold: R 平方阈值,低于此值模型无效
        train_len: 训练窗口长度

    返回:
        带有交易信号的 DataFrame
    """
    df = method(dataset)
    holding = 0  # 当前持仓方向:1 多头,-1 空头,0 空仓
    trained = False  # 模型是否已训练
    counter = 0  # 持仓天数计数器

    for i in range(train_len, len(df)):
        # 如果有持仓,检查是否需要平仓
        if holding != 0:
            # 条件 1:持仓时间超过阈值
            if counter > holding_threshold:
                df.at[i, 'signals'] = -holding  # 反向信号平仓
                holding = 0
                trained = False
                counter = 0
                continue

            # 条件 2:价格变动超过止损/止盈阈值
            if np.abs(df[y].iloc[i] - df[y][df['signals'] != 0].iloc[-1]) >= stop:
                df.at[i, 'signals'] = -holding
                holding = 0
                trained = False
                counter = 0
                continue

            counter += 1
        else:
            # 空仓状态,检查是否需要训练模型
            if not trained:
                X = sm.add_constant(df[x].iloc[i-train_len:i])
                Y = df[y].iloc[i-train_len:i]
                m = sm.OLS(Y, X).fit()

                # 只有 R 平方足够高时才使用模型
                if m.rsquared > rsquared_threshold:
                    trained = True
                    sigma = np.std(Y - m.predict(X))

                    # 计算预测值和置信区间
                    df.at[i:, 'forecast'] = m.predict(sm.add_constant(df[x].iloc[i:]))
                    df.at[i:, 'pos2 sigma'] = df['forecast'].iloc[i:] + 2 * sigma
                    df.at[i:, 'neg2 sigma'] = df['forecast'].iloc[i:] - 2 * sigma

            # 模型有效时,检查是否触发交易信号
            if trained:
                # 价格突破上轨 → 做多(预期均值回归)
                if df[y].iloc[i] > df['pos2 sigma'].iloc[i]:
                    df.at[i, 'signals'] = 1
                    holding = 1

                # 价格突破下轨 → 做空
                if df[y].iloc[i] < df['neg2 sigma'].iloc[i]:
                    df.at[i, 'signals'] = -1
                    holding = -1

    return df

4. 参数优化与热力图可视化

策略中有几个关键参数(如持仓期、止损阈值)需要优化。我们采用网格搜索配合热力图来直观地寻找最佳参数组合。

import seaborn as sns

# 网格搜索:遍历不同的持仓期和止损阈值
dic = {}
for holdingt in range(5, 20):          # 持仓期 5-19 天
    for stopp in np.arange(0.3, 1.1, 0.05):  # 止损阈值 0.3-1.05 点
        signals = signal_generation(dataset, 'brent', 'nok', oil_money,
                                    holding_threshold=holdingt,
                                    stop=stopp)
        p = portfolio(signals, 'nok')
        # 记录最终收益率
        dic[holdingt, stopp] = p['asset'].iloc[-1] / p['asset'].iloc[0] - 1

# 转换为矩阵格式用于热力图
profile = pd.DataFrame({'params': list(dic.keys()), 'return': list(dic.values())})

matrix = pd.DataFrame(columns=[round(i, 2) for i in np.arange(0.3, 1.1, 0.05)])
matrix['index'] = np.arange(5, 20)
matrix.set_index('index', inplace=True)

for i, j in profile['params']:
    matrix.at[i, round(j, 2)] = profile['return'][profile['params'] == (i, j)].item() * 100

# 绘制热力图
fig = plt.figure(figsize=(10, 5))
ax = fig.add_subplot(111)
sns.heatmap(matrix, cmap='gist_heat_r', square=True, xticklabels=3, yticklabels=3)
ax.collections[0].colorbar.set_label('收益率(%)\n', rotation=270)
plt.xlabel('\n止损/止盈阈值(点)')
plt.ylabel('持仓期(天)\n')
plt.title('收益率热力图\n', fontsize=10)
plt.show()

5. 全球石油生产成本曲线

除了核心策略,项目还包含了一个拓展的可视化模块——绘制全球石油生产成本曲线,这有助于从宏观基本面理解油价底线。

def cost_curve(x, y1, y2=None, hline_var=0, hline_color='k',
               hline_name='', colormap='tab20c', legends=None,
               notes=None, ylabel='', xlabel='', title='', fig_size=(10, 5)):
    """
    绘制商品成本曲线

    参数:
        x: 生产量(柱子宽度)
        y1: 主要成本(柱子高度)
        y2: 次要成本(堆叠在 y1 上方)
        hline_var: 水平参考线位置(如百分位数)
    """
    ax = plt.figure(figsize=fig_size).add_subplot(111)
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)

    # 计算累积宽度(使柱子相邻)
    wid = x
    cumwid = [0]
    for i in range(1, len(wid)):
        cumwid.append(wid[i-1] + cumwid[-1])

    # 获取颜色映射
    cmap = plt.cm.get_cmap(colormap)
    colors = [cmap(i) for i in np.linspace(0, 1, len(y1))]

    # 绘制柱状图
    for i in range(len(y1)):
        plt.bar(cumwid[i], y1[i], width=wid[i],
                label=legends[i] if legends else None,
                color=colors[i], align='edge')
        if y2 is not None and len(y2) > 0:
            # 堆叠第二层成本
            colors2 = tuple([c / 1.3 for c in colors[i][:3]] + [1.0])
            plt.bar(cumwid[i], y2[i], width=wid[i],
                    color=colors2, bottom=y1[i], align='edge')

    # 添加参考线(如 90% 分位数)
    plt.axhline(y=hline_var, linestyle='--', c=hline_color, label=hline_name)

    plt.title(title)
    plt.ylabel(ylabel)
    plt.xlabel(xlabel)
    plt.legend(loc=6, bbox_to_anchor=(0.12, -0.4), ncol=4)
    plt.show()

# 使用示例
df = pd.read_csv('global oil cost curve.csv')
cost_curve(df['Daily production mil barrels'],
           df['Operational cost dollar per barrel'],
           df['Capital cost dollar per barrel'],
           legends=df['Country'],
           notes=['运营成本', '资本成本'],
           hline_var=np.percentile(df['Total cost dollar per barrel'], 90),
           hline_name='90% 分位数',
           xlabel='日产量(百万桶)',
           ylabel='美元/桶',
           title='2015 年全球石油成本曲线')

策略逻辑总结

整个交易策略的核心思想是均值回归,其执行流程可以概括为以下几步:

  1. 训练阶段:使用滚动时间窗口(默认长度为50天)的数据,拟合普通最小二乘法(OLS)线性回归模型。
  2. 模型验证:计算模型的R²,只有当其超过预设的阈值(例如0.7)时,才认为模型在当前窗口内有效,可用于生成交易信号。
  3. 入场信号
    • 当货币价格向上突破“预测值 + 2倍标准差”的上轨时,视为价格过高,发出做空信号,预期价格将向下回归。
    • 当货币价格向下跌破“预测值 - 2倍标准差”的下轨时,视为价格过低,发出做多信号,预期价格将向上回归。
  4. 出场条件
    • 持仓天数达到预设的最大持有期(如10天)。
    • 价格变动触及预设的止损或止盈阈值。

关键发现

在对挪威克朗与布伦特原油的套利策略进行回测分析时,项目揭示了一个颇具启发性的现象:最赚钱的交易机会,有时恰恰出现在模型看似“失效”的时刻

当价格突破2σ边界后并未如预期般回归,反而继续运行并触发了止损位,这通常暗示市场的基本面关系可能发生了结构性变化,原有的均值回归逻辑暂时失效。有趣的是,如果此时不仅平仓了结,而是果断地反转仓位(例如原做空止损后反手做多),跟随新的趋势方向,往往能捕获更大的趋势性收益。

这一发现提醒我们,在量化交易中,统计套利模型的“失效信号”本身,可能就是一个极具价值的反向交易信号,需要我们设计更灵活的应对机制。

总结

本项目系统性地展示了如何使用 Python 构建一个从研究到实践的完整量化交易分析框架,涵盖的核心技术要点包括:

  • 数据可视化:双轴对比图、3D散点图、参数热力图、地理信息图。
  • 机器学习应用:K-Means聚类检测结构性断点。
  • 统计分析:OLS回归、置信区间构建。
  • 策略开发与回测:完整的信号生成、仓位管理与参数优化流程。

对于希望进入量化交易领域的 Python 开发者而言,这是一个极具学习价值的综合性项目。它不仅串联了多种技术工具的使用方法,更重要的是展示了如何将这些工具有机组合,用以解决真实的金融分析问题。

当然,必须明确指出,所有分析结果均基于历史数据,回测表现不代表未来收益。实际交易中还需充分考虑交易成本、滑点、市场流动性及极端行情等复杂因素。本文旨在提供技术思路与学习方法,更多精彩的 算法 与量化交易内容,欢迎在 云栈社区 交流探讨。




上一篇:Docker核心命令实战手册:镜像、容器与网络管理(2026最新版)
下一篇:SpringBoot接口优雅封装:统一响应、参数校验与全局异常处理
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-9 17:59 , Processed in 0.212289 second(s), 38 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表