重塑经典策略(第四部分):标普500指数与美国国债(基础篇)
📘

重塑经典策略(第四部分):标普500指数与美国国债(基础篇)

第 1/3 篇

「用国债收益率给标普500反向定位」

上一篇文章里我们试过用高权重股票组合去跟标普500搭策略,这一篇换条路:拿美国国债收益率当标普500的反向坐标。 很多基本面交易员早注意到,标普500的走向和国债收益率之间长期呈现负相关——资金从债券撤出奔向股市时,收益率往往往下走,指数往上推;风险厌恶抬头时钱回流国债,收益率抬升而股指承压。 这种负相关不是偶尔跳动。从多年跨周期观察看,它表现为一种倾向性规律:投资者风险偏好回升,债券配置比例下降,国债需求减弱带动收益率回落,股指端获得支撑的概率更高。外汇与贵金属交易者需注意,跨资产映射存在滞后与断裂,实盘前必须在 MT5 用历史数据自行验证相关性窗口。

股债跷跷板里的仓位信号

标普500代表美国工业经济整体冷暖,国债则被视作全球避险锚。买国债本质是借钱给政府,票息按面值付息;当债券需求走弱,收益率就被迫抬高来重新吸买盘。 基本面派长期盯这套关系:国债收益率往上走,说明资金没进债市、更可能涌去股市博更高回报;收益率往下,意味着避险需求极旺,股市情绪偏防御。 把这层逻辑搬进标普500交易,就是看趋势是否因债市流向而减弱。本文要验证的是——这种股债负向联动是否 statistically significant,以及能否直接长出一个可执行的策略。 外汇与贵金属交易者注意:跨市场资金逻辑同样适用于美债收益率与金银的跷跷板,但杠杆品种波动剧烈,任何信号都只是概率倾向,实盘前请在 MT5 用历史数据自测。

◍ 用标普与国债数据交叉验证预测效果

我们用标普500本身的普通OHLC数据去拟合多种模型,预测其收盘价,再换用美国5年期国债的OHLC做同样目标训练。时间序列交叉验证不随机洗牌,直接对比不同复杂度模型。结果很明确:换国债数据后模型性能全面下降,且误差水平的方差变大,标普数据对这类预测更合适。 进一步看,SGD回归器在误差比较中表现最佳,随后做特征选择时,选择器完全没挑中任何国债相关字段,说明国债与标普收盘的关系统计上可能并不显著。即便证据足够舍弃国债数据,我们仍保留它继续建模。 导出ONNX前最后调了超参数,用L-BFGS-B有限内存算法找最优设置,想压过默认配置。结果模型在训练集上过拟合,没能超越默认性能。外汇与贵金属市场同理,跨资产代理预测容易放大误差方差,实盘前务必在MT5用历史数据复算。

「用时间维度切分标普与美债收益率的散点」

从 MT5 导出的 CSV 只是原材料。先把标普500和美债5年期收益率各自倒序重排,保证最旧一根 K 线在首行,再按时间 inner join,合成一张带双市场字段的 DataFrame,后续所有观察都基于它。 合并后跑一次 corr(),标普开盘与5年期收益率开盘的相关性大约在 0.1 左右,属于弱相关。别急着下因果结论——弱相关不代表无关,强相关也不代表谁驱动谁,背后可能只是同一个宏观因子在同时搅动两个市场。 真正有意思的是分隔性测试。以「未来20根 K 线后收盘高于当前」为二元标签(1橙0蓝),拿时间序号做 x 轴、标普开盘做 y 轴散点,蓝橙两群自然分开,说明单纯时间轴就能把涨跌区间剥开。换成「美债开盘 vs 标普开盘」做散点,点全糊在一起,几乎无分隔。 下面这段 Python 可直接复刻上述过程,路径按你本地 CSV 改。逐行看:前3行引库;读入两份 CSV;look_ahead=20 定预测步长;[::-1] 把数据翻成旧→新并重置索引;shift(-20) 造标签,收盘升则二元标签置1;merge 用时间对齐;最后 corr() 与两幅 scatter 验证分隔。

MQL5 / C++
class="macro">#Import the libraries we need 
class="kw">import pandas as pd
class="kw">import numpy as np
class="kw">import seaborn as sns
class="macro">#Read in the data
SP500 = pd.read_csv("/home/volatily/market_data/Market Data US SP class="num">500.csv")
T5Y = pd.read_csv("/home/volatily/market_data/Market Data UST05Y_U4.csv")
class="macro">#How far into the future should we forecast?
look_ahead = class="num">20
class="macro">#Make sure the data starts with the oldest day first
SP500 = SP500[::-class="num">1].reset_index().set_index("Time").drop(columns=["index"])
T5Y = T5Y[::-class="num">1].reset_index().set_index("Time").drop(columns=["index"])
class="macro">#Insert the label
SP500["Target SP500"] = SP500["Close"].shift(-look_ahead)
SP500["Binary Target SP500"] = class="num">0
SP500.loc[SP500["Close"] < SP500["Target SP500"],"Binary Target SP500"] = class="num">1
SP500.dropna(inplace=True)
class="macro">#Merge the data
merged_df = pd.merge(SP500,T5Y,how="inner",left_index=True,right_index=True,suffixes=(" SP500"," T5Y"))
class="macro">#Let&class="macro">#x27;s observe the merged dataframe
merged_df
class="macro">#Merged data frame correlation
merged_df.corr()
class="macro">#It appears that one variable that separates the data well is time
sns.scatterplot(data=merged_df,x="Candle",y="Open SP500",hue="Binary Target SP500")
class="macro">#It appears that one variable that separates the data well is time
sns.scatterplot(data=merged_df,x="Open T5Y",y="Open SP500",hue="Binary Target SP500")

标普与国债收益率的回归模型交叉验证

把标普500的OHLC和国债收益率拼成一条时间序列后,先用 TimeSeriesSplit 做10折拆分(gap=look_ahead 防未来函数泄漏),再把9个回归模型塞进列表统一跑。对比维度分三套:只用标普自身OHLC、只用5年期国债收益率、两者全用,误差分别落进 SP500_error / TY5_error / total_error 三个DataFrame。 实际跑下来,纯标普OHLC输入时线性回归RMSE最低,SGD回归器紧随其后,神经网络(MLP,隐藏层10×4)表现偏差,大概率欠调参。纯国债收益率输入时所有模型都不理想,随机森林回归器相对最稳。 三组数据全上之后,SGD回归器综合误差表现不错,作者最终选它当主力模型。外汇/贵金属交易者注意:这类跨资产回归对样本外时段极敏感,高风险,直接搬用参数容易过拟合。 下面这段是模型初始化与误差表骨架,逐行看:从sklearn导入9个回归器与切分、缩放工具;splits=10 定义折数;tscv 用gap隔离前瞻;models列表装全部模型;initialize_models() 函数内重新实例化一遍;三个DataFrame按模型名作列、0~9作索引存误差;最后指定 target 与 predictors 待拟合。

MQL5 / C++
class="macro">#Import the libraries we need
<span class="keyword">from</span> sklearn.linear_model class="kw">import LinearRegression
<span class="keyword">from</span> sklearn.linear_model class="kw">import Lasso
<span class="keyword">from</span> sklearn.linear_model class="kw">import SGDRegressor
<span class="keyword">from</span> sklearn.svm class="kw">import LinearSVR
<span class="keyword">from</span> sklearn.ensemble class="kw">import RandomForestRegressor
<span class="keyword">from</span> sklearn.ensemble class="kw">import GradientBoostingRegressor
<span class="keyword">from</span> sklearn.ensemble class="kw">import BaggingRegressor
<span class="keyword">from</span> sklearn.ensemble class="kw">import AdaBoostRegressor
<span class="keyword">from</span> sklearn.neural_network class="kw">import MLPRegressor
<span class="keyword">from</span> sklearn.model_selection class="kw">import TimeSeriesSplit
<span class="keyword">from</span> sklearn.metrics class="kw">import root_mean_squared_error
<span class="keyword">from</span> sklearn.preprocessing class="kw">import RobustScaler
class="kw">import time
<span class="keyword">from</span> numpy.random class="kw">import rand,randn
<span class="keyword">from</span> scipy.optimize class="kw">import minimize
<span class="preprocessor">class="macro">#Define </span>the number of splits we want
splits = <span class="number">class="num">10</span>
<span class="preprocessor">class="macro">#Create </span>the time series split object
tscv = TimeSeriesSplit(n_splits = splits, gap=look_ahead)
<span class="preprocessor">class="macro">#Store </span>the models in a list
models = [LinearRegression(),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Lasso(),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; SGDRegressor(),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; LinearSVR(),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; RandomForestRegressor(),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; GradientBoostingRegressor(),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; BaggingRegressor(),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; AdaBoostRegressor(),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; MLPRegressor(hidden_layer_sizes=(<span class="number">class="num">10</span>,<span class="number">class="num">4</span>),early_stopping=True),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ]
<span class="preprocessor">class="macro">#Define </span>a function to initialize our models
def initialize_models():
&nbsp;&nbsp;&nbsp;&nbsp;models = [LinearRegression(),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Lasso(),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; SGDRegressor(),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; LinearSVR(),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; RandomForestRegressor(),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; GradientBoostingRegressor(),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; BaggingRegressor(),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; AdaBoostRegressor(),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; MLPRegressor(hidden_layer_sizes=(<span class="number">class="num">10</span>,<span class="number">class="num">4</span>),early_stopping=True),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ]
<span class="preprocessor">class="macro">#Create </span><span class="number">class="num">3</span> dataframes to measure our performance
<span class="preprocessor">class="macro">#Before </span>we <span class="keyword">do</span> that, we will define the columns and idexes
columns = [<span class="class="type">class="kw">string">"Linear Regression"</span>,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">"Lasso"</span>,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">"SGD Regressor"</span>,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">"Linear SVR"</span>,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">"Random Forest Regressor"</span>,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">"Gradient Boosting Regressor"</span>,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">"Bagging Regressor"</span>,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">"Ada Boost Regressor"</span>,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">"MLP Regressor"</span>]
indexes = np.arange(<span class="number">class="num">0</span>,<span class="number">class="num">10</span>)
<span class="preprocessor">class="macro">#First </span>dataframe stores our error levels using just the ordinary SP500 OHCL
SP500_error = pd.DataFrame(columns=columns,index=indexes)
<span class="preprocessor">class="macro">#Second </span>dataframe stores our error levels using just the ordinary Treasury Yield OHCL
TY5_error = pd.DataFrame(columns=columns,index=indexes)
<span class="preprocessor">class="macro">#Last </span>dataframe stores our error levels using all the data we have
total_error = pd.DataFrame(columns=columns,index=indexes)
<span class="preprocessor">class="macro">#Now </span>we will define the inputs and target
target = <span class="class="type">class="kw">string">"Target SP500"</span>
predictors = [<span class="class="type">class="kw">string">"Open T5Y"</span>,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">"Close T5Y"</span>,

◍ 把宏观字段喂进滚动回测

上面这段脚本把 5 年期美债高低价(T5Y)与标普 500 的开高低收都列进 predictors,说明跨资产特征已被纳入模型输入。注意这里用的是 RobustScaler 而非标准归一,对贵金属与外汇这种常出极端影线的品种更抗噪。 重置索引后做时间序列交叉验证(tscv),外层遍历多个模型、内层按折切分 train/test,每折都用 root_mean_squared_error 记一次误差到 SP500_error 矩阵。若你要把这套搬到 MT5 的 Python 环境跑,直接把 predictors 换成 XAUUSD 的 H1 高低收即可,但外汇杠杆风险高,回测盈利不等于实盘概率占优。 最后 SP500_error、TY5_error、total_error 三个对象留在命名空间,方便横向比哪个模型在股债双特征下更稳。打开 Jupyter 把这段贴进去,改两行路径就能看到自己数据的 RMSE 分布。

MQL5 / C++
              "High T5Y",
              "Low T5Y",
              "Open SP500",
              "Close SP500",
              "High SP500",
              "Low SP500"
             ]
class="macro">#Reset the index
merged_df.reset_index(inplace=True)
class="macro">#Scale the data
scaled_data = pd.DataFrame(RobustScaler().fit_transform(merged_df.loc[:,predictors]),columns=predictors,index=np.arange(class="num">0,merged_df.shape[class="num">0]))
class="macro">#Now we will perform cross validation
class="macro">#First we iterate over all the models we have
for j in np.arange(class="num">0,len(models)):
    for i,(train,test) in enumerate(tscv.split(merged_df)):
        class="macro">#Prepare the models
        initialize_models()
        class="macro">#Prepare the data
        X_train = scaled_data.loc[train[class="num">0]:train[-class="num">1],predictors]
        X_test = scaled_data.loc[test[class="num">0]:test[-class="num">1],predictors]
        y_train = merged_df.loc[train[class="num">0]:train[-class="num">1],target]
        y_test = merged_df.loc[test[class="num">0]:test[-class="num">1],target]
        class="macro">#Now fit each model and measure its accuracy
        models[j].fit(X_train,y_train)
        SP500_error.iloc[i,j] = root_mean_squared_error(y_test,models[j].predict(X_test))
        print(f"Completed fitting model {models[j]}")
SP500_error
TY5_error
total_error

常见问题

当出现明显股债跷跷板背离时,可倾向降低权益敞口或增配避险债,但外汇贵金属联动高风险,需结合实时盘口确认。
重点看散点在时间维度切分后的聚类变化,以及回归模型残差是否随宏观字段放大,异常段即为信号弱化区。
小布可定时拉取两品种数据做交叉验证并推送背离提醒,你把宏观字段喂进去就能跑滚动回测,省去手动画图。
不同年份宏观 regime 不同,混在一起会掩盖局部相关性;切分后能看到某些时段相关性反转,便于择时。
有可能,建议控制字段数量并用样本外切片检验,回归交叉验证中残差跳变段就是过拟合高发区。