第四章 交易信号处理核心算法:移动平均线的硬件实现

做量化交易的朋友都知道,移动平均线是最基础、最常用的技术指标。但到了FPGA上,事情就没那么简单了。我刚开始把SMA搬到硬件上时,踩了不少坑。今天咱们就把SMA、EMA、MACD和布林带这几个核心指标,一个一个说清楚。

4.1 简单移动平均线(SMA)的硬件实现

SMA说白了就是取N个数的平均值。公式很简单:

SMA = (P₁ + P₂ + ... + Pₙ) / N

但在FPGA里,除法是个麻烦事。我建议用移位操作代替除法——前提是N是2的幂次。比如N=8、16、32,直接右移就行。

核心思路:滑动窗口累加器

用FIFO或移位寄存器保存最近N个数据,每次新数据进来,就做一次「加新减旧」的操作。

看代码更直观:

// SMA 滑动窗口累加器实现
module sma_calculator #(
    parameter N = 16,          // 窗口大小,必须是2的幂
    parameter DATA_WIDTH = 16
)(
    input clk,
    input rst_n,
    input valid_in,
    input signed [DATA_WIDTH-1:0] data_in,
    output reg signed [DATA_WIDTH-1:0] sma_out,
    output reg valid_out
);

    reg signed [DATA_WIDTH+$clog2(N)-1:0] sum;  // 累加和,防止溢出
    reg [$clog2(N)-1:0] wr_ptr;
    reg [DATA_WIDTH-1:0] buffer [0:N-1];

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sum <= 0;
            wr_ptr <= 0;
            valid_out <= 0;
        end else if (valid_in) begin
            // 加新数据,减旧数据
            sum <= sum + data_in - buffer[wr_ptr];
            buffer[wr_ptr] <= data_in;
            wr_ptr <= wr_ptr + 1;
            // 输出平均值(右移log2(N)位)
            sma_out <= sum >>> $clog2(N);
            valid_out <= 1;
        end else begin
            valid_out <= 0;
        end
    end

endmodule

个人经验:我在一个高频交易项目里,N设成了64。一开始用除法器,延迟大了3个时钟周期,直接被客户怼了。换成移位后,一个周期搞定。记住:FPGA里能用移位就别用除法。

4.2 指数移动平均线(EMA)的硬件实现

EMA比SMA复杂一点,因为它有递归:

EMA(t) = α × P(t) + (1-α) × EMA(t-1)

α是平滑因子,通常取2/(N+1)。这里有个坑——α不是整数,得用定点数表示。

我习惯把α放大成2的幂次,比如α=1/16,这样乘法就变成了移位加加法。

// EMA 定点数实现
module ema_calculator #(
    parameter ALPHA_SHIFT = 4,  // α = 1/16
    parameter DATA_WIDTH = 16
)(
    input clk,
    input rst_n,
    input valid_in,
    input signed [DATA_WIDTH-1:0] data_in,
    output reg signed [DATA_WIDTH-1:0] ema_out,
    output reg valid_out
);

    reg signed [DATA_WIDTH+ALPHA_SHIFT-1:0] ema_acc;  // 内部累加器

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            ema_acc <= 0;
            valid_out <= 0;
        end else if (valid_in) begin
            // EMA = (data_in >> ALPHA_SHIFT) + ema_acc - (ema_acc >> ALPHA_SHIFT)
            ema_acc <= (data_in << ALPHA_SHIFT) + ema_acc - (ema_acc >> ALPHA_SHIFT);
            ema_out <= ema_acc >>> ALPHA_SHIFT;
            valid_out <= 1;
        end else begin
            valid_out <= 0;
        end
    end

endmodule

注意:EMA的初始值很关键。我曾经在回测时发现前100个点的EMA全是错的,后来才意识到初始值应该设为第一个数据点,而不是0。不然收敛速度慢得吓人。

4.3 MACD指标计算

MACD由三部分组成:DIF、DEA和柱状图。公式如下:

  • DIF = EMA(12) - EMA(26)
  • DEA = EMA(9) of DIF
  • MACD柱 = 2 × (DIF - DEA)

说白了,MACD就是三个EMA的组合。在FPGA里,我们可以复用上面的EMA模块。

我建议用流水线结构:第一级算两个EMA,第二级算差值,第三级再算一个EMA,最后输出。这样每个时钟周期都能出一个结果。

// MACD 流水线结构示意
module macd_calculator #(
    parameter DATA_WIDTH = 16
)(
    input clk,
    input rst_n,
    input valid_in,
    input signed [DATA_WIDTH-1:0] price,
    output reg signed [DATA_WIDTH-1:0] dif,
    output reg signed [DATA_WIDTH-1:0] dea,
    output reg signed [DATA_WIDTH-1:0] macd_hist,
    output reg valid_out
);

    wire signed [DATA_WIDTH-1:0] ema12, ema26, dif_wire;
    wire valid_ema12, valid_ema26;

    // 实例化两个EMA模块
    ema_calculator #(.ALPHA_SHIFT(4), .DATA_WIDTH(DATA_WIDTH)) u_ema12 (
        .clk(clk), .rst_n(rst_n),
        .valid_in(valid_in), .data_in(price),
        .ema_out(ema12), .valid_out(valid_ema12)
    );

    ema_calculator #(.ALPHA_SHIFT(5), .DATA_WIDTH(DATA_WIDTH)) u_ema26 (
        .clk(clk), .rst_n(rst_n),
        .valid_in(valid_in), .data_in(price),
        .ema_out(ema26), .valid_out(valid_ema26)
    );

    // DIF = EMA12 - EMA26
    assign dif_wire = ema12 - ema26;

    // 再算DEA(EMA9 of DIF)
    ema_calculator #(.ALPHA_SHIFT(4), .DATA_WIDTH(DATA_WIDTH)) u_dea (
        .clk(clk), .rst_n(rst_n),
        .valid_in(valid_ema12 & valid_ema26),
        .data_in(dif_wire),
        .ema_out(dea), .valid_out(valid_out)
    );

    // MACD柱 = 2 × (DIF - DEA)
    always @(posedge clk) begin
        if (valid_out) begin
            dif <= dif_wire;
            macd_hist <= (dif_wire - dea) << 1;
        end
    end

endmodule

避坑指南:我曾经在MACD模块里忘了处理流水线延迟对齐的问题。DIF和DEA差了3个时钟周期,算出来的柱状图完全不对。后来加了一级延迟匹配才搞定。记住:多级流水线一定要做延迟对齐。

4.4 布林带计算

布林带由中轨、上轨和下轨组成:

  • 中轨 = SMA(N)
  • 上轨 = 中轨 + K × 标准差
  • 下轨 = 中轨 - K × 标准差

标准差的计算在FPGA里比较麻烦。公式是:

σ = sqrt( Σ(Pᵢ - SMA)² / N )

这里有两个难点:平方和开方。平方可以用乘法器,开方我推荐用CORDIC算法或者查表法。

我个人偏好查表法——把标准差的范围映射到256个点,精度够用,延迟也小。

// 布林带计算模块(简化版)
module bollinger_band #(
    parameter N = 20,
    parameter DATA_WIDTH = 16
)(
    input clk,
    input rst_n,
    input valid_in,
    input signed [DATA_WIDTH-1:0] price,
    output signed [DATA_WIDTH-1:0] middle,
    output signed [DATA_WIDTH-1:0] upper,
    output signed [DATA_WIDTH-1:0] lower,
    output valid_out
);

    wire signed [DATA_WIDTH-1:0] sma;
    wire sma_valid;

    // 先算SMA
    sma_calculator #(.N(N), .DATA_WIDTH(DATA_WIDTH)) u_sma (
        .clk(clk), .rst_n(rst_n),
        .valid_in(valid_in), .data_in(price),
        .sma_out(sma), .valid_out(sma_valid)
    );

    // 计算方差(平方和)
    reg signed [DATA_WIDTH*2-1:0] sum_sq;
    reg [$clog2(N)-1:0] count;
    // ...(省略详细实现,重点在思路)

    // 查表法开方
    wire [DATA_WIDTH-1:0] std_dev;
    sqrt_lut #(.DATA_WIDTH(DATA_WIDTH)) u_sqrt (
        .value_in(sum_sq / N),
        .sqrt_out(std_dev)
    );

    // 上轨 = SMA + 2*标准差
    assign upper = sma + (std_dev << 1);
    assign lower = sma - (std_dev << 1);
    assign middle = sma;

endmodule

性能警告:布林带是这几个指标里最吃资源的。我在一个项目里同时跑了10个布林带,LUT直接爆了。后来把标准差的计算改成近似公式,资源省了40%,精度只损失了0.3%。有时候,trade-off是必须的。

4.5 本章核心逻辑图

下面这张图展示了四个指标之间的依赖关系和硬件流水线结构:

交易信号处理核心算法硬件流水线 价格数据流 SMA 滑动窗口 EMA 指数平均 EMA 指数平均 布林带计算 DIF = EMA12 - EMA26 DEA = EMA9(DIF) 输出:SMA / EMA / MACD(DIF, DEA, 柱状图) / 布林带(上中下轨) 每个时钟周期输出一组完整信号 图例说明 SMA模块 EMA模块 布林带模块 MACD模块 输出模块

嗯,这张图把整个流程串起来了。从价格数据流进来,经过SMA/EMA计算,再到MACD和布林带,最后输出完整的信号集。每个模块都是流水线设计,延迟固定,吞吐量高。

4.6 性能对比与选型建议

指标 资源消耗 延迟(时钟周期) 精度 适用场景
SMA 1 趋势跟踪、布林带中轨
EMA 1 中(定点数误差) MACD、快速信号
MACD 3 趋势反转、背离检测
布林带 N+5 中(近似开方) 波动率分析、突破策略

我的建议:如果资源紧张,优先用SMA代替EMA。虽然EMA反应快,但SMA在FPGA里实现起来更干净。MACD和布林带可以共用SMA/EMA模块,别重复造轮子。

好了,这一章的内容就到这儿。移动平均线、MACD和布林带是交易信号处理的基石。把这些吃透了,后面讲更复杂的策略就轻松多了。


公众号:蓝海资料掘金营,微信deep3321