From 985162242d6e65fd96243919779d264b6bea3b59 Mon Sep 17 00:00:00 2001 From: hjhan Date: Tue, 21 Jul 2026 15:28:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(greeks):=20=E6=96=B0=E5=A2=9E=20risk-facto?= =?UTF-8?q?r=20bump=20=E8=AE=A1=E7=AE=97=E4=B8=8E=E6=B3=A8=E5=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ParameterBase.Clone() 保留运行时类型深拷贝; ValueCalculator 两个薄接入方法; GreeksBumpCalculator/GreeksRiskFactor 引擎。加法性重定价桥,不动现有定价输出。 --- .../SwapModule/GreeksBumpCalculatorTests.cs | 114 +++++++++++++++ .../SwapModule/GreeksRiskFactorTests.cs | 127 +++++++++++++++++ .../Calculation/V2/GreeksBumpCalculator.cs | 133 ++++++++++++++++++ .../BLL/Calculation/V2/GreeksRiskFactor.cs | 112 +++++++++++++++ .../Calculation/V2/Parameter/ParameterBase.cs | 17 +++ .../BLL/Calculation/V2/ValueCalculator.cs | 32 +++++ 6 files changed, 535 insertions(+) create mode 100644 UnitTestProject/Modules/SwapModule/GreeksBumpCalculatorTests.cs create mode 100644 UnitTestProject/Modules/SwapModule/GreeksRiskFactorTests.cs create mode 100644 YLErpDAL/BLL/Calculation/V2/GreeksBumpCalculator.cs create mode 100644 YLErpDAL/BLL/Calculation/V2/GreeksRiskFactor.cs diff --git a/UnitTestProject/Modules/SwapModule/GreeksBumpCalculatorTests.cs b/UnitTestProject/Modules/SwapModule/GreeksBumpCalculatorTests.cs new file mode 100644 index 00000000..ee3e91d6 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/GreeksBumpCalculatorTests.cs @@ -0,0 +1,114 @@ +using YLErp.BLL.Calculation.V2; + +namespace UnitTestProject.Modules.SwapModule +{ + /// + /// GreeksBumpCalculator 纯数学契约测试(先写,锁定 ε / 差分逻辑)。 + /// 不碰 DB / QDP:用已知解析导数的函数(f(x)=x²,d/dx=2x,d²/dx²=2)验证中心差分本身正确。 + /// 业务侧集成测试(真实定价路径 + 真实 FR007)见后续 ValueCalculator 薄接入落地后再补。 + /// + [TestClass] + public class GreeksBumpCalculatorTests + { + /// + /// 一阶中心差分对 f(x)=x² 在 x=3 应精确等于 2x=6。 + /// 二次多项式的中心差分对任意 ε 都精确(截断误差为 0),因此用相对步长 0.05% 仍得 6。 + /// + [TestMethod] + public void CentralDelta_of_x2_equals_2x() + { + var calc = new GreeksBumpCalculator(); + Func f = x => x * x; // d/dx = 2x + var delta = calc.Delta(f, 3m, BumpSpec.Relative(0.0005m)); + Assert.AreEqual(6m, delta); + } + + /// + /// 二阶中心差分对 f(x)=x² 在 x=3 应精确等于 2(二阶导数恒为 2)。 + /// 验证 Gamma 的二阶差分算子正确。 + /// + [TestMethod] + public void CentralGamma_of_x2_equals_2() + { + var calc = new GreeksBumpCalculator(); + Func f = x => x * x; // d²/dx² = 2 + var gamma = calc.Gamma(f, 3m, BumpSpec.Relative(0.0005m)); + Assert.AreEqual(2m, gamma); + } + + /// + /// 1BP 变体(Dollar Greek)对 f(x)=x² 在 x=3、bump 1bp 应等于 (3.0001)² - 3²。 + /// 验证 BumpPv1Bp 直接前向 bump 的 PV 差逻辑。 + /// + [TestMethod] + public void BumpPv1Bp_of_x2_forward() + { + var calc = new GreeksBumpCalculator(); + Func f = x => x * x; + var bumped = calc.BumpPv1Bp(f, 3m); + Assert.AreEqual(3.0001m * 3.0001m - 3m * 3m, bumped); + } + + /// + /// 线性函数 f(x)=2x+1:一阶中心差分应精确等于斜率 2,二阶中心差分应精确为 0(线性无曲率)。 + /// 验证算子对"一次/零次"函数的精确性(二次之外另一种精确情形)。 + /// + [TestMethod] + public void CentralDelta_of_linear_is_slope() + { + var calc = new GreeksBumpCalculator(); + Func f = x => 2m * x + 1m; // d/dx = 2, d²/dx² = 0 + Assert.AreEqual(2m, calc.Delta(f, 5m, BumpSpec.Relative(0.0005m))); + Assert.AreEqual(0m, calc.Gamma(f, 5m, BumpSpec.Relative(0.0005m))); + } + + /// + /// 三次函数 f(x)=x³:中心差分对任意 ε 不精确(仅二次及以下精确),结果逼近解析导 3x² 但有 O(ε²) 误差。 + /// 验证 Layer B 集成测试必须带容差,不能 Assert.AreEqual 死等精确值。x=2 解析导=12。 + /// + [TestMethod] + public void CentralDelta_of_cubic_is_approx_analytic_with_tolerance() + { + var calc = new GreeksBumpCalculator(); + Func f = x => x * x * x; // d/dx = 3x² = 12 at x=2 + var delta = calc.Delta(f, 2m, BumpSpec.Relative(0.0005m)); + Assert.IsTrue(Math.Abs(delta - 12m) < 0.001m, $"中心差分三次函数应有 O(ε²) 误差,实际={delta}"); + } + + /// + /// 扭结点测试:看涨 payoff f(x)=max(x-3,0) 在行权价 x=3 处不可导。 + /// 中心差分跨扭结取到左右斜率的平均 (0+1)/2 = 0.5,说明对障碍/美式等扭结结构必须用单边差分。 + /// + [TestMethod] + public void CentralDelta_at_kink_is_average_of_one_sided() + { + var calc = new GreeksBumpCalculator(); + Func f = x => x > 3m ? x - 3m : 0m; // call payoff K=3 + var delta = calc.Delta(f, 3m, BumpSpec.Relative(0.0005m)); + Assert.AreEqual(0.5m, delta); // 中心差分给出左右斜率平均,非真实单边 Greek + } + + /// + /// 边界 x=0(相对步长会触到 Floor):f(x)=x² 在 0 处对称,Δ 应精确为 0,证明 Floor 兜底不产生噪声。 + /// + [TestMethod] + public void CentralDelta_at_zero_uses_floor_but_stays_correct() + { + var calc = new GreeksBumpCalculator(); + Func f = x => x * x; + Assert.AreEqual(0m, calc.Delta(f, 0m, BumpSpec.Relative(0.0005m))); + } + + /// + /// 利率量级小值 x=0.0001(1bp 量级)用相对步长:Resolve 返回 max(0.0001·0.0005, 1e-8)=5e-8, + /// 远大于 Floor,证明利率类小量级不会被舍入噪声吞掉。f=x² 在 0.0001 解析导=2·0.0001=0.0002。 + /// + [TestMethod] + public void CentralDelta_of_tiny_rate_like_x_is_stable() + { + var calc = new GreeksBumpCalculator(); + Func f = x => x * x; + Assert.AreEqual(0.0002m, calc.Delta(f, 0.0001m, BumpSpec.Relative(0.0005m))); + } + } +} diff --git a/UnitTestProject/Modules/SwapModule/GreeksRiskFactorTests.cs b/UnitTestProject/Modules/SwapModule/GreeksRiskFactorTests.cs new file mode 100644 index 00000000..7640d37a --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/GreeksRiskFactorTests.cs @@ -0,0 +1,127 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using YLErp.BLL.Calculation.V2; +using YLErp.BLL.Calculation.V2.Parameter; + +namespace UnitTestProject.Modules.SwapModule +{ + /// + /// RiskFactor(② 风险因子抽象)+ ParameterBase.Clone 的纯单测(Layer A,无 DB / QDP)。 + /// 用假 reprice 委托验证:克隆类型保持、字典深拷、三类因子落点正确、波动率因子施于非期权参数抛异常, + /// 以及经 BuildPvFunction 喂入 GreeksBumpCalculator 后 DeltaR / Delta / Vega / BumpPv1Bp 数值正确(线性函数精确)。 + /// + [TestClass] + public class GreeksRiskFactorTests + { + // —— Clone 行为与类型保持 —— + + [TestMethod] + public void Clone_PreservesRuntimeType_And_CopiesOptionFields() + { + var src = new VanillaOptionParameter + { + Volatility = 0.2, + RiskFreeRate = 0.03, + SpotPrices = new Dictionary { { "X", 100 } } + }; + ParameterBase clone = src.Clone(); + // MemberwiseClone 必须保留运行时类型,否则 ValueCalculator 内 parameter as VanillaOptionParameter 会 cast 成 null + Assert.IsInstanceOfType(clone, typeof(VanillaOptionParameter)); + Assert.AreEqual(0.2, ((BaseOptionParameter)clone).Volatility); + Assert.AreEqual(100, clone.SpotPrices["X"]); + } + + [TestMethod] + public void Clone_DeepCopiesSpotPrices_So_Bump_Does_Not_Pollute_Original() + { + var src = new ParameterBase + { + SpotPrices = new Dictionary { { "X", 100 } } + }; + ParameterBase clone = src.Clone(); + clone.SpotPrices["X"] = 999; // 改克隆体 + Assert.AreEqual(100, src.SpotPrices["X"], "原参数的 SpotPrices 不应被克隆体的 bump 污染"); + } + + // —— 三类因子 ApplyTo 落点正确 —— + + [TestMethod] + public void RateFactor_ApplyTo_Sets_RiskFreeRate() + { + var p = new ParameterBase(); + RiskFactor.Rate("CNY-OIS-2Y").ApplyTo(p, 0.025m); + Assert.AreEqual(0.025, p.RiskFreeRate); + } + + [TestMethod] + public void PriceFactor_ApplyTo_Sets_SpotPrices_By_TargetKey() + { + var p = new ParameterBase(); + RiskFactor.Price("000300.SH").ApplyTo(p, 3500m); + Assert.AreEqual(3500, p.SpotPrices["000300.SH"]); + } + + [TestMethod] + public void VolFactor_ApplyTo_On_OptionParameter_Sets_Volatility() + { + var p = new BaseOptionParameter(); + RiskFactor.Volatility("X").ApplyTo(p, 0.18m); + Assert.AreEqual(0.18, p.Volatility); + } + + [TestMethod] + [ExpectedException(typeof(System.InvalidOperationException))] + public void VolFactor_ApplyTo_On_PlainParameter_Throws() + { + // 波动率不在 ParameterBase 基类上,只能施于期权参数 + RiskFactor.Volatility("X").ApplyTo(new ParameterBase(), 0.1m); + } + + // —— 端到端:假 reprice 验证 中心差分 / 1bp 数值正确(线性函数精确) —— + + [TestMethod] + public void RateFactor_Through_Engine_DeltaR_Equals_Slope() + { + var baseParam = new ParameterBase { RiskFreeRate = 0.03 }; + Func reprice = p => (decimal)((p.RiskFreeRate ?? 0) * 1000); // PV = 1000 * r + var factor = RiskFactor.Rate("r"); // 标准步长:绝对 1bp + Func pv = factor.BuildPvFunction(reprice, baseParam); + + var calc = new GreeksBumpCalculator(); + decimal deltaR = calc.DeltaR(pv, 0.03m, factor.Shift); // 中心差分对线性函数精确 + Assert.AreEqual(1000m, deltaR, 1e-4m); + + decimal bump1bp = calc.BumpPv1Bp(pv, 0.03m); // 前向 1bp PV 差 + Assert.AreEqual(1000m * 0.0001m, bump1bp, 1e-9m); + } + + [TestMethod] + public void PriceFactor_Through_Engine_Delta_Equals_One() + { + var baseParam = new ParameterBase + { + SpotPrices = new Dictionary { { "X", 100 } } + }; + Func reprice = p => (decimal)p.SpotPrices["X"]; // PV = S + var factor = RiskFactor.Price("X"); // 标准步长:相对 1% → ε = 1 + Func pv = factor.BuildPvFunction(reprice, baseParam); + + var calc = new GreeksBumpCalculator(); + decimal delta = calc.Delta(pv, 100m, factor.Shift); // (101 - 99) / 2 = 1 精确 + Assert.AreEqual(1m, delta, 1e-6m); + } + + [TestMethod] + public void VolFactor_Through_Engine_Vega_Equals_Slope() + { + var baseParam = new BaseOptionParameter { Volatility = 0.2 }; + Func reprice = p => (decimal)(((BaseOptionParameter)p).Volatility ?? 0) * 50; // PV = 50 * σ + var factor = RiskFactor.Volatility("X"); // 标准步长:绝对 1bp vol + Func pv = factor.BuildPvFunction(reprice, baseParam); + + var calc = new GreeksBumpCalculator(); + decimal vega = calc.Vega(pv, 0.2m, factor.Shift); // 中心差分对线性函数精确 + Assert.AreEqual(50m, vega, 1e-4m); + } + } +} diff --git a/YLErpDAL/BLL/Calculation/V2/GreeksBumpCalculator.cs b/YLErpDAL/BLL/Calculation/V2/GreeksBumpCalculator.cs new file mode 100644 index 00000000..66565d3b --- /dev/null +++ b/YLErpDAL/BLL/Calculation/V2/GreeksBumpCalculator.cs @@ -0,0 +1,133 @@ +using System; + +namespace YLErp.BLL.Calculation.V2 +{ + /// + /// 风险因子种类,用于后续 ValueCalculator 薄接入层签名(CalcPvAtBumpedRiskFactor)。 + /// 当前 GreeksBumpCalculator 纯类本身不依赖它,仅作为对外契约的一部分。 + /// + public enum RiskFactorKind + { + /// 标的价 S + Price, + /// 无风险利率 r + Rate, + /// 波动率 σ + Volatility + } + + /// + /// 扰动步长规格:决定有限差分用的 ε。 + /// + /// Relative:ε = max(|x| * Value, Floor),适合指数等大量级标的,避免绝对 1bp 落入浮点舍入区。 + /// Absolute:ε = Value,等价系统现行“标的价格偏移绝对 1bp”的做法。 + /// + /// 默认 Floor 极小,仅防止 x=0 时 ε=0 导致除零。 + /// + public readonly struct BumpSpec + { + /// 步长模式 + public enum Mode + { + /// 相对步长(推荐,量级自适应) + Relative, + /// 绝对步长 + Absolute + } + + /// 步长模式 + public Mode Kind { get; } + + /// 步长数值(Relative 时为相对比例,Absolute 时为绝对量) + public decimal Value { get; } + + /// 相对步长下限(防止 |x| 过小导致 ε→0) + public decimal Floor { get; } + + /// 构造步长规格 + public BumpSpec(Mode kind, decimal value, decimal floor = 0.00000001m) + { + Kind = kind; + Value = value; + Floor = floor; + } + + /// 相对步长(value 为相对比例,如 0.0005m = 0.05%) + public static BumpSpec Relative(decimal value) => new(Mode.Relative, value); + + /// 绝对步长(value 为绝对量,如 0.0001m = 1bp) + public static BumpSpec Absolute(decimal value) => new(Mode.Absolute, value); + + /// 把规格解析成实际 ε(用 decimal,避免 double 精度漂移) + public decimal Resolve(decimal x) + { + if (Kind == Mode.Absolute) return Value; + var eps = Math.Abs(x) * Value; + return eps < Floor ? Floor : eps; + } + } + + /// + /// 有限差分希腊字母计算引擎(纯函数,不依赖 QDP / DB)。 + /// + /// 通过委托 pv(bumpedFactor) 取得“风险因子被扰动到某值时的衍生品价值”, + /// 再用中心差分估算一阶 / 二阶导。这是仓库层对 Greeks 口径的显式控制点, + /// 可绕开 QDP 内部黑盒的步长 / 差分选择,并解决指数类标的使用绝对 1bp 步长失真的口径问题。 + /// + /// + /// 用法:业务侧把“价格 / 利率 / 波动率”各自封装成一个 Func<decimal,decimal> 委托传给本类; + /// Delta/Gamma/Vega/Rho 等命名方法数学上都是同一差分算子,仅扰动的风险因子不同。 + /// + /// + public sealed class GreeksBumpCalculator + { + /// 一阶中心差分 Δ = [PV(x+ε) - PV(x-ε)] / (2ε),误差 O(ε²) + public decimal FirstOrderCentral(Func pv, decimal x, BumpSpec bump) + { + ArgumentNullException.ThrowIfNull(pv); + var eps = bump.Resolve(x); + return (pv(x + eps) - pv(x - eps)) / (2m * eps); + } + + /// 二阶中心差分 Γ = [PV(x+ε) - 2·PV(x) + PV(x-ε)] / ε²,误差 O(ε²) + public decimal SecondOrderCentral(Func pv, decimal x, BumpSpec bump) + { + ArgumentNullException.ThrowIfNull(pv); + var eps = bump.Resolve(x); + return (pv(x + eps) - 2m * pv(x) + pv(x - eps)) / (eps * eps); + } + + // —— 以下为按业务希腊字母命名的暴露,数学上都是上面两个差分算子,仅扰动因子不同 —— + + /// Delta:对标的价 S 的一阶中心差分 + public decimal Delta(Func pv, decimal s, BumpSpec bump) => FirstOrderCentral(pv, s, bump); + + /// Gamma:对标的价 S 的二阶中心差分 + public decimal Gamma(Func pv, decimal s, BumpSpec bump) => SecondOrderCentral(pv, s, bump); + + /// Vega:对波动率 σ 的一阶中心差分 + public decimal Vega(Func pv, decimal sigma, BumpSpec bump) => FirstOrderCentral(pv, sigma, bump); + + /// Vega_r:对波动率 σ 的一阶中心差分(需求命名变体,等价于 Vega) + public decimal VegaR(Func pv, decimal sigma, BumpSpec bump) => FirstOrderCentral(pv, sigma, bump); + + /// Rho:对无风险利率 r 的一阶中心差分 + public decimal Rho(Func pv, decimal r, BumpSpec bump) => FirstOrderCentral(pv, r, bump); + + /// Delta_r:对无风险利率 r 的一阶中心差分(需求命名变体,等价于 Rho) + public decimal DeltaR(Func pv, decimal r, BumpSpec bump) => FirstOrderCentral(pv, r, bump); + + /// Gamma_r:对无风险利率 r 的二阶中心差分 + public decimal GammaR(Func pv, decimal r, BumpSpec bump) => SecondOrderCentral(pv, r, bump); + + /// + /// 1BP 变体(Dollar Greek):直接前向 bump 1bp 的 PV 差 = PV(x+1bp) - PV(x),不除 ε,量纲为金额。 + /// 对应需求中的 Delta_r(1BP) / Gamma_r(1BP) / Vega_r(1BP)。 + /// + public decimal BumpPv1Bp(Func pv, decimal x, decimal oneBp = 0.0001m) + { + ArgumentNullException.ThrowIfNull(pv); + return pv(x + oneBp) - pv(x); + } + } +} diff --git a/YLErpDAL/BLL/Calculation/V2/GreeksRiskFactor.cs b/YLErpDAL/BLL/Calculation/V2/GreeksRiskFactor.cs new file mode 100644 index 00000000..8a8e03db --- /dev/null +++ b/YLErpDAL/BLL/Calculation/V2/GreeksRiskFactor.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using YLErp.BLL.Calculation.V2.Parameter; + +namespace YLErp.BLL.Calculation.V2 +{ + /// + /// 命名风险因子——业界"按因子建模、而非按资产类别建模"的核心抽象。 + /// + /// 任意衍生品都可视为 PV = Pricer(MarketData)。希腊字母 = 把某个命名风险因子 + /// 在 MarketData 上偏移一个标准步长、重定价、再差分。股票/债券指数/债券收益率期权 + /// 只是依赖不同的因子(spot / 收益率曲线 / 波动率面),天然被同一套引擎覆盖, + /// 无需为每种资产类别各写一套 Greek 代码。这是 OpenGamma/Strata、QuantLib、FRTB SBA 的同源做法。 + /// + /// + /// 每个因子自带:Id、类别()、标准扰动步长()、 + /// 作用目标键(TargetKey)。因子与 ParameterBase 的映射由 完成; + /// 与定价引擎的桥接由 ValueCalculator 薄接入层(CalcPvAtBumpedRiskFactor / PvAsFunctionOf)提供。 + /// + /// + /// 工厂方法 / / 即"步长注册表": + /// 按因子类别给定 FRTB SBA 标准偏移,统一全系统的 ε 与差分口径,解决需求 #2 中 + /// "绝对 1bp vs 相对 0.05%"的口径分歧(股指用绝对 1bp 会因量级大落入浮点舍入区,须相对步长)。 + /// + /// + public sealed class RiskFactor + { + /// 因子唯一标识 + public string Id { get; } + + /// 因子类别(决定施加到 ParameterBase 的哪个字段、用哪种标准步长) + public RiskFactorKind Category { get; } + + /// 标准扰动步长(FRTB SBA 口径,由工厂方法按类别给定) + public BumpSpec Shift { get; } + + /// + /// 作用目标键:Price / Volatility 为 UnderlyingCode;Rate 为曲线/期限标识(如 "CNY-OIS-2Y")。 + /// + public string TargetKey { get; } + + public RiskFactor(string id, RiskFactorKind category, BumpSpec shift, string targetKey) + { + Id = id ?? throw new ArgumentNullException(nameof(id)); + Category = category; + Shift = shift; + TargetKey = targetKey ?? string.Empty; + } + + // —— 步长注册表:按因子类别给定 FRTB SBA 标准偏移 —— + // 股票 / 指数 spot:相对 1%(指数量级大,绝对 1bp 会落入浮点舍入区,必须相对) + // 利率 / FX:绝对 1bp + // 波动率面:绝对 1bp vol + + /// 标的价/指数 spot 因子,默认相对 1% 步长(指数类用此可避免绝对 1bp 失真) + public static RiskFactor Price(string underlyingCode, decimal relativeStep = 0.01m) + => new($"{underlyingCode}-SPOT", RiskFactorKind.Price, BumpSpec.Relative(relativeStep), underlyingCode); + + /// 利率/贴现曲线因子,默认绝对 1bp 步长 + public static RiskFactor Rate(string curveTenorId, decimal oneBp = 0.0001m) + => new($"{curveTenorId}-RATE", RiskFactorKind.Rate, BumpSpec.Absolute(oneBp), curveTenorId); + + /// 波动率面因子,默认绝对 1bp vol 步长 + public static RiskFactor Volatility(string underlyingCode, decimal oneBp = 0.0001m) + => new($"{underlyingCode}-VOL", RiskFactorKind.Volatility, BumpSpec.Absolute(oneBp), underlyingCode); + + /// + /// 把因子扰动到 ,施加到参数副本上(不修改入参)。 + /// Rate → RiskFreeRate;Price → SpotPrices[TargetKey];Volatility → cast 期权参数设 Volatility。 + /// + public void ApplyTo(ParameterBase p, decimal value) + { + if (p == null) throw new ArgumentNullException(nameof(p)); + switch (Category) + { + case RiskFactorKind.Rate: + p.RiskFreeRate = (double)value; + break; + case RiskFactorKind.Price: + if (p.SpotPrices == null) p.SpotPrices = new Dictionary(); + p.SpotPrices[TargetKey] = (double)value; + break; + case RiskFactorKind.Volatility: + if (p is BaseOptionParameter bo) bo.Volatility = (double)value; + else throw new InvalidOperationException( + $"波动率风险因子[{Id}]只能施加于期权类参数(BaseOptionParameter),当前为 {p.GetType().Name}"); + break; + default: + throw new ArgumentOutOfRangeException(nameof(Category), Category, "未支持的风险因子类别"); + } + } + + /// + /// 构造"PV 作为本因子值的函数":克隆 baseParam → 施加扰动值 → 调 reprice 得 PV。 + /// + /// reprice 生产环境即 ValueCalculator 的真实定价(见 PvAsFunctionOf);测试可注入假函数,无需 DB / QDP。 + /// 返回的委托直接喂给 GreeksBumpCalculator 的中心差分方法。 + /// + /// + public Func BuildPvFunction(Func reprice, ParameterBase baseParam) + { + if (reprice == null) throw new ArgumentNullException(nameof(reprice)); + if (baseParam == null) throw new ArgumentNullException(nameof(baseParam)); + return bumpedValue => + { + var p = baseParam.Clone(); + ApplyTo(p, bumpedValue); + return reprice(p); + }; + } + } +} diff --git a/YLErpDAL/BLL/Calculation/V2/Parameter/ParameterBase.cs b/YLErpDAL/BLL/Calculation/V2/Parameter/ParameterBase.cs index cc312d88..73ecb89a 100644 --- a/YLErpDAL/BLL/Calculation/V2/Parameter/ParameterBase.cs +++ b/YLErpDAL/BLL/Calculation/V2/Parameter/ParameterBase.cs @@ -15,5 +15,22 @@ namespace YLErp.BLL.Calculation.V2.Parameter public bool HasNightMarket { get; set; } public bool PreciseTimeMode { get; set; } public int maturityShift { get; set; } + + /// + /// 深拷贝(保留运行时类型)。 + /// + /// 用 MemberwiseClone 保证克隆对象与 this 运行时类型一致—— + /// 例如 VanillaOptionParameter 克隆后仍是 VanillaOptionParameter, + /// 否则 ValueCalculator 内 parameter as VanillaOptionParameter 会因类型退化为基类而得到 null。 + /// 引用型字段 SpotPrices/Dividends 单独深拷,避免对克隆体 bump 时污染原参数。 + /// + /// + public virtual ParameterBase Clone() + { + var clone = (ParameterBase)MemberwiseClone(); + clone.SpotPrices = SpotPrices == null ? null : new Dictionary(SpotPrices); + clone.Dividends = Dividends == null ? null : new Dictionary(Dividends); + return clone; + } } } diff --git a/YLErpDAL/BLL/Calculation/V2/ValueCalculator.cs b/YLErpDAL/BLL/Calculation/V2/ValueCalculator.cs index b50ffa69..21aceac3 100644 --- a/YLErpDAL/BLL/Calculation/V2/ValueCalculator.cs +++ b/YLErpDAL/BLL/Calculation/V2/ValueCalculator.cs @@ -2300,6 +2300,38 @@ namespace YLErp.BLL.Calculation.V2 Console.WriteLine(ex.Message); } } + + #region Greeks 薄接入层(③ 重定价桥,加法性,不改现有定价输出) + + /// + /// 把某风险因子扰动到指定值后重定价,返回 PV(decimal)。 + /// 克隆参数后施加扰动,不污染入参;现有 CalculateTradeValue 输出一行不变。 + /// + /// + /// 这是把 GreeksBumpCalculator(有限差分引擎)接上真实定价路径的唯一接入点: + /// 业务侧拿到返回的 PV 后,配合 自带的标准步长, + /// 用 GreeksBumpCalculator.DeltaR/Delta/GammaR/VegaR 等即可算统一的希腊字母。 + /// + public static decimal CalcPvAtBumpedRiskFactor( + string userId, trade t, underlying_manager u, + RiskFactor factor, decimal bumpedValue, ParameterBase baseParam) + { + if (factor == null) throw new ArgumentNullException(nameof(factor)); + var p = baseParam.Clone(); + factor.ApplyTo(p, bumpedValue); + return (decimal)CalculateTradeValue(userId, t, u, p).Pv; + } + + /// + /// 构造"PV 作为某风险因子值的函数"委托,供 GreeksBumpCalculator 中心差分使用。 + /// 内部用真实 CalculateTradeValue 重定价;因子类别自带 FRTB SBA 标准步长(见 RiskFactor 工厂方法)。 + /// + public static Func PvAsFunctionOf( + string userId, trade t, underlying_manager u, + RiskFactor factor, ParameterBase baseParam) + => factor.BuildPvFunction(p => (decimal)CalculateTradeValue(userId, t, u, p).Pv, baseParam); + + #endregion } class OptionMarketObjectName