<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>配对交易 on 格物堂</title><link>https://gewutang.cn/tags/%E9%85%8D%E5%AF%B9%E4%BA%A4%E6%98%93/</link><description>Recent content in 配对交易 on 格物堂</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Fri, 10 Apr 2015 00:00:00 +0000</lastBuildDate><atom:link href="https://gewutang.cn/tags/%E9%85%8D%E5%AF%B9%E4%BA%A4%E6%98%93/index.xml" rel="self" type="application/rss+xml"/><item><title>基于 quantstrat 的配对交易策略实现与解析</title><link>https://gewutang.cn/2015/04/10/pair-trade/</link><pubDate>Fri, 10 Apr 2015 00:00:00 +0000</pubDate><guid>https://gewutang.cn/2015/04/10/pair-trade/</guid><description>&#10;&#10;&#10;&lt;p&gt;配对交易是一种基于统计套利的量化策略，其核心逻辑是通过识别价格走势高度相关的两只股票，当它们的价格偏离历史正常关系时建立对冲头寸，等待关系回归时获利。&lt;/p&gt;&#10;&lt;p&gt;本文将解析一段基于 R 语言 quantstrat 包的配对交易策略代码，展示其实现思路与核心意义。&lt;/p&gt;&#10;&lt;div id="策略核心逻辑与代码框架" class="section level1"&gt;&#10;&lt;h1&gt;策略核心逻辑与代码框架&lt;/h1&gt;&#10;&lt;p&gt;这段代码以 SPY 和 DIA 两只 ETF 为交易标的，通过构建价格比率的均值回归模型实现配对交易。策略的核心步骤包括：数据同步与准备、策略环境初始化、指标与信号构建、自定义订单规模控制、交易规则设置，以及回测与绩效分析。&lt;/p&gt;&#10;&lt;p&gt;以下是完整代码实现：&lt;/p&gt;&#10;&lt;pre class="r"&gt;&lt;code&gt;# Kindly contributed to quantstrat by Garrett See&#10;# code borrowed heavily from existing quantstrat demos&#10;&#10;# 这是一个简单的配对交易示例，旨在展示如何扩展现有 quantstrat 功能。&#10;# 它使用 addPosLimits 指定头寸水平和限制，并演示如何将自定义订单规模函数传递给 osFUN&#10;&#10;# 注意：先构建价差并将其视为单一工具，比处理股票组合更简单&#10;&#10;## 给定两只股票，计算其名义价值比率。如果比率低于 2 标准差区间，&#10;# 当它回升穿过区间时，买入股票 1 并卖出股票 2。如果比率高于 2 标准差区间，&#10;# 当它回落穿过区间时，卖出股票 1 并买入股票 2。如果比率穿过其移动平均线，则平仓所有头寸。&#10;&#10;# 股票 A 的买入（卖出）数量 = MaxPos / lvls&#10;# 股票 B 的卖出（买入）数量 = MaxPos * 比率 / lvls &#10;&#10;require(quantstrat)&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## Loading required package: quantstrat&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## Loading required package: quantmod&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## Loading required package: xts&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## Loading required package: zoo&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## &#10;## Attaching package: &amp;#39;zoo&amp;#39;&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## The following objects are masked from &amp;#39;package:base&amp;#39;:&#10;## &#10;## as.Date, as.Date.numeric&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## Loading required package: TTR&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## Registered S3 method overwritten by &amp;#39;quantmod&amp;#39;:&#10;## method from&#10;## as.zoo.data.frame zoo&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## Loading required package: blotter&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## Loading required package: FinancialInstrument&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## Loading required package: PerformanceAnalytics&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## &#10;## Attaching package: &amp;#39;PerformanceAnalytics&amp;#39;&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## The following object is masked from &amp;#39;package:graphics&amp;#39;:&#10;## &#10;## legend&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## Loading required package: foreach&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre class="r"&gt;&lt;code&gt;suppressWarnings(rm(&amp;quot;order_book.pair1&amp;quot;,pos=.strategy))&#10;suppressWarnings(rm(&amp;quot;account.pairs&amp;quot;, &amp;quot;portfolio.pair1&amp;quot;, pos=.blotter))&#10;suppressWarnings(rm(&amp;quot;startDate&amp;quot;, &amp;quot;endDate&amp;quot;, &amp;quot;startDate&amp;quot;, &amp;quot;initEq&amp;quot;, &amp;quot;SD&amp;quot;, &amp;quot;N&amp;quot;, &#10; &amp;quot;symb1&amp;quot;, &amp;quot;symb2&amp;quot;, &amp;quot;portfolio1.st&amp;quot;, &amp;quot;account.st&amp;quot;, &#10; &amp;quot;pairStrat&amp;quot;, &amp;quot;out1&amp;quot;))&#10;&#10;initDate &amp;lt;- &amp;#39;2013-12-31&amp;#39; &#10;startDate &amp;lt;- &amp;#39;2014-01-01&amp;#39;&#10;endDate &amp;lt;- &amp;#39;2015-05-01&amp;#39;&#10;initEq &amp;lt;- 100000&#10;SD &amp;lt;- 2 # 标准差倍数&#10;N &amp;lt;- 20 # 移动平均线周期&#10;&#10;MaxPos &amp;lt;- 1500 # 股票 A 的最大头寸；&#10;# 股票 B 的最大头寸将是 max * 比率，即对股票 B 没有硬性头寸限制&#10;lvls &amp;lt;- 3 # 分几步建仓；每笔订单数量 = MaxPos/lvls&#10;&#10;symb1 &amp;lt;- &amp;#39;SPY&amp;#39; # 可更改这些标的以尝试其他配对&#10;symb2 &amp;lt;- &amp;#39;DIA&amp;#39; # 如果更改，需确保头寸限制仍然合理&#10;&#10;portfolio1.st &amp;lt;- &amp;#39;pair1&amp;#39;&#10;account.st &amp;lt;- &amp;#39;pairs&amp;#39;&#10;&#10;# 获取历史数据&#10;getSymbols(c(symb1, symb2), from=startDate, to=endDate, adjust=TRUE) &lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## [1] &amp;quot;SPY&amp;quot; &amp;quot;DIA&amp;quot;&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre class="r"&gt;&lt;code&gt;# 以下函数用于确保所有标的的时间戳相同，删除其中一只股票数据缺失的行&#10;alignSymbols &amp;lt;- function(symbols, env=.GlobalEnv) {&#10; # 这是 qmao::alignSymbols() 的简化版本&#10; if (length(symbols) &amp;lt; 2) &#10; stop(&amp;quot;必须提供至少 2 个标的&amp;quot;)&#10; if (any(!is.character(symbols))) &#10; stop(&amp;quot;标的必须是字符向量。&amp;quot;)&#10; ff &amp;lt;- get(symbols[1],env=env)&#10; for (sym in symbols[-1]) {&#10; tmp.sym &amp;lt;- get(sym,env=env)&#10; ff &amp;lt;- merge(ff, tmp.sym, all=FALSE) # 只保留双方都有数据的行&#10; }&#10; for (sym in symbols) {&#10; assign(sym,ff[,grep(sym, colnames(ff))], env=env) # 更新标的数据&#10; }&#10; symbols&#10;}&#10;alignSymbols(c(symb1, symb2)) # 对齐两只股票的时间戳&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## [1] &amp;quot;SPY&amp;quot; &amp;quot;DIA&amp;quot;&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre class="r"&gt;&lt;code&gt;# 定义金融工具&#10;currency(&amp;quot;USD&amp;quot;) # 基础货币为美元&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## [1] &amp;quot;USD&amp;quot;&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre class="r"&gt;&lt;code&gt;stock(symb1, currency=&amp;quot;USD&amp;quot;, multiplier=1) # 定义股票 1 的属性&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## [1] &amp;quot;SPY&amp;quot;&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre class="r"&gt;&lt;code&gt;stock(symb2, currency=&amp;quot;USD&amp;quot;, multiplier=1) # 定义股票 2 的属性&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## [1] &amp;quot;DIA&amp;quot;&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre class="r"&gt;&lt;code&gt;# 初始化投资组合、账户和订单&#10;initPortf(name=portfolio1.st, c(symb1,symb2)) # 创建包含两只股票的组合&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## [1] &amp;quot;pair1&amp;quot;&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre class="r"&gt;&lt;code&gt;initAcct(account.st, portfolios=portfolio1.st, initEq=initEq) # 初始化账户，初始资金 10 万美元&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## [1] &amp;quot;pairs&amp;quot;&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre class="r"&gt;&lt;code&gt;initOrders(portfolio=portfolio1.st) # 初始化订单簿&#10;&#10;# osFUN 需要知道哪个标的是第一腿、哪个是第二腿，以及 MaxPos 和 lvls 的值。&#10;# 因此，在投资组合中创建一个插槽来存储这些信息。&#10;pair &amp;lt;- c(1, 2, MaxPos, lvls)&#10;names(pair) &amp;lt;- c(symb1, symb2, &amp;quot;MaxPos&amp;quot;, &amp;quot;lvls&amp;quot;)&#10;.blotter[[paste(&amp;#39;portfolio&amp;#39;, portfolio1.st, sep=&amp;#39;.&amp;#39;)]]$pair &amp;lt;- pair # 存储到组合属性中&#10;&#10;# 按标的创建初始头寸限制和水平&#10;# 如果 lvls=3，则允许多空各 3 次建仓。&#10;addPosLimit(portfolio=portfolio1.st, timestamp=startDate, symbol=symb1, &#10; maxpos=MaxPos, longlevels=lvls, minpos=-MaxPos, shortlevels=lvls)&#10;addPosLimit(portfolio=portfolio1.st, timestamp=startDate, symbol=symb2, &#10; maxpos=MaxPos, longlevels=lvls, minpos=-MaxPos, shortlevels=lvls)&#10;&#10;# 创建策略对象&#10;pairStrat &amp;lt;- strategy(&amp;#39;pairStrat&amp;#39;)&#10;&#10;# 指标函数：计算两只股票的价格比率&#10;calcRatio &amp;lt;- function(x) { &#10; # 返回两只股票的名义收盘价比率&#10; x1 &amp;lt;- get(x[1])&#10; x2 &amp;lt;- get(x[2])&#10; mult1 &amp;lt;- getInstrument(x[1])$multiplier # 合约乘数&#10; mult2 &amp;lt;- getInstrument(x[2])$multiplier&#10; rat &amp;lt;- (mult1 * Cl(x1)) / (mult2 * Cl(x2)) # 收盘价比率&#10; colnames(rat) &amp;lt;- &amp;#39;Ratio&amp;#39;&#10; rat&#10;}&#10;# 用于确定入场/出场的指标：比率及其布林带&#10;Ratio &amp;lt;- calcRatio(c(symb1[1], symb2[1])) &#10;&#10;# 将对冲比率存储在投资组合中，以便订单规模函数使用。&#10;# 在本示例中，对冲比率与 Ratio 指标相同。&#10;.blotter[[paste(&amp;#39;portfolio&amp;#39;,portfolio1.st,sep=&amp;#39;.&amp;#39;)]]$HedgeRatio &amp;lt;- Ratio&#10;# 创建获取最新对冲比率的函数&#10;getHedgeRatio &amp;lt;- function(portfolio, timestamp) {&#10; portf &amp;lt;- getPortfolio(portfolio)&#10; timestamp &amp;lt;- format(timestamp,&amp;quot;%Y-%m-%d %H:%M:%S&amp;quot;) # 格式化时间戳&#10; # 上述行确保在使用日内数据且时间戳为午夜时，不会获取次日的最后一个值&#10; toDate &amp;lt;- paste(&amp;quot;::&amp;quot;, timestamp, sep=&amp;quot;&amp;quot;)&#10; Ratio &amp;lt;- last(portf$HedgeRatio[toDate]) # 获取该时间点的最新比率&#10; as.numeric(Ratio)&#10;}&#10;&#10;# 添加指标：比率的布林带&#10;pairStrat &amp;lt;- add.indicator(strategy=pairStrat, name = &amp;quot;calcRatio&amp;quot;, &#10; arguments=list(x=c(symb1,symb2)))&#10;pairStrat &amp;lt;- add.indicator(strategy=pairStrat, name = &amp;quot;BBands&amp;quot;, &#10; arguments=list(HLC=quote(Ratio), sd=SD, n=N, &#10; maType=&amp;#39;SMA&amp;#39;)) # 基于 20 日 SMA 计算布林带&#10;&#10;# 生成信号：当比率从下方穿过下轨时买入，从上方穿过上轨时卖出，穿过中轨时平仓&#10;pairStrat &amp;lt;- add.signal(strategy=pairStrat, name=&amp;quot;sigCrossover&amp;quot;, &#10; arguments=list(columns=c(&amp;quot;Ratio&amp;quot;,&amp;quot;up&amp;quot;), &#10; relationship=&amp;quot;lt&amp;quot;), # 比率 &amp;lt; 上轨（从上方穿过）&#10; label=&amp;quot;cross.up&amp;quot;)&#10;pairStrat &amp;lt;- add.signal(strategy=pairStrat, name=&amp;quot;sigCrossover&amp;quot;, &#10; arguments=list(columns=c(&amp;quot;Ratio&amp;quot;,&amp;quot;dn&amp;quot;), &#10; relationship=&amp;quot;gt&amp;quot;), # 比率 &amp;gt; 下轨（从下方穿过）&#10; label=&amp;quot;cross.dn&amp;quot;)&#10;pairStrat &amp;lt;- add.signal(strategy=pairStrat, name=&amp;quot;sigCrossover&amp;quot;, &#10; arguments=list(columns=c(&amp;quot;Ratio&amp;quot;,&amp;quot;mavg&amp;quot;), &#10; relationship=&amp;quot;lt&amp;quot;), # 比率 &amp;lt; 中轨（从上方穿过）&#10; label=&amp;quot;cross.mid.fa&amp;quot;)&#10;pairStrat &amp;lt;- add.signal(strategy=pairStrat, name=&amp;quot;sigCrossover&amp;quot;, &#10; arguments=list(columns=c(&amp;quot;Ratio&amp;quot;,&amp;quot;mavg&amp;quot;), &#10; relationship=&amp;quot;gt&amp;quot;), # 比率 &amp;gt; 中轨（从下方穿过）&#10; label=&amp;quot;cross.mid.fb&amp;quot;)&#10;&#10;# 自定义订单规模函数&#10;#######################_ORDER SIZING FUNCTION_##################################&#10;osSpreadMaxPos &amp;lt;- function (data, timestamp, orderqty, ordertype, orderside, &#10; portfolio, symbol, ruletype, ..., orderprice) {&#10; portf &amp;lt;- getPortfolio(portfolio)&#10; # 检查 pair 插槽是否包含该函数所需的内容&#10; if (!any(portf$pair == 1) &amp;amp;&amp;amp; !(any(portf$pair == 2))) &#10; stop(&amp;#39;pair 必须包含值 1 和 2&amp;#39;)&#10; if (!any(names(portf$pair) == &amp;quot;MaxPos&amp;quot;) || !any(names(portf$pair) == &amp;quot;lvls&amp;quot;)) &#10; stop(&amp;#39;pair 必须包含 MaxPos 和 lvls&amp;#39;) &#10; &#10; if (portf$pair[symbol] == 1) legside &amp;lt;- &amp;quot;long&amp;quot; # 第一腿&#10; if (portf$pair[symbol] == 2) legside &amp;lt;- &amp;quot;short&amp;quot; # 第二腿（与第一腿方向相反）&#10; MaxPos &amp;lt;- portf$pair[&amp;quot;MaxPos&amp;quot;]&#10; lvls &amp;lt;- portf$pair[&amp;quot;lvls&amp;quot;]&#10; ratio &amp;lt;- getHedgeRatio(portfolio, timestamp) # 获取当前对冲比率&#10; pos &amp;lt;- getPosQty(portfolio, symbol, timestamp) # 当前头寸&#10; PosLimit &amp;lt;- getPosLimit(portfolio, symbol, timestamp) # 头寸限制&#10; qty &amp;lt;- orderqty&#10; if (legside == &amp;quot;short&amp;quot;) {# 标的是第二腿&#10; ## 注释掉下一行可使每只股票的订单规模相等。&#10; addPosLimit(portfolio=portfolio, timestamp=timestamp, symbol=symbol, &#10; maxpos=round(MaxPos*ratio,0), longlevels=lvls, &#10; minpos=round(-MaxPos*ratio,0), shortlevels=lvls) # 调整第二腿的头寸限制&#10; ## &#10; qty &amp;lt;- -orderqty # 反转股票 B 的订单数量（多空相反）&#10; }&#10; &#10; if (qty &amp;gt; 0) orderside = &amp;#39;long&amp;#39; # 确定订单方向&#10; if (qty &amp;lt; 0) orderside = &amp;#39;short&amp;#39;&#10; &#10; # 调用 osMaxPos 计算符合头寸限制的订单数量&#10; orderqty &amp;lt;- osMaxPos(data=data,timestamp=timestamp, orderqty=qty,&#10; ordertype=ordertype, orderside=orderside,&#10; portfolio=portfolio, symbol=symbol, ruletype=ruletype, &#10; ...)&#10; &#10; # 在规则信号函数中添加订单&#10; if (!is.null(orderqty) &amp;amp; !orderqty == 0 &amp;amp; !is.null(orderprice)) {&#10; addOrder(portfolio=portfolio, symbol=symbol, &#10; timestamp=timestamp, qty=orderqty, price=as.numeric(orderprice), &#10; ordertype=ordertype, side=orderside, replace=FALSE,&#10; status=&amp;quot;open&amp;quot;, ...=...)&#10; }&#10; return(0) # 使 ruleSignal 函数不会尝试下单&#10;}&#10;################################################################################&#10;&#10;# 为多空方向创建入场和出场规则。两个标的将获得相同的买卖信号，&#10;# 但 osMaxPos 会为第二个标的反转这些信号。&#10;# orderqty 大于头寸限制允许的值。osMaxPos 会将订单数量调整为最大允许值的 1/3。&#10;# （1/3 是因为我们在 PosLimit 中使用了 3 个水平）&#10;pairStrat &amp;lt;- add.rule(strategy=pairStrat, name=&amp;#39;ruleSignal&amp;#39;, &#10; arguments=list(sigcol=&amp;quot;cross.dn&amp;quot;, sigval=TRUE, &#10; orderqty=1e6, ordertype=&amp;#39;market&amp;#39;, &#10; orderside=NULL, osFUN=&amp;#39;osSpreadMaxPos&amp;#39;), &#10; type=&amp;#39;enter&amp;#39;) # cross.dn 信号触发入场&#10;pairStrat &amp;lt;- add.rule(strategy=pairStrat, name=&amp;#39;ruleSignal&amp;#39;, &#10; arguments=list(sigcol=&amp;quot;cross.up&amp;quot;, sigval=TRUE, &#10; orderqty=-1e6, ordertype=&amp;#39;market&amp;#39;, &#10; orderside=NULL, osFUN=&amp;#39;osSpreadMaxPos&amp;#39;), &#10; type=&amp;#39;enter&amp;#39;) # cross.up 信号触发入场&#10;pairStrat &amp;lt;- add.rule(strategy=pairStrat, name=&amp;#39;ruleSignal&amp;#39;, &#10; arguments=list(sigcol=&amp;quot;cross.mid.fb&amp;quot;, sigval=TRUE, &#10; orderqty=&amp;#39;all&amp;#39;, ordertype=&amp;#39;market&amp;#39;, &#10; orderside=NULL), &#10; type=&amp;#39;exit&amp;#39;) # cross.mid.fb 信号触发出场&#10;pairStrat &amp;lt;- add.rule(strategy=pairStrat, name=&amp;#39;ruleSignal&amp;#39;, &#10; arguments=list(sigcol=&amp;quot;cross.mid.fa&amp;quot;, sigval=TRUE, &#10; orderqty=&amp;#39;all&amp;#39;, ordertype=&amp;#39;market&amp;#39;, &#10; orderside=NULL), &#10; type=&amp;#39;exit&amp;#39;) # cross.mid.fa 信号触发出场&#10;&#10;&#10;# 执行策略回测&#10;out1&amp;lt;-applyStrategy(strategy=pairStrat, portfolios=portfolio1.st)&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## [1] &amp;quot;2014-02-04 00:00:00 DIA 500 @ 150.182621296714&amp;quot;&#10;## [1] &amp;quot;2014-03-07 00:00:00 DIA 568 @ 160.616223229789&amp;quot;&#10;## [1] &amp;quot;2014-03-17 00:00:00 DIA 573 @ 158.690860184405&amp;quot;&#10;## [1] &amp;quot;2014-03-25 00:00:00 DIA -1641 @ 159.963991320726&amp;quot;&#10;## [1] &amp;quot;2014-03-31 00:00:00 DIA -574 @ 160.796174254035&amp;quot;&#10;## [1] &amp;quot;2014-04-22 00:00:00 DIA 574 @ 161.4743809578&amp;quot;&#10;## [1] &amp;quot;2014-05-22 00:00:00 DIA 571 @ 162.261848260392&amp;quot;&#10;## [1] &amp;quot;2014-06-02 00:00:00 DIA 572 @ 164.246166224292&amp;quot;&#10;## [1] &amp;quot;2014-06-27 00:00:00 DIA 576 @ 165.51814455118&amp;quot;&#10;## [1] &amp;quot;2014-07-02 00:00:00 DIA 27 @ 166.728744615667&amp;quot;&#10;## [1] &amp;quot;2014-07-15 00:00:00 DIA -1746 @ 167.69330191996&amp;quot;&#10;## [1] &amp;quot;2014-07-21 00:00:00 DIA -583 @ 167.676368799237&amp;quot;&#10;## [1] &amp;quot;2014-07-24 00:00:00 DIA 583 @ 167.932553399241&amp;quot;&#10;## [1] &amp;quot;2014-09-17 00:00:00 DIA -580 @ 169.355777408222&amp;quot;&#10;## [1] &amp;quot;2014-09-24 00:00:00 DIA -584 @ 169.969729387294&amp;quot;&#10;## [1] &amp;quot;2014-10-15 00:00:00 DIA -582 @ 159.57109738308&amp;quot;&#10;## [1] &amp;quot;2014-10-21 00:00:00 DIA 1746 @ 164.043208292008&amp;quot;&#10;## [1] &amp;quot;2014-10-27 00:00:00 DIA 576 @ 166.101170700567&amp;quot;&#10;## [1] &amp;quot;2014-11-05 00:00:00 DIA -576 @ 172.700494456876&amp;quot;&#10;## [1] &amp;quot;2014-12-03 00:00:00 DIA -586 @ 177.512433609148&amp;quot;&#10;## [1] &amp;quot;2014-12-10 00:00:00 DIA -579 @ 173.872106214788&amp;quot;&#10;## [1] &amp;quot;2014-12-18 00:00:00 DIA 1165 @ 176.351884195197&amp;quot;&#10;## [1] &amp;quot;2015-01-15 00:00:00 DIA -579 @ 172.030741759151&amp;quot;&#10;## [1] &amp;quot;2015-01-21 00:00:00 DIA 579 @ 174.456324855292&amp;quot;&#10;## [1] &amp;quot;2015-01-29 00:00:00 DIA 578 @ 173.013702717688&amp;quot;&#10;## [1] &amp;quot;2015-02-05 00:00:00 DIA -578 @ 177.640012837652&amp;quot;&#10;## [1] &amp;quot;2015-04-21 00:00:00 DIA 583 @ 179.259994506836&amp;quot;&#10;## [1] &amp;quot;2015-04-28 00:00:00 DIA 583 @ 180.839996337891&amp;quot;&#10;## [1] &amp;quot;2014-02-04 00:00:00 SPY -500 @ 171.264049909226&amp;quot;&#10;## [1] &amp;quot;2014-03-07 00:00:00 SPY -500 @ 183.831285747939&amp;quot;&#10;## [1] &amp;quot;2014-03-17 00:00:00 SPY -500 @ 181.946695046646&amp;quot;&#10;## [1] &amp;quot;2014-03-25 00:00:00 SPY 1500 @ 182.730103228311&amp;quot;&#10;## [1] &amp;quot;2014-03-31 00:00:00 SPY 500 @ 183.416649931591&amp;quot;&#10;## [1] &amp;quot;2014-04-22 00:00:00 SPY -500 @ 184.279745767482&amp;quot;&#10;## [1] &amp;quot;2014-05-22 00:00:00 SPY -500 @ 185.94707775133&amp;quot;&#10;## [1] &amp;quot;2014-06-02 00:00:00 SPY -500 @ 189.193474635518&amp;quot;&#10;## [1] &amp;quot;2014-06-27 00:00:00 SPY -500 @ 192.977678507185&amp;quot;&#10;## [1] &amp;quot;2014-07-15 00:00:00 SPY 1500 @ 194.367200918673&amp;quot;&#10;## [1] &amp;quot;2014-07-21 00:00:00 SPY 500 @ 194.475604869373&amp;quot;&#10;## [1] &amp;quot;2014-07-24 00:00:00 SPY -500 @ 195.766587803966&amp;quot;&#10;## [1] &amp;quot;2014-09-17 00:00:00 SPY 500 @ 197.83611230376&amp;quot;&#10;## [1] &amp;quot;2014-09-24 00:00:00 SPY 500 @ 197.582667853886&amp;quot;&#10;## [1] &amp;quot;2014-10-15 00:00:00 SPY 500 @ 184.582760931563&amp;quot;&#10;## [1] &amp;quot;2014-10-21 00:00:00 SPY -1500 @ 192.147074898032&amp;quot;&#10;## [1] &amp;quot;2014-10-27 00:00:00 SPY -500 @ 194.216362617501&amp;quot;&#10;## [1] &amp;quot;2014-11-05 00:00:00 SPY 500 @ 200.335121161983&amp;quot;&#10;## [1] &amp;quot;2014-12-03 00:00:00 SPY 500 @ 205.830132301385&amp;quot;&#10;## [1] &amp;quot;2014-12-10 00:00:00 SPY 500 @ 201.147003486903&amp;quot;&#10;## [1] &amp;quot;2014-12-18 00:00:00 SPY -1000 @ 204.731130073504&amp;quot;&#10;## [1] &amp;quot;2015-01-15 00:00:00 SPY 500 @ 198.135576473045&amp;quot;&#10;## [1] &amp;quot;2015-01-21 00:00:00 SPY -500 @ 202.177531751319&amp;quot;&#10;## [1] &amp;quot;2015-01-29 00:00:00 SPY -500 @ 201.092379263502&amp;quot;&#10;## [1] &amp;quot;2015-02-05 00:00:00 SPY 500 @ 205.204015568481&amp;quot;&#10;## [1] &amp;quot;2015-04-21 00:00:00 SPY -500 @ 209.600006103516&amp;quot;&#10;## [1] &amp;quot;2015-04-28 00:00:00 SPY -500 @ 211.440002441406&amp;quot;&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre class="r"&gt;&lt;code&gt;# 更新组合和账户数据&#10;updatePortf(Portfolio=portfolio1.st,&#10; Dates=paste(&amp;quot;::&amp;quot;, as.Date(Sys.time()), sep=&amp;#39;&amp;#39;))&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## Warning in .updatePosPL(Portfolio = pname, Symbol = as.character(symbol), :&#10;## Could not parse ::2025-07-13 as ISO8601 string, or one/bothends of the range&#10;## were outside the available prices: 2014-01-02/2015-04-30. Using all data&#10;## instead.&#10;## Warning in .updatePosPL(Portfolio = pname, Symbol = as.character(symbol), :&#10;## Could not parse ::2025-07-13 as ISO8601 string, or one/bothends of the range&#10;## were outside the available prices: 2014-01-02/2015-04-30. Using all data&#10;## instead.&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## [1] &amp;quot;pair1&amp;quot;&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre class="r"&gt;&lt;code&gt;updateAcct(account.st, Dates=paste(startDate, endDate, sep=&amp;quot;::&amp;quot;)) &lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## [1] &amp;quot;pairs&amp;quot;&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre class="r"&gt;&lt;code&gt;updateEndEq(account.st, Dates=paste(startDate, endDate, sep=&amp;quot;::&amp;quot;))&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## [1] &amp;quot;pairs&amp;quot;&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre class="r"&gt;&lt;code&gt;getEndEq(account.st, Sys.time()) # 查看最终权益&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre&gt;&lt;code&gt;## [1] 103576.8&lt;/code&gt;&lt;/pre&gt;&#10;&lt;pre class="r"&gt;&lt;code&gt;# 绘制持仓图表&#10;# dev.new()&#10;chart.Posn(Portfolio=portfolio1.st, Symbol=symb1) # 股票 1 的持仓图&lt;/code&gt;&lt;/pre&gt;&#10;&lt;p&gt;&lt;img src="https://gewutang.cn/2015/04/10/pair-trade/index_files/figure-html/pairTrade-1.png" width="672" /&gt;&lt;/p&gt;&#10;&lt;pre class="r"&gt;&lt;code&gt;# dev.new()&#10;chart.Posn(Portfolio=portfolio1.st, Symbol=symb2) # 股票 2 的持仓图&lt;/code&gt;&lt;/pre&gt;&#10;&lt;p&gt;&lt;img src="https://gewutang.cn/2015/04/10/pair-trade/index_files/figure-html/pairTrade-2.png" width="672" /&gt;&lt;/p&gt;&#10;&lt;pre class="r"&gt;&lt;code&gt;# dev.new()&#10;chartSeries(Cl(get(symb1))/Cl(get(symb2)), TA=&amp;quot;addBBands(n=N,sd=SD)&amp;quot;) # 比率与布林带图&lt;/code&gt;&lt;/pre&gt;&#10;&lt;p&gt;&lt;img src="https://gewutang.cn/2015/04/10/pair-trade/index_files/figure-html/pairTrade-3.png" width="672" /&gt;&lt;/p&gt;&#10;&lt;pre class="r"&gt;&lt;code&gt;# 绩效分析&#10;ret1 &amp;lt;- PortfReturns(account.st) # 组合收益&#10;ret1$total &amp;lt;- rowSums(ret1) # 总收益&#10;&#10;# 绘制绩效摘要&#10;if(&amp;quot;package:PerformanceAnalytics&amp;quot; %in% search() || &#10; require(&amp;quot;PerformanceAnalytics&amp;quot;,quietly=TRUE)) {&#10; # dev.new()&#10; charts.PerformanceSummary(ret1$total, geometric=FALSE, wealth.index=TRUE) # 收益摘要图&#10;}&lt;/code&gt;&lt;/pre&gt;&#10;&lt;p&gt;&lt;img src="https://gewutang.cn/2015/04/10/pair-trade/index_files/figure-html/pairTrade-4.png" width="672" /&gt;&lt;/p&gt;&#10;&lt;/div&gt;&#10;&lt;div id="策略实现的核心思路" class="section level1"&gt;&#10;&lt;h1&gt;策略实现的核心思路&lt;/h1&gt;&#10;&lt;p&gt;这段代码的核心是通过量化手段实现配对交易的均值回归逻辑，具体可分为以下几个关键环节：&lt;/p&gt;&#10;&lt;p&gt;首先是数据预处理。配对交易对数据同步性要求极高，因此代码通过alignSymbols函数对齐两只股票的时间戳，删除存在数据缺失的时间点，确保价格比率的计算基于完整的共同数据。这种预处理是后续指标准确性的基础，避免因数据不同步导致的错误信号。&lt;/p&gt;&#10;&lt;p&gt;其次是指标与信号体系。策略的核心指标是两只股票的价格比率（由calcRatio函数计算），并通过布林带（BBands）分析该比率的波动特征。布林带由中轨（20 日移动平均线）和上下轨（中轨 ±2 倍标准差）构成，反映比率的正常波动区间。当比率从下方突破下轨（cross.dn信号）时，认为两只股票的价格偏离过大且即将回归，此时买入第一只股票、卖出第二只；当比率从上方向下突破上轨（cross.up信号）时，则反向操作；当比率回归至中轨时（cross.mid信号），平仓获利。这种基于统计偏离的信号逻辑，是均值回归策略的典型应用。&lt;/p&gt;&#10;&lt;p&gt;再者是头寸管理与订单规模控制。代码通过addPosLimits设置了最大头寸（MaxPos）和建仓层级（lvls），允许分 3 步建仓，避免单次入场风险过高。更关键的是自定义的osSpreadMaxPos函数，它根据两只股票的对冲比率动态调整订单数量，确保多空头寸的风险对冲平衡 —— 第一只股票的头寸为MaxPos/lvls，第二只则为其相反数乘以价格比率，使整体组合对市场系统性风险的暴露最小化。这种精细化的头寸控制，是配对交易实现风险对冲的核心。&lt;/p&gt;&#10;&lt;p&gt;最后是回测与绩效评估。代码通过applyStrategy执行回测，更新组合和账户数据后，利用chart.Posn展示两只股票的持仓变化，通过charts.PerformanceSummary分析策略的收益曲线、最大回撤等关键指标，全面评估策略效果。&#10;策略的意义与价值&lt;/p&gt;&#10;&lt;p&gt;这段代码的意义不仅在于实现了一个可运行的配对交易策略，更展示了如何在 quantstrat 框架下扩展自定义功能：通过addPosLimits灵活设置头寸规则，借助osFUN传递自定义订单规模函数，为复杂策略的实现提供了范例。对于量化研究者而言，它演示了均值回归思想在配对交易中的具体应用 —— 通过价格比率的统计特征捕捉套利机会，同时通过多空对冲控制风险。&lt;/p&gt;&#10;&lt;p&gt;此外，代码也为策略优化提供了方向：例如调整布林带的周期（N）和标准差倍数（SD）以适应不同市场环境，或修改osSpreadMaxPos函数优化头寸调整逻辑。这种模块化的实现方式，使得策略易于维护和扩展，为量化交易的实践提供了有价值的参考。&lt;/p&gt;&#10;&lt;/div&gt;&#10;</description></item></channel></rss>