diff --git a/UnitTestProject/Modules/DataProviderModule/EodPriceQueryServiceSettlementTest.cs b/UnitTestProject/Modules/DataProviderModule/EodPriceQueryServiceSettlementTest.cs new file mode 100644 index 00000000..ec615f82 --- /dev/null +++ b/UnitTestProject/Modules/DataProviderModule/EodPriceQueryServiceSettlementTest.cs @@ -0,0 +1,72 @@ +namespace YLErp.Modules.DataProviderModule +{ + /// + /// TryGetSettlementEodPrice(债券感知统一取价)的白盒测试。 + /// 覆盖期权/交易到期结算场景:债券标的应走中债估值表取到价(修复"结算价未找到"), + /// 非债券标的行为应与原 TryGetEodPrice 完全一致(不影响期货/股票)。 + /// 注:DB 驱动,需连测试库;无数据时 Assert.Inconclusive 跳过。 + /// + [TestClass] + public class EodPriceQueryServiceSettlementTest : YLUnitTestBase + { + [TestMethod] + public void BondUnderlying_RoutesToChinaBondValuation() + { + using var db = DbContextFactory.GetYLDbContext(); + var bond = (from b in db.china_bond_valuation + join u in db.underlying_manager on b.bond_id equals u.UnderlyingCode + where b.dirty_price_close > 0 + orderby b.valuation_date descending + select new { b.bond_id, vd = b.valuation_date }).FirstOrDefault(); + if (bond == null) Assert.Inconclusive("测试库无债券估值数据,跳过"); + + var ok = EodPriceQueryService.TryGetSettlementEodPrice(bond.vd, bond.bond_id, out var ep); + Assert.IsTrue(ok, "债券标的应走中债估值表取到价(修复点)"); + Assert.IsNotNull(ep); + // 债券 ClosePrice=全价(dirty_price_close),应与 GetBondPrice().ClosePrice 一致 + var bondPrice = EodPriceQueryService.GetBondPrice(bond.vd, bond.bond_id); + Assert.IsNotNull(bondPrice); + Assert.AreEqual(bondPrice.ClosePrice, ep.ClosePrice, 1e-6); + } + + [TestMethod] + public void NonBondUnderlying_RoutesToStockOrFuturePath() + { + using var db = DbContextFactory.GetYLDbContext(); + var stock = (from s in db.eod_stock_price + join u in db.underlying_manager on s.UnderlyingCode equals u.UnderlyingCode + where s.ClosePrice > 0 && u.UnderlyingInstrumentType == "Stock" + select new { s.UnderlyingCode, s.ValueDate }).FirstOrDefault(); + if (stock == null) Assert.Inconclusive("测试库无(股票类型)价格数据,跳过"); + + var ok = EodPriceQueryService.TryGetSettlementEodPrice(stock.ValueDate, stock.UnderlyingCode, out var ep); + var okOld = EodPriceQueryService.TryGetEodPrice(stock.ValueDate, stock.UnderlyingCode, out var epOld); + Assert.AreEqual(okOld, ok, "非债券标的行为应与原 TryGetEodPrice 一致"); + if (ok) + { + Assert.IsNotNull(ep); + Assert.AreEqual(epOld.ClosePrice, ep.ClosePrice, 1e-6, "非债券标的取到的收盘价应与原路径相同"); + } + } + + [TestMethod] + public void BondOptionExpiry_Regression_OldPathFailsNewPathSucceeds() + { + using var db = DbContextFactory.GetYLDbContext(); + var bond = (from b in db.china_bond_valuation + join u in db.underlying_manager on b.bond_id equals u.UnderlyingCode + where b.dirty_price_close > 0 + orderby b.valuation_date descending + select new { b.bond_id, vd = b.valuation_date }).FirstOrDefault(); + if (bond == null) Assert.Inconclusive("测试库无债券估值数据,跳过"); + + // 旧路径:TryGetEodPrice 只 join 期货/股票两表,债券取不到价 + var oldOk = EodPriceQueryService.TryGetEodPrice(bond.vd, bond.bond_id, out _); + // 新路径:债券感知统一取价,应能取到 + var newOk = EodPriceQueryService.TryGetSettlementEodPrice(bond.vd, bond.bond_id, out var ep); + Assert.IsFalse(oldOk, "回归基线:旧路径对债券标的应取不到价(这正是期权到期报'结算价未找到'的根因)"); + Assert.IsTrue(newOk && ep != null && ep.ClosePrice > 0, + "修复验证:统一取价应能为债券标的取到结算价,期权到期不再报'结算价未找到'"); + } + } +} diff --git a/YLErpDAL/Modules/DataProviderModule/EodPriceProvider.cs b/YLErpDAL/Modules/DataProviderModule/EodPriceProvider.cs index 18b31de5..c59eca99 100644 --- a/YLErpDAL/Modules/DataProviderModule/EodPriceProvider.cs +++ b/YLErpDAL/Modules/DataProviderModule/EodPriceProvider.cs @@ -140,6 +140,9 @@ namespace YLErp.Modules.DataProviderModule { if (item.UnderlyingInstrumentType == "Bonds") { + // [Layer2-待统一] 债券映射口径:SettlePrice=全价(dirty_price_close),ClosePrice=净价(net_price)。 + // 注意:这与 EodPriceQueryService.GetBondPrice 的映射【完全相反】(GetBondPrice: ClosePrice=全价,SettlePrice=净价)。 + // 两处对"债券收盘价/结算价"的净全价定义不一致属历史遗留,请勿随意改动单侧,需业务先定调后统一(见 TryGetSettlementEodPrice 注释)。 item.SettlePrice = Convert.ToDouble(BondPriceConverter.ToStorage(item.DeciSettlePrice)); item.ClosePrice = Convert.ToDouble(BondPriceConverter.ToStorage(item.DeciClosePrice)); item.ReferencePrice = Convert.ToDouble(BondPriceConverter.ToStorage(item.DeciReferencePrice)); diff --git a/YLErpDAL/Modules/DataProviderModule/EodPriceQueryService.cs b/YLErpDAL/Modules/DataProviderModule/EodPriceQueryService.cs index c660b0c6..7792e5e1 100644 --- a/YLErpDAL/Modules/DataProviderModule/EodPriceQueryService.cs +++ b/YLErpDAL/Modules/DataProviderModule/EodPriceQueryService.cs @@ -114,6 +114,23 @@ namespace YLErp.Modules.DataProviderModule return (eodPrice = GetBondPrice(valueDate, underlyingCode)) != null; } /// + /// 统一日终结算取价(债券感知)。 + /// 用于交易/期权到期结算:债券标的走中债估值表(TryGetBondEodPrice),期货/股票走原 InnerGetEodPrice。 + /// 解决到期路径(tradeExpireInner / MultipleTradeExpireConfirm)漏查债券表导致"结算价未找到"的问题。 + /// 注:债券 ClosePrice/SettlePrice 映射沿用 GetBondPrice 口径(ClosePrice=全价 dirty_price_close,SettlePrice=净价 net_price), + /// 与 EodPriceProvider 的映射(ClosePrice=净价,SettlePrice=全价)相反——属历史不一致(见 EodPriceProvider.Initialize 与 GetBondPrice 的注释), + /// 本方法保持与系统既有"债券现价"约定(UnderlyingCodePrice)一致,不引入新口径。 + /// + public static bool TryGetSettlementEodPrice(DateTime valueDate, string underlyingCode, out EodPrice eodPrice) + { + var um = DataCacheProvider.GetUnderlyingDataSource().GetData(underlyingCode); + if (um != null && ConsGlobal.InstrumentType.IsBond(um.UnderlyingInstrumentType)) + { + return TryGetBondEodPrice(valueDate, underlyingCode, out eodPrice); + } + return TryGetEodPrice(valueDate, underlyingCode, out eodPrice); + } + /// /// 尝试获取标的某日的日终价 /// public static bool TryGetEodPrice(DateTime valueDate, int underlyingId, out EodPrice eodPrice) @@ -231,6 +248,9 @@ namespace YLErp.Modules.DataProviderModule Vobp = bondPrice.vobp, ValueDate = valueDate, UnderlyingCode = underlyingCode, + // [Layer2-待统一] 债券映射口径:ClosePrice=全价(dirty_price_close),SettlePrice=净价(net_price)。 + // 注意:这与 EodPriceProvider.Initialize 的映射【完全相反】(EodPriceProvider: ClosePrice=净价,SettlePrice=全价)。 + // 两处对"债券收盘价/结算价"的净全价定义不一致属历史遗留,请勿随意改动单侧,需业务先定调后统一(见 TryGetSettlementEodPrice 注释)。 ClosePrice = Convert.ToDouble(BondPriceConverter.ToStorage(bondPrice.dirty_price_close)), SettlePrice = Convert.ToDouble(BondPriceConverter.ToStorage(bondPrice.net_price)), ReferencePrice = Convert.ToDouble(BondPriceConverter.ToStorage(bondPrice.yield)) diff --git a/YLErpDAL/Modules/TradeModule/DealModule/TradeExpireConfirmService.cs b/YLErpDAL/Modules/TradeModule/DealModule/TradeExpireConfirmService.cs index a523d7ca..ab9624f9 100644 --- a/YLErpDAL/Modules/TradeModule/DealModule/TradeExpireConfirmService.cs +++ b/YLErpDAL/Modules/TradeModule/DealModule/TradeExpireConfirmService.cs @@ -140,7 +140,9 @@ namespace YLErp.Modules.TradeModule.DealModule #region 设置期末价格和执行价格 - var finalPrice = EodPriceQueryService.TryGetEodPrice(exerciseDate, td.UnderlyingCode, out var eodPrice) + // 债券标的需走中债估值表取价,原 TryGetEodPrice 只查期货/股票两表会漏掉债券,导致"结算价未找到"。 + // 统一改用债券感知的 TryGetSettlementEodPrice(见 EodPriceQueryService)。 + var finalPrice = EodPriceQueryService.TryGetSettlementEodPrice(exerciseDate, td.UnderlyingCode, out var eodPrice) ? eodPrice.GetPrice(td.SettlementType) : 0; if (finalPrice <= 0) @@ -280,8 +282,6 @@ namespace YLErp.Modules.TradeModule.DealModule trade_cash tradeCash = null; //日终价格 - var underlyingIds = tradeUnwindTrades.Select(t => t.UnderlyingId).ToList(); - var EodPriceProvider = new EodPriceProvider(valueDate); //批量结算的全是现金流交易就不用结算价 if (!EodPriceQueryService.CheckDbExists(valueDate) && tradeQuery.Any(t => t.TradeType != "现金流交易")) { @@ -308,8 +308,8 @@ namespace YLErp.Modules.TradeModule.DealModule var CountRatio = 1; if (t.TradeType != "现金流交易") { - //结算价 - if (EodPriceProvider.TryGetEodPrice(t.UnderlyingCode, out var eodPrice)) + //结算价(债券感知统一取价:债券走中债估值,期货/股票走原路径,见 EodPriceQueryService.TryGetSettlementEodPrice) + if (EodPriceQueryService.TryGetSettlementEodPrice(valueDate, t.UnderlyingCode, out var eodPrice)) { settlePrice = eodPrice.GetPrice(t.SettlementType); }