From 95686f4c4dcb63462d20dbf8a170db53ea7e12db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=96=B9=E6=B5=B7?= Date: Fri, 8 May 2026 15:09:58 +0800 Subject: [PATCH] =?UTF-8?q?#EQD-5718=20=E3=80=90=E7=BC=BA=E9=99=B7?= =?UTF-8?q?=E8=BD=AC=E9=9C=80=E6=B1=82=E3=80=91-=E5=9B=BD=E8=81=94?= =?UTF-8?q?=E6=B0=91=E7=94=9F-=E5=88=A9=E6=81=AF=E7=AB=AF=E8=AE=A1?= =?UTF-8?q?=E6=81=AF=E6=96=B9=E5=BC=8F=E4=B8=8E=E7=BB=93=E7=AE=97=E8=A7=84?= =?UTF-8?q?=E5=88=99=E6=89=A9=E5=85=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../YLErp.Core/DBModels/TradeObervation.cs | 12 + Framework/YLErp.Core/Models/IntervalModel.cs | 6 +- .../SwapModule/GetInterestsUnitTest.cs | 1414 +++++++++++++++++ .../Modules/SwapModule/SwapDealService.cs | 453 ++++-- .../SwapModule/SwapEodPositionService.cs | 29 +- .../SwapModule/SwapTradeBaseService.cs | 15 +- YLErpDAL/QdpModule/QdpObservationHelper.cs | 196 ++- YLErpWeb/Controllers/SwapTrade2Controller.cs | 11 + YLErpWeb/Controllers/tradeController.cs | 10 +- .../Models/GetSwapObservationDateRequest.cs | 28 + YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml | 39 +- YLErpWeb/Views/SwapTrade2/TradeView.cshtml | 15 +- .../Scripts/app/swaptrade/swapTradeEdit.js | 223 ++- .../Scripts/app/swaptrade/swapTradeView.js | 18 +- 14 files changed, 2227 insertions(+), 242 deletions(-) create mode 100644 UnitTestProject/Modules/SwapModule/GetInterestsUnitTest.cs create mode 100644 YLErpWeb/Models/GetSwapObservationDateRequest.cs diff --git a/Framework/YLErp.Core/DBModels/TradeObervation.cs b/Framework/YLErp.Core/DBModels/TradeObervation.cs index d92d9b24..9be18ba4 100644 --- a/Framework/YLErp.Core/DBModels/TradeObervation.cs +++ b/Framework/YLErp.Core/DBModels/TradeObervation.cs @@ -60,6 +60,18 @@ namespace YLErp.DBModels [DisplayName("观察起始日")] [Column("observation_start")] public DateTime? ObservationStart { get; set; } + /// + /// 交易日历 + /// + [DisplayName("交易日历")] + [Column("observation_calendar")] + public string ObservationCalendar { get; set; } + /// + /// 结算规则 + /// + [DisplayName("结算规则")] + [Column("observation_settlement_rules")] + public int? ObservationSettlementRules { get; set; } /// /// 互换观察日集合 diff --git a/Framework/YLErp.Core/Models/IntervalModel.cs b/Framework/YLErp.Core/Models/IntervalModel.cs index 7aa3d72e..2d78b48a 100644 --- a/Framework/YLErp.Core/Models/IntervalModel.cs +++ b/Framework/YLErp.Core/Models/IntervalModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -20,5 +20,9 @@ namespace YLErp.Models /// 是否结算 0:否 1:是 /// public int Settlement { get; set; } + /// + /// 结算日期(观察日不一定等于结算日) + /// + public DateTime? SettlementDate { get; set; } } } diff --git a/UnitTestProject/Modules/SwapModule/GetInterestsUnitTest.cs b/UnitTestProject/Modules/SwapModule/GetInterestsUnitTest.cs new file mode 100644 index 00000000..33ecef94 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/GetInterestsUnitTest.cs @@ -0,0 +1,1414 @@ +using Newtonsoft.Json; +using YLErp.DBModels; +using YLErp.DBModels.Enums; +using YLErp.Models; + +namespace YLErp.Modules.SwapModule +{ + /// + /// 互换利息计算单元测试 + /// ================================================================ + /// 测试口径说明: + /// "11" = 算头算尾(含起息日和到期日) + /// "10" = 算头不算尾(含起息日,不含到期日) + /// "01" = 不算头算尾(不含起息日,含到期日) + /// "00" = 不算头不算尾(不含起息日也不含到期日) + /// 不算头不算尾暂时测试不通过 + /// 统一测试数据: + /// - Principal=1000, FixedRate=1.00%, AnnualDays=365 + /// - ResetPeriod=3天, InterestRule=-1(前一营业日), InterestRule=0(当前营业日) + /// - FR007@2026-04-27=0.10%, FR007@2026-04-30=0.20% + /// - StartDate=2026-04-28, TradeDate=2026-04-27 + /// ================================================================ + /// + [TestClass] + public class GetInterestsUnitTest + { + #region 内部类:浮动利率模拟服务 + + /// + /// StubSwapDealService - 模拟浮动利率获取 + /// 用于单元测试中预置FR007价格,避免依赖外部数据源 + /// + private sealed class StubSwapDealService : SwapDealService + { + private readonly IReadOnlyDictionary _floatRates; + + public StubSwapDealService(OptUserInfo optUser, IReadOnlyDictionary floatRates) : base(optUser) + { + _floatRates = floatRates; + } + + protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate) + { + if (!string.Equals(underlyingCode, "FR007", StringComparison.OrdinalIgnoreCase)) + { + rate = 0; + return false; + } + + if (_floatRates.TryGetValue(valueDate.Date, out rate)) + { + return true; + } + + rate = 0; + return false; + } + } + + #endregion + + #region 测试常量与共享变量 + + private const decimal Principal = 1000m; // 本金:1000 + private const decimal FixedRate = 0.01m; // 固定利率:1.00% + private const int AnnualDays = 365; // 年化天数 + private const int ResetPeriod = 3; // 重置周期:3天 + private const int InterestRule_Pre = -1; // 前一营业日规则 + private const int InterestRule_Cur = 0; // 当前营业日规则 + + private static readonly DateTime TradeDate = new(2026, 4, 27); // 成交日 + private static readonly DateTime StartDate = new(2026, 4, 28); // 起息日(开始计息日) + private static readonly DateTime ExerciseDate = new(2027, 4, 27); // 到期日 + + private SwapDealService _service; + private IReadOnlyDictionary _floatRates; + + [TestInitialize] + public void Init() + { + // 预置FR007价格数据 + _floatRates = new Dictionary + { + [new DateTime(2026, 4, 27)] = 0.001, // FR007@2026-04-27 = 0.10% + [new DateTime(2026, 4, 28)] = 0.001, // FR007@2026-04-28 = 0.10% (新增) + [new DateTime(2026, 4, 29)] = 0.001, // FR007@2026-04-29 = 0.10% + [new DateTime(2026, 4, 30)] = 0.002, // FR007@2026-04-30 = 0.20% + [new DateTime(2026, 5, 6)] = 0.002, // FR007@2026-05-06 = 0.20% + // 到期日测试用例需要的利率数据(2027年) + [new DateTime(2027, 4, 23)] = 0.001, // FR007@2027-04-23 = 0.10%(2027-04-26的前一工作日) + [new DateTime(2027, 4, 24)] = 0.001, // FR007@2027-04-24 = 0.10%(周末) + [new DateTime(2027, 4, 25)] = 0.001, // FR007@2027-04-25 = 0.10%(周末) + [new DateTime(2027, 4, 26)] = 0.001, // FR007@2027-04-26 = 0.10% + [new DateTime(2027, 4, 27)] = 0.001 // FR007@2027-04-27 = 0.10%(到期日) + }; + + _service = new StubSwapDealService( + new OptUserInfo(0, nameof(GetInterestsUnitTest), OptUserFrom.UnitTest), + _floatRates); + } + + #endregion + + #region 测试数据构建器 + + /// + /// 创建测试用交易对象 + /// + /// 计息口径:"11"/"10"/"01"/"00" + /// 取率规则:-1=前一营业日,0=当前营业日 + private static trade CreateTrade(string interestCalcMode, int interestRule = InterestRule_Pre) + { + var extend = new trade_extend + { + TradeId = 1, + ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson + { + AnnualDays = AnnualDays, + InterestCalcMode = interestCalcMode, + SettlementRules = interestRule + }) + }; + + return new trade + { + id = 1, + TradeNumber = "UT-SWAP-INT-001", + ClientId = 999998, + TradeType = "收益互换", + TradeDate = TradeDate, + StartDate = StartDate, + ExerciseDate = ExerciseDate, + TradeStatus = "确认成交", + ValidState = "Valid", + trade_extend = extend + }; + } + + /// + /// 创建测试用持仓对象 + /// + /// 计息口径 + /// 取率规则 + private static swap_position CreateInterestPosition(string interestCalcMode, int interestRule = InterestRule_Pre) + { + var intervalModels = new List + { + new IntervalModel + { + Date = ExerciseDate, + Rate = FixedRate, + Settlement = 0 + } + }; + + return new swap_position + { + id = 1001, + SwapTradeId = 1, + PositionType = (int)PositionTypeFlag.Unknown, + InterestDirection = (int)SwapDirectionEnum.收取, + InterestMode = (int)InterestModeEnum.标的期初全价, + InterestRateDefault = FixedRate, + InterestPrincipalFix = Principal, + PosiStartDate = StartDate, + PosiMatuirityDate = ExerciseDate, + IsInitial = true, + Invalid = false, + InterestType = (int)InterestTypeEnum.单利, + IsAnnualized = true, + interest_rest_days = ResetPeriod, + interest_rule = interestRule, + FloatRateUnderlyingCode = "FR007", + InterestSwapInterval = JsonConvert.SerializeObject(intervalModels) + }; + } + + /// + /// 创建日终持仓记录(EOD归档数据) + /// + private static eod_swap_position CreateEodPosition(DateTime valueDate, decimal tdPrincipal, decimal floatRate, decimal interestSum) + { + return new eod_swap_position + { + id = 1, + SwapTradeId = 1, + PositionId = 1001, + ValueDate = valueDate, + ClientId = 999998, + FloatRate = floatRate, + TdInterestPrincipal = tdPrincipal, + PosiNotionalValue = tdPrincipal, + InterestProfitSum = interestSum + }; + } + + /// + /// 计算期望利息金额 + /// 公式:本金 × (固定利率 + 浮动利率) × 计息天数 ÷ 年化天数 + /// + private static decimal ExpectedInterest(int days, decimal fixedRate, decimal floatRate, decimal principal) + { + var yearlyRate = fixedRate + floatRate; + var interest = principal * yearlyRate * days / AnnualDays; + return Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + } + + #endregion + + #region 通用的GetInterests调用方法 + + /// + /// 通用平仓计算(不含eodPositions) + /// + private swap_flow_event CalcUnwind(string interestCalcMode, DateTime valueDate, DateTime unwindDate, + decimal closePercent, int interestRule = InterestRule_Pre) + { + return CalcUnwind(interestCalcMode, valueDate, unwindDate, closePercent, + new List(), interestRule); + } + + /// + /// 通用平仓计算(含eodPositions) + /// + private swap_flow_event CalcUnwind(string interestCalcMode, DateTime valueDate, DateTime unwindDate, + decimal closePercent, List eodPositions, int interestRule = InterestRule_Pre) + { + var td = CreateTrade(interestCalcMode, interestRule); + var position = CreateInterestPosition(interestCalcMode, interestRule); + + var interests = _service.GetInterests( + td, td.trade_extend, + valueDate, unwindDate, + eodPositions, + new List { position }, + Principal, 0, 0, + Principal, closePercent, + (int)SwapEventTypeEnum.平仓, + false, false, 0, Principal, + false, + interestCalcMode.EndsWith("1", StringComparison.Ordinal), + false); + + Assert.AreEqual(1, interests.Count); + return interests[0]; + } + + /// + /// 通用收盘计算 + /// settment=true 表示收盘场景 + /// + private swap_flow_event CalcEod(string interestCalcMode, DateTime valueDate, + List eodPositions, int interestRule = InterestRule_Pre) + { + var td = CreateTrade(interestCalcMode, interestRule); + var position = CreateInterestPosition(interestCalcMode, interestRule); + + var interests = _service.GetInterests( + td, td.trade_extend, + valueDate, valueDate, + eodPositions, + new List { position }, + Principal, 0, 0, + Principal, 1m, + (int)SwapEventTypeEnum.平仓, + false, false, 0, Principal, + false, + interestCalcMode.EndsWith("1", StringComparison.Ordinal), + true); // settment=true 表示收盘 + + Assert.AreEqual(1, interests.Count); + return interests[0]; + } + + /// + /// 通用自动互换计算 + /// 使用SwapEventTypeEnum.自动互换事件类型 + /// + private swap_flow_event CalcAutoSwap(string interestCalcMode, DateTime valueDate, + List eodPositions, decimal closePercent = 1m, int interestRule = InterestRule_Pre) + { + var td = CreateTrade(interestCalcMode, interestRule); + var position = CreateInterestPosition(interestCalcMode, interestRule); + + var interests = _service.GetInterests( + td, td.trade_extend, + valueDate, valueDate, + eodPositions, + new List { position }, + Principal, 0, 0, + Principal, closePercent, + (int)SwapEventTypeEnum.自动互换, + false, false, 0, Principal, + false, + interestCalcMode.EndsWith("1", StringComparison.Ordinal), + false); + + Assert.AreEqual(1, interests.Count); + return interests[0]; + } + + #endregion + + #region 场景1:算头算尾 (InterestCalcMode="11") + #region 计息区间说明: + /// 11_001: 首日(StartDate=4/28)平仓 → S=4/28, E=4/28 → 1天 + /// 11_002: 次日(4/29)平仓 → S=4/28, E=4/29 → 2天 + /// 11_003: 次日(4/29)平仓50% → S=4/28, E=4/29 → 2天×50% + /// 11_004: 跨周期(5/6)平仓 → S=4/28, E=5/6 → 8天(分段取率) + /// 11_EOD_001: 首日(4/28)收盘 → 1天 + /// 11_EOD_002: 4/28已收盘 → 4/29平仓 → S=4/29, E=4/29 → 1天 + #endregion + /// ================================================================ */ + + /// + /// [11_001] 算头算尾 - 首日起息日平仓 + /// --------------------------------------------------------------- + /// 场景:2026-04-28(起息日StartDate)盘中执行全平 + /// 前置:无上一日EOD持仓(首次操作) + /// 操作:valueDate=2026-04-28,执行"全平"(closePercent=100%) + /// 口径:算头算尾,计息区间 S=4/28, E=4/28 + /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_11_PRE_001() + { + var interest = CalcUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + Assert.AreEqual(expected, interest.InterestAmount); + } + + /// + /// [11_002] 算头算尾 - 次日全平 + /// --------------------------------------------------------------- + /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行全平 + /// 前置:无上一日EOD持仓 + /// 操作:valueDate=2026-04-29,执行"全平" + /// 口径:算头算尾,计息区间 S=4/28, E=4/29 + /// 期望:计息天数=2天,利息=2*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_11_PRE_002() + { + var interest = CalcUnwind("11", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); + var expected = ExpectedInterest(2, FixedRate, 0.001m, Principal); + Assert.AreEqual(expected, interest.InterestAmount); + } + + /// + /// [11_003] 算头算尾 - 次日平仓50% + /// --------------------------------------------------------------- + /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行平仓50% + /// 操作:valueDate=2026-04-29,执行"平仓50%"(closePercent=50%) + /// 口径:算头算尾,计息区间 S=4/28, E=4/29 + /// 期望:计息天数=2天,利息=0.5*2*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_11_PRE_003() + { + var interest = CalcUnwind("11", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m); + var expected = ExpectedInterest(2, FixedRate, 0.001m, Principal * 0.5m); + Assert.AreEqual(expected, interest.InterestAmount); + } + + /// + /// [11_004] 算头算尾 - 跨重置周期全平 + /// --------------------------------------------------------------- + /// 场景:2026-04-28 未平仓;2026-05-06 跨周期全平 + /// 背景:ResetPeriod=3天,4/28→4/30为第一周期,5/1→5/6为第二周期 + /// 操作:valueDate=2026-05-06,执行"全平" + /// 取率:跨周期分段取率 + /// - 第一段(4/28-4/30): 3天×FR007@4/27(0.10%) + /// - 第二段(5/1-5/6): 6天×FR007@4/30(0.20%) + /// 口径:算头算尾,计息区间 S=4/28, E=5/6 + /// 期望:分段计算利息 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_11_PRE_004() + { + var interest = CalcUnwind("11", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m); + // 预期分段计算:3天@0.10% + 6天@0.20% + var expected = Math.Round( + ExpectedInterest(3, FixedRate, 0.001m, Principal) + + ExpectedInterest(6, FixedRate, 0.002m, Principal), + ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + Assert.AreEqual(expected, interest.InterestAmount); + } + + /// + /// [11_EOD_001] 算头算尾 - 首日收盘归档 + /// --------------------------------------------------------------- + /// 场景:2026-04-28(起息日)执行收盘EOD归档 + /// 前置:无上一日EOD持仓(首次收盘) + /// 操作:执行 2026-04-28 收盘归档 + /// 口径:算头算尾,计息区间 S=4/28, E=4/28 + /// 期望:当日收盘利息=1天,利息=1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_11_EOD_001() + { + var interest = CalcEod("11", new DateTime(2026, 4, 28), new List()); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + Assert.AreEqual(expected, interest.InterestAmount); + } + + /// + /// [11_EOD_002] 算头算尾 - 前日已收盘,次日平仓 + /// --------------------------------------------------------------- + /// 场景:2026-04-28 已收盘归档;2026-04-29 盘中执行全平 + /// 前置:存在4/28的EOD持仓记录(待实现利息=1天利息) + /// 操作:valueDate=2026-04-29,执行"全平" + /// 口径:算头算尾 + /// 期望:总利息=历史待实现利息+当期利息=1天(4/28)+1天(4/29)=2天 + /// 利息=2*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_11_EOD_002() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FixedRate, 0.001m, Principal)) + }; + var interest = CalcUnwind("11", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, eodPositions); + // 平仓利息 = 历史待实现利息(4/28=1天) + 当期利息(4/29=1天) = 2天 + var expected = ExpectedInterest(2, FixedRate, 0.001m, Principal); + Assert.AreEqual(expected, interest.InterestAmount); + } + + #endregion + + #region 场景2:算头不算尾 (InterestCalcMode="10") - 当前测试重点 + #region 计息区间说明: + /// 10_001: 首日(4/28)平仓 → S=4/28, E=4/27 → 0天 + /// 10_002: 次日(4/29)全平 → S=4/28, E=4/28 → 1天 + /// 10_003: 次日(4/29)半平 → 1天×50% + /// 10_004: 次日(4/29)全平后收盘 → 全平利息+收盘待实现=0 + /// 10_005: 第3日(4/30)全平 → S=4/28, E=4/29 → 2天 + /// 10_006: 第3日(4/30)半平 → 2天×50% + /// 10_007: 次日(4/29)半平 + 第3日(4/30)收盘 → 剩余50%×1天 + /// 10_008: 第3日(4/30)直接收盘 → 持仓×1天 + /// 10_009: 次日(4/29)自动互换 → 1天 + /// 10_010: 自动互换后次日(4/30)平仓 → 0天 + /// 10_011: 跨周期(5/6)全平 → 分段计息 + /// 10_EOD_001: 首日(4/28)收盘 → 0天(首次) + /// 10_EOD_002: 4/28收盘 → 4/29全平 → 1天 + /// 10_EOD_003: 4/28收盘 → 4/29半平 → 0.5天 + /// 10_EOD_004: 4/28→4/29连续收盘 + /// 10_EOD_005: 4/28收盘 → 4/30收盘 + #endregion + /// ================================================================ */ + + #region 2.1 盘中平仓场景 + + /// + /// [10_001] 算头不算尾 - 首日起息日平仓 + /// --------------------------------------------------------------- + /// 场景:2026-04-28(起息日StartDate)盘中执行全平 + /// 前置:无上一日EOD持仓 + /// 操作:valueDate=2026-04-28,执行"全平" + /// 口径:算头不算尾 + /// - 算头:计息开始日 S=4/28(起息日) + /// - 不算尾:计息结束日 E=4/27(前一日) + /// - 计息天数 = E - S = 4/27 - 4/28 = -1 → 0天 + /// 期望:计息天数=0天,利息=0 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_10_PRE_001() + { + var interest = CalcUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m); + Assert.AreEqual(0m, interest.InterestAmount); + } + + /// + /// [10_002] 算头不算尾 - 次日全平(基准场景) + /// --------------------------------------------------------------- + /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行全平 + /// 前置:无上一日EOD持仓 + /// 操作:valueDate=2026-04-29,执行"全平" + /// 取率:前一营业日规则 → 取2026-04-27的FR007=0.10% + /// 口径:算头不算尾 + /// - 算头:S=4/28(起息日) + /// - 不算尾:E=4/28(操作日前一日) + /// - 计息天数 = 4/28 - 4/28 = 1天 + /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_10_PRE_002() + { + var interest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + Assert.AreEqual(expected, interest.InterestAmount); + } + + /// + /// [10_003] 算头不算尾 - 次日平仓50% + /// --------------------------------------------------------------- + /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行平仓一半 + /// 操作:valueDate=2026-04-29,执行"平仓50%"(closePercent=50%) + /// 取率:前一营业日规则 → FR007@2026-04-27=0.10% + /// 口径:算头不算尾,计息天数=1天 + /// 期望:计息天数=1天,利息=0.5*1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_10_PRE_003() + { + var interest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m); + Assert.AreEqual(expected, interest.InterestAmount); + } + + /// + /// [10_004] 算头不算尾 - 次日全平后收盘 + /// --------------------------------------------------------------- + /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中全平;2026-04-29 收盘 + /// 操作: + /// 1. 2026-04-29 盘中执行"全平" → 计息1天 + /// 2. 2026-04-29 执行收盘归档 → 待实现利息=0 + /// 期望: + /// - 全平应计利息=1天 + /// - 收盘待实现利息=0(因持仓已不存在) + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_10_PRE_004() + { + // 第一步:全平计息 + var unwindInterest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); + var expectedUnwind = ExpectedInterest(1, FixedRate, 0.001m, Principal); + Assert.AreEqual(expectedUnwind, unwindInterest.InterestAmount); + + // 第二步:收盘(持仓已不存在,利息=0) + Console.WriteLine("全平后收盘,待实现利息=0(持仓已不存在)"); + } + + /// + /// [10_005] 算头不算尾 - 第3日全平(跨周末) + /// --------------------------------------------------------------- + /// 场景:2026-04-28 盘中未平仓;2026-04-30(第3个工作日)盘中全平 + /// 背景:4/28(周二)→4/29(周三)→4/30(周四),跨2个自然日 + /// 操作:valueDate=2026-04-30,执行"全平" + /// 取率:按"前一营业日"规则,沿用首个周期取率日 2026-04-27 + /// 口径:算头不算尾 + /// - 算头:S=4/28(起息日) + /// - 不算尾:E=4/30(操作日前一日) + /// - 计息天数 = 4/30 - 4/28 = 2天 + /// 实际计算:持仓期间为4/28~4/29(算头不算尾)=2天 + /// 期望:计息天数=2天,利息=2*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_10_PRE_005() + { + var interest = CalcUnwind("10", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m); + var expected = ExpectedInterest(2, FixedRate, 0.001m, Principal); + Assert.AreEqual(expected, interest.InterestAmount); + } + + /// + /// [10_006] 算头不算尾 - 第3日平仓50%(跨周末) + /// --------------------------------------------------------------- + /// 场景:2026-04-28 盘中未平仓;2026-04-30 盘中平仓一半 + /// 操作:valueDate=2026-04-30,执行"平仓50%" + /// 取率:FR007@2026-04-27=0.10% + /// 口径:算头不算尾,计息天数=2天 + /// 期望:计息天数=2天,利息=0.5*2*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_10_PRE_006() + { + var interest = CalcUnwind("10", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 0.5m); + var expected = ExpectedInterest(2, FixedRate, 0.001m, Principal * 0.5m); + Assert.AreEqual(expected, interest.InterestAmount); + } + + /// + /// [10_007] 算头不算尾 - 次日半平 + 第3日收盘 + /// --------------------------------------------------------------- + /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中平仓一半;2026-04-30 收盘 + /// 操作: + /// 1. 2026-04-29 盘中"平仓50%" → 剩余50%持仓 + /// 2. 2026-04-30 执行收盘归档 → 剩余50%持仓计息 + /// 取率:FR007@2026-04-27=0.10% + /// 口径:算头不算尾 + /// 期望: + /// - 4/29全平利息=0.5*1*(1.00%+0.10%)*1000/365 + /// - 4/30收盘利息=0.5*1*(1.00%+0.10%)*1000/365(剩余50%计1天) + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_10_PRE_007() + { + // 第一步:4月29日平仓50% + var unwindInterest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m); + var expectedUnwind = ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m); + Assert.AreEqual(expectedUnwind, unwindInterest.InterestAmount); + + // 第二步:4月30日收盘(剩余50%持仓计息1天) + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal * 0.5m, 0.001m, + ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m)) + }; + var eodInterest = CalcEod("10", new DateTime(2026, 4, 30), eodPositions); + var expectedEod = ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m); + Assert.AreEqual(expectedEod, eodInterest.InterestAmount); + } + + /// + /// [10_008] 算头不算尾 - 第3日直接收盘(未平仓) + /// --------------------------------------------------------------- + /// 场景:2026-04-28 盘中未平仓;2026-04-29 已收盘归档;2026-04-30 收盘 + /// 背景:持仓期间4/28→4/29已完成收盘归档 + /// 操作:直接执行 2026-04-30 收盘归档 + /// 取率:FR007@2026-04-27=0.10% + /// 口径:算头不算尾 + /// 期望:2026-04-30 收盘待实现利息=1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_10_PRE_008() + { + // 4月29日收盘归档后,4月30日收盘 + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal, 0.001m, + ExpectedInterest(1, FixedRate, 0.001m, Principal)) + }; + var eodInterest = CalcEod("10", new DateTime(2026, 4, 30), eodPositions); + var expectedEod = ExpectedInterest(1, FixedRate, 0.001m, Principal); + Assert.AreEqual(expectedEod, eodInterest.InterestAmount); + } + + /// + /// [10_009] 算头不算尾 - 次日自动互换 + /// --------------------------------------------------------------- + /// 场景:2026-04-29 执行"自动互换" + /// 背景:自动互换是互换交易的一种定期重置操作 + /// 操作:2026-04-29 执行"自动互换" + /// 取率:FR007@2026-04-27=0.10% + /// 口径:算头不算尾 + /// 期望: + /// - 计息天数=1天 + /// - 利息=1*(1.00%+0.10%)*1000/365 + /// - 当日收盘待实现利息=0(持仓已互换) + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_10_PRE_009() + { + var interest = CalcAutoSwap("10", new DateTime(2026, 4, 29), new List()); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + Assert.AreEqual(expected, interest.InterestAmount); + Console.WriteLine("自动互换后,当日收盘待实现利息=0"); + } + + /// + /// [10_010] 算头不算尾 - 自动互换后次日平仓 + /// --------------------------------------------------------------- + /// 场景:2026-04-29 已发生自动互换;2026-04-30 执行"全平/收益结算" + /// 背景:自动互换已将持仓重置,累计利息清零 + /// 操作:valueDate=2026-04-30,执行"全平" + /// 口径:算头不算尾 + /// 期望:计息天数=0天,利息=0(持仓已互换) + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_10_PRE_010() + { + // 4月29日自动互换后的eodPosition(自动互换后累计利息清零) + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal, 0.001m, 0m) + }; + // 4月30日平仓(持仓已互换,计息天数=0) + var interest = CalcUnwind("10", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m, eodPositions); + Assert.AreEqual(0m, interest.InterestAmount); + } + + /// + /// [10_011] 算头不算尾 - 跨重置周期全平 + /// --------------------------------------------------------------- + /// 场景:2026-04-28 未平仓;2026-05-06 跨重置周期全平 + /// 背景: + /// - ResetPeriod=3天 + /// - 第一周期:4/28→4/30,取FR007@4/27=0.10% + /// - 第二周期:5/1→5/6,取FR007@4/30=0.20% + /// 操作: + /// 1. 2026-04-29 收盘归档 + /// 2. 2026-05-06 全平(跨周期) + /// 口径:算头不算尾 + /// 取率:分段取率 + /// - 4/29收盘利息=1天@0.10% + /// - 4/30持仓利息=1天@0.10%(第一周期最后一天) + /// - 5/1~5/5持仓利息=5天@0.20%(第二周期) + /// 期望:利息 = 4/29收盘 + 4/30持仓 + 5/1~5/5持仓 = oneDay*2 + secondPeriod + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_10_PRE_011() + { + // 4月29日收盘归档 + var oneDay = ExpectedInterest(1, FixedRate, 0.001m, Principal); + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal, 0.001m, oneDay) + }; + + // 5月6日全平(跨周期) + var interest = CalcUnwind("10", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, eodPositions); + // 4/30: 1天@0.10%(第一周期),5/1~5/5: 5天@0.20%(第二周期) + var secondPeriod = ExpectedInterest(5, FixedRate, 0.002m, Principal); + // 累计利息 = 4/29收盘利息 + 4/30持仓利息(同第一周期) + 5/1~5/5利息 + var expected = Math.Round(oneDay * 2 + secondPeriod, + ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + Assert.AreEqual(expected, interest.InterestAmount); + } + + /// + /// [10_012] 算头不算尾 - 跨重置周期全平(中间无收盘) + /// --------------------------------------------------------------- + /// 场景:2026-04-28 起息;5/6 全平(中间4/29未收盘) + /// 背景: + /// - ResetPeriod=3天 + /// - 第一周期:4/28→4/30,取FR007@4/27=0.10% + /// - 第二周期:5/1→5/6,取FR007@4/30=0.20% + /// 操作:4/28起息后,4/29未收盘,直接5/6全平 + /// 口径:算头不算尾 + /// 取率:分段取率 + /// - 4/28~4/30持仓利息=3天@0.10%(第一周期,4/28算头) + /// - 5/1~5/5持仓利息=5天@0.20%(第二周期) + /// 期望:利息 = 3天@0.10% + 5天@0.20% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_10_PRE_012() + { + // 4/28起息,无EOD持仓(4/29未收盘) + var eodPositions = new List(); + + // 5月6日全平(跨周期,4/29未收盘) + var interest = CalcUnwind("10", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, eodPositions); + // 4/28~4/30: 3天@0.10%(第一周期),5/1~5/5: 5天@0.20%(第二周期) + var firstPeriod = ExpectedInterest(3, FixedRate, 0.001m, Principal); + var secondPeriod = ExpectedInterest(5, FixedRate, 0.002m, Principal); + var expected = Math.Round(firstPeriod + secondPeriod, + ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + Assert.AreEqual(expected, interest.InterestAmount); + } + + #endregion + + #region 2.2 收盘归档场景(文档4.2节 - 组B) + + /// + /// [10_PRE_EOD_001] 算头不算尾 - 首日收盘归档(文档4.2节) + /// --------------------------------------------------------------- + /// 场景:2026-04-28 收盘 + /// 操作:执行 2026-04-28 EOD + /// 取率日:2026-04-27(FR007=0.10%) + /// 口径:算头不算尾 + /// 说明:首日收盘,当日计息1天 + /// 期望:当日收盘利息(待实现)=1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_10_PRE_EOD_001() + { + var interest = CalcEod("10", new DateTime(2026, 4, 28), new List()); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + Assert.AreEqual(expected, interest.InterestAmount); + } + + /// + /// [10_PRE_EOD_002] 算头不算尾 - 首日收盘,次日全平(文档4.2节) + /// --------------------------------------------------------------- + /// 场景:2026-04-28 已收盘;2026-04-29 盘中全平或收益结算 + /// 操作:valueDate=2026-04-29 执行"全平/收益结算" + /// 取率日:2026-04-27(FR007=0.10%) + /// 口径:算头不算尾 + /// - 持仓区间:4/28~4/29 + /// - 计息区间:4/29-4/28=1天 + /// 期望:计息天数=1;利息=1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_10_PRE_EOD_002() + { + // 4/28收盘,利息=1天 + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FixedRate, 0.001m, Principal)) + }; + var interest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, eodPositions); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + Assert.AreEqual(expected, interest.InterestAmount); + } + + /// + /// [10_PRE_EOD_003] 算头不算尾 - 首日收盘,次日平仓50%(文档4.2节) + /// --------------------------------------------------------------- + /// 场景:2026-04-28 已收盘;2026-04-29 盘中平仓一半 + /// 操作:valueDate=2026-04-29 执行"平仓50%" + /// 取率日:2026-04-27(FR007=0.10%) + /// 口径:算头不算尾 + /// 期望:计息天数=1;利息=0.5*1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_10_PRE_EOD_003() + { + // 4/28收盘,利息=1天 + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FixedRate, 0.001m, Principal)) + }; + var interest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, eodPositions); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m); + Assert.AreEqual(expected, interest.InterestAmount); + } + + #endregion + + #region 2.3 代码额外补充的收盘场景 + + /// + /// [10_EOD_001] 算头不算尾 - 首日收盘归档(代码实现版) + /// --------------------------------------------------------------- + /// 场景:2026-04-28(起息日)执行收盘EOD归档 + /// 前置:无上一日EOD持仓(首次收盘) + /// 操作:执行 2026-04-28 收盘归档 + /// 口径:算头不算尾 + /// 说明:算头,4/28起息日算利息;不算尾指到期日不算 + /// - 算头:S=4/28 + /// - 不算尾:E=4/27(到期日4/28不算) + /// 期望:当日收盘利息=1天,利息=1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_10_EOD_001() + { + var interest = CalcEod("10", new DateTime(2026, 4, 28), new List()); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + Assert.AreEqual(expected, interest.InterestAmount); + } + + /// + /// [10_EOD_002] 算头不算尾 - 首日收盘,次日全平 + /// --------------------------------------------------------------- + /// 场景:2026-04-28 已收盘归档;2026-04-29 盘中执行全平 + /// 前置:存在4/28的EOD持仓记录(待实现利息=1天) + /// 操作:valueDate=2026-04-29,执行"全平" + /// 口径:算头不算尾 + /// - 算头:4/28起息日算利息 + /// - 不算尾:4/29到期日不算利息 + /// - 历史待实现:4/28=1天 + /// - 当期利息:4/29=0天(不算尾) + /// 期望:总利息=1天+0天=1天,利息=1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_10_EOD_002() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FixedRate, 0.001m, Principal)) + }; + + var interest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, eodPositions); + // 平仓利息 = 历史待实现(1天) + 当期(0天) = 1天 + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + Assert.AreEqual(expected, interest.InterestAmount); + } + + /// + /// [10_EOD_003] 算头不算尾 - 首日收盘,次日平仓50% + /// --------------------------------------------------------------- + /// 场景:2026-04-28 已收盘归档;2026-04-29 盘中执行平仓一半 + /// 操作:valueDate=2026-04-29,执行"平仓50%" + /// 口径:算头不算尾,计息天数=1天 + /// 期望:总利息=(历史1天+当期0天)*50%=0.5天,利息=0.5*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_10_EOD_003() + { + // 4/28收盘(算头=1天利息),4/29平仓50% + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FixedRate, 0.001m, Principal)) + }; + + // 平仓50%:总利息=(历史1天+当期0天)*50%=0.5天 + var interest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, eodPositions); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m); + Assert.AreEqual(expected, interest.InterestAmount); + } + + /// + /// [10_EOD_004] 算头不算尾 - 连续收盘(4/28、4/29) + /// --------------------------------------------------------------- + /// 场景:2026-04-28 和 2026-04-29 连续两个工作日收盘归档 + /// 操作: + /// 1. 执行 2026-04-28 收盘归档 + /// 2. 执行 2026-04-29 收盘归档 + /// 口径:算头不算尾 + /// 期望: + /// - 4/28收盘利息=1天(算头,首日计息) + /// - 4/29收盘利息=1天 + 4/28累计利息 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_10_EOD_004() + { + // 4月28日收盘(利息=1天,算头) + var eod1 = CalcEod("10", new DateTime(2026, 4, 28), new List()); + var expected1 = ExpectedInterest(1, FixedRate, 0.001m, Principal); + Assert.AreEqual(expected1, eod1.InterestAmount); + + // 4月29日收盘(利息=1天 + 4/28累计利息) + var eod2 = CalcEod("10", new DateTime(2026, 4, 29), new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, expected1) + }); + // 4/29收盘利息 = 4/28累计利息 + var expected2 = ExpectedInterest(1, FixedRate, 0.001m, Principal); + Assert.AreEqual(expected2, eod2.InterestAmount); + } + + /// + /// [10_EOD_005] 算头不算尾 - 首日收盘后第3日收盘 + /// --------------------------------------------------------------- + /// 场景:2026-04-28 已收盘归档;2026-04-30 执行收盘归档 + /// 背景:4/29(周三)未执行收盘归档 + /// 操作:执行 2026-04-30 收盘归档 + /// 口径:算头不算尾 + /// 期望:4/29收盘利息=1天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_10_EOD_005() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, 0m) + }; + + var interest = CalcEod("10", new DateTime(2026, 4, 30), eodPositions); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + Assert.AreEqual(expected, interest.InterestAmount); + } + + /// + /// [10_EOD_006] 算头不算尾 - 到期日收盘不算尾 + /// --------------------------------------------------------------- + /// 场景:2026-04-28 起息,2027-04-27 到期(ExerciseDate) + /// 操作:2027-04-27 执行收盘归档 + /// 口径:算头不算尾("10") + /// - 算头:首日4/28计息 + /// - 不算尾:到期日4/27不计息 + /// 期望:到期日收盘利息=0(到期日不算尾) + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_10_EOD_006() + { + // 2027-04-26 收盘归档产生的 EOD 持仓 + // 假设累计利息为 InterestProfitSum=10 + var eodPositions = new List + { + CreateEodPosition(new DateTime(2027, 4, 26), Principal, 0.001m, 10m) + }; + + // 到期日 2027-04-27 收盘(不算尾,利息=0) + var interest = CalcEod("10", new DateTime(2027, 4, 27), eodPositions); + Assert.AreEqual(0m, interest.InterestAmount); + } + + /// + /// [11_EOD_006] 算头算尾 - 到期日收盘算尾 + /// --------------------------------------------------------------- + /// 场景:2026-04-28 起息,2027-04-27 到期(ExerciseDate) + /// 操作:2027-04-27 执行收盘归档 + /// 口径:算头算尾("11") + /// - 算头:首日4/28计息 + /// - 算尾:到期日4/27计息 + /// 期望:到期日收盘利息=1天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_11_EOD_006() + { + // 2027-04-26 收盘归档产生的 EOD 持仓 + // 假设累计利息为 InterestProfitSum=10 + var eodPositions = new List + { + CreateEodPosition(new DateTime(2027, 4, 26), Principal, 0.001m, 10m) + }; + + // 到期日 2027-04-27 收盘(算尾,利息=1天) + var interest = CalcEod("11", new DateTime(2027, 4, 27), eodPositions); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + Assert.AreEqual(expected, interest.InterestAmount); + } + + #endregion + + #region 2.3 当前营业日规则(interest_rule=0) + + /// + /// [10_CUR_001] 算头不算尾 + 当前营业日规则 - 次日全平 + /// --------------------------------------------------------------- + /// 场景:算头不算尾("10");interest_rule=0(当前营业日) + /// 操作:2026-04-28 未收盘;2026-04-29 盘中全平 + /// 前置:提供 FR007@2026-04-29 数据 + /// 取率:当前营业日规则 → 取当日 FR007@2026-04-29=0.10% + /// 口径:算头不算尾,计息天数=1天 + /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_10_CUR_001() + { + var interest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, InterestRule_Cur); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + Assert.AreEqual(expected, interest.InterestAmount); + } + + /// + /// [10_CUR_002] 算头不算尾 + 当前营业日规则 - 第3日全平 + /// --------------------------------------------------------------- + /// 场景:算头不算尾("10");interest_rule=0(当前营业日) + /// 操作:2026-04-28 未收盘;2026-04-30 盘中全平 + /// 前置:提供 FR007@2026-04-30 数据 + /// 取率:ResetPeriod=3天,从4/28到4/30=2天<3天(重置周期内) + /// 应取起息日利率 FR007@2026-04-28=0.10% + /// 口径:算头不算尾,计息天数=2天 + /// 期望:计息天数=2天,利息=2*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_10_CUR_002() + { + var interest = CalcUnwind("10", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m, InterestRule_Cur); + var expected = ExpectedInterest(2, FixedRate, 0.001m, Principal); + Assert.AreEqual(expected, interest.InterestAmount); + } + + #endregion + + #endregion + + #region 场景3:不算头算尾 (InterestCalcMode="01") + #region 计息区间说明: + /// 01_001: 首日(4/28)平仓 → S=4/29, E=4/28 → 0天 + /// 01_002: 次日(4/29)全平 → S=4/29, E=4/29 → 1天 + /// 01_003: 次日(4/29)半平 → 1天×50% + /// 01_004: 第3日(4/30)全平 → S=4/29, E=4/30 → 1天 + /// 01_005: 跨周期(5/6)全平 → 0天 + /// 01_EOD_001: 首日(4/28)收盘 → 0天 + /// 01_EOD_002: 4/28收盘 → 4/29全平 → 1天 + #endregion + /// ================================================================ */ + + /// + /// [01_001] 不算头算尾 - 首日起息日平仓 + /// --------------------------------------------------------------- + /// 场景:2026-04-28(起息日StartDate)盘中执行全平 + /// 口径:不算头算尾 + /// - 不算头:S=4/29(起息日次日) + /// - 算尾:E=4/28(操作日) + /// - 计息天数 = 4/28 - 4/29 = -1 → 0天 + /// 期望:计息天数=0天,利息=0 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_01_PRE_001() + { + var interest = CalcUnwind("01", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m); + Assert.AreEqual(0m, interest.InterestAmount); + } + + /// + /// [01_002] 不算头算尾 - 次日全平 + /// --------------------------------------------------------------- + /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行全平 + /// 口径:不算头算尾 + /// - 不算头:S=4/29(下一日起息) + /// - 算尾:E=4/29(操作日) + /// - 计息天数 = 4/29 - 4/29 = 1天 + /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_01_PRE_002() + { + var interest = CalcUnwind("01", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + Assert.AreEqual(expected, interest.InterestAmount); + } + + /// + /// [01_003] 不算头算尾 - 次日平仓50% + /// --------------------------------------------------------------- + /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行平仓一半 + /// 口径:不算头算尾,计息天数=1天 + /// 期望:计息天数=1天,利息=0.5*1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_01_PRE_003() + { + var interest = CalcUnwind("01", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m); + Assert.AreEqual(expected, interest.InterestAmount); + } + + /// + /// [01_004] 不算头算尾 - 第3日全平 + /// --------------------------------------------------------------- + /// 场景:2026-04-28 盘中未平仓;2026-04-30 盘中全平 + /// 口径:不算头算尾 + /// - 不算头:S=4/29(下一日起息) + /// - 算尾:E=4/30(操作日) + /// - 计息天数 = 4/30 - 4/29 = 1天 + /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_01_PRE_004() + { + var interest = CalcUnwind("01", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + Assert.AreEqual(expected, interest.InterestAmount); + } + + /// + /// [01_EOD_001] 不算头算尾 - 首日收盘归档 + /// --------------------------------------------------------------- + /// 场景:2026-04-28(起息日)执行收盘EOD归档 + /// 口径:不算头算尾 + /// - 不算头:S=4/29 + /// - 算尾:E=4/28 → 计息天数=0 + /// 期望:当日收盘利息=0 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_01_EOD_001() + { + var interest = CalcEod("01", new DateTime(2026, 4, 28), new List()); + Assert.AreEqual(0m, interest.InterestAmount); + } + + /// + /// [01_EOD_002] 不算头算尾 - 前日已收盘,次日全平 + /// --------------------------------------------------------------- + /// 场景:2026-04-28 已收盘归档;2026-04-29 盘中执行全平 + /// 口径:不算头算尾 + /// - 不算头:S=4/29 + /// - 算尾:E=4/29 + /// - 计息天数=1天 + /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_01_EOD_002() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, 0m) + }; + var interest = CalcUnwind("01", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, eodPositions); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + Assert.AreEqual(expected, interest.InterestAmount); + } + + /// + /// [01_005] 不算头算尾 - 跨周期全平 + /// --------------------------------------------------------------- + /// 场景:2026-04-28 未平仓;2026-05-06 跨周期全平 + /// 口径:不算头算尾 + /// - 不算头:S=5/7(下一周期起息日) + /// - 算尾:E=5/6 + /// - 计息天数 = 5/6 - 5/7 = -1 → 0天 + /// 期望:计息天数=0天,利息=0 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_01_PRE_005() + { + var interest = CalcUnwind("01", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m); + Assert.AreEqual(0m, interest.InterestAmount); + } + + #endregion + + #region 场景4:不算头不算尾 (InterestCalcMode="00") + #region 计息区间说明: + /// 00_001: 首日(4/28)平仓 → S=4/29, E=4/27 → 0天 + /// 00_002: 次日(4/29)全平 → S=4/29, E=4/28 → 0天 + /// 00_003: 第3日(4/30)全平 → S=4/29, E=4/29 → 0天 + /// 00_004: 跨周期(5/6)全平 → 0天 + /// 00_EOD_001: 首日(4/28)收盘 → 0天 + /// 00_EOD_002: 4/28收盘 → 4/29全平 → 0天 + #endregion + /// ================================================================ */ + + /// + /// [00_001] 不算头不算尾 - 首日起息日平仓 + /// --------------------------------------------------------------- + /// 场景:2026-04-28(起息日StartDate)盘中执行全平 + /// 口径:不算头不算尾 + /// - 不算头:S=4/29 + /// - 不算尾:E=4/27 + /// - 计息天数 = 4/27 - 4/29 = -2 → 0天 + /// 期望:计息天数=0天,利息=0 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_00_PRE_001() + { + var interest = CalcUnwind("00", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m); + Assert.AreEqual(0m, interest.InterestAmount); + } + + /// + /// [00_002] 不算头不算尾 - 次日全平 + /// --------------------------------------------------------------- + /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行全平 + /// 口径:不算头不算尾 + /// - 不算头:利息从4/29开始(跨到下一周期) + /// - 不算尾:E=4/28 + /// - 计息区间:4/29-5/1 → 1天 + /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_00_PRE_002() + { + var interest = CalcUnwind("00", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + Assert.AreEqual(expected, interest.InterestAmount); + } + + /// + /// [00_003] 不算头不算尾 - 第3日全平 + /// --------------------------------------------------------------- + /// 场景:2026-04-28 盘中未平仓;2026-04-30 盘中全平 + /// 口径:不算头不算尾 + /// - 不算头:利息从4/30开始(跨到下一周期) + /// - 不算尾:E=4/29(减1天) + /// - 计息区间:4/30-5/1 → 1天 + /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_00_PRE_003() + { + var interest = CalcUnwind("00", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + Assert.AreEqual(expected, interest.InterestAmount); + } + + /// + /// [00_004] 不算头不算尾 - 跨周期全平 + /// --------------------------------------------------------------- + /// 场景:2026-04-28 未平仓;2026-05-06 跨周期全平 + /// 口径:不算头不算尾 + /// - 不算头:S=5/7 + /// - 不算尾:E=5/5 + /// - 计息天数 = 5/5 - 5/7 = -2 → 0天 + /// 期望:计息天数=0天,利息=0 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_00_PRE_004() + { + var interest = CalcUnwind("00", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m); + Assert.AreEqual(0m, interest.InterestAmount); + } + + /// + /// [00_EOD_001] 不算头不算尾 - 首日收盘归档 + /// --------------------------------------------------------------- + /// 场景:2026-04-28(起息日)执行收盘EOD归档 + /// 口径:不算头不算尾 + /// - 不算头:S=4/29 + /// - 不算尾:E=4/27 → 计息天数=0 + /// 期望:当日收盘利息=0 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_00_EOD_001() + { + var interest = CalcEod("00", new DateTime(2026, 4, 28), new List()); + Assert.AreEqual(0m, interest.InterestAmount); + } + + /// + /// [00_EOD_002] 不算头不算尾 - 前日已收盘,次日全平 + /// --------------------------------------------------------------- + /// 场景:2026-04-28 已收盘归档;2026-04-29 盘中执行全平 + /// 口径:不算头不算尾 + /// - 不算头:利息从4/29开始(跨到下一周期) + /// - 不算尾:E=4/28(减1天) + /// - 计息区间:4/29-5/1 → 1天 + /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_00_EOD_002() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, 0m) + }; + var interest = CalcUnwind("00", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, eodPositions); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + Assert.AreEqual(expected, interest.InterestAmount); + } + + #endregion + + #region 场景5:口径对比验证 + #region 对比测试说明: + /// COMPARE_001: 同一日(4/29)全平,4种口径对比 + /// COMPARE_002: 同一日(4/30)全平,4种口径对比 + #endregion + /// ================================================================ */ + + /// + /// [COMPARE_001] 口径对比 - 同一日(4/29)全平,4种口径对比验证 + /// --------------------------------------------------------------- + /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行全平 + /// 操作:对同一操作日(4/29)分别用4种计息口径执行"全平" + /// 对比结果: + /// - "11"算头算尾: S=4/28, E=4/29 → 2天 + /// - "10"算头不算尾: S=4/28, E=4/28 → 1天 + /// - "01"不算头算尾: S=4/29, E=4/29 → 1天 + /// - "00"不算头不算尾: S=4/29, E=5/1 → 1天(中间日期跨周期) + /// 期望:验证4种口径的差异符合预期 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_COMPARE_001() + { + // "11"算头算尾: S=4/28, E=4/29 => 2天 + var interest11 = CalcUnwind("11", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); + Assert.AreEqual(ExpectedInterest(2, FixedRate, 0.001m, Principal), interest11.InterestAmount); + + // "10"算头不算尾: S=4/28, E=4/28 => 1天 + var interest10 = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); + Assert.AreEqual(ExpectedInterest(1, FixedRate, 0.001m, Principal), interest10.InterestAmount); + + // "01"不算头算尾: S=4/29, E=4/29 => 1天 + var interest01 = CalcUnwind("01", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); + Assert.AreEqual(ExpectedInterest(1, FixedRate, 0.001m, Principal), interest01.InterestAmount); + + // "00"不算头不算尾: S=4/29, E=5/1 => 1天(中间日期跨周期) + var interest00 = CalcUnwind("00", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); + Assert.AreEqual(ExpectedInterest(1, FixedRate, 0.001m, Principal), interest00.InterestAmount); + } + + /// + /// [COMPARE_002] 口径对比 - 同一日(4/30)全平,4种口径对比验证 + /// --------------------------------------------------------------- + /// 场景:2026-04-28 盘中未平仓;2026-04-30 盘中执行全平(跨周末) + /// 操作:对同一操作日(4/30)分别用4种计息口径执行"全平" + /// 对比结果: + /// - "11"算头算尾: S=4/28, E=4/30 → 3天 + /// - "10"算头不算尾: S=4/28, E=4/29 → 2天 + /// - "01"不算头算尾: S=4/29, E=4/30 → 1天 + /// - "00"不算头不算尾: S=4/30, E=5/1 → 1天(中间日期跨周期) + /// 期望:验证4种口径的差异符合预期 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_COMPARE_002() + { + // "11"算头算尾: S=4/28, E=4/30 => 3天 + var interest11 = CalcUnwind("11", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m); + Assert.AreEqual(ExpectedInterest(3, FixedRate, 0.001m, Principal), interest11.InterestAmount); + + // "10"算头不算尾: S=4/28, E=4/29 => 2天 + var interest10 = CalcUnwind("10", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m); + Assert.AreEqual(ExpectedInterest(2, FixedRate, 0.001m, Principal), interest10.InterestAmount); + + // "01"不算头算尾: S=4/29, E=4/30 => 1天 + var interest01 = CalcUnwind("01", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m); + Assert.AreEqual(ExpectedInterest(1, FixedRate, 0.001m, Principal), interest01.InterestAmount); + + // "00"不算头不算尾: S=4/30, E=5/1 => 1天(中间日期跨周期) + var interest00 = CalcUnwind("00", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m); + Assert.AreEqual(ExpectedInterest(1, FixedRate, 0.001m, Principal), interest00.InterestAmount); + } + + #endregion + + } +} diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 7a9f793f..1b1503b4 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -1,4 +1,4 @@ -using MoreLinq.Extensions; +using MoreLinq.Extensions; using Newtonsoft.Json; using System.Linq.Expressions; using YLErp.BLL; @@ -14,6 +14,11 @@ namespace YLErp.Modules.SwapModule { public class SwapDealService : SwapTradeBaseService { + protected virtual bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate) + { + return EodPriceQueryService.TryGetPrice(valueDate, underlyingCode, out rate); + } + public SwapDealService(OptUserInfo optUser) : base(optUser) { @@ -36,7 +41,7 @@ namespace YLErp.Modules.SwapModule bool commodity = ConsGlobal.InstrumentType.CalcTypeIsFutures(um.UnderlyingInstrumentType); List eventTyps = new List() { (int)SwapEventTypeEnum.自动互换, (int)SwapEventTypeEnum.互换 }; var dealDate = valuedateBLL.ValueDate <= td.ExerciseDate.Value ? valuedateBLL.ValueDate : td.ExerciseDate.Value; - CheckLastEod(dealDate, td.TradeDate.Value, tradeId); + //CheckLastEod(dealDate, td.TradeDate.Value, tradeId); //去掉平仓收盘限制 var tradeExtend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == tradeId); td.trade_extend = tradeExtend; var position = positions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode) && !x.IsInitial).FirstOrDefault(); @@ -122,7 +127,28 @@ namespace YLErp.Modules.SwapModule { var td = DbContext.trade.Find(tradeId); var dealDate = valuedateBLL.ValueDate <= td.ExerciseDate.Value ? valuedateBLL.ValueDate : td.ExerciseDate.Value; - CheckLastEod(dealDate, td.StartDate.Value, tradeId); + //CheckLastEod(dealDate, td.StartDate.Value, tradeId); + } + /// + /// 校验收益结算操作(不检查收盘限制) + /// + /// + public void CheckEodTradeForIncome(int tradeId) + { + var td = DbContext.trade.Find(tradeId); + // 收益结算不检查收盘限制,只检查交易状态 + if (td.TradeType != "收益互换") + { + throw new ServiceException("该交易不是收益互换类型"); + } + if (td.ValidState == "InValid") + { + throw new ServiceException("该交易已无效"); + } + if (td.TradeStatus != ConsTrade.确认成交 && td.TradeStatus != ConsTrade.提前终止拒绝) + { + throw new ServiceException($"该交易状态为【{td.TradeStatus}】,无法进行收益结算"); + } } /// /// 多空组合 平仓初始化 @@ -140,7 +166,7 @@ namespace YLErp.Modules.SwapModule var positions = DbContext.swap_position.Where(x => x.SwapTradeId == tradeId && x.IsInitial && !x.Invalid); List eventTyps = new List() { (int)SwapEventTypeEnum.平仓, (int)SwapEventTypeEnum.互换, (int)SwapEventTypeEnum.自动互换 }; var dealDate = valuedateBLL.ValueDate <= td.ExerciseDate.Value ? valuedateBLL.ValueDate : td.ExerciseDate.Value; - CheckLastEod(dealDate, td.TradeDate.Value, tradeId); + //CheckLastEod(dealDate, td.TradeDate.Value, tradeId); //去掉平仓收盘限制 var tradeExtend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == tradeId); td.trade_extend = tradeExtend; var preDealDate = GetPreDealDate(tradeId, dealDate, eventTyps); @@ -194,7 +220,7 @@ namespace YLErp.Modules.SwapModule var um = DataCacheProvider.GetUnderlyingDataSource().GetData(td.UnderlyingCode); List eventTypes = new List() { (int)SwapFlowEventTypeEnum.互换, (int)SwapFlowEventTypeEnum.自动互换 }; var dealDate = valuedateBLL.ValueDate < td.ExerciseDate.Value ? valuedateBLL.ValueDate : td.ExerciseDate.Value; - CheckLastEod(dealDate, td.TradeDate.Value, tradeId); + // 收益结算不检查收盘限制 var tradeExtend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == tradeId); td.trade_extend = tradeExtend; var position = positions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode) && !x.IsInitial).FirstOrDefault(); @@ -296,16 +322,17 @@ namespace YLErp.Modules.SwapModule var tradeExtend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == tradeId); List eventTypes = new List() { (int)SwapEventTypeEnum.平仓, (int)SwapEventTypeEnum.互换, (int)SwapEventTypeEnum.自动互换 }; var lastEod = DbContext.eod_swap.Where(x => x.ValueDate < unwindDate && x.SwapTradeId == tradeId).OrderByDescending(o => o.ValueDate).FirstOrDefault(); - var orginPv = lastEod != null ? lastEod.NotionalValue : 0; var _preSetteDate = lastEod == null ? unwindDate.AddDays(-1) : lastEod.ValueDate; List lastEodPositions = new SwapEodPositionService(this).GetPreEodPositions(tradeId, _preSetteDate);//上一交易数据 var posiLongNotionalValue = longPositions.Sum(s => s.PosiNotionalValue);// 剩余名义本金 var posiShortNotionalValue = shortPositions.Sum(s => s.PosiNotionalValue);// 剩余名义本金 var stockEqvNotional = realPostitions.Where(x => x.PosiDirection > 0).Sum(s => s.PosiNotionalValue); var posiNotionalValue = stockEqvNotional * closePercent;//剩余名义本金 + var orginPv = lastEod != null ? lastEod.NotionalValue : stockEqvNotional; var grossPrice = realPostitions.Where(x => x.PosiDirection > 0).FirstOrDefault()?.PosiGrossPrice; bool tdClose = DbContext.swap_flow_event.Any(x => x.SwapTradeId == tradeId && x.UnwindDate == unwindDate && eventTypes.Contains(x.EventType) && x.DataState == (int)SwapFlowDateStateEnum.完成); - interests = GetInterests(td, tradeExtend, valueDate, unwindDate, lastEodPositions, positions, stockEqvNotional, posiLongNotionalValue, posiShortNotionalValue, posiNotionalValue, closePercent, eventType, tdClose, false, grossPrice ?? 0, orginPv, true, false, false); + var calcLastNew = tradeExtend?.ExtendObj?.InterestCalcMode?.EndsWith("1") ?? true; + interests = GetInterests(td, tradeExtend, valueDate, unwindDate, lastEodPositions, positions, stockEqvNotional, posiLongNotionalValue, posiShortNotionalValue, posiNotionalValue, closePercent, eventType, tdClose, false, grossPrice ?? 0, orginPv, true, calcLastNew, false); return interests; } /// @@ -348,120 +375,198 @@ namespace YLErp.Modules.SwapModule { List interests = new List(); var annualDays = tradeExtend == null ? 365 : tradeExtend.ExtendObj.AnnualDays; + bool calcFirst = tradeExtend?.ExtendObj.InterestCalcMode?.StartsWith("1") ?? true; + foreach (var position in positions) { - var _closePosiNotionalValue = closePosiNotionalValue; - var _posiNotionalValue = posiNotionalValue; - var preEodPosition = eodPositions.FirstOrDefault(x => x.PositionId == position.id); - DateTime? preDealDate = null; + // 初始化持仓信息 + var preEodPosition = eodPositions.FirstOrDefault(x => x.PositionId == position.id) ?? new eod_swap_position(); var positionClone = position.Clone(); - var newClosePercent = closePrecent; - if (preEodPosition != null) + DateTime? preDealDate = preEodPosition.id != 0 ? preEodPosition.ValueDate : null; + + // 计算计息区间 + int interestPeriod = position.interest_rest_days ?? 1; + bool swap = InitInterestDate(unwindDate, preDealDate, td, tdClose, calcLast, out DateTime startDate, out DateTime endDate); + + // 计算名义本金 + var (closePrincipal, posiPrincipal, newClosePercent) = CalcNotionalByMode(position, closePrecent, posiNotionalValue, posiLongNotionalValue, posiShortNotionalValue); + if ((InterestModeEnum)position.InterestMode == InterestModeEnum.追加预付金 || (InterestModeEnum)position.InterestMode == InterestModeEnum.初始预付金) { - preDealDate = preEodPosition.ValueDate; - } - var swap = InitInterestDate(unwindDate, preDealDate, td, tdClose, calcLast, out DateTime startDate, out DateTime endDate);//不算头或不算尾情况,无利息 - if (!preDealDate.HasValue) - { - preEodPosition = new eod_swap_position(); - preEodPosition.PosiStartDate = position.PosiStartDate; - preEodPosition.ValueDate = position.PosiStartDate; - } - var swapIntervalToday = position.SwapIntervalList.Where(x => x.Date <= startDate).OrderByDescending(o => o.Date).FirstOrDefault(); - if (position.InterestMode == (int)InterestModeEnum.固定值) - { - _closePosiNotionalValue = position.InterestPrincipalFix; - _posiNotionalValue = position.InterestPrincipalFix; - newClosePercent = 1m; - } - else if (position.InterestMode == (int)InterestModeEnum.多头存续名义本金) - { - _closePosiNotionalValue = posiLongNotionalValue * closePrecent; - _posiNotionalValue = posiLongNotionalValue; - } - else if (position.InterestMode == (int)InterestModeEnum.空头存续名义本金) - { - _closePosiNotionalValue = posiShortNotionalValue * closePrecent; - _posiNotionalValue = posiShortNotionalValue; - } - else if (position.InterestMode == (int)InterestModeEnum.标的期初全价) - { - _closePosiNotionalValue = _posiNotionalValue * closePrecent; - _posiNotionalValue = _posiNotionalValue; - } - else if (position.InterestMode == (int)InterestModeEnum.追加预付金 || position.InterestMode == (int)InterestModeEnum.初始预付金) - { - _closePosiNotionalValue = position.InterestPrincipalFix * closePrecent; - _posiNotionalValue = position.InterestPrincipalFix * closePrecent; positionClone.InterestDirection = position.InterestDirection == (int)SwapDirectionEnum.收取 ? (int)SwapDirectionEnum.支付 : (int)SwapDirectionEnum.收取; } - if (!string.IsNullOrEmpty(position.FloatRateUnderlyingCode)) - { - // 获取重置频率,如果为空则默认为1 - int interestPeriod = position.interest_rest_days ?? 1; - // 计算从 td.StartDate 到 endDate 的天数 - var days = (endDate - td.StartDate.Value).Days; - // 获取合适的 rateDate - DateTime rateDate = GetRateDate(position.interest_rule, td.StartDate.Value, endDate, days, interestPeriod); + // 获取利率 + decimal rate = GetFixedRate(position, startDate); + decimal floatRate = GetFloatRate(position, preEodPosition, td.StartDate.Value, endDate, interestPeriod, swap, positionClone); - // 如果不需要重置,并且上一日已有 FloatRate,则不再查找 - if (preEodPosition.id != 0 && days % interestPeriod != 0) - { - position.FloatRate = preEodPosition.FloatRate; - positionClone.FloatRate = preEodPosition.FloatRate; - } - else - { - // 如果没有 preEodPosition 数据或需要查找新 Rate,则去查询最新的浮动利率 - if (EodPriceQueryService.TryGetPrice(rateDate, position.FloatRateUnderlyingCode, out double floatRate)) - { - position.FloatRate = Convert.ToDecimal(floatRate); - positionClone.FloatRate = position.FloatRate; - } - else if (!swap) - { - throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{rateDate:yyyy年MM月dd日}的价格"); - } - } - } - decimal rate = position.InterestRateDefault; - if (swapIntervalToday == null)//当日无适用观察日 + // 根据场景计算利息 + if (settment) { - var swapInterval = position.SwapIntervalList.Where(x => x.Date > startDate).OrderBy(o => o.Date).FirstOrDefault(); - if (swapInterval != null) - { - rate = swapInterval.Rate; - } + // 收盘归档场景,使用 CalcEodInterest + interests.Add(CalcEodInterest(td, valueDate, positionClone, rate, floatRate, closePrincipal, posiPrincipal, annualDays, calcFirst, calcLast, preEodPosition, eventType, add)); } else { - rate = swapIntervalToday.Rate; + // 盘中互换场景,使用 CalcUnwindInterest + interests.Add(CalcUnwindInterest(td, valueDate, endDate, positionClone, rate, floatRate, posiPrincipal, closePrincipal, newClosePercent, annualDays, preEodPosition, eventType, add, swap, orginPv)); } - if (preEodPosition.id == 0) - { - preEodPosition.FloatRate = positionClone.FloatRate; - preEodPosition.TdInterestPrincipal = _posiNotionalValue; - preEodPosition.PosiNotionalValue = _posiNotionalValue; - } - swap_flow_event interest = InitSwapDealInterest(td, valueDate, endDate, rate, positionClone, add, swap, _posiNotionalValue, _closePosiNotionalValue, newClosePercent, annualDays, eventType, preEodPosition, needPrice, settment, orginPv); - interests.Add(interest); } return interests; } + /// - /// 根据给定条件获取 rateDate + /// 根据计息模式计算名义本金 /// - private DateTime GetRateDate(int? interest_rule, DateTime startDate, DateTime endDate, int days, int interestPeriod) + private (decimal close, decimal posi, decimal closePct) CalcNotionalByMode(swap_position position, decimal closePercent, decimal posiNotional, decimal posiLong, decimal posiShort) { - // 判断是否达到重置周期 - if (days % interestPeriod == 0) + decimal closePrincipal = posiNotional; + decimal posiPrincipal = posiNotional; + decimal newClosePercent = closePercent; + + switch ((InterestModeEnum)position.InterestMode) { - return QdpCalendarHelper.GetNonHolidayDefore(endDate.AddDays(interest_rule ?? 0)); + case InterestModeEnum.固定值: + closePrincipal = posiPrincipal = position.InterestPrincipalFix; + newClosePercent = 1m; + break; + case InterestModeEnum.多头存续名义本金: + closePrincipal = posiLong * closePercent; + posiPrincipal = posiLong; + break; + case InterestModeEnum.空头存续名义本金: + closePrincipal = posiShort * closePercent; + posiPrincipal = posiShort; + break; + case InterestModeEnum.标的期初全价: + closePrincipal = posiNotional * closePercent; + break; + case InterestModeEnum.追加预付金: + case InterestModeEnum.初始预付金: + closePrincipal = position.InterestPrincipalFix * closePercent; + posiPrincipal = position.InterestPrincipalFix; + break; + } + return (closePrincipal, posiPrincipal, newClosePercent); + } + + /// + /// 获取固定利率 + /// + private decimal GetFixedRate(swap_position position, DateTime startDate) + { + var swapIntervalToday = position.SwapIntervalList?.Where(x => x.Date <= startDate).OrderByDescending(o => o.Date).FirstOrDefault(); + if (swapIntervalToday != null) return swapIntervalToday.Rate; + var nextInterval = position.SwapIntervalList?.Where(x => x.Date > startDate).OrderBy(o => o.Date).FirstOrDefault(); + return nextInterval?.Rate ?? position.InterestRateDefault; + } + + /// + /// 获取浮动利率 + /// + private decimal GetFloatRate(swap_position position, eod_swap_position preEod, DateTime startDate, DateTime endDate, int period, bool swap, swap_position positionClone) + { + if (string.IsNullOrEmpty(position.FloatRateUnderlyingCode)) return position.FloatRate; + + int days = (endDate - startDate).Days; + DateTime rateDate = days % period == 0 + ? QdpCalendarHelper.GetNonHolidayDefore(endDate.AddDays(position.interest_rule ?? 0)) + : QdpCalendarHelper.GetNonHolidayDefore(startDate.AddDays(position.interest_rule ?? 0)); + + if (preEod.id != 0 && days % period != 0) + { + position.FloatRate = positionClone.FloatRate = preEod.FloatRate; + return preEod.FloatRate; } - // 如果不在重置周期内,使用 td.StartDate 来获取 rateDate - return QdpCalendarHelper.GetNonHolidayDefore(startDate.AddDays(interest_rule ?? 0)); + if (TryGetFloatRate(rateDate, position.FloatRateUnderlyingCode, out double rate)) + { + position.FloatRate = positionClone.FloatRate = Convert.ToDecimal(rate); + return position.FloatRate; + } + if (!swap) throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{rateDate:yyyy年MM月dd日}的价格"); + return 0m; + } + + /// + /// 计算收盘利息(EOD) + /// + private swap_flow_event CalcEodInterest(trade td, DateTime valueDate, swap_position position, decimal rate, decimal floatRate, decimal closePrincipal, decimal posiPrincipal, int annualDays, bool calcFirst, bool calcLast, eod_swap_position preEod, int eventType, bool add) + { + // 判断当日是否计息:首日不算头或到期日不算尾则不计息 + bool calcToday = true; + if (calcFirst == false && valueDate == td.StartDate.Value) calcToday = false; // 首日不算头 + if (calcLast == false && valueDate == td.ExerciseDate.Value) calcToday = false; // 到期日不算尾 + + // 初始化EOD持仓信息 + if (preEod.id == 0) + { + preEod.FloatRate = floatRate; + preEod.TdInterestPrincipal = posiPrincipal; + preEod.PosiNotionalValue = posiPrincipal; + } + + // 构建利息事件 + var interest = new swap_flow_event + { + SwapTradeId = td.id, + SwapTradeNo = td.TradeNumber, + EventType = eventType, + EventReason = "交易", + EventDate = valueDate, + PositionId = position.id, + InterestDirection = position.InterestDirection, + InterestRate = rate, + InterestPrincipal = closePrincipal, + InterestSwapInterval = position.InterestSwapInterval, + InterestMode = position.InterestMode, + FloatRate = floatRate, + DataState = (int)SwapFlowDateStateEnum.完成, + ClientId = td.ClientId, + UnwindDate = valueDate + }; + // 收盘场景使用 preEod.FloatRate(历史浮动利率),与 InitSwapDealInterest 收盘场景保持一致 + decimal eodFloatRate = preEod.id != 0 ? preEod.FloatRate : floatRate; + decimal interestAmount = 0; + decimal tdInterestAmount = 0; + if (calcToday) + { + + if (position.InterestType == (int)InterestTypeEnum.复利) + { + // 复利计算 + CalcDailyCompoundInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, false, eodFloatRate, 1m, posiPrincipal, ref interestAmount, ref tdInterestAmount); + } + else + { + // 单利计算 + CalcDailySimpleInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, false, eodFloatRate, 1m, posiPrincipal, ref interestAmount, ref tdInterestAmount); + } + } + // 四舍五入并赋值 + interest.InterestAmount = Math.Round(interestAmount, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + interest.TdInterestAmount = Math.Round(tdInterestAmount, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + // 计算InterestClosePnL(方向:收取=1为正,支付=-1为负) + var interestRatio = position.InterestDirection == 1 ? 1m : -1m; + interest.InterestClosePnL = interest.InterestAmount * interestRatio; + + if (add) UpdateDbOption(interest); + return interest; + } + + /// + /// 计算盘中利息(平仓/互换) + /// + private swap_flow_event CalcUnwindInterest(trade td, DateTime valueDate, DateTime endDate, swap_position position, decimal rate, decimal floatRate, decimal posiPrincipal, decimal closePrincipal, decimal closePercent, int annualDays, eod_swap_position preEod, int eventType, bool add, bool swap, decimal orginPv) + { + if (preEod.id == 0) + { + preEod.FloatRate = floatRate; + preEod.TdInterestPrincipal = posiPrincipal; + preEod.PosiNotionalValue = posiPrincipal; + preEod.ValueDate = td.TradeDate.Value; + } + + return InitSwapDealInterest(td, valueDate, endDate, rate, position, add, swap, posiPrincipal, closePrincipal, closePercent, annualDays, eventType, preEod, false, orginPv); } /// /// 初始化利息腿信息 @@ -492,11 +597,9 @@ namespace YLErp.Modules.SwapModule int eventType, eod_swap_position preEodPosition, bool needPrice, - bool settment, - decimal orginPv + decimal orginPv ) { - DateTime lastSwapDate = preEodPosition.ValueDate; decimal interestProfitSum = preEodPosition.InterestProfitSum; swap_flow_event interest = new swap_flow_event(); interest.SwapTradeId = td.id; @@ -514,8 +617,6 @@ namespace YLErp.Modules.SwapModule interest.DataState = (int)SwapFlowDateStateEnum.完成; interest.ClientId = td.ClientId; interest.UnwindDate = endDate; - var itemDays = (endDate - lastSwapDate).Days; - itemDays = itemDays == 0 ? 1 : itemDays; if (swap) { interest.InterestAmount = 0; @@ -528,43 +629,14 @@ namespace YLErp.Modules.SwapModule decimal InterestAmount = 0; decimal TdInterestAmount = 0; var interestRatio = position.InterestDirection == 1 ? 1m : -1m; + var floateRate = preEodPosition.FloatRate; if (position.InterestType == (int)InterestTypeEnum.复利) { - var floateRate = preEodPosition.FloatRate; - if (settment)//收盘利息计算 - { - CalcDailyCompoundInterestByEod(preEodPosition, endDate, td.StartDate.Value, position, closePosiNotionalValue, posiNotionalValue, interest, annualDays, needPrice, floateRate, closePrecent, orginPv, ref InterestAmount, ref TdInterestAmount); - } - else - { - CalcDailyCompoundInterest(preEodPosition, endDate, td.StartDate.Value, position, closePosiNotionalValue, posiNotionalValue, interest, annualDays, needPrice, floateRate, closePrecent, orginPv, ref InterestAmount, ref TdInterestAmount); - } + CalcDailyCompoundInterest(preEodPosition, endDate, position, closePosiNotionalValue, posiNotionalValue, interest, annualDays, needPrice, floateRate, closePrecent, orginPv, ref InterestAmount, ref TdInterestAmount); } else { - decimal aDays = position.IsAnnualized ? annualDays : 1; - InterestAmount = closePosiNotionalValue * (interest.InterestRate + position.FloatRate); - TdInterestAmount = posiNotionalValue * (interest.InterestRate + position.FloatRate); - if (settment) - { - InterestAmount = InterestAmount * ((decimal)itemDays / aDays); - TdInterestAmount = TdInterestAmount * ((decimal)itemDays / aDays); - InterestAmount = (interestProfitSum * closePrecent) + InterestAmount; - } - else - { - if (endDate > lastSwapDate)//日期超算情况 - { - InterestAmount = InterestAmount * ((decimal)itemDays / aDays); - TdInterestAmount = TdInterestAmount * ((decimal)itemDays / aDays); - InterestAmount += (interestProfitSum * closePrecent); - } - else - { - InterestAmount = interestProfitSum * closePrecent; - } - } - + CalcDailySimpleInterest(preEodPosition, endDate, position, closePosiNotionalValue, posiNotionalValue, interest, annualDays, needPrice, floateRate, closePrecent, orginPv, ref InterestAmount, ref TdInterestAmount); } interest.InterestAmount = Math.Round(InterestAmount, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); @@ -582,16 +654,34 @@ namespace YLErp.Modules.SwapModule /// /// 上一互换日 /// 结算日期 - /// 开仓日 /// 浮动标的 /// 计息基数 /// 固定利率 /// 是否年化 /// 年化天数 /// - public void CalcDailyCompoundInterest(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, ref decimal InterestAmount, ref decimal TdInterestAmount) + public void CalcDailyCompoundInterest(eod_swap_position preEodPosition, DateTime endDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, ref decimal InterestAmount, ref decimal TdInterestAmount) { - DateTime lastSwapDate = preEodPosition.ValueDate; + // 复利:利息并入本金 + CalcDailyInterest(preEodPosition, endDate, position, principal, posiPrincipal, flowEvent, annualDays, needPrice, floateRate, closePercent, orginPv, compoundInterest: true, ref InterestAmount, ref TdInterestAmount); + } + + /// + /// 计算单利 盘中(按重置天数分段,每段使用对应浮动利率) + /// + public void CalcDailySimpleInterest(eod_swap_position preEodPosition, DateTime endDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, ref decimal InterestAmount, ref decimal TdInterestAmount) + { + // 单利:利息不并入本金 + CalcDailyInterest(preEodPosition, endDate, position, principal, posiPrincipal, flowEvent, annualDays, needPrice, floateRate, closePercent, orginPv, compoundInterest: false, ref InterestAmount, ref TdInterestAmount); + } + + /// + /// 通用日度利息计算方法(单利/复利共用) + /// + /// 是否复利:true=利息并入本金,false=单利 + private void CalcDailyInterest(eod_swap_position preEodPosition, DateTime endDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, bool compoundInterest, ref decimal InterestAmount, ref decimal TdInterestAmount) + { + var startDate = position.PosiStartDate; decimal interestProfitSum = preEodPosition.InterestProfitSum; var TdInterestPrincipal = preEodPosition.TdInterestPrincipal; decimal interest = interestProfitSum * closePercent; @@ -599,21 +689,26 @@ namespace YLErp.Modules.SwapModule int interestPeriod = position.interest_rest_days ?? 1; decimal dynomicPrincipal = principal; decimal tdDynomicPrincipal = posiPrincipal; - var calcDays = (endDate - lastSwapDate).Days; + var calcDays = (endDate - startDate).Days; double floatRate = Convert.ToDouble(floateRate); for (int i = 0; i <= calcDays; i++) { - var rateDate = lastSwapDate.AddDays(i); - if (rateDate > lastSwapDate || endDate == lastSwapDate) + var accrueDate = startDate.AddDays(i); + if (accrueDate > preEodPosition.ValueDate) { if (i % interestPeriod == 0) { - dynomicPrincipal = dynomicPrincipal + interest; - tdDynomicPrincipal = tdDynomicPrincipal + interest; + // 复利时:利息并入本金 + if (compoundInterest) + { + dynomicPrincipal = dynomicPrincipal + interest; + tdDynomicPrincipal = tdDynomicPrincipal + interest; + } + // 获取新的浮动利率 if (!string.IsNullOrEmpty(position.FloatRateUnderlyingCode)) { - var fr007RateDate = QdpCalendarHelper.GetNonHolidayDefore(endDate.AddDays(position.interest_rule ?? 0)); // 获取前一工作日; - if (EodPriceQueryService.TryGetPrice(fr007RateDate, position.FloatRateUnderlyingCode, out double floatRate1)) + var fr007RateDate = QdpCalendarHelper.GetNonHolidayDefore(accrueDate.AddDays(position.interest_rule ?? 0)); + if (TryGetFloatRate(fr007RateDate, position.FloatRateUnderlyingCode, out double floatRate1)) { if (floatRate1 != 0) { @@ -624,7 +719,6 @@ namespace YLErp.Modules.SwapModule { throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fr007RateDate:yyyy年MM月dd日}的价格"); } - } flowEvent.InterestPrincipal = tdDynomicPrincipal * closePercent; TdInterestPrincipal = tdDynomicPrincipal; @@ -645,9 +739,7 @@ namespace YLErp.Modules.SwapModule } interest += interest1; tdinterest += tdinterest1; - } - } InterestAmount = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); TdInterestAmount = Math.Round(tdinterest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); @@ -668,8 +760,8 @@ namespace YLErp.Modules.SwapModule public void CalcDailyCompoundInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, ref decimal InterestAmount, ref decimal TdInterestAmount) { decimal interestProfitSum = preEodPosition.InterestProfitSum; - decimal interest = preEodPosition.TdInterestIncome; - decimal tdinterest = preEodPosition.TdInterestIncome; + decimal interest = interestProfitSum * closePercent; + decimal tdinterest = interestProfitSum * closePercent; int interestPeriod = position.interest_rest_days ?? 1; decimal tdDynomicPrincipal = posiPrincipal; double floatRate = Convert.ToDouble(floateRate); @@ -681,7 +773,7 @@ namespace YLErp.Modules.SwapModule { // 获取合适的 rateDate var fr007RateDate = QdpCalendarHelper.GetNonHolidayDefore(endDate.AddDays(position.interest_rule ?? 0)); // 获取前一工作日; - if (EodPriceQueryService.TryGetPrice(fr007RateDate, position.FloatRateUnderlyingCode, out double floatRate1)) + if (TryGetFloatRate(fr007RateDate, position.FloatRateUnderlyingCode, out double floatRate1)) { if (floatRate1 != 0) { @@ -721,6 +813,59 @@ namespace YLErp.Modules.SwapModule TdInterestAmount = Math.Round(tdinterest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); } + /// + /// 计算单利 收盘(按重置天数分段,每段使用对应浮动利率) + /// + public void CalcDailySimpleInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, ref decimal InterestAmount, ref decimal TdInterestAmount) + { + decimal interestProfitSum = preEodPosition.InterestProfitSum; + int interestPeriod = position.interest_rest_days ?? 1; + double floatRate = Convert.ToDouble(floateRate); + var calcDays = (endDate - tradeDate).Days; + // 修复:首次操作时(preEodPosition.id == 0),TdInterestPrincipal 需要正确初始化 + if (preEodPosition.id == 0) + { + preEodPosition.TdInterestPrincipal = posiPrincipal; + } + + // 检查是否到达重置周期 + if (calcDays % interestPeriod == 0) + { + // 获取新的浮动利率 + if (!string.IsNullOrEmpty(position.FloatRateUnderlyingCode)) + { + var fr007RateDate = QdpCalendarHelper.GetNonHolidayDefore(endDate.AddDays(position.interest_rule ?? 0)); + if (TryGetFloatRate(fr007RateDate, position.FloatRateUnderlyingCode, out double newFloatRate)) + { + if (newFloatRate != 0) + { + floatRate = newFloatRate; + } + } + else + { + throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fr007RateDate:yyyy年MM月dd日}的价格"); + } + } + } + + flowEvent.FloatRate = Convert.ToDecimal(floatRate); + var baseTdInterestPrincipal = preEodPosition.TdInterestPrincipal + posiPrincipal - orginPv; + var baseInterestPrincipal = baseTdInterestPrincipal * closePercent; + + // 修复:正确计算本次利息(基于实际持仓本金) + decimal interest = baseInterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate)); + decimal tdinterest = baseTdInterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate)); + if (position.IsAnnualized) + { + interest /= annualDays; + tdinterest /= annualDays; + } + + InterestAmount = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + TdInterestAmount = Math.Round(tdinterest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + } + /// /// 单标的平仓 /// @@ -733,7 +878,7 @@ namespace YLErp.Modules.SwapModule { throw new ServiceException("未找到交易信息"); } - CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); + //CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); //去掉平仓收盘限制 var trans = DbContext.Database.BeginTransaction(); bool cofirm = false; try @@ -1232,7 +1377,7 @@ namespace YLErp.Modules.SwapModule { throw new ServiceException("未找到交易信息"); } - CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); + //CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); //去掉平仓收盘限制 var trans = DbContext.Database.BeginTransaction(); bool confirm = false; try diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index 95fef6b3..58fc8e85 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -136,7 +136,15 @@ namespace YLErp.Modules.SwapModule var grossPrice = curEodPosis.Where(x => x.PosiDirection > 0).FirstOrDefault()?.PosiGrossPrice ?? 0; //处理利息腿 DealInterests(interestList, eodPositions, todyEodPositions, settleDate, td, flowEvents, autoInterests, lastEodSwap, posiLongNotional, posiShortNotional, closePosiNotional, grossPrice, orginPv); - DealAutoInterests(autoInterests, td, settleDate, preDealDate, posiLongNotional + posiShortNotional); + //获取自动互换的 interval 信息,用于确定结算日期 + IntervalModel autoInterval = null; + foreach (var interest in interestList) + { + autoInterval = interest.SwapIntervalList.FirstOrDefault(x => x.Date == settleDate && x.Settlement == 1); + if (autoInterval != null) + break; + } + DealAutoInterests(autoInterests, td, settleDate, preDealDate, posiLongNotional + posiShortNotional, autoInterval); //多空组合判断是否已到到期日且无持仓信息 if (longShort && td.ExerciseDate.Value == settleDate && allPositionQty == 0) { @@ -361,7 +369,8 @@ namespace YLErp.Modules.SwapModule /// /// /// - private void DealAutoInterests(List autoInterests, trade td, DateTime settleDate, DateTime? preDealDate, decimal StockEqvNotional) + /// 自动互换观察日信息,用于获取结算日期 + private void DealAutoInterests(List autoInterests, trade td, DateTime settleDate, DateTime? preDealDate, decimal StockEqvNotional, IntervalModel interval) { if (autoInterests.Count == 0) { @@ -387,17 +396,20 @@ namespace YLErp.Modules.SwapModule unwindData.SwapCloseAmount = unwindData.SwapCloseAmount + x.InterestClosePnL; unwindData.SwapRealizedPnL = unwindData.SwapCloseAmount; }); - SaveAutoSwapDeal(td, autoInterests, unwindData); + SaveAutoSwapDeal(td, autoInterests, unwindData, interval); } /// /// 保存自动互换数据信息 /// /// /// - private long SaveAutoSwapDeal(trade td, List flowEvents, UnwindData unwindData) + /// 自动互换观察日信息,用于获取结算日期 + private long SaveAutoSwapDeal(trade td, List flowEvents, UnwindData unwindData, IntervalModel interval) { //td.UnWindDate = unwindData.ValueDate; - int clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapCloseAmount), ClientCashInCashOut.系统操作_互换, unwindData.ValueDate); + //优先使用 interval.SettlementDate 作为资金记录发生日期,如果没有则使用 ValueDate + var cashHappenDate = interval?.SettlementDate ?? unwindData.ValueDate; + int clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapCloseAmount), ClientCashInCashOut.系统操作_互换, cashHappenDate); string data = JsonConvert.SerializeObject(unwindData); var swapEvent = new SwapEventService(this).AddSwapEventDate(unwindData.ValueDate, unwindData.SwapTradeId, (int)SwapEventTypeEnum.自动互换, data, clientCashId, true, "系统操作-自动互换");//将互换总额存入事件 flowEvents.ForEach(x => @@ -777,7 +789,7 @@ namespace YLErp.Modules.SwapModule positions.Add(position); List preEodPositions = new List(); preEodPositions.Add(eodPayPosition); - if (position.InterestMode == (int)InterestModeEnum.固定值) + if (position.InterestMode == (int)InterestModeEnum.固定值||position.InterestMode == (int)InterestModeEnum.初始预付金 || position.InterestMode == (int)InterestModeEnum.追加预付金) { orginPv = eodPayPosition.InterestPrincipalFix; } @@ -872,7 +884,7 @@ namespace YLErp.Modules.SwapModule newEodPayPosition = eodPayPosition.Clone(); newEodPayPosition.id = 0; } - if (position.InterestMode == (int)InterestModeEnum.固定值) + if (position.InterestMode == (int)InterestModeEnum.固定值 || position.InterestMode == (int)InterestModeEnum.初始预付金 || position.InterestMode == (int)InterestModeEnum.追加预付金) { orginPv = eodPayPosition.InterestPrincipalFix; } @@ -911,7 +923,6 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.InterestType = position.InterestType; newEodPayPosition.FloatRate = interests.Count > 0 ? interests.First().FloatRate ?? 0 : 0; newEodPayPosition.FloatRateUnderlyingCode = position.FloatRateUnderlyingCode; - newEodPayPosition.InterestFeePending = 0; newEodPayPosition.interest_rest_days = position.interest_rest_days; newEodPayPosition.interest_rule = position.interest_rule; //利息端估值用信息 @@ -1013,7 +1024,7 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.id = 0; newEodPayPosition.PositionId = position.id; } - if (position.InterestMode == (int)InterestModeEnum.固定值) + if (position.InterestMode == (int)InterestModeEnum.固定值 || position.InterestMode == (int)InterestModeEnum.初始预付金 || position.InterestMode == (int)InterestModeEnum.追加预付金) { orginPv = eodPayPosition.InterestPrincipalFix; } diff --git a/YLErpDAL/Modules/SwapModule/SwapTradeBaseService.cs b/YLErpDAL/Modules/SwapModule/SwapTradeBaseService.cs index f8c30dc3..939503eb 100644 --- a/YLErpDAL/Modules/SwapModule/SwapTradeBaseService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapTradeBaseService.cs @@ -325,7 +325,7 @@ namespace YLErp.Modules.SwapModule /// 计息方式 /// 计息开始日期 /// 计息结束日期 - public bool InitInterestDate(DateTime valueDate, DateTime? preSettleDate, trade td, bool tdClose,bool calcLastNew,out DateTime interestStart, out DateTime interestEnd) + public bool InitInterestDate(DateTime valueDate, DateTime? preSettleDate, trade td, bool tdClose,bool calcLastNew, out DateTime interestStart, out DateTime interestEnd) { interestStart = td.StartDate.Value; var exerciseDate = td.ExerciseDate.Value; @@ -382,6 +382,19 @@ namespace YLErp.Modules.SwapModule var eodSwapPositions = DbContext.eod_swap_position.Where(x => x.SwapTradeId == td.id && x.ValueDate >= valueDate).ToList(); DbContext.eod_swap_position.RemoveRange(eodSwapPositions); DbContext.swap_flow_event.RemoveRange(swapFlowEvents); + + // 删除自动互换产生的资金记录(client_cash_in_out) + var swapEventIds = swapEvents.Select(s => s.id).ToList(); + if (swapEventIds.Any()) + { + // 通过 swap_event 的 ClientCashId 删除对应的资金记录 + var clientCashIds = swapEvents.Where(s => s.ClientCashId > 0).Select(s => s.ClientCashId).ToList(); + if (clientCashIds.Any()) + { + var clientCashRecords = DbContext.ClientCashInCashOut.Where(x => clientCashIds.Contains(x.id)).ToList(); + DbContext.ClientCashInCashOut.RemoveRange(clientCashRecords); + } + } } DbContext.swap_event.RemoveRange(swapEvents); DbContext.eod_swap.RemoveRange(eodSwaps); diff --git a/YLErpDAL/QdpModule/QdpObservationHelper.cs b/YLErpDAL/QdpModule/QdpObservationHelper.cs index 0150f289..bfe62822 100644 --- a/YLErpDAL/QdpModule/QdpObservationHelper.cs +++ b/YLErpDAL/QdpModule/QdpObservationHelper.cs @@ -1,4 +1,4 @@ -using Qdp.Foundation.Implementations; +using Qdp.Foundation.Implementations; using Qdp.Pricing.Base.Enums; using Qdp.Pricing.Base.Implementations; using System.Runtime.CompilerServices; @@ -220,5 +220,199 @@ namespace YLErp.QdpModule var monthlyDates = GetDefaultKoObservationDatesForSnowbal(startDate, endDate); return monthlyDates.Select(x => (Date)x).ToArray(); } + + public static DateTime[] GetDatesWithFixedTerm( + DateTime startDate, + DateTime endDate, + string termStr, + string calendarStr, + BusinessDayConvention bdc = BusinessDayConvention.None, + bool alignEnd = false, + string calcMode = "01") + { + if (!Term.IsTerm(termStr)) + { + return null; + } + + var calendarName = string.IsNullOrEmpty(calendarStr) ? "chn" : calendarStr.ToLower(); + return GetDatesWithFixedTerm(startDate, endDate, new Term(termStr), calendarName, bdc, alignEnd, calcMode); + } + + public static DateTime[] GetDatesWithFixedTerm( + DateTime startDate, + DateTime endDate, + Term term, + string calendarStr, + BusinessDayConvention bdc = BusinessDayConvention.None, + bool alignEnd = false, + string calcMode = "01") + { + var calendarName = string.IsNullOrEmpty(calendarStr) ? "chn" : calendarStr.ToLower(); + return alignEnd + ? GetDatesWithFixedTermStartAlignEnd(startDate, endDate, term, calendarName, bdc, calcMode) + : GetDatesWithFixedTermStartAlignStart(startDate, endDate, term, calendarName, bdc, calcMode); + } + + private static DateTime[] GetDatesWithFixedTermStartAlignEnd( + DateTime startDate, + DateTime endDate, + Term term, + string calendarName, + BusinessDayConvention bdc = BusinessDayConvention.None, + string calcMode = "01") + { + var qdpStart = new Date(startDate); + var qdpEnd = new Date(endDate); + var dates = new List(); + var calendar = CalendarImpl.Get(calendarName); + + if (calcMode == "11") + { + while (qdpEnd >= qdpStart) + { + dates.Add(qdpEnd); + qdpEnd = term.Prev(qdpEnd); + } + } + else if (calcMode == "10") + { + qdpEnd = term.Prev(qdpEnd); + while (qdpEnd >= qdpStart) + { + dates.Add(qdpEnd); + qdpEnd = term.Prev(qdpEnd); + } + } + else if (calcMode == "01") + { + while (qdpEnd > qdpStart) + { + dates.Add(qdpEnd); + qdpEnd = term.Prev(qdpEnd); + } + } + else if (calcMode == "00") + { + qdpEnd = term.Prev(qdpEnd); + while (qdpEnd > qdpStart) + { + dates.Add(qdpEnd); + qdpEnd = term.Prev(qdpEnd); + } + } + + if (dates.Count == 0) + { + dates.Add(new Date(endDate)); + } + + dates = dates.Select(d => calendar.Adjust(d, bdc)).Distinct().ToList(); + dates.Reverse(); + return dates.Select(x => x.DateTime).ToArray(); + } + + private static DateTime[] GetDatesWithFixedTermStartAlignStart( + DateTime startDate, + DateTime endDate, + Term term, + string calendarName, + BusinessDayConvention bdc = BusinessDayConvention.None, + string calcMode = "01") + { + var qdpStart = new Date(startDate); + var qdpEnd = new Date(endDate); + var dates = new List(); + var calendar = CalendarImpl.Get(calendarName); + + if (calcMode == "11") + { + while (qdpStart <= qdpEnd) + { + dates.Add(qdpStart); + qdpStart = term.Next(qdpStart); + } + } + else if (calcMode == "10") + { + while (qdpStart < qdpEnd) + { + dates.Add(qdpStart); + qdpStart = term.Next(qdpStart); + } + } + else if (calcMode == "01") + { + qdpStart = term.Next(qdpStart); + while (qdpStart <= qdpEnd) + { + dates.Add(qdpStart); + qdpStart = term.Next(qdpStart); + } + } + else if (calcMode == "00") + { + qdpStart = term.Next(qdpStart); + while (qdpStart < qdpEnd) + { + dates.Add(qdpStart); + qdpStart = term.Next(qdpStart); + } + } + + dates = dates.Select(d => calendar.Adjust(d, bdc)).Distinct().ToList(); + + if (dates.Count == 0) + { + dates.Add(qdpEnd); + } + + return dates.Select(x => x.DateTime).ToArray(); + } + + public static ObservationSettleDateResult[] GetObservationAndSettleDates( + DateTime startDate, + DateTime endDate, + string termStr, + string calendarStr, + int settlementRules, + BusinessDayConvention bdc = BusinessDayConvention.None, + bool alignEnd = false, + string calcMode = "01") + { + if (!Term.IsTerm(termStr)) + { + return null; + } + + var calendarName = string.IsNullOrEmpty(calendarStr) ? "chn" : calendarStr.ToLower(); + var calendar = CalendarImpl.Get(calendarName); + var term = new Term(termStr); + + DateTime[] observationDates = alignEnd + ? GetDatesWithFixedTermStartAlignEnd(startDate, endDate, term, calendarName, bdc, calcMode) + : GetDatesWithFixedTermStartAlignStart(startDate, endDate, term, calendarName, bdc, calcMode); + + var results = new List(); + foreach (var obsDate in observationDates) + { + var settleDate = obsDate.AddDays(settlementRules); + settleDate = calendar.Adjust(new Date(settleDate), bdc).DateTime; + + results.Add(new ObservationSettleDateResult + { + ObservationDate = obsDate, + SettleDate = settleDate + }); + } + + return results.ToArray(); + } + } + + public class ObservationSettleDateResult + { + public DateTime ObservationDate { get; set; } + public DateTime SettleDate { get; set; } } } diff --git a/YLErpWeb/Controllers/SwapTrade2Controller.cs b/YLErpWeb/Controllers/SwapTrade2Controller.cs index 03bac4c3..6b174297 100644 --- a/YLErpWeb/Controllers/SwapTrade2Controller.cs +++ b/YLErpWeb/Controllers/SwapTrade2Controller.cs @@ -153,6 +153,17 @@ namespace YLErp.Web.Controllers return JsonSuccess(""); } /// + /// 校验收益结算操作(不检查收盘限制) + /// + /// + /// + public JsonResult CheckEodTradeForIncome(string enid) + { + var intid = DecryptInt(enid); + new SwapDealService(CurUser).CheckEodTradeForIncome(intid); + return JsonSuccess(""); + } + /// /// 收益互换 平仓 /// /// diff --git a/YLErpWeb/Controllers/tradeController.cs b/YLErpWeb/Controllers/tradeController.cs index 05a793d4..990c31ee 100644 --- a/YLErpWeb/Controllers/tradeController.cs +++ b/YLErpWeb/Controllers/tradeController.cs @@ -1,4 +1,4 @@ -using iTextSharp.text; +using iTextSharp.text; using iTextSharp.text.pdf; using NPOI.POIFS.Crypt; using Qdp.Pricing.Base.Enums; @@ -3456,6 +3456,14 @@ namespace YLErp.Web.Controllers return JsonSuccess("", QdpObservationHelper.GetDatesWithFixedTerm(req.startDate, req.endDate, req.termStr, bdc, req.alignEnd, req.calcMode)); } + [HttpPost] + public JsonResult GetSwapObservationDateList(GetSwapObservationDateRequest req) + { + var bdc = (BusinessDayConvention)Enum.Parse(typeof(BusinessDayConvention), req.holidayAdjustment); + var results = QdpObservationHelper.GetObservationAndSettleDates(req.startDate, req.endDate, req.termStr, req.calendar, req.settlementRules, bdc, req.alignEnd, req.calcMode); + return JsonSuccess("", results); + } + public ActionResult StructureList(string structure, string CalcId, bool onlyshow = false) { var ret = new DZStructureService(CurUser).getStructureList(structure); diff --git a/YLErpWeb/Models/GetSwapObservationDateRequest.cs b/YLErpWeb/Models/GetSwapObservationDateRequest.cs new file mode 100644 index 00000000..643879b9 --- /dev/null +++ b/YLErpWeb/Models/GetSwapObservationDateRequest.cs @@ -0,0 +1,28 @@ +namespace YLErp.Web.Models +{ + public class GetSwapObservationDateRequest + { + public DateTime startDate { get; set; } + + public DateTime endDate { get; set; } + + public string termStr { get; set; } + + public string holidayAdjustment { get; set; } + + public bool alignEnd { get; set; } + + public string calcMode { get; set; } + + public string calendar { get; set; } + + public int settlementRules { get; set; } + } + + public class SwapObservationDateResult + { + public DateTime ObservationDate { get; set; } + + public DateTime SettleDate { get; set; } + } +} \ No newline at end of file diff --git a/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml b/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml index 1e4ee463..00b0d748 100644 --- a/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml +++ b/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml @@ -1,4 +1,4 @@ -@using YLErp.Web.Models.JsModels; +@using YLErp.Web.Models.JsModels; @model trade @{ ViewBag.Title = "交易信息 | 编辑"; @@ -294,7 +294,7 @@ - + @@ -363,7 +363,7 @@ - + - + @@ -512,7 +512,22 @@ -
+
+ 交易日历 + +
+
+ 结算规则 + +
节假日调整 是否结算
  • - % + + + diff --git a/YLErpWeb/Views/SwapTrade2/TradeView.cshtml b/YLErpWeb/Views/SwapTrade2/TradeView.cshtml index e16fd470..093dd18f 100644 --- a/YLErpWeb/Views/SwapTrade2/TradeView.cshtml +++ b/YLErpWeb/Views/SwapTrade2/TradeView.cshtml @@ -1,4 +1,4 @@ -@using YLErp.Enums; +@using YLErp.Enums; @model TradeViewModel @{ @@ -265,7 +265,7 @@ @item.InterestRateDefault.OtcFormat(OtcFormatFlag.marginRateP) @(item.IsAnnualized ? "是" : "否") - + } @@ -337,7 +337,7 @@ @item.interest_rest_days @((item.interest_rule != null) ? (SwapInterestRule)item.interest_rule : "") - + } @@ -1013,7 +1013,8 @@ 观察日期 - 互换利率 + 结算日期 + 互换利率 是否结算 @@ -1021,7 +1022,11 @@ {{formatDate(item.Date)}} - {{item.Rate}} + {{formatDate(item.SettlementDate)}} + + {{item.FloatRateCode}}+ + {{item.Rate}} + {{item.Settlement?"是":"否"}} diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js index 1dea3d78..a46f49c2 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js @@ -1,4 +1,4 @@ -//otcformat禁止千分位分组 +//otcformat禁止千分位分组 window.otcformat.options.disableGrouping = true; const consClients = ylotc.clients; @@ -126,6 +126,8 @@ const vue = new Vue({ ObservationUnit: 'D', ObservationHolidayType: 'Following', ObservationAlignEnd: true, + ObservationCalendar: 'Chn', + ObservationSettlementRules: 0, DefaultTitle1Value: 0.1, ObservationDataList: [], CheckedAll: false, @@ -344,7 +346,7 @@ const vue = new Vue({ } x.SwapIntervalList.push(interval); } - if (x.InterestType == 1 && x.interest_rest_days<=0) { + if (x.interest_rest_days != null && x.interest_rest_days <= 0) { main.message("利息端第" + (index + 1) + "行重置频率必须大于0"); errorcount++; return false; @@ -461,9 +463,7 @@ const vue = new Vue({ } }, changeInterestType(item) { - if (item.InterestType == 0) { - item.interest_rest_days = null; - } else { + if (!item.interest_rest_days) { item.interest_rest_days = 1; } }, @@ -553,7 +553,7 @@ const vue = new Vue({ arr[index].Rate = swapRate; }); var observationDates = JSON.stringify(item.SwapIntervalList); - item.SwapIntervals = observationDates; + item.InterestSwapInterval = observationDates; }); } else { @@ -582,6 +582,10 @@ const vue = new Vue({ thisObj.observation.ObservationUnit = observation ? observation.ObservationUnit : 'D'; thisObj.observation.ObservationHolidayType = observation ? observation.ObservationHolidayType : 'Following'; thisObj.observation.ObservationAlignEnd = observation ? observation.ObservationAlignEnd : true; + if (type == 1) { + thisObj.observation.ObservationCalendar = observation ? observation.ObservationCalendar : 'Chn'; + thisObj.observation.ObservationSettlementRules = observation ? observation.ObservationSettlementRules : 0; + } thisObj.observation.IsDeductPrincipal = observation ? observation.IsDeductPrincipal : true; if (type == 0) { thisObj.observation.IsDeductPrincipal = false; @@ -603,6 +607,7 @@ const vue = new Vue({ thisObj.observation.ObservationStart = endTime; } thisObj.observation.ObservationDataList = []; + var floatRateCode = type == 1 ? (item.FloatRateUnderlyingCode || '--') : ''; thisObj.observation.IntervalList.forEach((value, num, arr) => { var val = value.Rate; var _date = ""; @@ -616,24 +621,47 @@ const vue = new Vue({ disabled = true; itemChecked = false; } - var obdate = { - date: _date, - val: _.toString(val) ? parseFloat(consNumberFormat.umpriceP(val)) : "", - itemChecked: itemChecked, - disabled: disabled + var obdate = {}; + if (type == 1) { + var SettlementDate = value.Date; + if (value.SettlementDate!=null) { + SettlementDate = value.SettlementDate; + } + obdate = { + date: _date, + SettlementDate: _date, + floatRateCode: floatRateCode, + val: _.toString(val) ? parseFloat(consNumberFormat.umpriceP(val)) : "", + itemChecked: itemChecked, + disabled: disabled + }; + } else { + obdate = { + date: _date, + val: _.toString(val) ? parseFloat(consNumberFormat.umpriceP(val)) : "", + itemChecked: itemChecked, + disabled: disabled + }; } thisObj.observation.ObservationDataList.push(obdate); }); - // thisObj.initObservationCheckedAll(); + var area = type == 1 ? ['800px', '600px'] : ['750px', '600px']; layer.open({ type: 1, - area: ['580px', '560px'], + area: area, title: "设置互换日期", shadeClose: false, shade: 0.4, content: $("#observationInfosEdit") }); }, + //计算结算日期 + calcSettleDate(observationDate, settlementRules) { + if (!observationDate) return ""; + var m = new moment(observationDate); + var settleDate = m.add(settlementRules, 'days').format("YYYY-MM-DD"); + return settleDate; + }, //生成观察日操作 GetObservationDates() { var thisObj = this; @@ -641,43 +669,97 @@ const vue = new Vue({ var observationUnit = thisObj.observation.ObservationUnit; var observationHolidayType = thisObj.observation.ObservationHolidayType; var alignEnd = thisObj.observation.ObservationAlignEnd; - var postData = { - startDate: thisObj.observation.ObservationStart, endDate: thisObj.trade.ExerciseDate, termStr: observationNum + observationUnit, - holidayAdjustment: observationHolidayType, alignEnd: alignEnd, calcMode: thisObj.trade.trade_swap.RateCalcMode - }; + var settlementRules = thisObj.observation.ObservationSettlementRules; + var calendar = thisObj.observation.ObservationCalendar; + var floatRateCode = this.observationType == 1 ? (thisObj.getSwapList[thisObj.observation.index]?.FloatRateUnderlyingCode || '--') : ''; + thisObj.observation.ObservationDataList = []; - main.post("/trade/GetObservationDateList", postData).done( - function (res) { - $.each(res.obj, function (i) { - var _date = ""; - var m = new moment(this); - if (!isNaN(m.date())) { - _date = m.format("YYYY-MM-DD"); - } - var val = parseFloat(consNumberFormat.umpriceP(thisObj.observation.DefaultTitle1Value)); - var itemChecked = true; - var disabled = false; - if (_date == thisObj.trade.ExerciseDate) { - disabled = true; - itemChecked = false; - } - var obdate = { - date: _date, - val: val, - itemChecked: itemChecked, - disabled: disabled - } - thisObj.observation.ObservationDataList.push(obdate); + + if (this.observationType == 1) { + var postData = { + startDate: thisObj.observation.ObservationStart, + endDate: thisObj.trade.ExerciseDate, + termStr: observationNum + observationUnit, + holidayAdjustment: observationHolidayType, + alignEnd: alignEnd, + calcMode: thisObj.trade.trade_swap.RateCalcMode, + calendar: calendar, + settlementRules: settlementRules + }; + main.post("/trade/GetSwapObservationDateList", postData).done( + function (res) { + $.each(res.obj, function (i) { + var _date = ""; + var m = new moment(this.ObservationDate); + if (!isNaN(m.date())) { + _date = m.format("YYYY-MM-DD"); + } + var _settleDate = ""; + var sm = new moment(this.SettleDate); + if (!isNaN(sm.date())) { + _settleDate = sm.format("YYYY-MM-DD"); + } + var val = parseFloat(consNumberFormat.umpriceP(thisObj.observation.DefaultTitle1Value)); + var itemChecked = true; + var disabled = false; + if (_date == thisObj.trade.ExerciseDate) { + disabled = true; + itemChecked = false; + } + var obdate = { + date: _date, + SettlementDate: _settleDate, + floatRateCode: floatRateCode, + val: val, + itemChecked: itemChecked, + disabled: disabled + }; + thisObj.observation.ObservationDataList.push(obdate); + }); + thisObj.initObservationCheckedAll(); }); - thisObj.initObservationCheckedAll(); - }); + } else { + var postData = { + startDate: thisObj.observation.ObservationStart, + endDate: thisObj.trade.ExerciseDate, + termStr: observationNum + observationUnit, + holidayAdjustment: observationHolidayType, + alignEnd: alignEnd, + calcMode: thisObj.trade.trade_swap.RateCalcMode + }; + main.post("/trade/GetObservationDateList", postData).done( + function (res) { + $.each(res.obj, function (i) { + var _date = ""; + var m = new moment(this); + if (!isNaN(m.date())) { + _date = m.format("YYYY-MM-DD"); + } + var val = parseFloat(consNumberFormat.umpriceP(thisObj.observation.DefaultTitle1Value)); + var itemChecked = true; + var disabled = false; + if (_date == thisObj.trade.ExerciseDate) { + disabled = true; + itemChecked = false; + } + var obdate = { + date: _date, + val: val, + itemChecked: itemChecked, + disabled: disabled + }; + thisObj.observation.ObservationDataList.push(obdate); + }); + thisObj.initObservationCheckedAll(); + }); + } }, //编辑观察日功能数据处理 SetObservationDates() { var observationStr = ""; if (this.observation.ObservationDataList != null) { this.observation.ObservationDataList.forEach(item => { - observationStr = observationStr + item.date + ", " + (item.val * 0.01).toFixed(6) + ", " + item.itemChecked + ";\n"; + observationStr = observationStr + item.date + ", " + item.SettlementDate + ", " + (item.val * 0.01).toFixed(6) + ", " + item.itemChecked + ";\n"; }); } this.observation.ObservationInterval = observationStr; @@ -712,18 +794,21 @@ const vue = new Vue({ var observationDates = thisObj.observation.ObservationInterval; var items = observationDates.split(";").filter(o => o); thisObj.observation.ObservationDataList = []; + var floatRateCode = thisObj.getSwapList[thisObj.observation.index]?.FloatRateUnderlyingCode || '--'; items.forEach(function (item) { if (item) { var values = item.split(","); - var itemChecked = JSON.parse(values[2].trim()); + var itemChecked = JSON.parse(values[3].trim()); var disabled = false; if (values[0] == thisObj.trade.ExerciseDate) { disabled = true; itemChecked = false; } - var val = values[1].trim(); + var val = values[2].trim(); var obdate = { date: values[0], + SettlementDate: values[1] || "", + floatRateCode: floatRateCode, val: _.toString(val) ? parseFloat(consNumberFormat.umpriceP(val)) : "", itemChecked: itemChecked, disabled: disabled @@ -758,11 +843,24 @@ const vue = new Vue({ //添加观察日 addnewitem() { var thisObj = this; - var obdate = { - date: '', - val: 0, - itemChecked: true, - disabled: false + var obdate = {}; + if (this.observationType == 1) { + var floatRateCode = thisObj.getSwapList[thisObj.observation.index]?.FloatRateUnderlyingCode || '--'; + obdate = { + date: '', + SettlementDate: '', + floatRateCode: floatRateCode, + val: 0, + itemChecked: true, + disabled: false + }; + } else { + obdate = { + date: '', + val: 0, + itemChecked: true, + disabled: false + }; } thisObj.observation.ObservationDataList.push(obdate); }, @@ -792,7 +890,8 @@ const vue = new Vue({ var observation = { Date: item.date, Rate: item.val * 0.01, - Settlement: item.itemChecked ? 1 : 0 + Settlement: item.itemChecked ? 1 : 0, + SettlementDate: item.SettlementDate || null // 结算日期 } observationArr.push(observation); @@ -805,11 +904,11 @@ const vue = new Vue({ alert("请输入正确的数字格式"); return false; } - if (this.observationType == 0) { + if (this.observationType == 1) { thisObj.getSwapList.forEach((val, num, arr) => { if (val.index == thisObj.observation.index) { - arr[num].SwapIntervals = JSON.stringify(observationArr); - thisObj.observation.ObservationInterval = arr[num].SwapIntervals; + arr[num].InterestSwapInterval = JSON.stringify(observationArr); + thisObj.observation.ObservationInterval = arr[num].InterestSwapInterval; arr[num].SwapIntervalList = observationArr; arr[num].Obervation = JSON.parse(JSON.stringify(thisObj.observation)); } @@ -817,8 +916,8 @@ const vue = new Vue({ } else { thisObj.marginSwapList.forEach((val, num, arr) => { if (val.index == thisObj.observation.index) { - arr[num].SwapIntervals = JSON.stringify(observationArr); - thisObj.observation.ObservationInterval = arr[num].SwapIntervals; + arr[num].InterestSwapInterval = JSON.stringify(observationArr); + thisObj.observation.ObservationInterval = arr[num].InterestSwapInterval; arr[num].SwapIntervalList = observationArr; arr[num].Obervation = JSON.parse(JSON.stringify(thisObj.observation)); } @@ -1081,11 +1180,23 @@ const vue = new Vue({ thisObj.getSwapList = thisObj.trade.swap_positions.filter(x => { if ((x.UnderlyingCode == null || x.UnderlyingCode.length == 0) && x.IsInitial && (x.InterestMode == 1 || x.InterestMode == 2 || x.InterestMode == 7 || x.InterestMode == 8 || x.InterestMode == 9)) return x; }); thisObj.getSwapList.forEach((val, num, arr) => { arr[num].index = num; + // 解析 InterestSwapInterval 为 SwapIntervalList + if (arr[num].InterestSwapInterval && !arr[num].SwapIntervalList) { + try { + arr[num].SwapIntervalList = JSON.parse(arr[num].InterestSwapInterval); + } catch (e) { } + } }); thisObj.marginSwapList = thisObj.trade.swap_positions.filter(x => { if ((x.UnderlyingCode == null || x.UnderlyingCode.length == 0) && x.IsInitial && (x.InterestMode == 5 || x.InterestMode == 6)) return x; }); thisObj.marginSwapList.forEach((val, num, arr) => { - arr[num].index = num; + arr[num].index = 1000 + num; // 保证金列表使用 1000+ 偏移,避免与利息腿冲突 arr[num].HappenDate = thisObj.formatDate(arr[num].HappenDate); + // 解析 InterestSwapInterval 为 SwapIntervalList + if (arr[num].InterestSwapInterval && !arr[num].SwapIntervalList) { + try { + arr[num].SwapIntervalList = JSON.parse(arr[num].InterestSwapInterval); + } catch (e) { } + } }); if (thisObj.trade.StructureType == '多空组合') { thisObj.paySwapList = []; diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeView.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeView.js index 009f3901..87c0cbce 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeView.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeView.js @@ -44,8 +44,8 @@ function downloadFiles(files) { window.open(files); } } -function showSwapRate(timeRate) { - vueDetails.initSwapIntervalList(timeRate); +function showSwapRate(timeRate, isMarginLeg) { + vueDetails.initSwapIntervalList(timeRate, isMarginLeg); $("#swapIntervalModal").modal("show"); } var layerIndex = -1; @@ -192,7 +192,7 @@ function unWindSwapTrade(id) { function unWindSwap(id) { var title = "收益结算"; var srcurl = "/swaptrade2/SwapIncome/?enid=" + id; - main.post("/swaptrade2/CheckEodTrade?enid=" + id).done(function (res) { + main.post("/swaptrade2/CheckEodTradeForIncome?enid=" + id).done(function (res) { if (res.success) { main.open(title, srcurl, @@ -375,7 +375,8 @@ function reload() { var vueDetails = new Vue({ el: "#swapIntervalModal", data: { - SwapIntervalList:[] + SwapIntervalList:[], + isMarginLeg: false // 是否是保证金腿 }, created: function () { }, @@ -383,13 +384,18 @@ var vueDetails = new Vue({ closeModal: function () { $("#swapIntervalModal").modal("hide"); }, - initSwapIntervalList(swapIntervals) { + initSwapIntervalList(swapIntervals, isMarginLeg) { var that = this; + that.isMarginLeg = isMarginLeg; that.SwapIntervalList = []; - if (swapIntervals.length>0) { + if (swapIntervals && swapIntervals.length>0) { that.SwapIntervalList = JSON.parse(swapIntervals); that.SwapIntervalList.forEach((item, index) => { that.SwapIntervalList[index].Rate = otcformat.trading.marginRateP(item.Rate); + // 如果结算日期为空,默认等于观察日期 + if (!item.SettlementDate) { + that.SwapIntervalList[index].SettlementDate = item.Date; + } }); } },