You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何优化Python Polars指标计算代码的运行效率?

优化IndicatorManager类的run函数以提升运行速度与效率

问题背景

我有一个Python的IndicatorManager类,接收Polars DataFrame并计算60个指标(部分基于TA-Lib实现,其余基于Polars代码开发)。发现当以210行数据为限制运行时,受限数据的计算结果与全量历史数据的结果存在差异。为解决此问题,我尝试以210行窗口滑动计算并提取每个窗口的最后一行,但当前实现效率极低,需要优化。

数据为通过python-binance库从Binance API获取的加密货币K线数据(例如BTCUSDT的5分钟线,实际包含751583行)。

现有代码分析

当前run函数实现

def run(self, data: pl.DataFrame) -> pl.DataFrame:
    return pl.concat(
        [
            self._calculate(data.slice(i, 210)).tail(-1)
            for i in range(len(data) - 209)
        ]
    )

该实现通过循环逐个切片210行数据计算,每个窗口与前一个窗口有209行重叠,导致大量重复计算,效率极低。

_calculate方法实现

def _calculate(self, data: pl.DataFrame) -> pl.DataFrame:
    indicators = [
        self._ao, self._cci, self._cfo, self._macd, self._rsi, self._tsi, self._uo, self._willr,
        *[lambda d: self._ema(d, p) for p in (10, 20, 50, 100, 200)],
        *[lambda d: self._sma(d, p) for p in (10, 20, 50, 100, 200)], self._supertrend,
        *[lambda d: self._wma(d, p) for p in (10, 20, 50, 100, 200)],
        self._adx, self._aroon, self._bbands, self._cmf, self._mfi, self._obv, self._kama, 
        self._ppo, self._pvo, self._roc, self._stochrsi, self._stoch, self._ad, self._eom, 
        self._efi, self._nvi, self._pvt, self._vwap, self._donchian, self._kc, self._ui, 
        self._ichimoku, self._kst, self._massi, self._psar, self._stc, self._trix, self._vortex, 
        self._cmo, self._wvf, self._crsi, self._dmi, self._ttm, self._vwmacd, self._ci,
        self._ft, self._atr
    ]

    for indicator in indicators:
        data = indicator(data)

    return data

逐个调用指标函数并生成新的DataFrame,存在不必要的内存复制开销。

示例指标与静态方法

def _ao(self, data: pl.DataFrame) -> pl.DataFrame:
    fast = 5
    slow = 34

    median_price = 0.5 * (data["high"] + data["low"])
    return data.with_columns(
        (self.__sma(median_price, fast) - self.__sma(median_price, slow)).alias(f"AO_{fast}_{slow}")
    )

@staticmethod
def __sma(
        close: pl.Series,
        length: int
) -> pl.Series:
    return talib.SMA(close, length)

失败的LazyDataFrame尝试

def run(self, data: pl.DataFrame) -> pl.DataFrame:
    window_size = 210
    
    data = data.with_row_index()
    
    result = (
        data
        .lazy()
        .filter(pl.col("index") >= window_size - 1)
        .with_columns(
            (pl.col("index") - (window_size - 1)).alias("window_start")
        )
        .group_by("window_start", maintain_order=True)
        .apply(lambda df: self._calculate(
            data.slice(int(df["window_start"][0]), window_size)
        ).tail(1))
    ).collect()

    return result

Polars LazyDataFrame的group_by不支持apply方法,导致该方案无法运行。

优化方案

1. 利用滚动计算替代逐窗口切片

核心思路是避免重复计算重叠窗口数据,直接在全量数据上使用Polars或TA-Lib的滚动计算能力,最后只保留窗口完整的行(从第210行开始)。

2. 重构指标计算逻辑,批量生成列

将逐个调用指标函数改为一次性生成所有指标的表达式,通过with_columns批量添加,减少DataFrame复制开销。

3. 优化TA-Lib指标计算

TA-Lib的多数函数支持直接传入全量序列和窗口长度,自动返回滚动计算结果,无需逐窗口切片。

4. 自定义指标改用Polars窗口函数

对于非TA-Lib实现的指标,用Polars原生窗口函数(如rolling_mean)替代,利用Polars的向量化运算提升效率。

优化后的代码示例

优化后的run函数

def run(self, data: pl.DataFrame) -> pl.DataFrame:
    # 直接在全量数据上计算所有指标
    calculated_data = self._calculate(data)
    # 只保留窗口完整的行(从第210行开始,索引从0开始即slice(209))
    return calculated_data.slice(209)

重构后的_calculate方法

def _calculate(self, data: pl.DataFrame) -> pl.DataFrame:
    exprs = []
    
    # AO指标:用Polars滚动均值替代TA-Lib,实现全量滚动计算
    median_price = 0.5 * (pl.col("high") + pl.col("low"))
    exprs.append(
        (median_price.rolling_mean(window_size=5) - median_price.rolling_mean(window_size=34)).alias("AO_5_34")
    )
    
    # TA-Lib SMA指标:直接传入全量序列计算滚动值
    close_series = data["close"]
    for period in (10, 20, 50, 100, 200):
        exprs.append(
            pl.Series(talib.SMA(close_series, period)).alias(f"SMA_{period}")
        )
    
    # EMA指标同理,用TA-Lib直接计算全量滚动值
    for period in (10, 20, 50, 100, 200):
        exprs.append(
            pl.Series(talib.EMA(close_series, period)).alias(f"EMA_{period}")
        )
    
    # 其他指标按此逻辑批量添加表达式
    # ...(补充剩余50+个指标的表达式)
    
    # 一次性添加所有指标列,减少内存复制
    return data.with_columns(exprs)

特殊指标处理(如Supertrend)

对于需要递进计算的指标(如Supertrend),可使用Polars的rolling_apply或自定义函数实现滚动窗口计算:

def _supertrend(self, data: pl.DataFrame, period=10, multiplier=3) -> pl.Expr:
    def calculate_supertrend(window_df: pl.DataFrame) -> float:
        # 实现Supertrend的计算逻辑,返回窗口最后一行的结果
        atr = talib.ATR(window_df["high"], window_df["low"], window_df["close"], period)[-1]
        basic_upper = (window_df["high"] + window_df["low"]) / 2 + multiplier * atr
        basic_lower = (window_df["high"] + window_df["low"]) / 2 - multiplier * atr
        # 完成剩余Supertrend逻辑计算
        # ...
        return final_supertrend_value
    
    # 使用rolling_apply对全量数据滚动计算
    return pl.struct(["high", "low", "close"]).rolling_apply(
        window_size=210,
        function=calculate_supertrend,
        dtype=pl.Float64
    ).alias("Supertrend")

注意:rolling_apply的性能略低于原生窗口函数,但远优于逐窗口切片计算。

优化效果说明

  • 避免了重叠窗口的重复计算,计算量从O(N*210)降至O(N)
  • 利用Polars向量化运算和TA-Lib的底层优化,大幅提升计算速度
  • 减少DataFrame复制次数,降低内存开销

内容的提问来源于stack exchange,提问作者YenilmezHunt

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 05:34:59