R语言中增量计算新增交易日RSI、DEMA等技术指标的实现方法
核心问题梳理
当前使用的计算代码
library(quantmod) library(TTR) getSymbols("AAPL") chartSeries(AAPL, TA=NULL) data=AAPL[,4] AAPL$rsi = TTR::RSI(data) AAPL$dema = TTR::DEMA(data) # 对象B存储AAPL对象的副本并保存为CSV文件 B = AAPL
现存痛点与诉求
- 目前每次新增1行最新交易日的AAPL数据,都需要对12年以上的全量历史数据重跑RSI、DEMA计算,会浪费大量CPU资源与时间
- 核心诉求1:能否仅计算最新交易日对应的RSI、DEMA值,直接追加到原有数据集B中,无需全量重算
- 核心诉求2:高频场景下(每秒获取tick数据、需要基于新数据实时计算RSI这类指标),量化从业者是如何解决全量重算耗时长、行情滞后的问题的
具体实现方案
一、日线场景下的增量指标计算方法
RSI、DEMA都属于 有状态的递推类指标,计算最新时点的指标值根本不需要全量历史数据参与,只要保留上一步的递推状态,用单条新数据就能算出最新的指标值,完全可以避免全量重算。
每个指标需要维护的状态值非常少:
- DEMA的递推核心是嵌套的EMA计算,只需要存最新一阶EMA的结果、二阶嵌套EMA的结果两个值,公式为
DEMA = 2*EMA(n) - EMA(EMA(n)) - 默认14周期Wilder平滑的RSI,只需要存上一个时点的平均涨幅、平均跌幅两个值,公式为
RSI = 100 - 100/(1 + 平均涨幅/平均跌幅),Wilder平滑的递推规则为「新的平滑值 = (上期平滑值*(周期数-1) + 当期值)/周期数」
你可以按照下面的逻辑改造代码,实现零全量重算的增量更新:
# 第一次全量初始化,仅跑一次,把递推状态持久化到本地 init_metric_state <- function(price_vec, rsi_period = 14, dema_period = 20) { # 计算RSI需要的初始状态 daily_diff <- diff(price_vec) up_vec <- ifelse(daily_diff > 0, daily_diff, 0) down_vec <- ifelse(daily_diff < 0, -daily_diff, 0) # 取Wilder平滑最后一步的平均涨、跌幅作为初始状态 initial_gain <- SMA(up_vec, rsi_period)[length(up_vec)] initial_loss <- SMA(down_vec, rsi_period)[length(down_vec)] # 计算DEMA需要的初始状态 ema_alpha <- 2/(dema_period + 1) initial_ema1 <- EMA(price_vec, dema_period)[length(price_vec)] initial_ema2 <- EMA(EMA(price_vec, dema_period), dema_period)[length(price_vec)] # 把所有需要的状态存成本地rds文件,之后直接读取即可 state <- list( last_price = tail(price_vec, 1), rsi_period = rsi_period, rsi_gain = initial_gain, rsi_loss = initial_loss, dema_period = dema_period, ema_alpha = ema_alpha, ema1 = initial_ema1, ema2 = initial_ema2 ) saveRDS(state, "metric_state.rds") return(state) } # 增量更新函数:每次新数据来了直接调用,不需要碰历史数据 update_latest_metric <- function(new_price) { # 读取之前存好的状态 state <- readRDS("metric_state.rds") # 计算最新RSI price_change <- new_price - state$last_price curr_gain <- max(price_change, 0) curr_loss <- max(-price_change, 0) # 递推更新Wilder平滑值 new_gain <- (state$rsi_gain * (state$rsi_period - 1) + curr_gain) / state$rsi_period new_loss <- (state$rsi_loss * (state$rsi_period - 1) + curr_loss) / state$rsi_period new_rsi <- 100 - 100/(1 + new_gain/new_loss) # 计算最新DEMA new_ema1 <- state$ema_alpha * new_price + (1 - state$ema_alpha) * state$ema1 new_ema2 <- state$ema_alpha * new_ema1 + (1 - state$ema_alpha) * state$ema2 new_dema <- 2 * new_ema1 - new_ema2 # 更新状态文件 new_state <- list( last_price = new_price, rsi_period = state$rsi_period, rsi_gain = new_gain, rsi_loss = new_loss, dema_period = state$dema_period, ema_alpha = state$ema_alpha, ema1 = new_ema1, ema2 = new_ema2 ) saveRDS(new_state, "metric_state.rds") # 返回最新的两个指标值,直接rbind追加到数据集B即可 return(list(rsi = new_rsi, dema = new_dema)) }
整个增量计算过程的耗时是微秒级的,完全不需要读取12年的历史数据,也不会浪费CPU资源。
二、高频tick场景下的实时指标计算方案
量化行业里处理每秒甚至更高频率的tick数据、实时计算RSI这类指标,从来不会采用全量重算的方式,通用的实现逻辑非常简单:
- 核心原则:状态全量常驻内存。程序启动时,先一次性加载全量历史数据,跑完所有指标的递推流程,把每个指标计算需要的所有中间状态(比如RSI的平均涨/跌幅、EMA的平滑值、固定窗口的价格缓存)全部存在内存里,不涉及磁盘读写。
- 单步递推计算:每收到一个新的tick数据,直接在内存里读取对应指标的状态值,用递推公式算出最新的指标值,再更新内存里的状态值。整个过程的计算复杂度是O(1),和历史数据的总长度完全无关,单条tick的计算耗时基本在微秒级,完全赶得上行情推送的速度,根本不会出现行情已经变动完才算出指标的情况。
- 环形缓冲区适配窗口类指标:针对需要固定窗口计算的指标(比如20周期最高价、简单移动平均),会用环形缓冲区存储最近N个周期的有效数据,新数据到来时直接覆盖缓冲区里最老的记录,计算时直接读取缓冲区的N个值即可,不会加载窗口范围外的历史数据。
很多新手的误区是:必须把全量时间序列传给TTR、pandas-ta这类指标库才能算出结果。实际上这类库的全量计算接口,本质也是内部从时间序列起点开始一路递推,把每一步的计算结果全部返回出来而已,生产环境里根本不会调用这种全量返回的接口,都是调用库内部的单步递推能力,自己维护状态。
内容的提问来源于stack exchange,提问作者Saurabh
相关产品推荐
相关产品推荐

