Files
zszq-trs/YLErpDAL/Helpers/BondCalcHepler.cs
T

214 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Org.BouncyCastle.Asn1.Ocsp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using YLErp.Model;
namespace YLErp.Helpers
{
/// <summary>
/// 计算器帮助类
/// 调用链:zszq-trs → zszq-bond-oms(/calc/cal_bond_value) → bond-calc 微服务。
/// 契约(与 bond-oms-ui / zszq-bond-oms 保持一致):
/// 请求体 { bondId, price, priceType(DP全价/CP净价/YD收益率), targetDate?(估值日 yyyy-MM-dd,缺省代理取 T+1) }
/// 成功响应 data:{ errCode:0, errMsg:null, dirtyPrice(全价), cleanPrice(净价), ytm(收益率%) }
/// 失败响应:success=false 且 message=中文原因(债券不存在/信息不全/参数非法/服务异常)
/// </summary>
public class BondCalcHepler
{
private const string CalcUrl = "/calc/cal_bond_value";
/// <summary>
/// 计算器(债券净价/全价/收益率互算)。
/// 兼容旧调用方(如 RealtimePnlCalc):返回 CalBondResult,失败返回 null。
/// targetDate 可选:估值日(yyyy-MM-dd)。前端(fe)已在债券互换录入页强制要求 StartDate(开始日)必填、
/// 为空则报错不调用本接口;故经 UI 的计算请求总会带上估值日,此处的"代理默认 T+1"仅兜底非 UI 调用方(如 RealtimePnlCalc)。
/// </summary>
public static CalBondResult BondCalc(string underlyingCode, decimal price, string priceType = "DP", string targetDate = null)
{
string _err;
return BondCalc(underlyingCode, price, priceType, out _err, targetDate);
}
/// <summary>
/// 计算器(带错误信息输出)。
/// 任何失败(地址未配/无响应/success=false/业务错误码 errCode!=0/异常)都返回 null
/// 并通过 errorMsg 带上**真实原因**,供上层(BondController)清晰反馈给用户。
/// 关键:即使 success=true,只要内部 errCode!=0(债券不存在/信息不全/参数非法)也视为失败,
/// 避免把 0/异常价格静默回写覆盖用户手工输入。
/// targetDate 可选:估值日(yyyy-MM-dd),透传给代理的 targetDate(底层 bond-calc 的 settlementDate);
/// 不传则代理默认取 T+1(下一工作日)。
/// </summary>
public static CalBondResult BondCalc(string underlyingCode, decimal price, string priceType, out string errorMsg, string targetDate = null)
{
errorMsg = null;
var baseUrl = Environment.GetEnvironmentVariable("BondOmsInterface_BaseUrl");
CalcBondRequest request = new CalcBondRequest()
{
bondId = underlyingCode,
price = price.ToString(),
priceType = priceType,
targetDate = targetDate,
};
if (string.IsNullOrEmpty(baseUrl))
{
errorMsg = "计算器服务地址未配置(BondOmsInterface_BaseUrl)";
return null;
}
try
{
LogFactory.GetLogger("BondCalcHepler").Info(
$"债券计算器请求: url={baseUrl}{CalcUrl} bondId={underlyingCode} price={price} priceType={priceType} targetDate={(string.IsNullOrEmpty(targetDate) ? "(空→代理默认T+1)" : targetDate)}");
var httpHelper = new HttpHelper(baseUrl, null);
// http 请求 Web项目接口
var result = httpHelper.PostRequestNoAuth<CalcBondRequest, CalcBondReponse>(CalcUrl, request).Result;
if (result == null)
{
errorMsg = "计算器服务无响应";
LogFactory.GetLogger("BondCalcHepler").Error("债券计算器无响应,地址:" + baseUrl + CalcUrl);
return null;
}
if (!result.success)
{
errorMsg = result.message ?? "债券计算失败";
LogFactory.GetLogger("BondCalcHepler").Error("债券计算失败:" + errorMsg);
return null;
}
if (result.data == null)
{
errorMsg = "计算器返回数据为空";
LogFactory.GetLogger("BondCalcHepler").Error("债券计算器返回 data 为空");
return null;
}
// 业务层错误码(债券不存在 / 债券信息不全 / 参数非法),zszq-bond-oms 在 success=true 时仍可能带 errCode!=0
if (result.data.errCode != 0)
{
errorMsg = result.data.errMsg ?? "债券计算业务错误";
LogFactory.GetLogger("BondCalcHepler").Error("债券计算业务错误(code=" + result.data.errCode + ")" + errorMsg);
return null;
}
// 防御性值域校验:errCode=0 但数值离谱(如净价变负 / 收益率量级爆炸 / 行权收益率哨兵 -999999)。
// 这类"成功但不可信"的响应现有 errCode 守卫拦不住,此处兜底:宁可返回 null+提示用户核对,
// 也绝不把垃圾值回写覆盖手工输入。
string absurdReason;
if (IsResultAbsurd(result.data, out absurdReason))
{
errorMsg = absurdReason;
LogFactory.GetLogger("BondCalcHepler").Error(
$"债券计算返回离谱值(bondId={underlyingCode}){absurdReason} | cleanPrice={result.data.cleanPrice} dirtyPrice={result.data.dirtyPrice} ytm={result.data.ytm}");
return null;
}
LogFactory.GetLogger("BondCalcHepler").Info(
$"债券计算器成功: bondId={underlyingCode} targetDate={targetDate} cleanPrice={result.data.cleanPrice} dirtyPrice={result.data.dirtyPrice} ytm={result.data.ytm}");
return result.data;
}
catch (Exception ex)
{
errorMsg = "请求债券计算器异常:" + ex.Message;
LogFactory.GetLogger("BondCalcHepler").Error("请求债券计算器时发生异常", ex);
return null;
}
}
/// <summary>
/// 按结算日计算(自动结算路径用)。与 BondCalc 同款健壮处理:
/// 地址未配/无响应/success=false/业务错误码/异常 均返回 null(不回写坏数据)。
/// </summary>
public static CalBondResult BondCalcByDate(string underlyingCode, decimal price, String targetDate, string priceType = "DP")
{
var baseUrl = Environment.GetEnvironmentVariable("BondOmsInterface_BaseUrl");
CalcBondRequest request = new CalcBondRequest()
{
bondId = underlyingCode,
price = price.ToString(),
priceType = priceType,
targetDate = targetDate,
};
if (!string.IsNullOrEmpty(baseUrl))
{
var httpHelper = new HttpHelper(baseUrl, null);
try
{
// http 请求 Web项目接口
var result = httpHelper.PostRequestNoAuth<CalcBondRequest, CalcBondReponse>(CalcUrl, request).Result;
if (result == null || !result.success || result.data == null || result.data.errCode != 0)
{
var msg = result != null ? (result.message ?? (result.data != null ? result.data.errMsg : null)) : "计算器服务无响应";
LogFactory.GetLogger("BondCalcHelper").Error("债券计算失败:" + msg);
return null;
}
// 防御性值域校验(同 BondCalc):拦截"成功但数值离谱"的响应,不回写坏数据
string absurdReason;
if (IsResultAbsurd(result.data, out absurdReason))
{
LogFactory.GetLogger("BondCalcHelper").Error(
$"债券计算返回离谱值(bondId={underlyingCode}){absurdReason} | cleanPrice={result.data.cleanPrice} dirtyPrice={result.data.dirtyPrice} ytm={result.data.ytm}");
return null;
}
return result.data;
}
catch (Exception ex)
{
LogFactory.GetLogger("BondCalcHelper").Error("请求计算器时发生异常", ex);
}
}
return null;
}
/// <summary>
/// 防御性值域校验:判断计算器返回值是否"离谱"errCode=0 但净价变负 / 收益率量级爆炸 / 命中哨兵值)。
/// 命中则上层返回 null、不回写,避免把不可信结果覆盖用户手工输入。规则:
/// - 净价/全价:占面值百分比,正常约 20~300,必须为正且不破千(≤0 或 &gt;1000 视为离谱);
/// - 收益率(ytm,百分数口径)|ytm| &gt; 100 视为爆炸(允许负利率债,但量级须合理);
/// - 哨兵值:任一字段命中 -999999 或 -999999×100(如 yieldToCP 不可用哨兵透传);
/// - 疑似到期/无剩余现金流:见 <see cref="IsMaturedDegenerate"/>。
/// 与前端 swapCalc.js::getBondCalcErrorMessage 的值域闸门保持一致口径。
/// </summary>
private static bool IsResultAbsurd(CalBondResult data, out string reason)
{
reason = null;
if (data == null) return false;
const decimal SENTINEL = -999999m;
bool Absurd(decimal v) => v == SENTINEL || v == SENTINEL * 100m;
if (Absurd(data.cleanPrice) || Absurd(data.dirtyPrice) || (data.ytm.HasValue && Absurd(data.ytm.Value)))
{
reason = "债券计算返回哨兵值(部分指标不可用),请检查估值日/价格输入或联系管理员核对债券计算服务";
return true;
}
if (data.cleanPrice <= 0m || data.cleanPrice > 1000m || data.dirtyPrice <= 0m || data.dirtyPrice > 1000m)
{
reason = "债券计算净价/全价超出合理范围(应为正值且接近面值百分比),请检查估值日/价格输入或联系管理员核对债券计算服务";
return true;
}
if (data.ytm.HasValue && Math.Abs(data.ytm.Value) > 100m)
{
reason = "债券计算收益率量级异常(" + data.ytm.Value + "),请检查估值日/价格输入或联系管理员核对债券计算服务";
return true;
}
if (IsMaturedDegenerate(data))
{
reason = "该债券疑似已到期或无剩余现金流(收益率=0、净价/全价均为面值100),无法反算,请核对债券到期日或手工填写";
return true;
}
return false;
}
/// <summary>
/// 疑似到期/无剩余现金流债券:估值日晚于到期日时底层 jquantlib 对空现金流求解得 ytm=0、
/// 净价/全价均为面值100——三者同时成立在现实在市券中几乎不可能(UAT 060203.IB 实证)。
/// 判定条件与前端 swapCalc.js::getBondCalcErrorMessage 内同款闸门保持一致,勿单边改动。
/// public 供单测直接覆盖(BondCalcHeplerTest)。
/// </summary>
public static bool IsMaturedDegenerate(CalBondResult data)
{
return data != null
&& data.ytm == 0m
&& data.cleanPrice == 100m
&& data.dirtyPrice == 100m;
}
}
}