目 录CONTENT

文章目录

美式期权二叉树算法 — 七语言实现与交叉验证报告

DarkAthena
2026-05-04 / 0 评论 / 0 点赞 / 6 阅读 / 0 字

背景:最近帮忙给客户优化了一个算法,经过了解,是期货行业的一个标准算法,叫“美式期权二叉树算法”,由于是标准算法,因此本文并不提供客户的原始实现和在其基础上的优化版本,而是完全由AI自主进行分析、测试、编写报告、推送到github。

本次使用的工具为运行在macos上的WPS灵犀CLAW,仅通过飞书聊天完成所有任务

相关代码开源地址:
https://gitee.com/darkathena/binomial-option-pricing
https://github.com/Dark-Athena/binomial-option-pricing

下面附上验证报告正文,注意AI这里其实是把GaussDB当成PostgreSQL,把存储过程进行了转换,但实际上ORACLE的此存储过程在GaussDB是完全兼容的,只是性能比不上ORACLE。

美式期权二叉树算法 — 七语言实现与交叉验证报告

测试日期:2026-04-16  |  算法:CRR 二叉树模型  |  语言:Python / C / Java / Go / Rust / Oracle PL/SQL / GaussDB PL/pgSQL

1. 算法概述

1.1 Cox-Ross-Rubinstein (CRR) 二叉树模型

二叉树模型是期权定价的经典数值方法。将期权到期时间 T 分成 N 个等长的时间步,每个节点处标的资产价格向上或向下移动:

参数公式说明
时间步长Δt = T / N
上升因子u = exp(σ√Δt)
下降因子d = 1 / u保证重组树
风险中性概率p = (exp(rΔt) - d) / (u - d)
折现因子disc = exp(-rΔt)每步折现

1.2 美式 vs 欧式期权的区别

欧式期权:只能在到期日行权。倒推时直接折现。

美式期权:可在任意时刻提前行权。倒推时需比较「持有价值(折现)」和「立即行权收益」,取较大值。

💡 **金融理论验证点:**对于不付红利的标的资产,美式看涨期权不会提前行权,因此美式看涨 = 欧式看涨。而美式看跌 > 欧式看跌。

2. 实现语言与开发环境

语言 版本 文件名
🐍 Python 3 option_pricing.py
⚡ C Clang option_pricing.c
☕ Java 17 OptionPricing.java
🐹 Go 1.26 option_pricing.go
🦀 Rust 1.94 option_pricing.rs
🏛️ Oracle PL/SQL - binomial_option_price
🐘 GaussDB PL/pgSQL - binomial_amer_opt_v3

3. Oracle PL/SQL 与 GaussDB PL/pgSQL 移植要点

在将 Oracle PL/SQL 存储过程移植到 GaussDB PL/pgSQL 的过程中,记录了以下关键差异。

3.1 REVERSE FOR 循环语法

GaussDB 支持 REVERSE 关键字,但两个边界参数的顺序与 Oracle 相反

数据库语法说明
OracleFOR i IN REVERSE 0..p_n-1 LOOP小数在前,大数在后
GaussDBFOR i IN REVERSE p_n-1..0 LOOP大数在前,小数在后

两者均产生递减序列,但参数顺序恰好相反。

3.2 变量声明格式

Oracle PL/SQLGaussDB PL/pgSQL
v_dt NUMBER;v_dt NUMERIC;DOUBLE PRECISION
TYPE t_arr IS TABLE OF NUMBER INDEX BY PLS_INTEGER;v_opt DOUBLE PRECISION[];
v_opt t_arr;v_opt := array_fill(0, ARRAY[p_n + 2]);

3.3 数组操作差异

操作OracleGaussDB
声明数组TYPE ... IS TABLE OF ... INDEX BY ...DOUBLE PRECISION[]
初始化自动扩展,无需预设大小array_fill(0, ARRAY[n+1])
访问v_opt(j)v_opt[j] (方括号)

3.4 GREATEST / LEAST 支持

两者均支持 GREATEST(a, b) 函数,行为一致,可直接迁移。

4. 数据库存储过程源码

4.1 Oracle PL/SQL — V1 (原始版, NUMBER)

CREATE OR REPLACE PROCEDURE binomial_option_price (
    p_s0   IN NUMBER,   p_k     IN NUMBER,
    p_t    IN NUMBER,   p_r     IN NUMBER,
    p_sigma IN NUMBER,  p_n     IN NUMBER,
    p_is_call IN NUMBER, p_is_amer IN NUMBER,
    p_result OUT NUMBER
) IS
    v_dt    NUMBER; v_u NUMBER; v_d NUMBER;
    v_p     NUMBER; v_disc NUMBER;
    TYPE t_arr IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
    v_opt   t_arr;
    i       PLS_INTEGER; j PLS_INTEGER;
    v_stk   NUMBER; v_cont NUMBER; v_intr NUMBER;
BEGIN
    v_dt   := p_t / p_n;
    v_u    := EXP(p_sigma * SQRT(v_dt));
    v_d    := 1.0 / v_u;
    v_p    := (EXP(p_r * v_dt) - v_d) / (v_u - v_d);
    v_disc := EXP(-p_r * v_dt);

    FOR j IN 0..p_n LOOP
        v_stk := p_s0 * POWER(v_u, p_n - j) * POWER(v_d, j);
        IF p_is_call = 1 THEN v_opt(j+1) := GREATEST(v_stk - p_k, 0);
        ELSE v_opt(j+1) := GREATEST(p_k - v_stk, 0); END IF;
    END LOOP;

    FOR i IN REVERSE 0..p_n-1 LOOP
        FOR j IN 0..i LOOP
            v_cont := v_disc * (v_p * v_opt(j+2) + (1-v_p) * v_opt(j+3));
            IF p_is_amer = 1 THEN
                v_stk := p_s0 * POWER(v_u, i-j) * POWER(v_d, j);
                IF p_is_call = 1 THEN v_intr := GREATEST(v_stk - p_k, 0);
                ELSE v_intr := GREATEST(p_k - v_stk, 0); END IF;
                v_opt(j+1) := GREATEST(v_intr, v_cont);
            ELSE v_opt(j+1) := v_cont; END IF;
        END LOOP;
    END LOOP;
    p_result := v_opt(1);
END;
/

4.2 Oracle PL/SQL — V2 (优化版, BINARY_DOUBLE + intr 递推)

CREATE OR REPLACE PROCEDURE binomial_option_price_v2 (
    p_s0    IN BINARY_DOUBLE,   p_k      IN BINARY_DOUBLE,
    p_t     IN BINARY_DOUBLE,   p_r      IN BINARY_DOUBLE,
    p_sigma IN BINARY_DOUBLE,   p_n      IN PLS_INTEGER,
    p_is_call IN PLS_INTEGER,   p_is_amer IN PLS_INTEGER,
    p_result OUT BINARY_DOUBLE, p_delta OUT BINARY_DOUBLE,
    p_rc     OUT NUMBER,        p_msg    OUT VARCHAR2
) IS
    v_dt     BINARY_DOUBLE; v_u BINARY_DOUBLE;
    v_d      BINARY_DOUBLE; v_p BINARY_DOUBLE;
    v_disc   BINARY_DOUBLE; v_q BINARY_DOUBLE;
    v_dd     BINARY_DOUBLE; v_sign BINARY_DOUBLE;
    v_signK  BINARY_DOUBLE; v_base BINARY_DOUBLE;
    v_intr   BINARY_DOUBLE; v_intrStep BINARY_DOUBLE;
    v_opt    SYS.ODCINUMBERLIST;
    v_cont   BINARY_DOUBLE;
    i        PLS_INTEGER; j PLS_INTEGER;
BEGIN
    v_dt   := p_t / p_n;
    v_u    := EXP(p_sigma * SQRT(v_dt));
    v_d    := 1.0 / v_u;
    v_p    := (EXP(p_r * v_dt) - v_d) / (v_u - v_d);
    v_disc := EXP(-p_r * v_dt);
    v_q    := 1.0 - v_p;

    v_dd       := v_d * v_d;
    v_sign     := CASE WHEN p_is_call = 1 THEN 1.0 ELSE -1.0 END;
    v_signK    := v_sign * p_k;
    v_intrStep := v_signK * (v_dd - 1.0);

    v_opt := SYS.ODCINUMBERLIST();
    v_opt.EXTEND(p_n + 2);

    v_base := p_s0 * POWER(v_u, p_n);
    FOR j IN 0..p_n LOOP
        v_intr := v_sign * v_base - v_signK;
        v_opt(j+1) := GREATEST(v_intr, 0.0);
        v_base := v_base * v_d;
    END LOOP;

    FOR i IN REVERSE 0..p_n-1 LOOP
        v_base  := v_base * v_d;
        v_intr  := v_sign * v_base - v_signK;
        FOR j IN 0..i LOOP
            v_cont := v_disc * (v_p * v_opt(j+2) + v_q * v_opt(j+3));
            v_opt(j+1) := GREATEST(v_cont, v_intr);
            v_intr     := v_intr * v_dd + v_intrStep;
        END LOOP;
    END LOOP;

    p_result := v_opt(1);
    p_delta  := (v_opt(2) - v_opt(3)) / (p_s0 * (v_u - v_d));
    p_rc := 0; p_msg := 'OK';
EXCEPTION WHEN OTHERS THEN
    p_rc := SQLCODE; p_msg := SQLERRM;
END;
/

4.3 GaussDB PL/pgSQL — V1 (NUMERIC)

CREATE OR REPLACE FUNCTION binomial_amer_opt_v3(
    p_s0 NUMERIC, p_k NUMERIC, p_t NUMERIC,
    p_r NUMERIC, p_sigma NUMERIC,
    p_n INTEGER, p_is_call INTEGER, p_is_amer INTEGER
) RETURNS NUMERIC AS $$
DECLARE
    v_dt NUMERIC; v_u NUMERIC; v_d NUMERIC;
    v_p NUMERIC; v_disc NUMERIC;
    v_opt NUMERIC[];
    i INTEGER; j INTEGER;
    v_stk NUMERIC; v_cont NUMERIC; v_intr NUMERIC;
BEGIN
    v_dt   := p_t / p_n;
    v_u    := EXP(p_sigma * SQRT(v_dt));
    v_d    := 1.0 / v_u;
    v_p    := (EXP(p_r * v_dt) - v_d) / (v_u - v_d);
    v_disc := EXP(-p_r * v_dt);
    v_opt  := array_fill(0, ARRAY[p_n + 2]);

    FOR j IN 0..p_n LOOP
        v_stk := p_s0 * POWER(v_u, p_n - j) * POWER(v_d, j);
        IF p_is_call = 1 THEN v_opt[j+1] := GREATEST(v_stk - p_k, 0);
        ELSE v_opt[j+1] := GREATEST(p_k - v_stk, 0); END IF;
    END LOOP;

    FOR i IN REVERSE p_n-1..0 LOOP
        FOR j IN 0..i LOOP
            v_cont := v_disc * (v_p * v_opt[j+1] + (1 - v_p) * v_opt[j+2]);
            IF p_is_amer = 1 THEN
                v_stk := p_s0 * POWER(v_u, i - j) * POWER(v_d, j);
                IF p_is_call = 1 THEN v_intr := GREATEST(v_stk - p_k, 0);
                ELSE v_intr := GREATEST(p_k - v_stk, 0); END IF;
                v_opt[j+1] := GREATEST(v_intr, v_cont);
            ELSE v_opt[j+1] := v_cont; END IF;
        END LOOP;
    END LOOP;
    RETURN v_opt[1];
END;
$$ LANGUAGE plpgsql;

4.4 GaussDB PL/pgSQL — V2 (float8 + intr 递推优化)

CREATE OR REPLACE FUNCTION binomial_amer_opt_v3_opt(
    p_s0 float8, p_k float8, p_t float8,
    p_r float8, p_sigma float8,
    p_n integer, p_is_call integer, p_is_amer integer
) RETURNS float8 AS $$
DECLARE
    v_u float8; v_d float8; v_p float8;
    v_disc float8; v_q float8; v_dd float8;
    v_sign float8; v_signK float8;
    v_base float8; v_intr float8; v_intrStep float8;
    v_opt float8[];
    i integer; j integer; v_cont float8;
BEGIN
    v_u    := EXP(p_sigma * SQRT(p_t / p_n));
    v_d    := 1.0 / v_u;
    v_p    := (EXP(p_r * (p_t / p_n)) - v_d) / (v_u - v_d);
    v_disc := EXP(-p_r * (p_t / p_n));
    v_q    := 1.0 - v_p;
    v_dd   := v_d * v_d;
    v_sign    := CASE WHEN p_is_call = 1 THEN 1.0 ELSE -1.0 END;
    v_signK   := v_sign * p_k;
    v_intrStep := v_signK * (v_dd - 1.0);
    v_opt  := array_fill(0.0, ARRAY[p_n + 2]);

    v_base := p_s0 * POWER(v_u, p_n);
    FOR j IN 0..p_n LOOP
        v_intr := v_sign * v_base - v_signK;
        v_opt[j+1] := GREATEST(v_intr, 0.0);
        v_base := v_base * v_d;
    END LOOP;

    FOR i IN REVERSE p_n-1..0 LOOP
        v_base := v_base * v_d;
        v_intr := v_sign * v_base - v_signK;
        FOR j IN 0..i LOOP
            v_cont     := v_disc * (v_p * v_opt[j+1] + v_q * v_opt[j+2]);
            v_opt[j+1] := GREATEST(v_cont, v_intr);
            v_intr     := v_intr * v_dd + v_intrStep;
        END LOOP;
    END LOOP;
    RETURN v_opt[1];
END;
$$ LANGUAGE plpgsql;

5. 准确性验证

5.1 测试参数

场景S₀KTrσN
ATM, 1年期1001001.00.050.21000
实值看涨 (ITM)100901.00.050.21000
虚值看涨 (OTM)1001101.00.050.21000
高波动率, 半年1001000.50.050.31000
低波动率1001001.00.020.11000
高利率高波动1001001.00.100.42000
短期高波动50550.250.050.31000

5.2 跨语言跨数据库价格一致性

所有 7 种实现(5 语言 + 2 数据库 V2 优化版)在所有测试场景下输出价格完全一致(差异 < 10-10),验证了算法正确性。

场景CGoJavaPythonRustOracle V2GaussDB V2
AC ATM 1Y10.43061310.43061310.43061310.43061310.43061310.43061310.430613
AP ATM 1Y5.9371515.9371515.9371515.9371515.9371515.9371515.937151
EC ATM 1Y8.0214258.0214258.0214258.0214258.0214258.0214258.021425
EP ATM 1Y4.6449754.6449754.6449754.6449754.6449754.6449754.644975

5.3 V1 vs V2 一致性

Oracle 和 GaussDB 的 V1(原始版)与 V2(优化版)输出价格完全一致。V2 的 intr 递推仅改变了股价的计算方式(代数等价),不影响最终结果。

6. 性能基准测试

6.1 七语言纯计算性能(N=1000,单位 ms)

  • 预热 2 轮 + 正式测量 5 轮,取中位数
  • 5 种开发语言为纯本地计算(客户端 Apple M1)
  • 测试参数:S=100, K=100, T=1, r=0.05, σ=0.2
语言美式看涨美式看跌欧式看涨欧式看跌备注
Rust0.480.490.190.19纯计算,最快
C0.620.630.180.18纯计算
Go0.820.830.280.28纯计算
Java3.43.41.31.3纯计算,含 JIT 预热
Python32.132.213.013.1CPython 解释执行

C/Rust/Go 处于同一量级(亚毫秒),Java 约慢 3-5 倍(GC + JIT 开销),Python 慢约 50-100 倍。欧式期权因跳过提前行权计算,比美式快 2-3 倍。

6.2 Oracle V1 vs V2 优化对比(NUMBER vs BINARY_DOUBLE + intr 递推)

测试在数据库服务器上执行,参数:S=100, K=100, T=1, r=0.05, σ=0.2, 每组 3 次取平均。

NV1 NUMBER(ms/run)V2 BINARY_DOUBLE(ms/run)加速比V1 价格V2 价格
10038142.7x10.43061310.430613
5008207510.9x10.44392110.443921
10003,24417618.4x10.44392210.443922
200013,07871718.2x10.44392210.443922
500082,6914,52618.3x10.44392210.443922

💡 **核心结论:**Oracle 上 V2 (BINARY_DOUBLE + intr 递推) 相比 V1 (NUMBER) 最大加速 18.4 倍(N=1000)。类型替换(NUMBER → BINARY_DOUBLE)贡献约 3-5 倍,intr 递推贡献剩余倍数。加速比在 N≥1000 后趋于稳定。

6.3 GaussDB V1 vs V2 优化对比(NUMERIC vs float8 + intr 递推)

同样的 intr 递推优化,在 GaussDB 上呈现不同的性能特征。注意:早期测试中因 GaussDB 函数重载机制,JDBC setDouble 优先匹配了 float8 参数重载,导致 V1 实际走了 float8 路径。本次数据已删除旧重载,确认 V1 走真正的全 NUMERIC 路径。

场景NV1 全 NUMERIC(ms/run)V2 float8 优化(ms/run)加速比V1 价格V2 价格
AC1001,0929201.2x10.43061310.430613
AC2001,5151,1381.3x10.44059110.440591
AC3002,7061,1792.3x10.44392110.443921
AP1001,1039071.2x5.9371515.937151
AP3002,7121,1822.3x5.9515645.951564
EC3001,8427612.4x8.0214258.021425
EP3001,8417602.4x4.6449754.644975

**NUMERIC 溢出限制:**V1 (全 NUMERIC) 在 N ≥ 500 时报错 value overflows numeric format,无法运行。V2 (float8) 不受此限制,可支持 N=5000+。

与 Oracle 的差异分析:

  • Oracle 上类型替换(NUMBER → BINARY_DOUBLE)贡献 3-5 倍加速,是主要优化来源
  • GaussDB 上类型替换(NUMERIC → float8)仅贡献 1.2-2.3 倍,intr 递推的算法优化才是主因
  • 原因是 Oracle 的 NUMBER 在软件层面模拟十进制,开销远大于 GaussDB 的 NUMERIC(后者内部仍委托 float64 计算)

6.4 优化策略总结

优化项原理POWER() 调用减少Oracle 贡献GaussDB 贡献
intr 递推利用重合树等比结构,将 S(i,j) = S(i,0)×dj 转为 O(1) 递推 intr(j+1) = intr(j) × d² + signK × (d² - 1)99%+(N=1000: 100万→1000)~3-5x主因
SIGN 统一sign = ±1 统一看涨/看跌为 GREATEST(sign×S - sign×K, 0),消除分支辅助辅助
float8 类型硬件浮点替代软件十进制主因 (3-5x)~1.2-2.3x
常量外提1-p, d², intrStep 预计算辅助辅助

6.5 两侧环境性能差异说明

开发语言(C/Go/Java/Python/Rust)运行在客户端 Apple M1 上,数据库存储过程运行在服务器 Intel CC150 上。两者 CPU 架构不同(ARM vs x86_64),因此跨平台绝对耗时不可直接对比,但以下结论成立:

  • 同平台内的相对性能排名可靠
  • 数据库 V1 vs V2 的对比在同一服务器上完成,加速比数据可靠
  • 客户端纯计算 (C ~0.6ms) vs 服务器存储过程 (Oracle V2 ~176ms) 的差距主要来自:JDBC 网络往返 ~50ms + 存储过程调用开销 + 不同的 CPU 架构

7. 交付文件

文件语言说明
option_pricing.pyPython完整实现 + 收敛性测试 + argparse CLI
option_pricing.cC使用动态内存分配的一维数组实现
OptionPricing.javaJava静态方法,支持命令行参数
option_pricing.goGo标准库实现,支持命令行参数
option_pricing.rsRustVec 实现,O2 优化编译
DBTestAll.javaJava双数据库测试(准确性 + 性能)
PerfBench.javaJava性能基准(多步数梯度)
lib/ojdbc11.jar-Oracle JDBC 驱动
lib/postgresql.jar-PostgreSQL/GaussDB JDBC 驱动

8. 测试环境

数据库服务器(Oracle + GaussDB 共用)

OSCentOS / Linux (x86_64, Hyper-V)
CPUIntel CC150 @ 3.50GHz, 8C/16T
CacheL1 32K / L2 256K / L3 16384K
Memory10 GB DDR4
Oracle19c
GaussDBKernel 506.0.0.SPC0500

客户端(开发语言测试环境)

OSmacOS 26.4 (Apple Silicon, ARM64)
CPUApple M1, 8 Core
Memory16 GB
C/C++Apple Clang 21.0.0
Go1.26.2 darwin/arm64
JavaOpenJDK 17.0.18
Python3.12.13
Rust远程编译测试
0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin
博主关闭了所有页面的评论