diff --git a/Framework/YLErp.Core/DBModels/Consts/ConsTrade.cs b/Framework/YLErp.Core/DBModels/Consts/ConsTrade.cs
index cd243a6a..08664c06 100644
--- a/Framework/YLErp.Core/DBModels/Consts/ConsTrade.cs
+++ b/Framework/YLErp.Core/DBModels/Consts/ConsTrade.cs
@@ -131,11 +131,11 @@ namespace YLErp.DBModels
///
/// 互换利率端计息方式
///
- public static readonly List InterestModels = new List() { (int)InterestModeEnum.固定值, (int)InterestModeEnum.合约名义本金规模, (int)InterestModeEnum.空头存续名义本金,(int)InterestModeEnum.多头存续名义本金, (int)InterestModeEnum.标的期初全价 };
+ public static readonly List InterestModels = new List() { (int)InterestModeEnum.固定值, (int)InterestModeEnum.合约名义本金规模, (int)InterestModeEnum.标的期初全价 };
///
/// 互换利率端计息方式-名义本金相关
///
- public static readonly List InterestNotionalModels = new List() {(int)InterestModeEnum.合约名义本金规模, (int)InterestModeEnum.空头存续名义本金, (int)InterestModeEnum.多头存续名义本金, (int)InterestModeEnum.标的期初全价 };
+ public static readonly List InterestNotionalModels = new List() {(int)InterestModeEnum.合约名义本金规模, (int)InterestModeEnum.标的期初全价 };
///
/// 互换利率计息方式 预付金
///
diff --git a/Framework/YLErp.Core/DBModels/Enums/InterestModeEnum.cs b/Framework/YLErp.Core/DBModels/Enums/InterestModeEnum.cs
index fcc031f8..42c59ad9 100644
--- a/Framework/YLErp.Core/DBModels/Enums/InterestModeEnum.cs
+++ b/Framework/YLErp.Core/DBModels/Enums/InterestModeEnum.cs
@@ -7,27 +7,31 @@ using System.Threading.Tasks;
namespace YLErp.DBModels
{
///
- /// 计息类型
+ /// 计息类型。显式赋值,数字即 DB 存储值,不可随意改动。
+ ///
+ /// 已删除的成员(保留编号记录,避免误回收):
+ /// 持仓名义本金 = 3 —— 死代码,零引用
+ /// 持仓市值 = 4 —— 死代码,零引用
+ /// 多头存续名义本金 = 7 —— 界面已禁用,无历史数据
+ /// 空头存续名义本金 = 8 —— 界面已禁用,无历史数据
///
public enum InterestModeEnum
{
- Unknown,
- 固定值,
- 合约名义本金规模,
- 持仓名义本金,
- 持仓市值,
- 初始预付金,
- 追加预付金,
- 多头存续名义本金,
- 空头存续名义本金,
- 标的期初全价
+ Unknown = 0,
+ 固定值 = 1,
+ 合约名义本金规模 = 2,
+ // 3=持仓名义本金(已删), 4=持仓市值(已删)
+ 初始预付金 = 5,
+ 追加预付金 = 6,
+ // 7=多头存续名义本金(已删), 8=空头存续名义本金(已删)
+ 标的期初全价 = 9
}
///
/// 计息方式
///
public enum InterestTypeEnum
{
- 单利,
- 复利
+ 单利 = 0,
+ 复利 = 1
}
}
diff --git a/Framework/YLErp.Core/Interest/IIndexFixer.cs b/Framework/YLErp.Core/Interest/IIndexFixer.cs
new file mode 100644
index 00000000..b7a7dea9
--- /dev/null
+++ b/Framework/YLErp.Core/Interest/IIndexFixer.cs
@@ -0,0 +1,11 @@
+namespace YLErp.Derivatives.Interest;
+
+///
+/// 浮动利率指数取价器(fixing)。给定取价日和标的代码,返回当日利率。
+/// 参考 QuantLib IborIndex:取价与计息分离,计息函数只消费 rate。
+///
+public interface IIndexFixer
+{
+ /// 该日利率(小数,如 0.0134 = 1.34%)。取不到时为 0。
+ bool TryGetFixing(DateTime fixingDate, string underlyingCode, out decimal rate);
+}
diff --git a/Framework/YLErp.Core/Interest/IndexFixerBase.cs b/Framework/YLErp.Core/Interest/IndexFixerBase.cs
new file mode 100644
index 00000000..72f958d5
--- /dev/null
+++ b/Framework/YLErp.Core/Interest/IndexFixerBase.cs
@@ -0,0 +1,20 @@
+using YLErp.QdpModule;
+
+namespace YLErp.Derivatives.Interest;
+
+///
+/// 取价日计算工具,供 IIndexFixer 实现复用。
+/// 收敛原 SwapDealService 里散落 6 处的 GetNonHolidayDefore(date.AddDays(rule))。
+///
+public abstract class IndexFixerBase
+{
+ ///
+ /// 重置日 + 利率规则 → 取价日(工作日回拨)。
+ /// interestRule: 0 = 当前营业日,-1 = 前一营业日。
+ ///
+ public static DateTime GetFixingDate(DateTime resetDate, int interestRule)
+ => QdpCalendarHelper.GetNonHolidayDefore(resetDate.AddDays(interestRule));
+
+ public static DateTime GetFixingDate(DateTime resetDate, int? interestRule)
+ => GetFixingDate(resetDate, interestRule ?? 0);
+}
diff --git a/UnitTestProject/Modules/SwapModule/FundingLegs/FundingLegStrategyTest.cs b/UnitTestProject/Modules/SwapModule/FundingLegs/FundingLegStrategyTest.cs
new file mode 100644
index 00000000..c3afb09f
--- /dev/null
+++ b/UnitTestProject/Modules/SwapModule/FundingLegs/FundingLegStrategyTest.cs
@@ -0,0 +1,143 @@
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using YLErp.DBModels.Enums;
+using YLErp.Modules.SwapModule.FundingLegs;
+
+namespace UnitTestProject.Modules.SwapModule.FundingLegs
+{
+ ///
+ /// 融资腿策略单测。验证每个策略的 CalcNotional 与现有 CalcNotionalByMode switch 完全一致。
+ /// 这组测试是后续"迁移调用点"的安全网——迁移前后行为必须不变。
+ ///
+ [TestClass]
+ public class FundingLegStrategyTest
+ {
+ private const decimal Fix = 2_000_000m;
+ private const decimal Notional = 100_000_000m;
+ private const decimal LongNotional = 60_000_000m;
+ private const decimal ShortNotional = 40_000_000m;
+
+ #region 固定值(mode 1)
+
+ [TestMethod]
+ public void 固定值_部分平仓_计息基数恒等于Fix()
+ {
+ var leg = new FixedAmountLeg();
+ var r = leg.CalcNotional(Fix, Notional, LongNotional, ShortNotional, 0.5m);
+
+ Assert.AreEqual(Fix, r.ClosePrincipal, "平仓本金恒=Fix");
+ Assert.AreEqual(Fix, r.PosiPrincipal, "持仓本金恒=Fix");
+ Assert.AreEqual(1m, r.ClosePercent);
+ }
+
+ [TestMethod]
+ public void 固定值_全平_计息基数仍等于Fix()
+ {
+ var leg = new FixedAmountLeg();
+ var r = leg.CalcNotional(Fix, Notional, LongNotional, ShortNotional, 1m);
+ Assert.AreEqual(Fix, r.ClosePrincipal);
+ }
+
+ #endregion
+
+ #region 合约名义本金规模(mode 2)
+
+ [TestMethod]
+ public void 合约名义本金_部分平仓_本金按比例缩放()
+ {
+ var leg = new ContractNotionalLeg();
+ var r = leg.CalcNotional(Fix, Notional, LongNotional, ShortNotional, 0.5m);
+
+ Assert.AreEqual(50_000_000m, r.ClosePrincipal);
+ Assert.AreEqual(Notional, r.PosiPrincipal);
+ Assert.AreEqual(0.5m, r.ClosePercent);
+ }
+
+ [TestMethod]
+ public void 合约名义本金_全平_本金等于全额()
+ {
+ var leg = new ContractNotionalLeg();
+ var r = leg.CalcNotional(Fix, Notional, LongNotional, ShortNotional, 1m);
+ Assert.AreEqual(Notional, r.ClosePrincipal);
+ }
+
+ [TestMethod]
+ public void 合约名义本金_零平仓_本金为零()
+ {
+ var leg = new ContractNotionalLeg();
+ var r = leg.CalcNotional(Fix, Notional, LongNotional, ShortNotional, 0m);
+
+ Assert.AreEqual(0m, r.ClosePrincipal);
+ Assert.AreEqual(Notional, r.PosiPrincipal);
+ }
+
+ #endregion
+
+ #region 标的期初全价(mode 9)
+
+ [TestMethod]
+ public void 标的期初全价_部分平仓_主路径公式同mode2()
+ {
+ var leg = new UnderlyingEntryFullPriceLeg();
+ var r = leg.CalcNotional(Fix, Notional, LongNotional, ShortNotional, 0.5m);
+
+ Assert.AreEqual(50_000_000m, r.ClosePrincipal);
+ Assert.AreEqual(Notional, r.PosiPrincipal);
+ Assert.AreEqual(0.5m, r.ClosePercent);
+ }
+
+ [TestMethod]
+ public void 标的期初全价_全平_本金等于全额()
+ {
+ var leg = new UnderlyingEntryFullPriceLeg();
+ var r = leg.CalcNotional(Fix, Notional, LongNotional, ShortNotional, 1m);
+ Assert.AreEqual(Notional, r.ClosePrincipal);
+ }
+
+ #endregion
+
+ #region 守卫
+
+ [TestMethod]
+ public void 各策略对应正确枚举值()
+ {
+ Assert.AreEqual(InterestModeEnum.固定值, new FixedAmountLeg().Mode);
+ Assert.AreEqual(InterestModeEnum.合约名义本金规模, new ContractNotionalLeg().Mode);
+ Assert.AreEqual(InterestModeEnum.标的期初全价, new UnderlyingEntryFullPriceLeg().Mode);
+ }
+
+ #endregion
+
+ #region 工厂
+
+ [TestMethod]
+ public void 工厂_返回各活跃mode的策略()
+ {
+ Assert.IsInstanceOfType(FundingLegStrategyFactory.Get(InterestModeEnum.固定值), typeof(FixedAmountLeg));
+ Assert.IsInstanceOfType(FundingLegStrategyFactory.Get(InterestModeEnum.合约名义本金规模), typeof(ContractNotionalLeg));
+ Assert.IsInstanceOfType(FundingLegStrategyFactory.Get(InterestModeEnum.标的期初全价), typeof(UnderlyingEntryFullPriceLeg));
+ }
+
+ [TestMethod]
+ public void 工厂_未注册mode抛异常()
+ {
+ // mode 3/4/7/8 已从枚举删除,用 Unknown(0)验证未注册抛异常
+ Assert.ThrowsException(() =>
+ FundingLegStrategyFactory.Get(InterestModeEnum.Unknown));
+ Assert.ThrowsException(() =>
+ FundingLegStrategyFactory.Get(InterestModeEnum.初始预付金));
+ Assert.ThrowsException(() =>
+ FundingLegStrategyFactory.Get(InterestModeEnum.追加预付金));
+ }
+
+ [TestMethod]
+ public void 工厂_int重载和枚举重载等价()
+ {
+ var byEnum = FundingLegStrategyFactory.Get(InterestModeEnum.固定值);
+ var byInt = FundingLegStrategyFactory.Get((int)InterestModeEnum.固定值);
+ Assert.AreEqual(byEnum.Mode, byInt.Mode);
+ }
+
+ #endregion
+ }
+}
diff --git a/UnitTestProject/Modules/SwapModule/Margin/MarginLegTest.cs b/UnitTestProject/Modules/SwapModule/Margin/MarginLegTest.cs
new file mode 100644
index 00000000..156eb932
--- /dev/null
+++ b/UnitTestProject/Modules/SwapModule/Margin/MarginLegTest.cs
@@ -0,0 +1,121 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using YLErp.Derivatives.Interest;
+using YLErp.Modules.SwapModule.Margin;
+
+namespace UnitTestProject.Modules.SwapModule.Margin
+{
+ ///
+ /// 保证金账户(MarginAccount)单测。验证余额变动(追加/释放/返还)。
+ /// 保证金就是保证金——有余额、有利率、有利息,不存在"计息基数/Notional"概念。
+ ///
+ [TestClass]
+ public class MarginLegTest
+ {
+ private const decimal Opening = 2_000_000m;
+
+ #region MarginAccount 余额变动
+
+ [TestMethod]
+ public void 账户_初始余额等于期初保证金()
+ {
+ var account = new MarginAccount(new MarginBalance(Opening));
+ Assert.AreEqual(Opening, account.Balance.Balance);
+ }
+
+ [TestMethod]
+ public void 账户_追加保证金_余额增加()
+ {
+ var account = new MarginAccount(new MarginBalance(Opening));
+ account.Deposit(500_000m);
+ Assert.AreEqual(2_500_000m, account.Balance.Balance);
+ }
+
+ [TestMethod]
+ public void 账户_释放保证金_余额减少()
+ {
+ var account = new MarginAccount(new MarginBalance(Opening));
+ account.Withdraw(800_000m);
+ Assert.AreEqual(1_200_000m, account.Balance.Balance);
+ }
+
+ [TestMethod]
+ public void 账户_释放超过余额_不低于零()
+ {
+ var account = new MarginAccount(new MarginBalance(Opening));
+ account.Withdraw(3_000_000m);
+ Assert.AreEqual(0m, account.Balance.Balance, "保证金余额不低于零");
+ }
+
+ #endregion
+
+ #region 三种保证金形态解析器
+
+ [TestMethod]
+ public void 三种形态解析器_各自返回正确Form和余额()
+ {
+ IMarginResolver cash = new CashMargin();
+ IMarginResolver credit = new CreditMargin();
+ IMarginResolver guarantee = new GuaranteeMargin();
+
+ Assert.AreEqual(MarginForm.Cash, cash.Form);
+ Assert.AreEqual(MarginForm.Credit, credit.Form);
+ Assert.AreEqual(MarginForm.Guarantee, guarantee.Form);
+
+ Assert.AreEqual(Opening, cash.Resolve(Opening).Balance);
+ Assert.AreEqual(Opening, credit.Resolve(Opening).Balance);
+ Assert.AreEqual(Opening, guarantee.Resolve(Opening).Balance);
+ }
+
+ #endregion
+
+ #region MarginAccount 计息
+
+ [TestMethod]
+ public void 计息_单利7天_余额200万年化3pct()
+ {
+ var account = new MarginAccount(new MarginBalance(2_000_000m));
+ // 200万 × 3% / 365 × 7天 = 1150.68...
+ var r = account.AccrueInterest(
+ rate: 0.03m,
+ startDate: new System.DateTime(2026, 5, 4),
+ endDate: new System.DateTime(2026, 5, 11),
+ boundary: AccrualBoundary.StartOnly,
+ annualDays: 365);
+
+ Assert.IsTrue(r.Accrued > 0, "7天利息应大于0");
+ System.Console.WriteLine($"保证金7天利息={r.Accrued}");
+ }
+
+ [TestMethod]
+ public void 计息_零余额_利息为零()
+ {
+ var account = new MarginAccount(new MarginBalance(0m));
+ var r = account.AccrueInterest(0.03m,
+ new System.DateTime(2026, 5, 4), new System.DateTime(2026, 5, 11),
+ AccrualBoundary.StartOnly, 365);
+
+ Assert.AreEqual(0m, r.Accrued);
+ }
+
+ [TestMethod]
+ public void 计息_释放后余额减少_利息相应减少()
+ {
+ var full = new MarginAccount(new MarginBalance(2_000_000m));
+ var half = new MarginAccount(new MarginBalance(2_000_000m));
+ half.Withdraw(1_000_000m);
+
+ var rFull = full.AccrueInterest(0.03m,
+ new System.DateTime(2026, 5, 4), new System.DateTime(2026, 5, 11),
+ AccrualBoundary.StartOnly, 365);
+ var rHalf = half.AccrueInterest(0.03m,
+ new System.DateTime(2026, 5, 4), new System.DateTime(2026, 5, 11),
+ AccrualBoundary.StartOnly, 365);
+
+ Assert.IsTrue(rHalf.Accrued < rFull.Accrued, "释放后利息应更少");
+ Assert.IsTrue(System.Math.Abs(rFull.Accrued - rHalf.Accrued * 2m) < 0.01m,
+ "余额减半, 利息也应减半");
+ }
+
+ #endregion
+ }
+}
diff --git a/UnitTestProject/Modules/SwapModule/Margin/MarginModesTest.cs b/UnitTestProject/Modules/SwapModule/Margin/MarginModesTest.cs
new file mode 100644
index 00000000..edf94c89
--- /dev/null
+++ b/UnitTestProject/Modules/SwapModule/Margin/MarginModesTest.cs
@@ -0,0 +1,48 @@
+using System.Linq;
+using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using YLErp.DBModels;
+using YLErp.Modules.SwapModule.Margin;
+
+namespace UnitTestProject.Modules.SwapModule.Margin
+{
+ ///
+ /// MarginModes 统一判断口径测试。
+ /// 验证它和现有散落的 marginTypes/InterestMarginModels/premiumModes 内容一致。
+ ///
+ [TestClass]
+ public class MarginModesTest
+ {
+ [TestMethod]
+ public void All_只含初始预付金和追加预付金()
+ {
+ CollectionAssert.AreEquivalent(
+ new[] { (int)InterestModeEnum.初始预付金, (int)InterestModeEnum.追加预付金 },
+ MarginModes.All.ToList());
+ }
+
+ [TestMethod]
+ public void Contains_初始预付金_返回true()
+ => Assert.IsTrue(MarginModes.Contains((int)InterestModeEnum.初始预付金));
+
+ [TestMethod]
+ public void Contains_追加预付金_返回true()
+ => Assert.IsTrue(MarginModes.Contains((int)InterestModeEnum.追加预付金));
+
+ [TestMethod]
+ public void Contains_融资腿mode_返回false()
+ {
+ Assert.IsFalse(MarginModes.Contains((int)InterestModeEnum.固定值));
+ Assert.IsFalse(MarginModes.Contains((int)InterestModeEnum.合约名义本金规模));
+ Assert.IsFalse(MarginModes.Contains((int)InterestModeEnum.标的期初全价));
+ }
+
+ /// 守护:和 ConsTrade.InterestMarginModels 内容必须一致(迁移期对齐)。
+ [TestMethod]
+ public void 与ConsTradeInterestMarginModels内容一致()
+ {
+ var consTrade = new List { (int)InterestModeEnum.初始预付金, (int)InterestModeEnum.追加预付金 };
+ CollectionAssert.AreEquivalent(consTrade, MarginModes.All.ToList());
+ }
+ }
+}
diff --git a/UnitTestProject/Modules/SwapModule/ReturnLegs/DirectionRatioTest.cs b/UnitTestProject/Modules/SwapModule/ReturnLegs/DirectionRatioTest.cs
new file mode 100644
index 00000000..ac36a4f2
--- /dev/null
+++ b/UnitTestProject/Modules/SwapModule/ReturnLegs/DirectionRatioTest.cs
@@ -0,0 +1,27 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using YLErp.DBModels;
+using YLErp.DBModels.Enums;
+using YLErp.Modules.SwapModule.ReturnLegs;
+
+namespace UnitTestProject.Modules.SwapModule.ReturnLegs
+{
+ [TestClass]
+ public class DirectionRatioTest
+ {
+ [TestMethod]
+ public void LongShort_多头_返回1()
+ => Assert.AreEqual(1, DirectionRatio.LongShort((int)PositionTypeFlag.Long));
+
+ [TestMethod]
+ public void LongShort_空头_返回负1()
+ => Assert.AreEqual(-1, DirectionRatio.LongShort((int)PositionTypeFlag.Short));
+
+ [TestMethod]
+ public void ReceivePay_收取_返回1()
+ => Assert.AreEqual(1, DirectionRatio.ReceivePay((int)SwapDirectionEnum.收取));
+
+ [TestMethod]
+ public void ReceivePay_支付_返回负1()
+ => Assert.AreEqual(-1, DirectionRatio.ReceivePay((int)SwapDirectionEnum.支付));
+ }
+}
diff --git a/UnitTestProject/Modules/SwapModule/ReturnLegs/DividendCalcTest.cs b/UnitTestProject/Modules/SwapModule/ReturnLegs/DividendCalcTest.cs
new file mode 100644
index 00000000..20a842ad
--- /dev/null
+++ b/UnitTestProject/Modules/SwapModule/ReturnLegs/DividendCalcTest.cs
@@ -0,0 +1,37 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using YLErp.Modules.SwapModule.ReturnLegs;
+
+namespace UnitTestProject.Modules.SwapModule.ReturnLegs
+{
+ [TestClass]
+ public class DividendCalcTest
+ {
+ [TestMethod]
+ public void AfterTax_零税率_等于原值()
+ {
+ Assert.AreEqual(1000m, DividendCalc.AfterTax(1000m, 0m));
+ }
+
+ [TestMethod]
+ public void AfterTax_6pct增值税()
+ {
+ // 1000 / 1.06 * 0.94 = 886.79...
+ Assert.AreEqual(886.79m, DividendCalc.AfterTax(1000m, 0.06m));
+ }
+
+ [TestMethod]
+ public void AfterTax_负票息()
+ {
+ // -500 / 1.06 * 0.94 = -443.40
+ Assert.AreEqual(-443.40m, DividendCalc.AfterTax(-500m, 0.06m));
+ }
+
+ [TestMethod]
+ public void AfterTaxRaw_不四舍五入()
+ {
+ var raw = DividendCalc.AfterTaxRaw(1000m, 0.06m);
+ Assert.AreNotEqual(886.79m, raw, "Raw 版本不四舍五入");
+ Assert.IsTrue(raw > 886.79m && raw < 886.80m);
+ }
+ }
+}
diff --git a/UnitTestProject/Modules/SwapModule/ReturnLegs/MtmCalcTest.cs b/UnitTestProject/Modules/SwapModule/ReturnLegs/MtmCalcTest.cs
new file mode 100644
index 00000000..da6969dd
--- /dev/null
+++ b/UnitTestProject/Modules/SwapModule/ReturnLegs/MtmCalcTest.cs
@@ -0,0 +1,45 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using YLErp.Modules.SwapModule.ReturnLegs;
+
+namespace UnitTestProject.Modules.SwapModule.ReturnLegs
+{
+ [TestClass]
+ public class MtmCalcTest
+ {
+ [TestMethod]
+ public void MarketValue_多头_为正()
+ {
+ Assert.AreEqual(100_000m, MtmCalc.MarketValue(100m, 1000m, 1m, 1));
+ }
+
+ [TestMethod]
+ public void MarketValue_空头_为负()
+ {
+ Assert.AreEqual(-100_000m, MtmCalc.MarketValue(100m, 1000m, 1m, -1));
+ }
+
+ [TestMethod]
+ public void MarketValue_合约乘数10()
+ {
+ Assert.AreEqual(1_000_000m, MtmCalc.MarketValue(100m, 1000m, 10m, 1));
+ }
+
+ [TestMethod]
+ public void UnrealizedPnl_多头浮盈()
+ {
+ Assert.AreEqual(5000m, MtmCalc.UnrealizedPnl(105m, 100m, 1000m, 1m, 1, 1));
+ }
+
+ [TestMethod]
+ public void UnrealizedPnl_多头浮亏()
+ {
+ Assert.AreEqual(-5000m, MtmCalc.UnrealizedPnl(95m, 100m, 1000m, 1m, 1, 1));
+ }
+
+ [TestMethod]
+ public void UnrealizedPnl_空头反向()
+ {
+ Assert.AreEqual(5000m, MtmCalc.UnrealizedPnl(95m, 100m, 1000m, 1m, -1, 1));
+ }
+ }
+}
diff --git a/UnitTestProject/Modules/SwapModule/ReturnLegs/QtyRollforwardTest.cs b/UnitTestProject/Modules/SwapModule/ReturnLegs/QtyRollforwardTest.cs
new file mode 100644
index 00000000..3c423d33
--- /dev/null
+++ b/UnitTestProject/Modules/SwapModule/ReturnLegs/QtyRollforwardTest.cs
@@ -0,0 +1,60 @@
+using YLErp.Modules.SwapModule.ReturnLegs;
+
+namespace UnitTestProject.Modules.SwapModule.ReturnLegs
+{
+ ///
+ /// 标的端数量递推测试。验证开仓/平仓/公司行为场景下数量正确推进。
+ ///
+ [TestClass]
+ public class QtyRollforwardTest
+ {
+ [TestMethod]
+ public void 无交易_数量不变()
+ {
+ Assert.AreEqual(1000m, QtyRollforward.Calc(1000m, 0m, 0m));
+ }
+
+ [TestMethod]
+ public void 开仓_数量增加()
+ {
+ Assert.AreEqual(1500m, QtyRollforward.Calc(1000m, 500m, 0m));
+ }
+
+ [TestMethod]
+ public void 平仓_数量减少()
+ {
+ Assert.AreEqual(600m, QtyRollforward.Calc(1000m, 0m, 400m));
+ }
+
+ [TestMethod]
+ public void 开仓加平仓_净值()
+ {
+ Assert.AreEqual(1200m, QtyRollforward.Calc(1000m, 500m, 300m));
+ }
+
+ [TestMethod]
+ public void 全平_数量归零()
+ {
+ Assert.AreEqual(0m, QtyRollforward.Calc(1000m, 0m, 1000m));
+ }
+
+ [TestMethod]
+ public void 超额平仓_不低于零()
+ {
+ Assert.AreEqual(0m, QtyRollforward.Calc(1000m, 0m, 1500m));
+ }
+
+ [TestMethod]
+ public void 公司行为送股_数量增加_默认不影响()
+ {
+ Assert.AreEqual(1000m, QtyRollforward.Calc(1000m, 0m, 0m));
+ Assert.AreEqual(1100m, QtyRollforward.Calc(1000m, 0m, 0m, corpActionDeltaQty: 100m));
+ }
+
+ [TestMethod]
+ public void 公司行为拆股_数量翻倍()
+ {
+ Assert.AreEqual(2000m, QtyRollforward.Calc(1000m, 0m, 0m, corpActionDeltaQty: 1000m));
+ }
+ }
+}
diff --git a/UnitTestProject/Modules/SwapModule/ReturnLegs/ReturnLegSummaryTest.cs b/UnitTestProject/Modules/SwapModule/ReturnLegs/ReturnLegSummaryTest.cs
new file mode 100644
index 00000000..61f9a69c
--- /dev/null
+++ b/UnitTestProject/Modules/SwapModule/ReturnLegs/ReturnLegSummaryTest.cs
@@ -0,0 +1,46 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using YLErp.Modules.SwapModule.ReturnLegs;
+
+namespace UnitTestProject.Modules.SwapModule.ReturnLegs
+{
+ ///
+ /// ReturnLegSummary 值对象测试。
+ /// 验证标的端归档产出的汇总值正确构造、字段语义清晰。
+ ///
+ [TestClass]
+ public class ReturnLegSummaryTest
+ {
+ [TestMethod]
+ public void 构造_四个字段正确赋值()
+ {
+ var s = new ReturnLegSummary(
+ longNotional: 100_000_000m,
+ shortNotional: 0m,
+ closeNotional: 50_000_000m,
+ grossPrice: 1.02m);
+
+ Assert.AreEqual(100_000_000m, s.LongNotional);
+ Assert.AreEqual(0m, s.ShortNotional);
+ Assert.AreEqual(50_000_000m, s.CloseNotional);
+ Assert.AreEqual(1.02m, s.GrossPrice);
+ }
+
+ [TestMethod]
+ public void 构造_空头场景()
+ {
+ var s = new ReturnLegSummary(0m, 80_000_000m, 30_000_000m, 0.98m);
+
+ Assert.AreEqual(0m, s.LongNotional, "无多头");
+ Assert.AreEqual(80_000_000m, s.ShortNotional, "空头名义本金");
+ }
+
+ [TestMethod]
+ public void 构造_全平场景()
+ {
+ var s = new ReturnLegSummary(0m, 0m, 100_000_000m, 1.00m);
+
+ Assert.AreEqual(100_000_000m, s.CloseNotional, "全平:平仓名义本金=全额");
+ Assert.AreEqual(0m, s.LongNotional, "全平后无多头剩余");
+ }
+ }
+}
diff --git a/UnitTestProject/Modules/SwapModule/SwapFixedInterestLegClosePercentBugTest.cs b/UnitTestProject/Modules/SwapModule/SwapFixedInterestLegClosePercentBugTest.cs
deleted file mode 100644
index 3bdd657c..00000000
--- a/UnitTestProject/Modules/SwapModule/SwapFixedInterestLegClosePercentBugTest.cs
+++ /dev/null
@@ -1,341 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-using Newtonsoft.Json;
-using YLErp;
-using YLErp.DBModels;
-using YLErp.DBModels.Enums;
-using YLErp.Modules.SwapModule;
-
-namespace UnitTestProject.Modules.SwapModule
-{
- ///
- /// 【缺陷证明测试】界面「全部平仓 → 部分平仓」,改变平仓比例,下方利息腿数据不变。
- /// ------------------------------------------------------------------------------
- /// 现象(前端):SwapUnwind.cshtml 的 ClosePercent 输入框 → changeClosePercent →
- /// getInterestList → POST /swaptrade2/GetUnwindInterestList → 后端返回的利息腿列表数值不随比例变化。
- ///
- /// 根因(后端,本测试要钉死的):SwapDealService.CalcNotionalByMode
- /// case InterestModeEnum.固定值:
- /// closePrincipal = posiPrincipal = position.InterestPrincipalFix;
- /// newClosePercent = 1m; // ← 这一行把平仓比例直接抹成 1
- /// 固定值利息腿的计息本金恒等于 InterestPrincipalFix(与比例无关),
- /// 同时 newClosePercent 被强制置 1,后续 CalcDailySimpleInterest / CalcDailyCompoundInterest
- /// 拿到的 closePercent 也是 1 → 无论前端传 30% / 50% / 70% / 100%,返回利息完全相同。
- ///
- /// 对照组:InterestModeEnum.标的期初全价(9) 走 closePrincipal = posiNotional * closePercent,
- /// 线性缩放,作为「正确行为」的基准。若对照组也不变,说明问题不在本函数(排除法)。
- ///
- /// 调用方式:完全复刻生产 GetUnwindInterests(SwapDealService.cs:640-645) 的入参构造——
- /// posiNotionalValue = stockEqvNotional(剩余名义本金,不乘比例)
- /// posiLongNotionalValue = 多头剩余
- /// closePosiNotionalValue = stockEqvNotional * closePercent
- /// closePrecent = closePercent(控制器已做 A→B 口径转换)
- /// add=true, settment=false(盘中预览,不落库)
- /// 因此这是**真实生产函数**的行为,不是夹具自造逻辑。
- ///
- /// 期望(修复前 红 / 修复后 绿):固定值腿利息应与 closePercent 成正比。
- ///
- [TestClass]
- public class SwapFixedInterestLegClosePercentBugTest
- {
- private const decimal Notional = 100_000_000m; // 期初=剩余名义本金 1 亿
- private const decimal FixedRate = 0.03m; // 固定年利率 3%
- private const int AnnualDays = 365;
-
- private static readonly DateTime StartDate = new DateTime(2026, 4, 21);
- private static readonly DateTime Maturity = new DateTime(2026, 6, 30);
- private static readonly DateTime CloseDate = new DateTime(2026, 5, 11);
-
- #region Stub(无库)
-
- private sealed class StubSvc : SwapDealService
- {
- public StubSvc() : base(new OptUserInfo(0, nameof(SwapFixedInterestLegClosePercentBugTest), OptUserFrom.UnitTest)) { }
- // 无库环境:已消耗利息视为 0
- public override decimal GetConsumedInterest(int tradeId, long positionId, DateTime beforeDate) => 0m;
- }
-
- #endregion
-
- #region 构造器
-
- private static trade CreateTrade(string interestCalcMode)
- {
- var extend = new trade_extend
- {
- TradeId = 1,
- ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
- {
- AnnualDays = AnnualDays,
- InterestCalcMode = interestCalcMode,
- SettlementRules = 0
- })
- };
- return new trade
- {
- id = 1,
- TradeNumber = "UT-CLOSEPCT-BUG",
- ClientId = 999998,
- TradeType = "收益互换",
- TradeDate = StartDate,
- StartDate = StartDate,
- ExerciseDate = Maturity,
- TradeStatus = "确认成交",
- ValidState = "Valid",
- trade_extend = extend
- };
- }
-
- /// 固定利率利息腿(不走浮动曲线),仅 InterestMode 不同
- private static swap_position CreateLeg(int interestMode, InterestTypeEnum interestType)
- {
- var intervalModels = new List
- {
- new IntervalModel { Date = Maturity, Rate = FixedRate, Settlement = 0 }
- };
- return new swap_position
- {
- id = 1001,
- SwapTradeId = 1,
- PositionType = (int)PositionTypeFlag.Unknown,
- PosiDirection = 0, // 利息腿
- InterestDirection = (int)SwapDirectionEnum.收取,
- InterestMode = interestMode,
- InterestRateDefault = FixedRate,
- InterestPrincipalFix = Notional, // 固定值腿的计息本金
- PosiStartDate = StartDate,
- PosiMatuirityDate = Maturity,
- IsInitial = true,
- Invalid = false,
- InterestType = (int)interestType,
- IsAnnualized = true,
- interest_rest_days = 1,
- interest_rule = 0,
- FloatRateUnderlyingCode = null, // 固定利率
- InterestSwapInterval = JsonConvert.SerializeObject(intervalModels)
- };
- }
-
- ///
- /// 复刻 GetUnwindInterests(SwapDealService.cs:640-645) 的入参构造,
- /// 唯一变量是界面输入的 closePercent。
- ///
- private static swap_flow_event CallProductionPath(trade td, swap_position leg, decimal closePercent)
- {
- var svc = new StubSvc();
- var stockEqvNotional = Notional; // 剩余名义本金
- var posiLongNotionalValue = Notional;
- var posiShortNotionalValue = 0m;
- var closePosiNotionalValue = stockEqvNotional * closePercent; // 生产:posiNotionalValue = stockEqvNotional * closePercent
- var orginPv = stockEqvNotional;
-
- var interests = svc.GetInterests(
- td, td.trade_extend, CloseDate, CloseDate,
- new List(), // 无上一日 EOD
- new List { leg },
- stockEqvNotional,
- posiLongNotionalValue,
- posiShortNotionalValue,
- closePosiNotionalValue,
- closePercent,
- (int)SwapEventTypeEnum.平仓,
- tdClose: false,
- needPrice: false,
- grossPrice: 0m,
- orginPv: orginPv,
- add: true,
- settment: false);
-
- Assert.AreEqual(1, interests.Count, "应返回 1 条利息腿");
- return interests[0];
- }
-
- #endregion
-
- /// 与 CallProductionPath 相同,但允许指定平仓日期(用于两段式生命周期)
- private static swap_flow_event CallWithDate(trade td, swap_position leg, decimal closePercent, DateTime closeDate)
- {
- var svc = new StubSvc();
- var stockEqvNotional = Notional;
- var posiLongNotionalValue = Notional;
- var posiShortNotionalValue = 0m;
- var closePosiNotionalValue = stockEqvNotional * closePercent;
- var orginPv = stockEqvNotional;
-
- var interests = svc.GetInterests(
- td, td.trade_extend, closeDate, closeDate,
- new List(),
- new List { leg },
- stockEqvNotional,
- posiLongNotionalValue,
- posiShortNotionalValue,
- closePosiNotionalValue,
- closePercent,
- (int)SwapEventTypeEnum.平仓,
- tdClose: false,
- needPrice: false,
- grossPrice: 0m,
- orginPv: orginPv,
- add: true,
- settment: false);
-
- Assert.AreEqual(1, interests.Count, "应返回 1 条利息腿");
- return interests[0];
- }
-
- #region 业务场景4-2:固定值腿 部分平仓后再全平(两段式,GLMS-债券TRS期间结算 260702「未测」缺口)
-
- ///
- /// 覆盖「国联民生-债券TRS期间结算功能测试260702.xlsx」Sheet1 矩阵中唯一标记为「未测」的格子:
- /// 业务场景4-2(固定值腿,部分平仓一次后经过数日再全平)。
- /// 同时守护 d1badfe4 的固定值腿平仓比例缩放修复——
- /// 修复前 newClosePercent=1,部分平仓利息与全平完全相同(界面改比例利息腿不变)。
- /// 本测试断言两段利息各自随平仓比例线性缩放(30% / 70%),正是该场景的回归守卫。
- ///
- [DataTestMethod]
- [DataRow("11")] // 算头算尾
- [DataRow("10")] // 算头不算尾
- public void 业务场景4_2_固定值腿_部分平仓后再全平_两段利息均随比例线性缩放(string calcMode)
- {
- var td = CreateTrade(calcMode);
- var leg = CreateLeg((int)InterestModeEnum.固定值, InterestTypeEnum.单利);
-
- // 单步全平(100%)作为基准
- var full = CallProductionPath(td, leg, 1m);
- Assert.AreNotEqual(0m, full.InterestAmount, "全平利息不应为 0,否则用例无区分度");
-
- var partialDate = new DateTime(2026, 5, 11);
- // 第一段:部分平仓 30%(较早日期 2026-05-11)
- var i30 = CallWithDate(td, leg, 0.3m, partialDate);
- // 第二段:剩余 70% 全平(到期日,算尾)
- var i70 = CallWithDate(td, leg, 0.7m, Maturity);
- // 各段对照「同日期全平」基准:缩放必须在相同日期范围内比较
- var fullAtPartial = CallWithDate(td, leg, 1m, partialDate);
- var fullAtMaturity = CallWithDate(td, leg, 1m, Maturity);
-
- Console.WriteLine($"[场景4-2 mode=1 calcMode={calcMode}] 5/11全平={fullAtPartial.InterestAmount} 30%={i30.InterestAmount} 到期全平={fullAtMaturity.InterestAmount} 70%={i70.InterestAmount}");
-
- // 核心断言:两段利息必须各自随平仓比例线性缩放(守护 mode-1 修复,修复前会等于全平)
- Assert.AreEqual((double)(fullAtPartial.InterestAmount * 0.3m), (double)i30.InterestAmount, 0.01,
- "固定值腿第一段(30%)利息应≈同日期全平×30%(修复前会等于全平,即 Bug)");
- Assert.AreEqual((double)(fullAtMaturity.InterestAmount * 0.7m), (double)i70.InterestAmount, 0.01,
- "固定值腿第二段(70%)利息应≈同日期全平×70%");
- }
-
- #endregion
-
- #region 对照组:标的期初全价(9) —— 正确行为,比例线性缩放
-
- [DataTestMethod]
- [DataRow("11")] // 算头算尾
- [DataRow("10")] // 算头不算尾
- public void 对照组_标的期初全价腿_利息应随平仓比例线性变化(string calcMode)
- {
- var td = CreateTrade(calcMode);
- var full = CallProductionPath(td, CreateLeg((int)InterestModeEnum.标的期初全价, InterestTypeEnum.单利), 1m);
-
- Console.WriteLine($"[对照组 mode=9 calcMode={calcMode}] 100% 利息 = {full.InterestAmount}");
- Assert.AreNotEqual(0m, full.InterestAmount, "全平利息不应为 0,否则用例无区分度");
-
- foreach (var pct in new[] { 0.3m, 0.5m, 0.7m })
- {
- var part = CallProductionPath(td, CreateLeg((int)InterestModeEnum.标的期初全价, InterestTypeEnum.单利), pct);
- var expect = full.InterestAmount * pct;
- Console.WriteLine($" {pct:P0} 实际={part.InterestAmount} 期望={expect} 差={part.InterestAmount - expect}");
- Assert.AreEqual((double)expect, (double)part.InterestAmount, 0.01,
- $"mode=9 应线性缩放:{pct:P0}");
- }
- }
-
- #endregion
-
- #region 缺陷组:固定值(1) —— 当前不随比例变化(修复前红)
-
- [DataTestMethod]
- [DataRow("11", 0)] // 算头算尾 + 单利
- [DataRow("10", 0)] // 算头不算尾 + 单利
- [DataRow("11", 1)] // 算头算尾 + 复利
- [DataRow("10", 1)] // 算头不算尾 + 复利
- public void 缺陷_固定值利息腿_利息必须随平仓比例线性变化(string calcMode, int typeFlag)
- {
- var type = typeFlag == 1 ? InterestTypeEnum.复利 : InterestTypeEnum.单利;
- var td = CreateTrade(calcMode);
- var full = CallProductionPath(td, CreateLeg((int)InterestModeEnum.固定值, type), 1m);
-
- Console.WriteLine($"[缺陷组 mode=1(固定值) calcMode={calcMode} {type}] 100% 利息 = {full.InterestAmount}");
- Assert.AreNotEqual(0m, full.InterestAmount, "全平利息不应为 0,否则用例无区分度");
-
- var diffs = new List();
- var unchanged = 0;
- foreach (var pct in new[] { 0.3m, 0.5m, 0.7m })
- {
- var part = CallProductionPath(td, CreateLeg((int)InterestModeEnum.固定值, type), pct);
- var expect = full.InterestAmount * pct;
- var delta = part.InterestAmount - expect;
- if (part.InterestAmount == full.InterestAmount) unchanged++;
- diffs.Add($" {pct:P0} 实际={part.InterestAmount} 期望={expect} 偏差={delta}");
- }
- diffs.ForEach(Console.WriteLine);
- if (unchanged == 3)
- {
- Console.WriteLine(" ▲ 三个比例返回值与 100% 完全一致 → 复现「界面改比例利息腿数据不变」");
- Console.WriteLine(" ▲ 根因:CalcNotionalByMode case 固定值 → newClosePercent = 1m");
- }
-
- foreach (var pct in new[] { 0.3m, 0.5m, 0.7m })
- {
- var part = CallProductionPath(td, CreateLeg((int)InterestModeEnum.固定值, type), pct);
- Assert.AreEqual((double)(full.InterestAmount * pct), (double)part.InterestAmount, 0.01,
- $"固定值腿应随比例缩放,但 {pct:P0} 与全平返回相同值(Bug)");
- }
- }
-
- #endregion
-
- #region 单元级:直接断言 CalcNotionalByMode 的比例语义
-
- ///
- /// 反射直击私有方法 CalcNotionalByMode,剥离所有计息细节,
- /// 只看「(closePrincipal, newClosePercent) 是否体现了平仓比例」。
- /// 注意:closePrincipal 与 newClosePercent 作用在**不同项**上——
- /// closePrincipal 决定"本期新增利息"的基数,newClosePercent 决定"历史累计/已消耗利息"的缩放,
- /// 二者不相乘(相乘会得到双重缩放的错误度量)。因此这里分别断言:
- /// 1) closePrincipal 应 = Fix × pct
- /// 2) newClosePercent 应 = pct(而非被强制置 1)
- ///
- [DataTestMethod]
- [DataRow((int)InterestModeEnum.固定值, "0.3")]
- [DataRow((int)InterestModeEnum.固定值, "0.5")]
- [DataRow((int)InterestModeEnum.固定值, "0.7")]
- public void CalcNotionalByMode_固定值腿_有效缩放系数必须等于平仓比例(int mode, string pctStr)
- {
- var pct = decimal.Parse(pctStr, System.Globalization.CultureInfo.InvariantCulture);
- var leg = CreateLeg(mode, InterestTypeEnum.单利);
- var m = typeof(SwapDealService).GetMethod("CalcNotionalByMode",
- System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
- Assert.IsNotNull(m, "未找到 CalcNotionalByMode,签名可能已变更");
-
- var full = m.Invoke(new StubSvc(), new object[] { leg, 1m, Notional, Notional, 0m });
- var part = m.Invoke(new StubSvc(), new object[] { leg, pct, Notional, Notional, 0m });
-
- var fullClose = (decimal)full.GetType().GetField("Item1").GetValue(full);
- var fullPct = (decimal)full.GetType().GetField("Item3").GetValue(full);
- var partClose = (decimal)part.GetType().GetField("Item1").GetValue(part);
- var partPct = (decimal)part.GetType().GetField("Item3").GetValue(part);
-
- Console.WriteLine($"[CalcNotionalByMode mode={mode}] 100%: closePrincipal={fullClose} newClosePercent={fullPct}");
- Console.WriteLine($"[CalcNotionalByMode mode={mode}] {pct:P0}: closePrincipal={partClose} newClosePercent={partPct}");
-
- // 1) 计息本金必须按比例缩放(本期新增利息的基数)
- Assert.AreEqual((double)(fullClose * pct), (double)partClose, 0.01,
- $"固定值腿 closePrincipal 应为 Fix×{pct:P0},实际未缩放");
- // 2) 传给 CalcDaily*Interest 的比例必须是真实平仓比例(历史累计/已消耗利息的缩放)
- Assert.AreEqual((double)pct, (double)partPct, 1e-9,
- $"固定值腿 newClosePercent 应为 {pct:P0},被强制置 1 会抹掉比例语义");
- }
-
- #endregion
- }
-}
diff --git a/UnitTestProject/Modules/SwapModule/SwapInterestScenario3And4FloatingTest.cs b/UnitTestProject/Modules/SwapModule/SwapInterestScenario3And4FloatingTest.cs
index e06b31a2..4d396b0a 100644
--- a/UnitTestProject/Modules/SwapModule/SwapInterestScenario3And4FloatingTest.cs
+++ b/UnitTestProject/Modules/SwapModule/SwapInterestScenario3And4FloatingTest.cs
@@ -100,6 +100,13 @@ namespace UnitTestProject.Modules.SwapModule
private readonly List _eodPositions = new();
private readonly IReadOnlyDictionary _floatRates;
+ ///
+ /// 捕获最近一次 CalcSwapInterests 返回的 interests.First().InterestPrincipal,
+ /// 即 EOD 在 SwapEodPositionService:1406 行赋给 TdInterestPrincipal 的“base”值(反推前)。
+ /// 用于测试中精确镜像 mode 2/9 分叉(:1458 反推 / :1465 不反推),避免对复利累计利息做人工猜测。
+ ///
+ public decimal LastBaseInterestPrincipal { get; private set; }
+
public E2EEodService(trade td, List positions, trade_extend extend,
IReadOnlyDictionary floatRates)
: base(nameof(SwapInterestScenario3And4FloatingTest))
@@ -123,11 +130,40 @@ namespace UnitTestProject.Modules.SwapModule
protected override List FindSwapPositions(int swapTradeId)
=> _positions.Where(x => x.SwapTradeId == swapTradeId && !x.IsInitial).ToList();
+ // --- 真实交易要素:标的与付息数据(替代原过度简化 stub)---
+ // 本用例 = FR007 浮动利率互换,真实要素:标的是利率指数(非债券),增值税率 0,无债券付息事件。
+ // 这些值与生产一致(利率指数 VAT 免、不进付息路径),因此不改变任何计息结果,只是不再写死魔法值。
+ private static readonly IReadOnlyDictionary _realUnderlyings =
+ new Dictionary
+ {
+ ["FR007"] = new underlying_manager
+ {
+ UnderlyingCode = "FR007",
+ UnderlyingInstrumentType = "FR007", // 利率指数,非债券,不触发付息/含税路径
+ ValueAddedTax = 0m,
+ },
+ };
+
+ // 真实付息数据源(内存镜像 BondPaymentService.GetBondPayments,按登记/付息日区间 (from, to] 筛选)。
+ // FR007 无付息事件 → 恒为 0;若接入真实债券标的,应在此注入 bond_payment_info 记录(含 reg_date 登记日)。
+ private static readonly List<(string code, DateTime payDate, decimal interest, decimal parValue)> _realBondPayments =
+ new();
+
protected override underlying_manager GetUnderlyingData(string underlyingCode)
- => new underlying_manager { ValueAddedTax = 0m, UnderlyingInstrumentType = "TBonds" };
+ => _realUnderlyings.TryGetValue(underlyingCode, out var u)
+ ? u
+ : new underlying_manager { UnderlyingCode = underlyingCode, UnderlyingInstrumentType = "Other", ValueAddedTax = 0m };
+
protected override decimal GetUnderlyingPrice(string code, DateTime settleDate, out decimal vobp)
{ vobp = 0m; return 100m; }
- protected override decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate, decimal qty, int shortRatio, int directionRatio) => 0m;
+
+ protected override decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate, decimal qty, int shortRatio, int directionRatio)
+ {
+ var interest = _realBondPayments
+ .Where(x => x.code == underlyingCode && x.payDate > fromDate && x.payDate <= toDate)
+ .Sum(x => x.interest);
+ return interest * qty; // 本用例恒为 0(FR007 无付息);金额换算对齐 BondPaymentService 口径
+ }
protected override void SaveEodSwapRecord(trade td, DateTime settleDate, DateTime preSettleDate) { }
protected override void ExecuteInTransaction(Action action) => action();
@@ -149,10 +185,13 @@ namespace UnitTestProject.Modules.SwapModule
{
var svc = new RealSwapDealService(
new OptUserInfo(0, nameof(SwapInterestScenario3And4FloatingTest), OptUserFrom.UnitTest), _floatRates, FlowEvents);
- return svc.GetInterests(td, tradeExtend, valueDate, unwindDate,
+ var interests = svc.GetInterests(td, tradeExtend, valueDate, unwindDate,
eodPositions, positions, posiNotionalValue, posiLongNotionalValue, posiShortNotionalValue,
closePosiNotionalValue, closePrecent, eventType, tdClose, needPrice,
grossPrice, orginPv, add, settment, newCalcLast, closeList);
+ // 捕获 base InterestPrincipal(= EOD:1406 行赋给 TdInterestPrincipal 的值,反推前),供 TdInterestPrincipal 断言镜像分叉。
+ LastBaseInterestPrincipal = interests.Count > 0 ? interests[0].InterestPrincipal : 0m;
+ return interests;
}
/// 对指定日期做真实日终收盘(无平仓),构建/累积 eod 链。
@@ -404,6 +443,9 @@ namespace UnitTestProject.Modules.SwapModule
DebugCompare("场景3 " + note, oracle, eod.TdCloseInterest, eod);
AssertStrict(oracle, eod.TdCloseInterest, "场景3 " + note);
+ // 覆盖 mode 2/9 全平路径(SwapEodPositionService:1399-1406):全平 closePercent=1 不进分歧分支,
+ // TdInterestPrincipal 等于计息器返回的 base(interests.First().InterestPrincipal,本服务已捕获到 LastBaseInterestPrincipal)。
+ AssertStrict(_eod.LastBaseInterestPrincipal, eod.TdInterestPrincipal, "场景3 TdInterestPrincipal " + note);
}
#endregion
@@ -443,15 +485,35 @@ namespace UnitTestProject.Modules.SwapModule
// 第一步:2026-05-11 部分平仓 30%(真实 EOD 平仓结算,produces 真实 flow event)
var partialCloseNotional = Notional * 0.3m;
+ var remainingNotional = Notional - partialCloseNotional; // 提前声明,供 TdInterestPrincipal 断言使用
var partialFlow = CalcCloseFlow(td, position, new DateTime(2026, 5, 11), new List(), partialCloseNotional, partialCloseNotional);
var partialEod = _eod.ExecuteClose(position, new DateTime(2026, 5, 11),
- Notional - partialCloseNotional, 0m, new List { partialFlow }, partialCloseNotional, prevEodPartial);
+ remainingNotional, 0m, new List { partialFlow }, partialCloseNotional, prevEodPartial);
_eod.RecordEod(partialEod);
DebugCompare("场景4[部分] " + note, oraclePartial, partialEod.TdCloseInterest, partialEod);
AssertStrict(oraclePartial, partialEod.TdCloseInterest, "场景4[部分] " + note);
+ // 覆盖 mode 2/9 分叉(SwapEodPositionService:1436-1469):部分平仓后 TdInterestPrincipal 的经济口径
+ // 必须 = 剩余动态本金(剩余名义本金 + 已并入本金的重置日待实现利息),mode2/9 应当一致。
+ // 单利:line 1491 直接取 posiNotionalValue = remainingNotional,无累计利息。
+ // 复利:base = interests.First().InterestPrincipal(本服务 CalcSwapInterests 已捕获到 LastBaseInterestPrincipal);
+ // mode2 仅在 calcLast 时于 1464 行反推剩余(× (1-cp)/cp),mode9 直取 base(GLMS-20260421-0004 禁止反推)。
+ // calcLast=false(如“算头不算尾”)或 mode9 被错误反推会膨胀 ~2.3 倍(494982903.27),下方断言精确拦截回归。
+ decimal expectedTdPrincipal;
+ if (!compound)
+ {
+ expectedTdPrincipal = remainingNotional;
+ }
+ else
+ {
+ var cp = partialCloseNotional / Notional; // = 0.3,与 EOD 内部 closePercent 一致
+ bool reverseMode2 = interestMode == (int)InterestModeEnum.合约名义本金规模 && calcLast;
+ expectedTdPrincipal = reverseMode2
+ ? _eod.LastBaseInterestPrincipal * (1m - cp) / cp
+ : _eod.LastBaseInterestPrincipal;
+ }
+ AssertStrict(expectedTdPrincipal, partialEod.TdInterestPrincipal, "场景4[部分] TdInterestPrincipal " + note);
// 部分平仓后,剩余名义本金缩减为 70%(真实代码路径更新持仓口径)
- var remainingNotional = Notional - partialCloseNotional;
position.InterestPrincipalFix = remainingNotional;
position.PosiNotionalValue = remainingNotional;
diff --git a/UnitTestProject/Modules/SwapModule/SwapUnwindFloatingLegDiagnosticTdd.cs b/UnitTestProject/Modules/SwapModule/SwapUnwindFloatingLegDiagnosticTdd.cs
index c8645fac..9340fe93 100644
--- a/UnitTestProject/Modules/SwapModule/SwapUnwindFloatingLegDiagnosticTdd.cs
+++ b/UnitTestProject/Modules/SwapModule/SwapUnwindFloatingLegDiagnosticTdd.cs
@@ -94,19 +94,5 @@ namespace YLErp.Modules.SwapModule
Assert.AreEqual(RealFix * ClosePct, close, "mode5 应 = 实时腿剩余本金(real Fix) * closePct,与 posiLong(orig 100M) 无关");
Assert.AreNotEqual(OrigFix * ClosePct, close, "务必不是期初 99,000 * closePct(证明后端修复生效)");
}
-
- [TestMethod]
- public void 诊断_若将来有_多头存续名义本金_腿_posiLong用orig才出错_本deal无此腿_故不影响()
- {
- // 构造一个「多头存续名义本金」腿,证明此时 posiLong 取值(orig vs real)会直接决定结果——
- // 说明本 deal 没有这种腿,所以 fpositions 用 orig 100M 不影响;但普通收益互换若有此腿则会踩坑。
- var longLeg = new swap_position { id = 35799, InterestMode = (int)InterestModeEnum.多头存续名义本金 };
- var byOrig = CallCalcNotionalByMode(longLeg, ClosePct, RealLong * ClosePct, OrigLong, 0m); // 当前代码:posiLong=orig 100M
- var byReal = CallCalcNotionalByMode(longLeg, ClosePct, RealLong * ClosePct, RealLong, 0m); // 若修正为 real 75.6M
- Console.WriteLine($"[多头存续名义本金] orig100M→close={byOrig.close} ; real75.6M→close={byReal.close}");
- Assert.AreEqual(OrigLong * ClosePct, byOrig.close, "现状:多头存续名义本金用 orig 100M → 多次部分平仓后会偏大");
- Assert.AreEqual(RealLong * ClosePct, byReal.close, "正确应:用 real 剩余本金 75.6M");
- Assert.AreNotEqual(byOrig.close, byReal.close, "★ 潜在同类 bug:普通收益互换(含多头/空头存续名义本金腿)在多次部分平仓后,posiLong/posiShort 用 orig 会算错——本 deal 无此腿故不触发,属本轮修复范围外");
- }
}
}
diff --git a/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs b/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs
index 557bdb25..ee29d70d 100644
--- a/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs
+++ b/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs
@@ -505,27 +505,6 @@ namespace YLErp.Modules.SwapModule
Assert.AreEqual(918_000m, fe1.InterestPrincipal, "追加预付金 10% 应=Fix×0.1");
}
- // ---- 多头/空头存续名义本金(7/8):经同一 CalcDailySimpleInterest,需证明修复通用 ----
- [TestMethod]
- public void 多头存续名义本金腿_盘中_部分平仓重置周期7天_应线性缩放()
- {
- const decimal baseP = 2_000_000m;
- var fe = CalcByMode((int)InterestModeEnum.多头存续名义本金, baseP, 0.5m);
- Assert.AreEqual(1_000_000m, fe.InterestPrincipal, "多头存续 50% 应=posiLong×0.5");
- var fe1 = CalcByMode((int)InterestModeEnum.多头存续名义本金, baseP, 0.1m);
- Assert.AreEqual(200_000m, fe1.InterestPrincipal, "多头存续 10% 应=posiLong×0.1");
- }
-
- [TestMethod]
- public void 空头存续名义本金腿_盘中_部分平仓重置周期7天_应线性缩放()
- {
- const decimal baseP = 2_000_000m;
- var fe = CalcByMode((int)InterestModeEnum.空头存续名义本金, baseP, 0.5m);
- Assert.AreEqual(1_000_000m, fe.InterestPrincipal, "空头存续 50% 应=posiShort×0.5");
- var fe1 = CalcByMode((int)InterestModeEnum.空头存续名义本金, baseP, 0.1m);
- Assert.AreEqual(200_000m, fe1.InterestPrincipal, "空头存续 10% 应=posiShort×0.1");
- }
-
// ---- 合约名义本金规模(2):CalcNotionalByMode 默认分支(posiNotional×cp) ----
[TestMethod]
public void 合约名义本金规模腿_盘中_部分平仓重置周期7天_应线性缩放()
@@ -535,17 +514,15 @@ namespace YLErp.Modules.SwapModule
Assert.AreEqual(1_000_000m, fe.InterestPrincipal, "合约名义本金规模 50% 应=posiNotional×0.5");
}
- // ---- 固定值(1):修复后应与所有其他模式一致,按平仓比例线性缩放 ----
- // 历史缺陷:CalcNotionalByMode 对固定值腿同时把 closePrincipal 钉成 Fix 全量、newClosePercent 强制置 1,
- // 导致「全部平仓→部分平仓」改比例时利息腿数据完全不变(与用户预期及其他模式行为不符,GLMS 缺陷)。
- // 修复见 SwapDealService.cs CalcNotionalByMode 固定值分支(引入提交 95686f4c,吴方海,EQD-5718,2026-05-08)。
- // 旧断言「对平仓比例免疫、恒=Fix」本身就是在保卫该缺陷,此处纠正为正确的线性缩放。
+ // ---- 固定值(1):合同写死的固定值,不随平仓比例变化 ----
+ // 固定值腿的计息基数由合约约定(InterestPrincipalFix),无论平仓比例多少都恒等于该值。
+ // closePrincipal = posiPrincipal = Fix; newClosePercent = 1m 是正确的业务行为,不是缺陷。
[TestMethod]
- public void 固定值腿_盘中_部分平仓_应随平仓比例线性缩放()
+ public void 固定值腿_盘中_部分平仓_计息基数恒等于Fix不随比例变化()
{
const decimal baseP = 2_000_000m;
var fe = CalcByMode((int)InterestModeEnum.固定值, baseP, 0.5m);
- Assert.AreEqual(1_000_000m, fe.InterestPrincipal, "固定值腿修复后 50% 应=Fix×0.5(与其他模式一致,缺陷已修复)");
+ Assert.AreEqual(baseP, fe.InterestPrincipal, "固定值腿计息基数恒=Fix,不随平仓比例变化(合同约定)");
}
// ---- 日终路径(settment=true):证明走 CalcDailySimpleInterestByEod,结果恒为线性 closePrincipal,不受盘中 bug 影响 ----
diff --git a/YLErpDAL/BLL/EodSettlement/ClientBalanceUtility.cs b/YLErpDAL/BLL/EodSettlement/ClientBalanceUtility.cs
index 81363154..4a64222f 100644
--- a/YLErpDAL/BLL/EodSettlement/ClientBalanceUtility.cs
+++ b/YLErpDAL/BLL/EodSettlement/ClientBalanceUtility.cs
@@ -1402,7 +1402,6 @@ namespace YLErp.BLL.EodSettlement
public static void FillClientBalanceHisTradeFee()
{
- List marginTypes = new List() { (int)InterestModeEnum.追加预付金, (int)InterestModeEnum.初始预付金 };
var clients = DataCacheProvider.GetClientDataSource().AsQueryable().Where(x => x.ProcessStatus == "已开户").ToList();
using var db = new YLContext();
var firstEodStatusDate = db.eodStatus.OrderBy(o => o.ValueDate).FirstOrDefault();
diff --git a/YLErpDAL/BLL/EodSettlement/RealTimeClientBanlanceService.cs b/YLErpDAL/BLL/EodSettlement/RealTimeClientBanlanceService.cs
index aeedd67e..33704017 100644
--- a/YLErpDAL/BLL/EodSettlement/RealTimeClientBanlanceService.cs
+++ b/YLErpDAL/BLL/EodSettlement/RealTimeClientBanlanceService.cs
@@ -20,6 +20,7 @@ using YLErp.Modules;
using YLErp.Modules.DataProviderModule;
using YLErp.Modules.EodModule;
using YLErp.Modules.EodModule.QueryModule;
+using YLErp.Modules.SwapModule.Margin;
using YLErp.Modules.UnderlyingModule;
namespace YLErp.BLL.Eod
@@ -1294,7 +1295,7 @@ namespace YLErp.BLL.Eod
var balance = _clientBalanceDic[item.Key];
var clientTradeIds = item.Select(s => s.id).ToList();
var clientPositions = positions.Where(x => clientTradeIds.Contains(x.SwapTradeId)).ToList();
- var marginList = clientPositions.Where(x =>ConsTrade.InterestMarginModels.Contains(x.InterestMode)).Sum(s=>s.InterestPrincipalFix * (s.InterestDirection == 1 ? -1 : 1));
+ var marginList = clientPositions.Where(x => MarginModes.Contains(x.InterestMode)).Sum(s=>s.InterestPrincipalFix * (s.InterestDirection == 1 ? -1 : 1));
balance.SwapMargin =Convert.ToDouble(marginList);
balance.PositionCount= clientTradeIds.Count();
// balance.PositionNotionalPrincipal = Convert.ToDouble(positions.Sum(s=>s.PosiNotionalValue));//实时
diff --git a/YLErpDAL/Modules/EodModule/SettlementModule/EodClientBalanceCalc.cs b/YLErpDAL/Modules/EodModule/SettlementModule/EodClientBalanceCalc.cs
index 927b2157..94337c80 100644
--- a/YLErpDAL/Modules/EodModule/SettlementModule/EodClientBalanceCalc.cs
+++ b/YLErpDAL/Modules/EodModule/SettlementModule/EodClientBalanceCalc.cs
@@ -15,6 +15,7 @@ using YLErp.Model.Enum;
using YLErp.Models;
using YLErp.Modules.CalculationModule;
using YLErp.Modules.EodModule.QueryModule;
+using YLErp.Modules.SwapModule.Margin;
using YLErp.Modules.TradeRiskCalcModule;
namespace YLErp.Modules.EodModule.SettlementModule
@@ -40,7 +41,6 @@ namespace YLErp.Modules.EodModule.SettlementModule
var currencyCodes = _context.CurrencyCodes;
var currencyProvider = _context.EodCurrencyProvider;
- List marginTypes = new List() { (int)InterestModeEnum.追加预付金, (int)InterestModeEnum.初始预付金 };
//预付金可取上浮比率
var marginRatio = Math.Round((decimal)(_context.SystemValue.MarginRatio ?? 0.15), 2, MidpointRounding.AwayFromZero);
var marginMaxRatio = marginRatio + 0.02m;
@@ -132,7 +132,7 @@ namespace YLErp.Modules.EodModule.SettlementModule
var positionList = DbContext.trade.Where(t => (ConsTrade.TradeStatusAfterConfirmed.Contains(t.TradeStatus)||t.UnWindDate> balanceDate) && t.ValidState != "InValid"&&t.TradeType=="收益互换");
var marignQuery = from t in positionList
join s in DbContext.eod_swap_position on t.id equals s.SwapTradeId
- where ConsTrade.InterestMarginModels.Contains(s.InterestMode) && !s.Invalid && s.HappenDate <= balanceDate &&s.ValueDate==balanceDate
+ where MarginModes.ForLinq.Contains(s.InterestMode) && !s.Invalid && s.HappenDate <= balanceDate &&s.ValueDate==balanceDate
select new
{
ClientId = t.ClientId,
diff --git a/YLErpDAL/Modules/SwapModule/Fr007IndexFixer.cs b/YLErpDAL/Modules/SwapModule/Fr007IndexFixer.cs
new file mode 100644
index 00000000..ddce860e
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/Fr007IndexFixer.cs
@@ -0,0 +1,36 @@
+using System;
+using YLErp.Derivatives.Interest;
+using YLErp.Modules.DataProviderModule;
+
+namespace YLErp.Modules.SwapModule;
+
+///
+/// IIndexFixer 的生产实现:FR007 等浮动利率取价。
+/// 直接调用 EodPriceQueryService.TryGetPrice,不依赖 SwapDealService 实例,便于独立测试。
+/// 未来 SwapDealService 的 6 处 inline 取价代码迁移到本类后,取价规则只在此一处维护。
+///
+public sealed class Fr007IndexFixer : IndexFixerBase, IIndexFixer
+{
+ public static readonly Fr007IndexFixer Instance = new();
+
+ public bool TryGetFixing(DateTime fixingDate, string underlyingCode, out decimal rate)
+ {
+ bool ok = EodPriceQueryService.TryGetPrice(fixingDate, underlyingCode, out double r);
+ rate = Convert.ToDecimal(r);
+ return ok;
+ }
+
+ ///
+ /// 取价日 → 取价 → 取不到抛异常。等价于原 SwapDealService 里散落 6 处的 inline 代码:
+ /// GetNonHolidayDefore(date.AddDays(rule)) + TryGetFloatRate + throw
+ /// 取到 0 视为"取到一个 0 值"(返回 0),由调用方决定是否沿用旧值;
+ /// 只有 TryGetFixing 返回 false 才抛异常——对齐既有语义。
+ ///
+ public decimal GetFixingOrThrow(DateTime resetDate, int? interestRule, string underlyingCode)
+ {
+ var fixingDate = GetFixingDate(resetDate, interestRule ?? 0);
+ if (TryGetFixing(fixingDate, underlyingCode, out decimal rate))
+ return rate;
+ throw new Exception($"获取不到{underlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
+ }
+}
diff --git a/YLErpDAL/Modules/SwapModule/FundingLegs/ContractNotionalLeg.cs b/YLErpDAL/Modules/SwapModule/FundingLegs/ContractNotionalLeg.cs
new file mode 100644
index 00000000..f1d62dd0
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/FundingLegs/ContractNotionalLeg.cs
@@ -0,0 +1,16 @@
+using YLErp.DBModels;
+
+namespace YLErp.Modules.SwapModule.FundingLegs;
+
+///
+/// 合约名义本金规模融资腿(InterestMode=合约名义本金规模)。
+/// 站在"合约规模"视角:平仓本金 = 剩余名义本金 × 平仓比例。
+/// 与标的期初全价(9)在 CalcNotionalByMode 里公式相同,差异在衡泰路径 grossPrice 折算和 EOD 复利反推。
+///
+public sealed class ContractNotionalLeg : IFundingLegStrategy
+{
+ public InterestModeEnum Mode => InterestModeEnum.合约名义本金规模;
+
+ public NotionalResult CalcNotional(decimal fix, decimal posiNotional, decimal posiLong, decimal posiShort, decimal closePercent)
+ => new(posiNotional * closePercent, posiNotional, closePercent);
+}
diff --git a/YLErpDAL/Modules/SwapModule/FundingLegs/FixedAmountLeg.cs b/YLErpDAL/Modules/SwapModule/FundingLegs/FixedAmountLeg.cs
new file mode 100644
index 00000000..6e477b07
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/FundingLegs/FixedAmountLeg.cs
@@ -0,0 +1,17 @@
+using YLErp.DBModels;
+
+namespace YLErp.Modules.SwapModule.FundingLegs;
+
+///
+/// 固定值融资腿(InterestMode=固定值)。
+/// 计息基数由合约约定(InterestPrincipalFix),无论平仓比例多少都恒等于该值。
+/// 业务规则:固定值就是合同写死的固定数额(Amount),永远不变。
+/// 不叫 FixedNotionalLeg——"固定值"不一定是"名义本金(Notional)",避免与 ContractNotionalLeg 概念撞车。
+///
+public sealed class FixedAmountLeg : IFundingLegStrategy
+{
+ public InterestModeEnum Mode => InterestModeEnum.固定值;
+
+ public NotionalResult CalcNotional(decimal fix, decimal posiNotional, decimal posiLong, decimal posiShort, decimal closePercent)
+ => new(fix, fix, 1m);
+}
diff --git a/YLErpDAL/Modules/SwapModule/FundingLegs/FundingLegStrategyFactory.cs b/YLErpDAL/Modules/SwapModule/FundingLegs/FundingLegStrategyFactory.cs
new file mode 100644
index 00000000..ee4114ef
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/FundingLegs/FundingLegStrategyFactory.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using YLErp.DBModels;
+
+namespace YLErp.Modules.SwapModule.FundingLegs;
+
+///
+/// 融资腿策略工厂。按 InterestMode 返回对应策略实例。
+/// 替换原 SwapDealService.CalcNotionalByMode 的 switch,收敛 mode 分发逻辑到一处。
+///
+/// 只管融资腿(funding leg),不管保证金——保证金是独立的资金管理体系,
+/// 不应该出现在融资腿策略里。mode 5/6(预付金)属于保证金,不在此注册。
+/// 死代码 mode 3/4 不注册;半死 mode 7/8 不注册(界面已注释)。
+/// 传入未注册的 mode 会抛异常,防止静默走默认分支。
+///
+public static class FundingLegStrategyFactory
+{
+ private static readonly Dictionary _strategies = new()
+ {
+ [InterestModeEnum.固定值] = new FixedAmountLeg(),
+ [InterestModeEnum.合约名义本金规模] = new ContractNotionalLeg(),
+ [InterestModeEnum.标的期初全价] = new UnderlyingEntryFullPriceLeg(),
+ };
+
+ /// 按 mode 返回对应策略。未注册的 mode 抛 ArgumentException。
+ public static IFundingLegStrategy Get(InterestModeEnum mode)
+ {
+ if (_strategies.TryGetValue(mode, out var strategy))
+ return strategy;
+ throw new ArgumentException($"未注册的计息模式: {mode}(mode 3/4/7/8 当前未启用)", nameof(mode));
+ }
+
+ /// 按 mode 值返回对应策略,便于调用方直接传 int。
+ public static IFundingLegStrategy Get(int mode) => Get((InterestModeEnum)mode);
+}
diff --git a/YLErpDAL/Modules/SwapModule/FundingLegs/IFundingLegStrategy.cs b/YLErpDAL/Modules/SwapModule/FundingLegs/IFundingLegStrategy.cs
new file mode 100644
index 00000000..2d29f8c0
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/FundingLegs/IFundingLegStrategy.cs
@@ -0,0 +1,50 @@
+using YLErp.DBModels;
+
+namespace YLErp.Modules.SwapModule.FundingLegs;
+
+///
+/// 融资腿(Funding Leg)计息基数策略。每个 InterestMode 一个实现。
+///
+/// 命名说明:用 FundingLeg(业界标准 Financing/Funding Leg),不用 InterestLeg——
+/// "interest"易和通用"利息"混淆;funding 精确表达"融资成本"。
+/// 融资腿 = 客户付给券商的杠杆成本(spread + FR007),与保证金(Margin)、标的端(ReturnLeg)各自独立。
+///
+/// 职责单一:给定持仓参数与平仓比例,算出"平仓本金 / 存续本金 / 有效比例"。
+/// 不做计息(计息由 SwapInterest 纯函数完成),不取价(取价由 IIndexFixer 完成)。
+///
+public interface IFundingLegStrategy
+{
+ /// 该策略对应的计息模式。
+ InterestModeEnum Mode { get; }
+
+ ///
+ /// 根据持仓参数与平仓比例计算计息本金三元组。
+ ///
+ /// 合约固定本金(固定值/预付金腿用;其余腿忽略)。
+ /// 当前剩余名义本金(数量 × 全价)。
+ /// 多头剩余名义本金(多空存续腿用,当前界面已禁用)。
+ /// 空头剩余名义本金。
+ /// 平仓比例(占剩余,0~1)。
+ NotionalResult CalcNotional(decimal fix, decimal posiNotional, decimal posiLong, decimal posiShort, decimal closePercent);
+}
+
+///
+/// 融资腿策略的计算结果。对应原 CalcNotionalByMode 返回的三元组,用自描述名。
+///
+public readonly struct NotionalResult
+{
+ /// 本次平仓部分的计息本金。
+ public decimal ClosePrincipal { get; }
+
+ /// 存续持仓部分的计息本金(全额,不缩放)。
+ public decimal PosiPrincipal { get; }
+
+ /// 有效平仓比例。固定值腿恒为 1(计息基数不随比例变);其余沿用入参。
+ public decimal ClosePercent { get; }
+
+ public NotionalResult(decimal closePrincipal, decimal posiPrincipal, decimal closePercent)
+ => (ClosePrincipal, PosiPrincipal, ClosePercent) = (closePrincipal, posiPrincipal, closePercent);
+
+ public void Deconstruct(out decimal close, out decimal posi, out decimal pct)
+ => (close, posi, pct) = (ClosePrincipal, PosiPrincipal, ClosePercent);
+}
diff --git a/YLErpDAL/Modules/SwapModule/FundingLegs/UnderlyingEntryFullPriceLeg.cs b/YLErpDAL/Modules/SwapModule/FundingLegs/UnderlyingEntryFullPriceLeg.cs
new file mode 100644
index 00000000..f78d9b74
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/FundingLegs/UnderlyingEntryFullPriceLeg.cs
@@ -0,0 +1,19 @@
+using YLErp.DBModels;
+
+namespace YLErp.Modules.SwapModule.FundingLegs;
+
+///
+/// 标的期初全价融资腿(InterestMode=标的期初全价)。
+/// 计息基数 = 标的期初含费全价(PosiGrossPrice/EntryDirtyPrice) × 数量。
+/// "期初(Entry)"是关键——建仓时点的全价,非当前估值全价。
+/// 主路径 CalcNotionalByMode 公式与合约名义本金规模(2)相同;
+/// 差异在衡泰路径会乘 grossPrice 折算(SwapDealService.GetUnwindInterestsByHT),
+/// 以及 EOD 复利部分平仓后直接返回剩余本金(禁止反推,SwapEodPositionService:1458-1465)。
+///
+public sealed class UnderlyingEntryFullPriceLeg : IFundingLegStrategy
+{
+ public InterestModeEnum Mode => InterestModeEnum.标的期初全价;
+
+ public NotionalResult CalcNotional(decimal fix, decimal posiNotional, decimal posiLong, decimal posiShort, decimal closePercent)
+ => new(posiNotional * closePercent, posiNotional, closePercent);
+}
diff --git a/YLErpDAL/Modules/SwapModule/Margin/CashMargin.cs b/YLErpDAL/Modules/SwapModule/Margin/CashMargin.cs
new file mode 100644
index 00000000..128f34ba
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/Margin/CashMargin.cs
@@ -0,0 +1,10 @@
+namespace YLErp.Modules.SwapModule.Margin;
+
+/// 现金保证金:余额 = 现金余额。
+public sealed class CashMargin : IMarginResolver
+{
+ public MarginForm Form => MarginForm.Cash;
+
+ public MarginBalance Resolve(decimal postedAmount)
+ => new(postedAmount);
+}
diff --git a/YLErpDAL/Modules/SwapModule/Margin/CreditMargin.cs b/YLErpDAL/Modules/SwapModule/Margin/CreditMargin.cs
new file mode 100644
index 00000000..dd3c0032
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/Margin/CreditMargin.cs
@@ -0,0 +1,10 @@
+namespace YLErp.Modules.SwapModule.Margin;
+
+/// 授信保证:余额 = 已用授信额度。
+public sealed class CreditMargin : IMarginResolver
+{
+ public MarginForm Form => MarginForm.Credit;
+
+ public MarginBalance Resolve(decimal postedAmount)
+ => new(postedAmount);
+}
diff --git a/YLErpDAL/Modules/SwapModule/Margin/GuaranteeMargin.cs b/YLErpDAL/Modules/SwapModule/Margin/GuaranteeMargin.cs
new file mode 100644
index 00000000..8506a687
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/Margin/GuaranteeMargin.cs
@@ -0,0 +1,10 @@
+namespace YLErp.Modules.SwapModule.Margin;
+
+/// 担保品:余额 = 担保品市值。
+public sealed class GuaranteeMargin : IMarginResolver
+{
+ public MarginForm Form => MarginForm.Guarantee;
+
+ public MarginBalance Resolve(decimal postedAmount)
+ => new(postedAmount);
+}
diff --git a/YLErpDAL/Modules/SwapModule/Margin/IMarginResolver.cs b/YLErpDAL/Modules/SwapModule/Margin/IMarginResolver.cs
new file mode 100644
index 00000000..1e24f876
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/Margin/IMarginResolver.cs
@@ -0,0 +1,24 @@
+namespace YLErp.Modules.SwapModule.Margin;
+
+/// 保证金形态:现金 / 授信 / 担保。预留扩展。
+public enum MarginForm
+{
+ /// 现金保证金:余额 = 现金余额。
+ Cash,
+ /// 授信保证:余额 = 已用授信额度。
+ Credit,
+ /// 担保品:余额 = 担保品市值。
+ Guarantee,
+}
+
+///
+/// 按保证金形态解析余额。三种形态可互换地产出一个 MarginBalance(满足 LSP),
+/// 这是保证金领域唯一合理的多态点(差异仅在"余额如何取得")。
+/// 具体余额来源(资金流水 / 授信占用 / 担保估值)后续按形态填充。
+///
+public interface IMarginResolver
+{
+ MarginForm Form { get; }
+
+ MarginBalance Resolve(decimal postedAmount);
+}
diff --git a/YLErpDAL/Modules/SwapModule/Margin/MarginAccount.cs b/YLErpDAL/Modules/SwapModule/Margin/MarginAccount.cs
new file mode 100644
index 00000000..57dc508f
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/Margin/MarginAccount.cs
@@ -0,0 +1,42 @@
+using YLErp.Derivatives.Interest;
+
+namespace YLErp.Modules.SwapModule.Margin;
+
+///
+/// 保证金账户。管理保证金余额的变动(追加/释放/返还),并提供计息入口。
+///
+/// 保证金是独立的资金管理概念(初始保证金/维持保证金/保证金余额/追保),
+/// 与融资腿(funding leg)完全无关。现有代码把保证金塞进 InterestMode==5/6
+/// 当计息腿处理是错误的,本类是正确建模的起点。
+///
+/// 利息计算委托 SwapInterest 纯函数(余额×利率×天数/年化),
+/// 保证金账户只提供余额和计息入口,不自己实现计息算法。
+///
+public sealed class MarginAccount
+{
+ /// 当前保证金余额。
+ public MarginBalance Balance { get; private set; }
+
+ public MarginAccount(MarginBalance openingBalance)
+ => Balance = openingBalance;
+
+ /// 追加保证金(余额增加)。
+ public void Deposit(decimal amount)
+ => Balance = new MarginBalance(Balance.Balance + amount);
+
+ /// 释放/返还保证金(余额减少,不低于 0)。
+ public void Withdraw(decimal amount)
+ => Balance = new MarginBalance(Math.Max(0m, Balance.Balance - amount));
+
+ ///
+ /// 按当前余额计算保证金利息。委托 SwapInterest.AccrueSimple。
+ /// 保证金利息是券商对客户保证金存款付息(方向与融资腿相反)。
+ ///
+ /// 保证金利率(年化,如 0.03 = 3%)。
+ /// 计息开始日。
+ /// 计息结束日。
+ /// 算头算尾规则。
+ /// 年化天数(365 或 360)。
+ public InterestResult AccrueInterest(decimal rate, System.DateTime startDate, System.DateTime endDate, AccrualBoundary boundary, int annualDays)
+ => SwapInterest.AccrueSimple(Balance.Balance, rate, startDate, endDate, boundary, annualDays);
+}
diff --git a/YLErpDAL/Modules/SwapModule/Margin/MarginBalance.cs b/YLErpDAL/Modules/SwapModule/Margin/MarginBalance.cs
new file mode 100644
index 00000000..491de03c
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/Margin/MarginBalance.cs
@@ -0,0 +1,16 @@
+namespace YLErp.Modules.SwapModule.Margin;
+
+///
+/// 保证金余额。现金、授信、担保等多种保证金形态的统一表达。
+///
+/// 保证金就是保证金——有余额、有利率、有利息,不存在"计息基数/Notional"概念。
+/// 余额随追加/释放/盈亏变动,利息由 SwapInterest 纯函数按 余额×利率×天数/年化 计算。
+///
+public readonly struct MarginBalance
+{
+ /// 保证金余额:现金余额 / 授信占用 / 担保品市值。
+ public decimal Balance { get; }
+
+ public MarginBalance(decimal balance)
+ => Balance = balance;
+}
diff --git a/YLErpDAL/Modules/SwapModule/Margin/MarginModes.cs b/YLErpDAL/Modules/SwapModule/Margin/MarginModes.cs
new file mode 100644
index 00000000..7232022f
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/Margin/MarginModes.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using System.Linq;
+using YLErp.DBModels;
+
+namespace YLErp.Modules.SwapModule.Margin;
+
+///
+/// 保证金计息模式(mode 5 初始预付金 / mode 6 追加预付金)的统一判断口径。
+///
+/// 现状(待收敛):同一集合 {初始预付金, 追加预付金} 在代码里复制了至少 6 次——
+/// ConsTrade.InterestMarginModels(框架级)
+/// SwapEodPositionService.marginTypes(实例字段)
+/// SwapEodPositionService.premiumModes(局部变量)
+/// SwapEventEmailService.marginTypes
+/// EodClientBalanceCalc.marginTypes
+/// ClientBalanceUtility.marginTypes
+/// 任何一处漏改(如新增保证金形态)都会导致口径分裂。本类收敛到单一来源。
+///
+/// 注意:这里的"保证金 mode"是现有系统把保证金错误建模为计息腿的历史遗留。
+/// 按 Margin 限界上下文的设计方向,未来保证金不应用 InterestMode 标识,
+/// 但在历史代码完全迁出前,需要一个统一判断点。
+///
+public static class MarginModes
+{
+ /// 所有属于保证金的 InterestMode(初始预付金 / 追加预付金)。
+ public static readonly IReadOnlyCollection All = new HashSet
+ {
+ (int)InterestModeEnum.初始预付金,
+ (int)InterestModeEnum.追加预付金,
+ };
+
+ ///
+ /// List 形态,供 EF Core LINQ 表达式用(HashSet.Contains 无法翻译成 SQL)。
+ /// 替代 ConsTrade.InterestMarginModels。
+ ///
+ public static readonly List ForLinq = new()
+ {
+ (int)InterestModeEnum.初始预付金,
+ (int)InterestModeEnum.追加预付金,
+ };
+
+ /// 判断 mode 是否属于保证金(非 LINQ 场景用)。
+ public static bool Contains(int interestMode) => All.Contains(interestMode);
+}
diff --git a/YLErpDAL/Modules/SwapModule/ReturnLegs/DirectionRatio.cs b/YLErpDAL/Modules/SwapModule/ReturnLegs/DirectionRatio.cs
new file mode 100644
index 00000000..13808855
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/ReturnLegs/DirectionRatio.cs
@@ -0,0 +1,22 @@
+using YLErp.DBModels;
+using YLErp.DBModels.Enums;
+
+namespace YLErp.Modules.SwapModule.ReturnLegs;
+
+///
+/// 方向因子计算。把 PositionType / SwapDirection 转成 +1/-1 乘数。
+///
+/// 多空方向(Long/Short)和收付方向(收取/支付)在代码里反复写成三元表达式,
+/// 散落在 SwapEodPositionService 15处 + SwapDealService 多处。
+/// 收敛到统一方法, 消除 +1/-1 写反的风险。
+///
+public static class DirectionRatio
+{
+ /// 多空方向因子。多头=+1, 空头=-1。
+ public static int LongShort(int positionType)
+ => positionType == (int)PositionTypeFlag.Long ? 1 : -1;
+
+ /// 收付方向因子。收取=+1, 支付=-1。
+ public static int ReceivePay(int direction)
+ => direction == (int)SwapDirectionEnum.收取 ? 1 : -1;
+}
diff --git a/YLErpDAL/Modules/SwapModule/ReturnLegs/DividendCalc.cs b/YLErpDAL/Modules/SwapModule/ReturnLegs/DividendCalc.cs
new file mode 100644
index 00000000..d2472360
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/ReturnLegs/DividendCalc.cs
@@ -0,0 +1,27 @@
+namespace YLErp.Modules.SwapModule.ReturnLegs;
+
+///
+/// 标的端分红/票息的增值税计算。
+///
+/// 业务含义:TRS 标的如果是债券,标的端会收到债券票息(coupon);
+/// 如果是股票,会收到现金分红(cash dividend)。系统统一用 Dividend 字段链路表达。
+/// 本类只负责"增值税后金额"这一步纯数学,不涉及取价/归属/落库。
+///
+/// 税务口径:票息/分红属于金融服务应税收入,先除以(1+税率)还原不含税收入,
+/// 再乘以(1-税率)扣减税负。公式:afterTax = payment / (1+tax) × (1-tax)。
+/// 原代码在 SwapEodPositionService 4处 + SwapDealService 1处 重复此公式。
+///
+/// 命名保持 DividendCalc 而非改为 CashFlow/Coupon——
+/// 因为系统里"分红"字段(DividendIn/PosiDividendSum)是统一链路,同时覆盖债券票息和股票分红,
+/// 改名会制造新歧义且需同步改 DB 字段,收益不匹配。
+///
+public static class DividendCalc
+{
+ /// 增值税后票息/分红,四舍五入到分(2位)。
+ public static decimal AfterTax(decimal payment, decimal tax)
+ => Math.Round(payment / (1 + tax) * (1 - tax), 2);
+
+ /// 增值税后票息/分红(不四舍五入,供中间计算用)。
+ public static decimal AfterTaxRaw(decimal payment, decimal tax)
+ => payment / (1 + tax) * (1 - tax);
+}
diff --git a/YLErpDAL/Modules/SwapModule/ReturnLegs/MtmCalc.cs b/YLErpDAL/Modules/SwapModule/ReturnLegs/MtmCalc.cs
new file mode 100644
index 00000000..4bbb5620
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/ReturnLegs/MtmCalc.cs
@@ -0,0 +1,22 @@
+namespace YLErp.Modules.SwapModule.ReturnLegs;
+
+///
+/// 标的端盯市(Mark-to-Market)计算。
+///
+/// UnderlyingMarketValue = 标的价 × 数量 × 合约乘数 × 多空方向
+/// PosiMtmPnL = (标的价 - 成本全价) × 数量 × 合约乘数 × 多空方向 × 收付方向
+///
+/// 原代码在 SwapEodPositionService 4处重复 UnderlyingMarketValue 公式(1731/1813/1898/2101)。
+///
+public static class MtmCalc
+{
+ /// 标的市值。多头为正、空头为负。shortRatio: 多头=1, 空头=-1。
+ public static decimal MarketValue(decimal price, decimal qty, decimal contractSize, int shortRatio)
+ => price * qty * contractSize * shortRatio;
+
+ /// 盯市未实现盈亏 = (标的价 - 成本全价) × 数量 × 合约乘数 × 多空 × 收付。
+ /// 多头=1, 空头=-1。
+ /// 收取=1, 支付=-1。
+ public static decimal UnrealizedPnl(decimal price, decimal costGrossPrice, decimal qty, decimal contractSize, int shortRatio, int ratio)
+ => (price - costGrossPrice) * qty * contractSize * shortRatio * ratio;
+}
diff --git a/YLErpDAL/Modules/SwapModule/ReturnLegs/QtyRollforward.cs b/YLErpDAL/Modules/SwapModule/ReturnLegs/QtyRollforward.cs
new file mode 100644
index 00000000..990a265f
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/ReturnLegs/QtyRollforward.cs
@@ -0,0 +1,27 @@
+namespace YLErp.Modules.SwapModule.ReturnLegs;
+
+///
+/// 标的端数量递推(Qty Rollforward)。
+///
+/// 逐日推进持仓数量:今日数量 = 上一日终数量 + 今日开仓 - 今日平仓 + 公司行为调整。
+///
+/// 公司行为调整(corpActionDeltaQty)默认 0,当前不影响行为;
+/// 预留给公司行为改造(送股/拆股/配股)——届时填入非零值。
+/// 原代码(SwapEodPositionService:1980)只有开仓/平仓两项,硬编码了"数量只因交易变动"假设。
+///
+public static class QtyRollforward
+{
+ ///
+ /// 计算今日持仓数量。
+ ///
+ /// 上一日终持仓数量。
+ /// 今日开仓数量。
+ /// 今日平仓数量。
+ /// 公司行为导致数量变动(送股/拆股),默认 0。
+ /// 今日持仓数量(不低于 0)。
+ public static decimal Calc(decimal previousQty, decimal openQty, decimal unwindQty, decimal corpActionDeltaQty = 0m)
+ {
+ var qty = previousQty + openQty - unwindQty + corpActionDeltaQty;
+ return qty < 0 ? 0 : Math.Abs(qty);
+ }
+}
diff --git a/YLErpDAL/Modules/SwapModule/ReturnLegs/ReturnLegSummary.cs b/YLErpDAL/Modules/SwapModule/ReturnLegs/ReturnLegSummary.cs
new file mode 100644
index 00000000..442d64aa
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/ReturnLegs/ReturnLegSummary.cs
@@ -0,0 +1,31 @@
+namespace YLErp.Modules.SwapModule.ReturnLegs;
+
+///
+/// 标的端(Total Return Leg)归档产出的汇总值。
+///
+/// 这些值由标的盯市+分红计算得出,传给融资端(FundingLeg)作为计息基数参考。
+/// 对应原 SwapPositionCompose 里 DealFloatPositions 的产出:
+/// posiLongNotional / posiShortNotional / closePosiNotional / grossPrice
+///
+/// 命名说明:这里叫 ReturnLeg(业界标准 Total Return Leg),不叫 FloatLeg——
+/// "float" 在金融里首要含义是"浮动利率"(如 FR007),用于标的端会产生歧义。
+/// 标的端 = 标的资产的总回报(价格涨跌 + 票息/分红),与浮动利率无关。
+///
+public readonly struct ReturnLegSummary
+{
+ /// 多头剩余名义本金(= 数量 × 全价)。融资端用它算多头腿计息基数。
+ public decimal LongNotional { get; }
+
+ /// 空头剩余名义本金。
+ public decimal ShortNotional { get; }
+
+ /// 本次平仓名义本金(平仓数量 × 合约乘数 × 含费全价)。
+ public decimal CloseNotional { get; }
+
+ /// 标的含费全价(PosiGrossPrice / EntryDirtyPrice)。融资端 mode 9 衡泰路径折算用。
+ public decimal GrossPrice { get; }
+
+ public ReturnLegSummary(decimal longNotional, decimal shortNotional, decimal closeNotional, decimal grossPrice)
+ => (LongNotional, ShortNotional, CloseNotional, GrossPrice)
+ = (longNotional, shortNotional, closeNotional, grossPrice);
+}
diff --git a/YLErpDAL/Modules/SwapModule/SwapDealIndexFixer.cs b/YLErpDAL/Modules/SwapModule/SwapDealIndexFixer.cs
new file mode 100644
index 00000000..e8fe8931
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/SwapDealIndexFixer.cs
@@ -0,0 +1,28 @@
+using System;
+using YLErp.Derivatives.Interest;
+
+namespace YLErp.Modules.SwapModule;
+
+///
+/// 把一个取价委托包装成 IIndexFixer。
+///
+/// 为什么不直接用 Fr007IndexFixer.Instance:SwapDealService.TryGetFloatRate 是
+/// protected virtual,测试 override 它注入 stub。静态 Instance 直接调
+/// EodPriceQueryService 会绕过这个接缝,导致测试失败。
+/// 本类接受取价委托(SwapDealService 传入 this.TryGetFloatRate 的包装),
+/// 既保留 virtual 接缝,又用上 IndexFixerBase,且无需访问 protected 成员。
+///
+internal sealed class SwapDealIndexFixer : IndexFixerBase, IIndexFixer
+{
+ private readonly Func _tryGet;
+
+ internal SwapDealIndexFixer(Func tryGet)
+ => _tryGet = tryGet;
+
+ public bool TryGetFixing(DateTime fixingDate, string underlyingCode, out decimal rate)
+ {
+ var (ok, r) = _tryGet(fixingDate, underlyingCode);
+ rate = Convert.ToDecimal(r);
+ return ok;
+ }
+}
diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs
index c9605a21..6e4aa810 100644
--- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs
+++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs
@@ -5,11 +5,15 @@ using System.Linq.Expressions;
using YLErp.BLL;
using YLErp.BLL.Eod;
using YLErp.DBModels.Enums;
+using YLErp.Derivatives.Interest;
using YLErp.Helpers;
using YLErp.Modules.DataProviderModule;
using YLErp.Modules.EodModule;
using YLErp.Modules.TradeModule;
using YLErp.Modules.TradeModule.DealModule;
+using YLErp.Modules.SwapModule.FundingLegs;
+using YLErp.Modules.SwapModule.Margin;
+using YLErp.Modules.SwapModule.ReturnLegs;
using YLErp.QdpModule;
namespace YLErp.Modules.SwapModule
@@ -22,6 +26,12 @@ namespace YLErp.Modules.SwapModule
return EodPriceQueryService.TryGetPrice(valueDate, underlyingCode, out rate);
}
+ private IIndexFixer _indexFixer;
+ /// FR007 取价器,委托 TryGetFloatRate(保留 virtual 接缝供测试 stub)。
+ protected virtual IIndexFixer IndexFixer
+ => _indexFixer ??= new SwapDealIndexFixer((d, c) =>
+ TryGetFloatRate(d, c, out double r) ? (true, r) : (false, 0d));
+
#region 可测试化接缝(Seams)——override 这些虚方法可在测试中替换 DB/外部调用,生产代码行为不变
// FindTrade 已上提到基类 SwapTradeBaseService(三子类实现一致,消除重复)
@@ -87,8 +97,7 @@ namespace YLErp.Modules.SwapModule
};
foreach (var leg in unwindData.FlowEvents.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)))
{
- var target = leg.InterestMode == (int)InterestModeEnum.初始预付金
- || leg.InterestMode == (int)InterestModeEnum.追加预付金
+ var target = MarginModes.Contains(leg.InterestMode)
? input.MarginLegs
: input.InterestLegs;
target.Add(new LegInput { InterestClosePnL = leg.InterestClosePnL });
@@ -669,7 +678,7 @@ namespace YLErp.Modules.SwapModule
realPositions ??= new List();
return origPositions.Where(x => x.PosiDirection == 0).Select(p =>
{
- if (p.InterestMode == (int)InterestModeEnum.初始预付金 || p.InterestMode == (int)InterestModeEnum.追加预付金)
+ if (MarginModes.Contains(p.InterestMode))
{
var realLeg = realPositions.FirstOrDefault(r => r.PositionId == p.id);
if (realLeg != null && realLeg.InterestPrincipalFix != p.InterestPrincipalFix)
@@ -705,14 +714,12 @@ namespace YLErp.Modules.SwapModule
.Sum(x => x.TradingAmount);
var hasNotionalFlows = futureCloseNotional > 0 && originalNotional > 0;
var futureClosePrincipal = futureFlows
- .Where(x => x.InterestMode == (int)InterestModeEnum.初始预付金
- || x.InterestMode == (int)InterestModeEnum.追加预付金)
+ .Where(x => MarginModes.Contains(x.InterestMode))
.GroupBy(x => x.PositionId)
.ToDictionary(x => x.Key, x => x.Sum(v => v.InterestPrincipal));
return origPositions.Where(x => x.PosiDirection == 0).Select(p =>
{
- if (p.InterestMode == (int)InterestModeEnum.初始预付金
- || p.InterestMode == (int)InterestModeEnum.追加预付金)
+ if (MarginModes.Contains(p.InterestMode))
{
var realLeg = realPositions.FirstOrDefault(r => r.PositionId == p.id);
if (realLeg != null)
@@ -840,9 +847,9 @@ namespace YLErp.Modules.SwapModule
{
closePrincipal = closePosiNotionalValue;
}
- if ((InterestModeEnum)position.InterestMode == InterestModeEnum.追加预付金 || (InterestModeEnum)position.InterestMode == InterestModeEnum.初始预付金)
+ if (MarginModes.Contains(position.InterestMode))
{
- positionClone.InterestDirection = position.InterestDirection == (int)SwapDirectionEnum.收取 ? (int)SwapDirectionEnum.支付 : (int)SwapDirectionEnum.收取;
+ positionClone.InterestDirection = FlipMarginDirection(position.InterestDirection);
}
// 获取利率
@@ -898,34 +905,38 @@ namespace YLErp.Modules.SwapModule
}
///
- /// 根据计息模式计算名义本金
+ /// 根据计息模式计算名义本金。
+ ///
+ /// 现状(过渡期):
+ /// - 融资腿(mode 1/2/9)已委托 FundingLegStrategyFactory,走新策略体系。
+ /// - 保证金(mode 5/6)暂保留——待 Margin 独立计息入口建成后迁出。
+ /// - 多空存续(mode 7/8)界面已禁用,保留 case 仅为防御性兜底。
+ /// - 死代码(mode 3/4)走 default。
+ ///
+ /// 待保证金(mode 5/6)迁入 Margin 上下文后,本方法可整体删除,
+ /// 调用点直接走 FundingLegStrategyFactory.Get(mode)。
///
private (decimal close, decimal posi, decimal closePct) CalcNotionalByMode(swap_position position, decimal closePercent, decimal posiNotional, decimal posiLong, decimal posiShort)
{
- decimal closePrincipal = posiNotional; // 平仓部分的名义本金
- decimal posiPrincipal = posiNotional; // 持仓部分的名义本金
- decimal newClosePercent = closePercent; // 调整后的平仓比例
+ var mode = (InterestModeEnum)position.InterestMode;
- switch ((InterestModeEnum)position.InterestMode)
+ // 融资腿(1/2/9)走策略工厂
+ if (mode == InterestModeEnum.固定值
+ || mode == InterestModeEnum.合约名义本金规模
+ || mode == InterestModeEnum.标的期初全价)
+ {
+ var r = FundingLegStrategyFactory.Get(mode)
+ .CalcNotional(position.InterestPrincipalFix, posiNotional, posiLong, posiShort, closePercent);
+ return (r.ClosePrincipal, r.PosiPrincipal, r.ClosePercent);
+ }
+
+ // 以下 mode 尚未迁入新架构,保留原逻辑
+ decimal closePrincipal = posiNotional;
+ decimal posiPrincipal = posiNotional;
+ decimal newClosePercent = closePercent;
+
+ switch (mode)
{
- 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.标的期初全价:
- closePrincipal = posiNotional * closePercent;
- break;
case InterestModeEnum.追加预付金:
case InterestModeEnum.初始预付金:
closePrincipal = position.InterestPrincipalFix * closePercent;
@@ -1031,9 +1042,8 @@ namespace YLErp.Modules.SwapModule
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));
+ DateTime rateDate = IndexFixerBase.GetFixingDate(
+ days % period == 0 ? endDate : startDate, position.interest_rule);
if (preEod.id != 0 && days % period != 0)
{
@@ -1041,9 +1051,9 @@ namespace YLErp.Modules.SwapModule
return preEod.FloatRate;
}
- if (TryGetFloatRate(rateDate, position.FloatRateUnderlyingCode, out double rate))
+ if (IndexFixer.TryGetFixing(rateDate, position.FloatRateUnderlyingCode, out decimal rate))
{
- position.FloatRate = positionClone.FloatRate = Convert.ToDecimal(rate);
+ position.FloatRate = positionClone.FloatRate = rate;
return position.FloatRate;
}
if (!swap) throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{rateDate:yyyy年MM月dd日}的价格");
@@ -1138,6 +1148,34 @@ namespace YLErp.Modules.SwapModule
return InitSwapDealInterest(td, valueDate, endDate, rate, position, add, swap, posiPrincipal, closePrincipal, closePercent, annualDays, eventType, preEod, false, orginPv, calcFirst, calcLast, consumedInterest);
}
+ ///
+ /// 保证金腿的 orginPv 维度重映射。
+ ///
+ /// 保证金腿被迫走融资腿的差分公式(dynomicPrincipal = TdInterestPrincipal + posiPrincipal - orginPv),
+ /// 但 orginPv 对融资腿是"交易名义本金(千万~亿级)",对保证金腿必须是"保证金本金"——
+ /// 否则维度不匹配会算出巨负值。本方法把保证金场景的 orginPv 对齐到"上一日保证金本金"。
+ ///
+ /// 待迁入 Margin 模块:保证金独立计息入口建好后,此方法移入 MarginAccount/MarginService。
+ ///
+ private decimal ResolveMarginOrginPv(swap_position position, eod_swap_position preEodPosition, decimal fallback)
+ {
+ var previousBalance = preEodPosition.InterestPrincipalFix != 0m
+ ? preEodPosition.InterestPrincipalFix
+ : preEodPosition.TdInterestPrincipal;
+ return preEodPosition.id != 0 && previousBalance != 0m
+ ? previousBalance
+ : fallback;
+ }
+
+ ///
+ /// 保证金腿的利息方向翻转。保证金利息是券商付给客户(方向与融资腿相反)。
+ /// 待迁入 Margin 模块。
+ ///
+ private static int FlipMarginDirection(int direction)
+ => direction == (int)SwapDirectionEnum.收取
+ ? (int)SwapDirectionEnum.支付
+ : (int)SwapDirectionEnum.收取;
+
///
/// 初始化利息腿信息
///
@@ -1191,22 +1229,10 @@ namespace YLErp.Modules.SwapModule
interest.ClientId = td.ClientId;
interest.UnwindDate = endDate;
- // 根因修复:预付金(保证金)腿的计息基数维度应为"保证金本金"自身,而非整笔交易的名义本金(orginPv)。
- // 否则公式 dynomicPrincipal = TdInterestPrincipal + posiPrincipal - orginPv
- // 会把交易名义本金(千万~亿级)当减项扣掉,使"应返还本金"(InterestPrincipal)与计息基数变成巨负值。
- // 此处将预付金腿的 orginPv 对齐为上一日保证金本金;无历史归档时才取当前本金。
- // 差分公式必须使用同一时点口径:上一日本金 + 当前本金 - 上一本金 = 当前本金。
- // 若已有部分平仓后仍取当前本金,会把上一日本金原样保留,导致当日继续按平仓前本金计息。
- // 仅作用于初始预付金(5)/追加预付金(6);其它计息模式(含债券本金腿 标的期初全价=9)仍用交易名义本金,不受影响。
- if (position.InterestMode == (int)InterestModeEnum.初始预付金
- || position.InterestMode == (int)InterestModeEnum.追加预付金)
+ // 保证金腿的 orginPv 对齐到保证金本金维度,避免差分公式维度不匹配算出巨负值
+ if (MarginModes.Contains(position.InterestMode))
{
- var previousPrincipal = preEodPosition.InterestPrincipalFix != 0m
- ? preEodPosition.InterestPrincipalFix
- : preEodPosition.TdInterestPrincipal;
- orginPv = preEodPosition.id != 0 && previousPrincipal != 0m
- ? previousPrincipal
- : position.InterestPrincipalFix;
+ orginPv = ResolveMarginOrginPv(position, preEodPosition, position.InterestPrincipalFix);
}
if (swap)
@@ -1346,14 +1372,14 @@ namespace YLErp.Modules.SwapModule
if (accrueDate >= startDate && i % interestPeriod == 0
&& !string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
{
- var fr007RateDate = QdpCalendarHelper.GetNonHolidayDefore(accrueDate.AddDays(position.interest_rule ?? 0));
- if (TryGetFloatRate(fr007RateDate, position.FloatRateUnderlyingCode, out double floatRate1))
+ var fixingDate = IndexFixerBase.GetFixingDate(accrueDate, position.interest_rule);
+ if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing))
{
- if (floatRate1 != 0) floatRate = floatRate1;
+ if (fixing != 0m) floatRate = Convert.ToDouble(fixing);
}
else
{
- throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fr007RateDate:yyyy年MM月dd日}的价格");
+ throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
}
}
if (accrueDate >= startDate)
@@ -1437,14 +1463,14 @@ namespace YLErp.Modules.SwapModule
// tdDynomicPrincipal = flowEvent.InterestPrincipal 使本金累积乘 closePercent^N)。
if (i % interestPeriod == 0 && !string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
{
- var fr007RateDate = QdpCalendarHelper.GetNonHolidayDefore(accrueDate.AddDays(position.interest_rule ?? 0));
- if (TryGetFloatRate(fr007RateDate, position.FloatRateUnderlyingCode, out double floatRate1))
+ var fixingDate = IndexFixerBase.GetFixingDate(accrueDate, position.interest_rule);
+ if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing))
{
- if (floatRate1 != 0) floatRate = floatRate1;
+ if (fixing != 0m) floatRate = Convert.ToDouble(fixing);
}
else
{
- throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fr007RateDate:yyyy年MM月dd日}的价格");
+ throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
}
}
@@ -1501,18 +1527,14 @@ namespace YLErp.Modules.SwapModule
tdDynomicPrincipal = tdDynomicPrincipal + interestProfitSum * remainingPercent;
if (!string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
{
- // 获取合适的 rateDate
- var fr007RateDate = QdpCalendarHelper.GetNonHolidayDefore(endDate.AddDays(position.interest_rule ?? 0)); // 获取前一工作日;
- if (TryGetFloatRate(fr007RateDate, position.FloatRateUnderlyingCode, out double floatRate1))
+ var fixingDate = IndexFixerBase.GetFixingDate(endDate, position.interest_rule);
+ if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing))
{
- if (floatRate1 != 0)
- {
- floatRate = floatRate1;
- }
+ if (fixing != 0m) floatRate = Convert.ToDouble(fixing);
}
else
{
- throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fr007RateDate:yyyy年MM月dd日}的价格");
+ throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
}
}
@@ -1566,17 +1588,14 @@ namespace YLErp.Modules.SwapModule
// 获取新的浮动利率
if (!string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
{
- var fr007RateDate = QdpCalendarHelper.GetNonHolidayDefore(endDate.AddDays(position.interest_rule ?? 0));
- if (TryGetFloatRate(fr007RateDate, position.FloatRateUnderlyingCode, out double newFloatRate))
+ var fixingDate = IndexFixerBase.GetFixingDate(endDate, position.interest_rule);
+ if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing))
{
- if (newFloatRate != 0)
- {
- floatRate = newFloatRate;
- }
+ if (fixing != 0m) floatRate = Convert.ToDouble(fixing);
}
else
{
- throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fr007RateDate:yyyy年MM月dd日}的价格");
+ throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
}
}
}
@@ -1778,13 +1797,13 @@ namespace YLErp.Modules.SwapModule
BondPaymentService servie = new BondPaymentService(UserInfo);
var payments = servie.GetBondPayments(flowEvent.UnderlyingCode, td.StartDate.Value, date);
- int shortRatio = flowEvent.PositionType == (int)PositionTypeFlag.Long ? 1 : -1;
- int directionRatio = flowEvent.PayDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;
+ int shortRatio = DirectionRatio.LongShort(flowEvent.PositionType);
+ int directionRatio = DirectionRatio.ReceivePay(flowEvent.PayDirection);
// + 付息日>上日日终且小于等于平仓日期的分红数据
var dividendIn = servie.CalcPayment(payments, unwindQty, shortRatio, directionRatio);
var um = DataCacheProvider.GetUnderlyingDataSource().GetData(flowEvent.UnderlyingCode);
decimal tax = um.ValueAddedTax ?? 0;
- dividendIn = dividendIn / (1 + tax) * (1 - tax);
+ dividendIn = DividendCalc.AfterTaxRaw(dividendIn, tax);
flowEvent.DividendIn = Math.Round(dividendIn, 2, MidpointRounding.AwayFromZero);
}
@@ -1906,10 +1925,10 @@ namespace YLErp.Modules.SwapModule
_closePosiNotionalValue = _posiNotionalValue * grossPrice * newClosePercent;
_posiNotionalValue = _posiNotionalValue * grossPrice;
}
- else if (item.InterestMode == (int)InterestModeEnum.追加预付金 || item.InterestMode == (int)InterestModeEnum.初始预付金)
+ else if (MarginModes.Contains(item.InterestMode))
{
_closePosiNotionalValue = 0;
- positionClone.InterestDirection = position.InterestDirection == (int)SwapDirectionEnum.收取 ? (int)SwapDirectionEnum.支付 : (int)SwapDirectionEnum.收取;
+ positionClone.InterestDirection = FlipMarginDirection(position.InterestDirection);
}
decimal rate = item.InterestRateDefault;
if (swapIntervalToday != null)//当日无适用观察日
@@ -1999,7 +2018,7 @@ namespace YLErp.Modules.SwapModule
{
interestList.ForEach(x =>
{
- if (x.InterestMode == (int)InterestModeEnum.追加预付金 || x.InterestMode == (int)InterestModeEnum.初始预付金)
+ if (MarginModes.Contains(x.InterestMode))
{
decimal interestRatio = x.InterestDirection == 1 ? -1m : 1m;
unwindData.SwapMarginRebatePnl += x.InterestClosePnL;
@@ -2417,7 +2436,7 @@ namespace YLErp.Modules.SwapModule
{
position.InterestAmount += interest.InterestAmount;
position.InterestFeePending += interest.InterestFee;
- if ((interest.InterestMode == (int)InterestModeEnum.追加预付金 || interest.InterestMode == (int)InterestModeEnum.初始预付金) && eventType == (int)SwapEventTypeEnum.平仓)
+ if (MarginModes.Contains(interest.InterestMode) && eventType == (int)SwapEventTypeEnum.平仓)
{
var remainingInterestPrincipal = Math.Round(
position.InterestPrincipalFix - interest.InterestPrincipal,
diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs
index adb87861..4aa6adc0 100644
--- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs
+++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs
@@ -12,6 +12,8 @@ using YLErp.Model.Enum;
using YLErp.Models;
using YLErp.Modules.DataProviderModule;
using YLErp.Modules.EodModule;
+using YLErp.Modules.SwapModule.Margin;
+using YLErp.Modules.SwapModule.ReturnLegs;
using YLErp.QdpModule;
namespace YLErp.Modules.SwapModule
@@ -21,7 +23,6 @@ namespace YLErp.Modules.SwapModule
///
public class SwapEodPositionService : SwapTradeBaseService
{
- private List marginTypes = new List() { (int)InterestModeEnum.追加预付金, (int)InterestModeEnum.初始预付金 };
private static readonly IYcLogger Log = LogFactory.GetLogger(typeof(SwapEodPositionService).FullName);
public SwapEodPositionService(OptUserInfo optUser) : base(optUser)
{
@@ -1041,7 +1042,7 @@ namespace YLErp.Modules.SwapModule
}
var tradeExtend = td.trade_extend.ExtendObj;
decimal ratio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;//收取为正,支付为负
- if (marginTypes.Contains(position.InterestMode))
+ if (MarginModes.Contains(position.InterestMode))
{
ratio = -ratio;
}
@@ -1176,7 +1177,7 @@ namespace YLErp.Modules.SwapModule
decimal posiNotionalValue = posiLongNotional + posiShortNational;
decimal closePercent = 1;
decimal ratio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;//收取为正,支付为负
- if (marginTypes.Contains(position.InterestMode))
+ if (MarginModes.Contains(position.InterestMode))
{
ratio = -ratio;
}
@@ -1296,7 +1297,7 @@ namespace YLErp.Modules.SwapModule
decimal posiNotionalValue = posiLongNotional + posiShortNational;
// ratio 只负责把腿内原始金额转换为本方盈亏方向,不参与计息金额本身的计算。
decimal ratio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;//收取为正,支付为负
- if (marginTypes.Contains(position.InterestMode))
+ if (MarginModes.Contains(position.InterestMode))
{
ratio = -ratio;
}
@@ -1604,7 +1605,7 @@ namespace YLErp.Modules.SwapModule
closePercent = 1;
}
decimal ratio = eodPayPosition.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;//收取为正,支付为负
- if (marginTypes.Contains(position.InterestMode))
+ if (MarginModes.Contains(position.InterestMode))
{
ratio = -ratio;
}
@@ -1691,8 +1692,8 @@ namespace YLErp.Modules.SwapModule
bool open)
{
payQty = Math.Abs(payQty);
- decimal ratio = eventFlow.PayDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;//收取为正,支付为负
- decimal shortRatio = newEodPayPosition.PositionType == (int)PositionTypeFlag.Long ? 1 : -1;//多空方向
+ int ratio = eventFlow.PayDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;//收取为正,支付为负
+ int shortRatio = DirectionRatio.LongShort(newEodPayPosition.PositionType);
newEodPayPosition.ValueDate = eventFlow.PayDate.Value;
newEodPayPosition.PositionId = eventFlow.PositionId;
newEodPayPosition.ClientId = td.ClientId;
@@ -1727,7 +1728,7 @@ namespace YLErp.Modules.SwapModule
//浮动端估值用信息
newEodPayPosition.UnderlyingPrice = UnderlyingCodePrice(newEodPayPosition.UnderlyingCode, eventFlow.EventDate, out decimal vobp);
newEodPayPosition.dv01 = Dv01Helper.CalcDv01(newEodPayPosition.UnderlyingCode, newEodPayPosition.PosiQuantity, newEodPayPosition.PosiDirection, newEodPayPosition.PositionType, vobp);
- newEodPayPosition.UnderlyingMarketValue = newEodPayPosition.UnderlyingPrice * newEodPayPosition.PosiQuantity * newEodPayPosition.ContractSize * shortRatio;
+ newEodPayPosition.UnderlyingMarketValue = MtmCalc.MarketValue(newEodPayPosition.UnderlyingPrice, newEodPayPosition.PosiQuantity, newEodPayPosition.ContractSize, shortRatio);
//当日已实现
newEodPayPosition.TdCloseQty = closeQty;
newEodPayPosition.TdChangedQty = 0;
@@ -1737,7 +1738,7 @@ namespace YLErp.Modules.SwapModule
//持仓内容-浮动收益腿-损益统计(本方视角
newEodPayPosition.TdPosiDividend = Math.Round(dividendIn * ratio, 2);
- newEodPayPosition.PosiMtmPnL = (newEodPayPosition.UnderlyingPrice - newEodPayPosition.PosiGrossPrice) * newEodPayPosition.PosiQuantity * newEodPayPosition.ContractSize * shortRatio * ratio;
+ newEodPayPosition.PosiMtmPnL = MtmCalc.UnrealizedPnl(newEodPayPosition.UnderlyingPrice, newEodPayPosition.PosiGrossPrice, newEodPayPosition.PosiQuantity, newEodPayPosition.ContractSize, shortRatio, ratio);
newEodPayPosition.PosiDividendSum = Math.Round(newEodPayPosition.TdPosiDividend - newEodPayPosition.TdCloseDividend, 2);
newEodPayPosition.PosiProfitSum = newEodPayPosition.PosiMtmPnL + newEodPayPosition.PosiDividendSum + newEodPayPosition.PosiFeePending;
@@ -1790,7 +1791,7 @@ namespace YLErp.Modules.SwapModule
return curretEod;
}
var dealDate = curretEod.ValueDate;
- int shortRatio = eod.PositionType == (int)PositionTypeFlag.Long ? 1 : -1;
+ int shortRatio = DirectionRatio.LongShort(eod.PositionType);
int directionRatio = eod.PosiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;
curretEod.PosiStatus = curretEod.PosiQuantity == 0 ? 1 : 0;
var price = GetSwapValuationPrice(eod.UnderlyingCode, dealDate, out decimal vobp);
@@ -1799,8 +1800,7 @@ namespace YLErp.Modules.SwapModule
if (valueDate > td.StartDate.Value && curretEod.PosiQuantity > 0)
{
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio);
- // 考虑增值税
- curretEod.TdPosiDividend = Math.Round(payment / (1 + tax) * (1 - tax), 2);
+ curretEod.TdPosiDividend = DividendCalc.AfterTax(payment, tax);
}
curretEod.PosiDividendSum = eod.PosiQuantity > 0 ? Math.Round(eod.PosiDividendSum + curretEod.TdPosiDividend, 2) : 0;
curretEod.PosiQuantity = eod.PosiQuantity;
@@ -1809,8 +1809,8 @@ namespace YLErp.Modules.SwapModule
curretEod.PosiNotionalValue = 0;
}
curretEod.UnderlyingPrice = price;
- curretEod.UnderlyingMarketValue = curretEod.UnderlyingPrice * curretEod.PosiQuantity * curretEod.ContractSize * shortRatio;
- curretEod.PosiMtmPnL = (curretEod.UnderlyingPrice - curretEod.PosiGrossPrice) * curretEod.PosiQuantity * curretEod.ContractSize * shortRatio * directionRatio;
+ curretEod.UnderlyingMarketValue = MtmCalc.MarketValue(curretEod.UnderlyingPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio);
+ curretEod.PosiMtmPnL = MtmCalc.UnrealizedPnl(curretEod.UnderlyingPrice, curretEod.PosiGrossPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio, directionRatio);
//curretEod.TdPosiDividend = 0;
//curretEod.PosiDividendSum = eod.PosiDividendSum + curretEod.TdPosiDividend;
curretEod.PosiProfitSum = curretEod.PosiMtmPnL + curretEod.PosiDividendSum + curretEod.PosiFeePending;
@@ -1883,26 +1883,26 @@ namespace YLErp.Modules.SwapModule
return curretEod;
}
var dealDate = curretEod.ValueDate;
- int shortRatio = eod.PositionType == (int)PositionTypeFlag.Long ? 1 : -1;
+ int shortRatio = DirectionRatio.LongShort(eod.PositionType);
int directionRatio = eod.PosiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;
var price = GetSwapValuationPrice(eod.UnderlyingCode, dealDate, out decimal vobp);
var todayConsumedDividend = CalcConsumedDividend(curretEod, unwindEvents);
var originNotional = (decimal)td.OriginalStockEqvNotional / swapPosition.PosiNetPrice;
decimal totalPayment = CalcBondPayment(curretEod.UnderlyingCode, td.StartDate.Value, valueDate, (decimal)originNotional, shortRatio, directionRatio);
decimal tax = um.ValueAddedTax ?? 0;
- decimal totalInterest = totalPayment / (1 + tax) * (1 - tax);
+ decimal totalInterest = DividendCalc.AfterTaxRaw(totalPayment, tax);
SetPriceInfoByFlowEvent(eod, curretEod, unwindEvents, swapPosition);
curretEod.dv01 = Dv01Helper.CalcDv01(eod.UnderlyingCode, curretEod.PosiQuantity, eod.PosiDirection, eod.PositionType, vobp);
curretEod.UnderlyingPrice = price;
- curretEod.UnderlyingMarketValue = curretEod.UnderlyingPrice * curretEod.PosiQuantity * curretEod.ContractSize * shortRatio;
- curretEod.PosiMtmPnL = (curretEod.UnderlyingPrice - curretEod.PosiGrossPrice) * curretEod.PosiQuantity * curretEod.ContractSize * shortRatio * directionRatio;
+ curretEod.UnderlyingMarketValue = MtmCalc.MarketValue(curretEod.UnderlyingPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio);
+ curretEod.PosiMtmPnL = MtmCalc.UnrealizedPnl(curretEod.UnderlyingPrice, curretEod.PosiGrossPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio, directionRatio);
curretEod.TdPosiDividend = 0;
// 分红与互换无关,只要持仓>0且起始日早于当前日,正常计算当日分红
// 修改,互换事件会影响待实现的分红的,现在要算上
if (valueDate > td.StartDate.Value && (curretEod.PosiQuantity > 0))
{
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio);
- curretEod.TdPosiDividend = Math.Round(payment / (1 + tax) * (1 - tax), 2);
+ curretEod.TdPosiDividend = DividendCalc.AfterTax(payment, tax);
}
curretEod.RealizedMtmPnL = eod.RealizedMtmPnL + curretEod.TdCloseMtmPnl;
// 当日浮动端平仓盈亏·分红(仅来自平仓事件 和 互换 中已实现的分红)
@@ -1971,14 +1971,13 @@ namespace YLErp.Modules.SwapModule
{
return;
}
- int shortRatio = eod.PositionType == (int)PositionTypeFlag.Long ? 1 : -1;
+ int shortRatio = DirectionRatio.LongShort(eod.PositionType);
int directionRatio = eod.PosiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;
var unwindFlowEvents = unwindEvents.Where(x => x.EventType == (int)SwapFlowEventTypeEnum.平仓).ToList();
var openFlowEvents = unwindEvents.Where(x => x.EventType == (int)SwapFlowEventTypeEnum.开仓).ToList();
decimal unwindQty = unwindFlowEvents.Sum(s => s.Quantity);
decimal openQty = openFlowEvents.Sum(s => s.Quantity);
- var qty = eod.PosiQuantity + openQty - unwindQty;
- curretEod.PosiQuantity = qty < 0 ? 0 : Math.Abs(qty);
+ curretEod.PosiQuantity = QtyRollforward.Calc(eod.PosiQuantity, openQty, unwindQty);
if (unwindEvents.Count == 0)
{
curretEod.PosiNetPrice = position.PosiNetPrice;
@@ -2053,7 +2052,7 @@ namespace YLErp.Modules.SwapModule
curretEod.SwapTradeId = td.id;
curretEod.PositionId = position.id;
curretEod.ClientId = td.ClientId;
- int shortRatio = position.PositionType == (int)PositionTypeFlag.Long ? 1 : -1;
+ int shortRatio = DirectionRatio.LongShort(position.PositionType);
int directionRatio = position.PosiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;
curretEod.PositionType = position.PositionType;
var eod = new eod_swap_position()
@@ -2092,14 +2091,14 @@ namespace YLErp.Modules.SwapModule
{
decimal tax = um.ValueAddedTax ?? 0;
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, td.StartDate.Value, settleDate, curretEod.PosiQuantity, shortRatio, directionRatio);
- payment = Math.Round(payment / (1 + tax) * (1 - tax), 2);
+ payment = DividendCalc.AfterTax(payment, tax);
//var consumedDividend = CalcConsumedDividend(curretEod, unwindEvents); 首日应该没有分红
curretEod.TdPosiDividend = payment;
curretEod.PosiDividendSum = payment;
}
- curretEod.UnderlyingMarketValue = curretEod.UnderlyingPrice * curretEod.PosiQuantity * curretEod.ContractSize * shortRatio;
- curretEod.PosiMtmPnL = (curretEod.UnderlyingPrice - curretEod.PosiGrossPrice) * curretEod.PosiQuantity * curretEod.ContractSize * shortRatio * directionRatio;
+ curretEod.UnderlyingMarketValue = MtmCalc.MarketValue(curretEod.UnderlyingPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio);
+ curretEod.PosiMtmPnL = MtmCalc.UnrealizedPnl(curretEod.UnderlyingPrice, curretEod.PosiGrossPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio, directionRatio);
curretEod.PosiProfitSum = curretEod.PosiMtmPnL + curretEod.PosiDividendSum + curretEod.PosiFeePending;
curretEod.RealizedMtmPnL = curretEod.TdCloseMtmPnl;
curretEod.RealizedDividend = curretEod.TdCloseDividend;
@@ -2207,7 +2206,7 @@ namespace YLErp.Modules.SwapModule
interestPositions.ForEach(x =>
{
decimal ratio = x.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;//收取为正,支付为负
- if (marginTypes.Contains(x.InterestMode))
+ if (MarginModes.Contains(x.InterestMode))
{
ratio = -ratio;
}
@@ -2275,7 +2274,7 @@ namespace YLErp.Modules.SwapModule
interestPositions.ForEach(x =>
{
decimal ratio = x.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;//收取为正,支付为负
- if (marginTypes.Contains(x.InterestMode))
+ if (MarginModes.Contains(x.InterestMode))
{
ratio = -ratio;
}
@@ -2284,7 +2283,7 @@ namespace YLErp.Modules.SwapModule
eodSwapPositions.ForEach(x =>
{
decimal ratio = x.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;//收取为正,支付为负
- if (marginTypes.Contains(x.InterestMode))
+ if (MarginModes.Contains(x.InterestMode))
{
ratio = -ratio;
}
@@ -2314,7 +2313,7 @@ namespace YLErp.Modules.SwapModule
public static decimal CalculateSwapRealizedPnl(eod_swap_position position)
{
var interestRatio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;
- if (ConsTrade.InterestMarginModels.Contains(position.InterestMode))
+ if (MarginModes.Contains(position.InterestMode))
{
interestRatio = -interestRatio;
}
@@ -2338,9 +2337,13 @@ namespace YLErp.Modules.SwapModule
if (position.InterestDirection <= 0) return;
var interestRatio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;
- if (ConsTrade.InterestMarginModels.Contains(position.InterestMode))
+ if (MarginModes.Contains(position.InterestMode))
{
interestRatio = -interestRatio;
+ }
+ else if (position.InterestMode == (int)InterestModeEnum.标的期初全价)
+ {
+ return;
}
position.TdCloseInterest = Math.Abs(position.TdCloseInterest) * interestRatio;
position.RealizedInterest = Math.Abs(position.RealizedInterest) * interestRatio;
@@ -2527,7 +2530,7 @@ namespace YLErp.Modules.SwapModule
.Where(x => x.SwapTradeId == item.position.SwapTradeId && x.ValueDate == item.position.ValueDate)
.ToList();
var floatingLegs = details.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList();
- var marginLegs = details.Where(x => marginTypes.Contains(x.InterestMode)).ToList();
+ var marginLegs = details.Where(x => MarginModes.Contains(x.InterestMode)).ToList();
var tradeExtend = tradeExtends.FirstOrDefault(x => x.TradeId == item.position.SwapTradeId);
var dividendPayDate = tradeExtend?.ExtendObj?.DividendPayDate ?? 1;
@@ -2776,7 +2779,7 @@ namespace YLErp.Modules.SwapModule
// 主查询分页后再取同交易、同估值日的全部辅助腿,避免利息/保证金归集跨估值日串数据。
var eodPositions = DbContext.eod_swap_position.Where(interestPredicate).ToList();
var marginPositions = DbContext.swap_position
- .Where(x => tradeIds.Contains(x.SwapTradeId) && marginTypes.Contains(x.InterestMode) && x.IsInitial && !x.Invalid)
+ .Where(x => tradeIds.Contains(x.SwapTradeId) && MarginModes.Contains(x.InterestMode) && x.IsInitial && !x.Invalid)
.ToList();
var tradeExtends = DbContext.trade_extend.Where(x => tradeIds.Contains(x.TradeId)).ToList();
Dictionary tradeDic = new Dictionary();
@@ -2828,8 +2831,8 @@ namespace YLErp.Modules.SwapModule
&& (!x.HappenDate.HasValue || x.HappenDate.Value <= item.position.ValueDate))
.ToList();
var interests = eodPositions.Where(x => x.SwapTradeId == item.position.SwapTradeId && x.ValueDate == item.position.ValueDate);
- var eodMargins = interests.Where(x => marginTypes.Contains(x.InterestMode)).ToList();
- var eodInterests = interests.Where(x => !marginTypes.Contains(x.InterestMode)).ToList();
+ var eodMargins = interests.Where(x => MarginModes.Contains(x.InterestMode)).ToList();
+ var eodInterests = interests.Where(x => !MarginModes.Contains(x.InterestMode)).ToList();
var initialMargins = tradeMargins.Where(x => x.InterestMode == (int)InterestModeEnum.初始预付金).ToList();
var additionalMargins = tradeMargins.Where(x => x.InterestMode == (int)InterestModeEnum.追加预付金).ToList();
var floatRateInterest = eodInterests.Where(x => !string.IsNullOrEmpty(x.FloatRateUnderlyingCode)).FirstOrDefault();
diff --git a/YLErpDAL/Modules/SwapModule/SwapEventEmailService.cs b/YLErpDAL/Modules/SwapModule/SwapEventEmailService.cs
index 73cd88ab..a6bad2cc 100644
--- a/YLErpDAL/Modules/SwapModule/SwapEventEmailService.cs
+++ b/YLErpDAL/Modules/SwapModule/SwapEventEmailService.cs
@@ -22,6 +22,7 @@ using YLErp.MailKit;
using YLErp.Model;
using YLErp.Models;
using YLErp.Modules.EodModule.QueryModule;
+using YLErp.Modules.SwapModule.Margin;
using YLErp.Office.Converters;
using YLErp.Office.Helpers;
using static YLErp.ConsGlobal;
@@ -31,7 +32,7 @@ namespace YLErp.Modules.SwapModule
public class SwapEventEmailService : YLBaseService
{
private List eventTypes = new List() { (int)SwapFlowEventTypeEnum.开仓, (int)SwapFlowEventTypeEnum.平仓 };
- private List marginTypes = new List() { (int)InterestModeEnum.追加预付金, (int)InterestModeEnum.初始预付金 };
+ private List marginTypes = MarginModes.All.ToList();
private decimal wan = 10000m;
public SwapEventEmailService(OptUserInfo userInfo) : base(userInfo)
{