feat(greeks): 新增 risk-factor bump 计算与注册
ParameterBase.Clone() 保留运行时类型深拷贝; ValueCalculator 两个薄接入方法; GreeksBumpCalculator/GreeksRiskFactor 引擎。加法性重定价桥,不动现有定价输出。
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
using YLErp.BLL.Calculation.V2;
|
||||
|
||||
namespace UnitTestProject.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// GreeksBumpCalculator 纯数学契约测试(先写,锁定 ε / 差分逻辑)。
|
||||
/// 不碰 DB / QDP:用已知解析导数的函数(f(x)=x²,d/dx=2x,d²/dx²=2)验证中心差分本身正确。
|
||||
/// 业务侧集成测试(真实定价路径 + 真实 FR007)见后续 ValueCalculator 薄接入落地后再补。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class GreeksBumpCalculatorTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 一阶中心差分对 f(x)=x² 在 x=3 应精确等于 2x=6。
|
||||
/// 二次多项式的中心差分对任意 ε 都精确(截断误差为 0),因此用相对步长 0.05% 仍得 6。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void CentralDelta_of_x2_equals_2x()
|
||||
{
|
||||
var calc = new GreeksBumpCalculator();
|
||||
Func<decimal, decimal> f = x => x * x; // d/dx = 2x
|
||||
var delta = calc.Delta(f, 3m, BumpSpec.Relative(0.0005m));
|
||||
Assert.AreEqual(6m, delta);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 二阶中心差分对 f(x)=x² 在 x=3 应精确等于 2(二阶导数恒为 2)。
|
||||
/// 验证 Gamma 的二阶差分算子正确。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void CentralGamma_of_x2_equals_2()
|
||||
{
|
||||
var calc = new GreeksBumpCalculator();
|
||||
Func<decimal, decimal> f = x => x * x; // d²/dx² = 2
|
||||
var gamma = calc.Gamma(f, 3m, BumpSpec.Relative(0.0005m));
|
||||
Assert.AreEqual(2m, gamma);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 1BP 变体(Dollar Greek)对 f(x)=x² 在 x=3、bump 1bp 应等于 (3.0001)² - 3²。
|
||||
/// 验证 BumpPv1Bp 直接前向 bump 的 PV 差逻辑。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void BumpPv1Bp_of_x2_forward()
|
||||
{
|
||||
var calc = new GreeksBumpCalculator();
|
||||
Func<decimal, decimal> f = x => x * x;
|
||||
var bumped = calc.BumpPv1Bp(f, 3m);
|
||||
Assert.AreEqual(3.0001m * 3.0001m - 3m * 3m, bumped);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 线性函数 f(x)=2x+1:一阶中心差分应精确等于斜率 2,二阶中心差分应精确为 0(线性无曲率)。
|
||||
/// 验证算子对"一次/零次"函数的精确性(二次之外另一种精确情形)。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void CentralDelta_of_linear_is_slope()
|
||||
{
|
||||
var calc = new GreeksBumpCalculator();
|
||||
Func<decimal, decimal> 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)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 三次函数 f(x)=x³:中心差分对任意 ε 不精确(仅二次及以下精确),结果逼近解析导 3x² 但有 O(ε²) 误差。
|
||||
/// 验证 Layer B 集成测试必须带容差,不能 Assert.AreEqual 死等精确值。x=2 解析导=12。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void CentralDelta_of_cubic_is_approx_analytic_with_tolerance()
|
||||
{
|
||||
var calc = new GreeksBumpCalculator();
|
||||
Func<decimal, decimal> 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}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 扭结点测试:看涨 payoff f(x)=max(x-3,0) 在行权价 x=3 处不可导。
|
||||
/// 中心差分跨扭结取到左右斜率的平均 (0+1)/2 = 0.5,说明对障碍/美式等扭结结构必须用单边差分。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void CentralDelta_at_kink_is_average_of_one_sided()
|
||||
{
|
||||
var calc = new GreeksBumpCalculator();
|
||||
Func<decimal, decimal> 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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 边界 x=0(相对步长会触到 Floor):f(x)=x² 在 0 处对称,Δ 应精确为 0,证明 Floor 兜底不产生噪声。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void CentralDelta_at_zero_uses_floor_but_stays_correct()
|
||||
{
|
||||
var calc = new GreeksBumpCalculator();
|
||||
Func<decimal, decimal> f = x => x * x;
|
||||
Assert.AreEqual(0m, calc.Delta(f, 0m, BumpSpec.Relative(0.0005m)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 利率量级小值 x=0.0001(1bp 量级)用相对步长:Resolve 返回 max(0.0001·0.0005, 1e-8)=5e-8,
|
||||
/// 远大于 Floor,证明利率类小量级不会被舍入噪声吞掉。f=x² 在 0.0001 解析导=2·0.0001=0.0002。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void CentralDelta_of_tiny_rate_like_x_is_stable()
|
||||
{
|
||||
var calc = new GreeksBumpCalculator();
|
||||
Func<decimal, decimal> f = x => x * x;
|
||||
Assert.AreEqual(0.0002m, calc.Delta(f, 0.0001m, BumpSpec.Relative(0.0005m)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// RiskFactor(② 风险因子抽象)+ ParameterBase.Clone 的纯单测(Layer A,无 DB / QDP)。
|
||||
/// 用假 reprice 委托验证:克隆类型保持、字典深拷、三类因子落点正确、波动率因子施于非期权参数抛异常,
|
||||
/// 以及经 BuildPvFunction 喂入 GreeksBumpCalculator 后 DeltaR / Delta / Vega / BumpPv1Bp 数值正确(线性函数精确)。
|
||||
/// </summary>
|
||||
[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<string, double> { { "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<string, double> { { "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<ParameterBase, decimal> reprice = p => (decimal)((p.RiskFreeRate ?? 0) * 1000); // PV = 1000 * r
|
||||
var factor = RiskFactor.Rate("r"); // 标准步长:绝对 1bp
|
||||
Func<decimal, decimal> 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<string, double> { { "X", 100 } }
|
||||
};
|
||||
Func<ParameterBase, decimal> reprice = p => (decimal)p.SpotPrices["X"]; // PV = S
|
||||
var factor = RiskFactor.Price("X"); // 标准步长:相对 1% → ε = 1
|
||||
Func<decimal, decimal> 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<ParameterBase, decimal> reprice = p => (decimal)(((BaseOptionParameter)p).Volatility ?? 0) * 50; // PV = 50 * σ
|
||||
var factor = RiskFactor.Volatility("X"); // 标准步长:绝对 1bp vol
|
||||
Func<decimal, decimal> pv = factor.BuildPvFunction(reprice, baseParam);
|
||||
|
||||
var calc = new GreeksBumpCalculator();
|
||||
decimal vega = calc.Vega(pv, 0.2m, factor.Shift); // 中心差分对线性函数精确
|
||||
Assert.AreEqual(50m, vega, 1e-4m);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user