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

193 lines
10 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),不传则沿用计算器代理默认 T+1。
/// </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 不可用哨兵透传)。
/// 与前端 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;
}
return false;
}
}
}