Merge remote-tracking branch 'origin/glms/feature/1.4.2' into glms/feature/1.4.2
This commit is contained in:
@@ -131,11 +131,11 @@ namespace YLErp.DBModels
|
||||
/// <summary>
|
||||
/// 互换利率端计息方式
|
||||
/// </summary>
|
||||
public static readonly List<int> InterestModels = new List<int>() { (int)InterestModeEnum.固定值, (int)InterestModeEnum.合约名义本金规模, (int)InterestModeEnum.空头存续名义本金,(int)InterestModeEnum.多头存续名义本金, (int)InterestModeEnum.标的期初全价 };
|
||||
public static readonly List<int> InterestModels = new List<int>() { (int)InterestModeEnum.固定值, (int)InterestModeEnum.合约名义本金规模, (int)InterestModeEnum.标的期初全价 };
|
||||
/// <summary>
|
||||
/// 互换利率端计息方式-名义本金相关
|
||||
/// </summary>
|
||||
public static readonly List<int> InterestNotionalModels = new List<int>() {(int)InterestModeEnum.合约名义本金规模, (int)InterestModeEnum.空头存续名义本金, (int)InterestModeEnum.多头存续名义本金, (int)InterestModeEnum.标的期初全价 };
|
||||
public static readonly List<int> InterestNotionalModels = new List<int>() {(int)InterestModeEnum.合约名义本金规模, (int)InterestModeEnum.标的期初全价 };
|
||||
/// <summary>
|
||||
/// 互换利率计息方式 预付金
|
||||
/// </summary>
|
||||
|
||||
@@ -7,27 +7,31 @@ using System.Threading.Tasks;
|
||||
namespace YLErp.DBModels
|
||||
{
|
||||
/// <summary>
|
||||
/// 计息类型
|
||||
/// 计息类型。显式赋值,数字即 DB 存储值,不可随意改动。
|
||||
///
|
||||
/// 已删除的成员(保留编号记录,避免误回收):
|
||||
/// 持仓名义本金 = 3 —— 死代码,零引用
|
||||
/// 持仓市值 = 4 —— 死代码,零引用
|
||||
/// 多头存续名义本金 = 7 —— 界面已禁用,无历史数据
|
||||
/// 空头存续名义本金 = 8 —— 界面已禁用,无历史数据
|
||||
/// </summary>
|
||||
public enum InterestModeEnum
|
||||
{
|
||||
Unknown,
|
||||
固定值,
|
||||
合约名义本金规模,
|
||||
持仓名义本金,
|
||||
持仓市值,
|
||||
初始预付金,
|
||||
追加预付金,
|
||||
多头存续名义本金,
|
||||
空头存续名义本金,
|
||||
标的期初全价
|
||||
Unknown = 0,
|
||||
固定值 = 1,
|
||||
合约名义本金规模 = 2,
|
||||
// 3=持仓名义本金(已删), 4=持仓市值(已删)
|
||||
初始预付金 = 5,
|
||||
追加预付金 = 6,
|
||||
// 7=多头存续名义本金(已删), 8=空头存续名义本金(已删)
|
||||
标的期初全价 = 9
|
||||
}
|
||||
/// <summary>
|
||||
/// 计息方式
|
||||
/// </summary>
|
||||
public enum InterestTypeEnum
|
||||
{
|
||||
单利,
|
||||
复利
|
||||
单利 = 0,
|
||||
复利 = 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace YLErp.Derivatives.Interest;
|
||||
|
||||
/// <summary>
|
||||
/// 浮动利率指数取价器(fixing)。给定取价日和标的代码,返回当日利率。
|
||||
/// 参考 QuantLib IborIndex:取价与计息分离,计息函数只消费 rate。
|
||||
/// </summary>
|
||||
public interface IIndexFixer
|
||||
{
|
||||
/// <param name="rate">该日利率(小数,如 0.0134 = 1.34%)。取不到时为 0。</param>
|
||||
bool TryGetFixing(DateTime fixingDate, string underlyingCode, out decimal rate);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using YLErp.QdpModule;
|
||||
|
||||
namespace YLErp.Derivatives.Interest;
|
||||
|
||||
/// <summary>
|
||||
/// 取价日计算工具,供 IIndexFixer 实现复用。
|
||||
/// 收敛原 SwapDealService 里散落 6 处的 GetNonHolidayDefore(date.AddDays(rule))。
|
||||
/// </summary>
|
||||
public abstract class IndexFixerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 重置日 + 利率规则 → 取价日(工作日回拨)。
|
||||
/// interestRule: 0 = 当前营业日,-1 = 前一营业日。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 融资腿策略单测。验证每个策略的 CalcNotional 与现有 CalcNotionalByMode switch 完全一致。
|
||||
/// 这组测试是后续"迁移调用点"的安全网——迁移前后行为必须不变。
|
||||
/// </summary>
|
||||
[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<ArgumentException>(() =>
|
||||
FundingLegStrategyFactory.Get(InterestModeEnum.Unknown));
|
||||
Assert.ThrowsException<ArgumentException>(() =>
|
||||
FundingLegStrategyFactory.Get(InterestModeEnum.初始预付金));
|
||||
Assert.ThrowsException<ArgumentException>(() =>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using YLErp.Derivatives.Interest;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
namespace UnitTestProject.Modules.SwapModule.Margin
|
||||
{
|
||||
/// <summary>
|
||||
/// 保证金账户(MarginAccount)单测。验证余额变动(追加/释放/返还)。
|
||||
/// 保证金就是保证金——有余额、有利率、有利息,不存在"计息基数/Notional"概念。
|
||||
/// </summary>
|
||||
[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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// MarginModes 统一判断口径测试。
|
||||
/// 验证它和现有散落的 marginTypes/InterestMarginModels/premiumModes 内容一致。
|
||||
/// </summary>
|
||||
[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.标的期初全价));
|
||||
}
|
||||
|
||||
/// <summary>守护:和 ConsTrade.InterestMarginModels 内容必须一致(迁移期对齐)。</summary>
|
||||
[TestMethod]
|
||||
public void 与ConsTradeInterestMarginModels内容一致()
|
||||
{
|
||||
var consTrade = new List<int> { (int)InterestModeEnum.初始预付金, (int)InterestModeEnum.追加预付金 };
|
||||
CollectionAssert.AreEquivalent(consTrade, MarginModes.All.ToList());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.支付));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using YLErp.Modules.SwapModule.ReturnLegs;
|
||||
|
||||
namespace UnitTestProject.Modules.SwapModule.ReturnLegs
|
||||
{
|
||||
/// <summary>
|
||||
/// 标的端数量递推测试。验证开仓/平仓/公司行为场景下数量正确推进。
|
||||
/// </summary>
|
||||
[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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using YLErp.Modules.SwapModule.ReturnLegs;
|
||||
|
||||
namespace UnitTestProject.Modules.SwapModule.ReturnLegs
|
||||
{
|
||||
/// <summary>
|
||||
/// ReturnLegSummary 值对象测试。
|
||||
/// 验证标的端归档产出的汇总值正确构造、字段语义清晰。
|
||||
/// </summary>
|
||||
[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, "全平后无多头剩余");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 【缺陷证明测试】界面「全部平仓 → 部分平仓」,改变平仓比例,下方利息腿数据不变。
|
||||
/// ------------------------------------------------------------------------------
|
||||
/// 现象(前端):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 成正比。
|
||||
/// </summary>
|
||||
[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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>固定利率利息腿(不走浮动曲线),仅 InterestMode 不同</summary>
|
||||
private static swap_position CreateLeg(int interestMode, InterestTypeEnum interestType)
|
||||
{
|
||||
var intervalModels = new List<IntervalModel>
|
||||
{
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 复刻 GetUnwindInterests(SwapDealService.cs:640-645) 的入参构造,
|
||||
/// 唯一变量是界面输入的 closePercent。
|
||||
/// </summary>
|
||||
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_swap_position>(), // 无上一日 EOD
|
||||
new List<swap_position> { 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
|
||||
|
||||
/// <summary>与 CallProductionPath 相同,但允许指定平仓日期(用于两段式生命周期)</summary>
|
||||
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<eod_swap_position>(),
|
||||
new List<swap_position> { 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「未测」缺口)
|
||||
|
||||
/// <summary>
|
||||
/// 覆盖「国联民生-债券TRS期间结算功能测试260702.xlsx」Sheet1 矩阵中唯一标记为「未测」的格子:
|
||||
/// 业务场景4-2(固定值腿,部分平仓一次后经过数日再全平)。
|
||||
/// 同时守护 d1badfe4 的固定值腿平仓比例缩放修复——
|
||||
/// 修复前 newClosePercent=1,部分平仓利息与全平完全相同(界面改比例利息腿不变)。
|
||||
/// 本测试断言两段利息各自随平仓比例线性缩放(30% / 70%),正是该场景的回归守卫。
|
||||
/// </summary>
|
||||
[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<string>();
|
||||
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 的比例语义
|
||||
|
||||
/// <summary>
|
||||
/// 反射直击私有方法 CalcNotionalByMode,剥离所有计息细节,
|
||||
/// 只看「(closePrincipal, newClosePercent) 是否体现了平仓比例」。
|
||||
/// <para>注意:closePrincipal 与 newClosePercent 作用在**不同项**上——
|
||||
/// closePrincipal 决定"本期新增利息"的基数,newClosePercent 决定"历史累计/已消耗利息"的缩放,
|
||||
/// 二者不相乘(相乘会得到双重缩放的错误度量)。因此这里分别断言:
|
||||
/// 1) closePrincipal 应 = Fix × pct
|
||||
/// 2) newClosePercent 应 = pct(而非被强制置 1)</para>
|
||||
/// </summary>
|
||||
[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
|
||||
}
|
||||
}
|
||||
@@ -100,6 +100,13 @@ namespace UnitTestProject.Modules.SwapModule
|
||||
private readonly List<eod_swap_position> _eodPositions = new();
|
||||
private readonly IReadOnlyDictionary<DateTime, double> _floatRates;
|
||||
|
||||
/// <summary>
|
||||
/// 捕获最近一次 CalcSwapInterests 返回的 interests.First().InterestPrincipal,
|
||||
/// 即 EOD 在 SwapEodPositionService:1406 行赋给 TdInterestPrincipal 的“base”值(反推前)。
|
||||
/// 用于测试中精确镜像 mode 2/9 分叉(:1458 反推 / :1465 不反推),避免对复利累计利息做人工猜测。
|
||||
/// </summary>
|
||||
public decimal LastBaseInterestPrincipal { get; private set; }
|
||||
|
||||
public E2EEodService(trade td, List<swap_position> positions, trade_extend extend,
|
||||
IReadOnlyDictionary<DateTime, double> floatRates)
|
||||
: base(nameof(SwapInterestScenario3And4FloatingTest))
|
||||
@@ -123,11 +130,40 @@ namespace UnitTestProject.Modules.SwapModule
|
||||
protected override List<swap_position> FindSwapPositions(int swapTradeId)
|
||||
=> _positions.Where(x => x.SwapTradeId == swapTradeId && !x.IsInitial).ToList();
|
||||
|
||||
// --- 真实交易要素:标的与付息数据(替代原过度简化 stub)---
|
||||
// 本用例 = FR007 浮动利率互换,真实要素:标的是利率指数(非债券),增值税率 0,无债券付息事件。
|
||||
// 这些值与生产一致(利率指数 VAT 免、不进付息路径),因此不改变任何计息结果,只是不再写死魔法值。
|
||||
private static readonly IReadOnlyDictionary<string, underlying_manager> _realUnderlyings =
|
||||
new Dictionary<string, underlying_manager>
|
||||
{
|
||||
["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;
|
||||
}
|
||||
|
||||
/// <summary>对指定日期做真实日终收盘(无平仓),构建/累积 eod 链。</summary>
|
||||
@@ -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<eod_swap_position>(), partialCloseNotional, partialCloseNotional);
|
||||
var partialEod = _eod.ExecuteClose(position, new DateTime(2026, 5, 11),
|
||||
Notional - partialCloseNotional, 0m, new List<swap_flow_event> { partialFlow }, partialCloseNotional, prevEodPartial);
|
||||
remainingNotional, 0m, new List<swap_flow_event> { 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;
|
||||
|
||||
|
||||
@@ -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 无此腿故不触发,属本轮修复范围外");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 影响 ----
|
||||
|
||||
@@ -1402,7 +1402,6 @@ namespace YLErp.BLL.EodSettlement
|
||||
|
||||
public static void FillClientBalanceHisTradeFee()
|
||||
{
|
||||
List<int> marginTypes = new List<int>() { (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();
|
||||
|
||||
@@ -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));//实时
|
||||
|
||||
@@ -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<int> marginTypes = new List<int>() { (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,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using YLErp.Derivatives.Interest;
|
||||
using YLErp.Modules.DataProviderModule;
|
||||
|
||||
namespace YLErp.Modules.SwapModule;
|
||||
|
||||
/// <summary>
|
||||
/// IIndexFixer 的生产实现:FR007 等浮动利率取价。
|
||||
/// 直接调用 EodPriceQueryService.TryGetPrice,不依赖 SwapDealService 实例,便于独立测试。
|
||||
/// 未来 SwapDealService 的 6 处 inline 取价代码迁移到本类后,取价规则只在此一处维护。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取价日 → 取价 → 取不到抛异常。等价于原 SwapDealService 里散落 6 处的 inline 代码:
|
||||
/// GetNonHolidayDefore(date.AddDays(rule)) + TryGetFloatRate + throw
|
||||
/// 取到 0 视为"取到一个 0 值"(返回 0),由调用方决定是否沿用旧值;
|
||||
/// 只有 TryGetFixing 返回 false 才抛异常——对齐既有语义。
|
||||
/// </summary>
|
||||
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日}的价格");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.FundingLegs;
|
||||
|
||||
/// <summary>
|
||||
/// 合约名义本金规模融资腿(InterestMode=合约名义本金规模)。
|
||||
/// 站在"合约规模"视角:平仓本金 = 剩余名义本金 × 平仓比例。
|
||||
/// 与标的期初全价(9)在 CalcNotionalByMode 里公式相同,差异在衡泰路径 grossPrice 折算和 EOD 复利反推。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.FundingLegs;
|
||||
|
||||
/// <summary>
|
||||
/// 固定值融资腿(InterestMode=固定值)。
|
||||
/// 计息基数由合约约定(InterestPrincipalFix),无论平仓比例多少都恒等于该值。
|
||||
/// 业务规则:固定值就是合同写死的固定数额(Amount),永远不变。
|
||||
/// 不叫 FixedNotionalLeg——"固定值"不一定是"名义本金(Notional)",避免与 ContractNotionalLeg 概念撞车。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.FundingLegs;
|
||||
|
||||
/// <summary>
|
||||
/// 融资腿策略工厂。按 InterestMode 返回对应策略实例。
|
||||
/// 替换原 SwapDealService.CalcNotionalByMode 的 switch,收敛 mode 分发逻辑到一处。
|
||||
///
|
||||
/// 只管融资腿(funding leg),不管保证金——保证金是独立的资金管理体系,
|
||||
/// 不应该出现在融资腿策略里。mode 5/6(预付金)属于保证金,不在此注册。
|
||||
/// 死代码 mode 3/4 不注册;半死 mode 7/8 不注册(界面已注释)。
|
||||
/// 传入未注册的 mode 会抛异常,防止静默走默认分支。
|
||||
/// </summary>
|
||||
public static class FundingLegStrategyFactory
|
||||
{
|
||||
private static readonly Dictionary<InterestModeEnum, IFundingLegStrategy> _strategies = new()
|
||||
{
|
||||
[InterestModeEnum.固定值] = new FixedAmountLeg(),
|
||||
[InterestModeEnum.合约名义本金规模] = new ContractNotionalLeg(),
|
||||
[InterestModeEnum.标的期初全价] = new UnderlyingEntryFullPriceLeg(),
|
||||
};
|
||||
|
||||
/// <summary>按 mode 返回对应策略。未注册的 mode 抛 ArgumentException。</summary>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>按 mode 值返回对应策略,便于调用方直接传 int。</summary>
|
||||
public static IFundingLegStrategy Get(int mode) => Get((InterestModeEnum)mode);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.FundingLegs;
|
||||
|
||||
/// <summary>
|
||||
/// 融资腿(Funding Leg)计息基数策略。每个 InterestMode 一个实现。
|
||||
///
|
||||
/// 命名说明:用 FundingLeg(业界标准 Financing/Funding Leg),不用 InterestLeg——
|
||||
/// "interest"易和通用"利息"混淆;funding 精确表达"融资成本"。
|
||||
/// 融资腿 = 客户付给券商的杠杆成本(spread + FR007),与保证金(Margin)、标的端(ReturnLeg)各自独立。
|
||||
///
|
||||
/// 职责单一:给定持仓参数与平仓比例,算出"平仓本金 / 存续本金 / 有效比例"。
|
||||
/// 不做计息(计息由 SwapInterest 纯函数完成),不取价(取价由 IIndexFixer 完成)。
|
||||
/// </summary>
|
||||
public interface IFundingLegStrategy
|
||||
{
|
||||
/// <summary>该策略对应的计息模式。</summary>
|
||||
InterestModeEnum Mode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 根据持仓参数与平仓比例计算计息本金三元组。
|
||||
/// </summary>
|
||||
/// <param name="fix">合约固定本金(固定值/预付金腿用;其余腿忽略)。</param>
|
||||
/// <param name="posiNotional">当前剩余名义本金(数量 × 全价)。</param>
|
||||
/// <param name="posiLong">多头剩余名义本金(多空存续腿用,当前界面已禁用)。</param>
|
||||
/// <param name="posiShort">空头剩余名义本金。</param>
|
||||
/// <param name="closePercent">平仓比例(占剩余,0~1)。</param>
|
||||
NotionalResult CalcNotional(decimal fix, decimal posiNotional, decimal posiLong, decimal posiShort, decimal closePercent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 融资腿策略的计算结果。对应原 CalcNotionalByMode 返回的三元组,用自描述名。
|
||||
/// </summary>
|
||||
public readonly struct NotionalResult
|
||||
{
|
||||
/// <summary>本次平仓部分的计息本金。</summary>
|
||||
public decimal ClosePrincipal { get; }
|
||||
|
||||
/// <summary>存续持仓部分的计息本金(全额,不缩放)。</summary>
|
||||
public decimal PosiPrincipal { get; }
|
||||
|
||||
/// <summary>有效平仓比例。固定值腿恒为 1(计息基数不随比例变);其余沿用入参。</summary>
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.FundingLegs;
|
||||
|
||||
/// <summary>
|
||||
/// 标的期初全价融资腿(InterestMode=标的期初全价)。
|
||||
/// 计息基数 = 标的期初含费全价(PosiGrossPrice/EntryDirtyPrice) × 数量。
|
||||
/// "期初(Entry)"是关键——建仓时点的全价,非当前估值全价。
|
||||
/// 主路径 CalcNotionalByMode 公式与合约名义本金规模(2)相同;
|
||||
/// 差异在衡泰路径会乘 grossPrice 折算(SwapDealService.GetUnwindInterestsByHT),
|
||||
/// 以及 EOD 复利部分平仓后直接返回剩余本金(禁止反推,SwapEodPositionService:1458-1465)。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
/// <summary>现金保证金:余额 = 现金余额。</summary>
|
||||
public sealed class CashMargin : IMarginResolver
|
||||
{
|
||||
public MarginForm Form => MarginForm.Cash;
|
||||
|
||||
public MarginBalance Resolve(decimal postedAmount)
|
||||
=> new(postedAmount);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
/// <summary>授信保证:余额 = 已用授信额度。</summary>
|
||||
public sealed class CreditMargin : IMarginResolver
|
||||
{
|
||||
public MarginForm Form => MarginForm.Credit;
|
||||
|
||||
public MarginBalance Resolve(decimal postedAmount)
|
||||
=> new(postedAmount);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
/// <summary>担保品:余额 = 担保品市值。</summary>
|
||||
public sealed class GuaranteeMargin : IMarginResolver
|
||||
{
|
||||
public MarginForm Form => MarginForm.Guarantee;
|
||||
|
||||
public MarginBalance Resolve(decimal postedAmount)
|
||||
=> new(postedAmount);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
/// <summary>保证金形态:现金 / 授信 / 担保。预留扩展。</summary>
|
||||
public enum MarginForm
|
||||
{
|
||||
/// <summary>现金保证金:余额 = 现金余额。</summary>
|
||||
Cash,
|
||||
/// <summary>授信保证:余额 = 已用授信额度。</summary>
|
||||
Credit,
|
||||
/// <summary>担保品:余额 = 担保品市值。</summary>
|
||||
Guarantee,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按保证金形态解析余额。三种形态可互换地产出一个 MarginBalance(满足 LSP),
|
||||
/// 这是保证金领域唯一合理的多态点(差异仅在"余额如何取得")。
|
||||
/// 具体余额来源(资金流水 / 授信占用 / 担保估值)后续按形态填充。
|
||||
/// </summary>
|
||||
public interface IMarginResolver
|
||||
{
|
||||
MarginForm Form { get; }
|
||||
|
||||
MarginBalance Resolve(decimal postedAmount);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using YLErp.Derivatives.Interest;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
/// <summary>
|
||||
/// 保证金账户。管理保证金余额的变动(追加/释放/返还),并提供计息入口。
|
||||
///
|
||||
/// 保证金是独立的资金管理概念(初始保证金/维持保证金/保证金余额/追保),
|
||||
/// 与融资腿(funding leg)完全无关。现有代码把保证金塞进 InterestMode==5/6
|
||||
/// 当计息腿处理是错误的,本类是正确建模的起点。
|
||||
///
|
||||
/// 利息计算委托 SwapInterest 纯函数(余额×利率×天数/年化),
|
||||
/// 保证金账户只提供余额和计息入口,不自己实现计息算法。
|
||||
/// </summary>
|
||||
public sealed class MarginAccount
|
||||
{
|
||||
/// <summary>当前保证金余额。</summary>
|
||||
public MarginBalance Balance { get; private set; }
|
||||
|
||||
public MarginAccount(MarginBalance openingBalance)
|
||||
=> Balance = openingBalance;
|
||||
|
||||
/// <summary>追加保证金(余额增加)。</summary>
|
||||
public void Deposit(decimal amount)
|
||||
=> Balance = new MarginBalance(Balance.Balance + amount);
|
||||
|
||||
/// <summary>释放/返还保证金(余额减少,不低于 0)。</summary>
|
||||
public void Withdraw(decimal amount)
|
||||
=> Balance = new MarginBalance(Math.Max(0m, Balance.Balance - amount));
|
||||
|
||||
/// <summary>
|
||||
/// 按当前余额计算保证金利息。委托 SwapInterest.AccrueSimple。
|
||||
/// 保证金利息是券商对客户保证金存款付息(方向与融资腿相反)。
|
||||
/// </summary>
|
||||
/// <param name="rate">保证金利率(年化,如 0.03 = 3%)。</param>
|
||||
/// <param name="startDate">计息开始日。</param>
|
||||
/// <param name="endDate">计息结束日。</param>
|
||||
/// <param name="boundary">算头算尾规则。</param>
|
||||
/// <param name="annualDays">年化天数(365 或 360)。</param>
|
||||
public InterestResult AccrueInterest(decimal rate, System.DateTime startDate, System.DateTime endDate, AccrualBoundary boundary, int annualDays)
|
||||
=> SwapInterest.AccrueSimple(Balance.Balance, rate, startDate, endDate, boundary, annualDays);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
/// <summary>
|
||||
/// 保证金余额。现金、授信、担保等多种保证金形态的统一表达。
|
||||
///
|
||||
/// 保证金就是保证金——有余额、有利率、有利息,不存在"计息基数/Notional"概念。
|
||||
/// 余额随追加/释放/盈亏变动,利息由 SwapInterest 纯函数按 余额×利率×天数/年化 计算。
|
||||
/// </summary>
|
||||
public readonly struct MarginBalance
|
||||
{
|
||||
/// <summary>保证金余额:现金余额 / 授信占用 / 担保品市值。</summary>
|
||||
public decimal Balance { get; }
|
||||
|
||||
public MarginBalance(decimal balance)
|
||||
=> Balance = balance;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
/// <summary>
|
||||
/// 保证金计息模式(mode 5 初始预付金 / mode 6 追加预付金)的统一判断口径。
|
||||
///
|
||||
/// 现状(待收敛):同一集合 {初始预付金, 追加预付金} 在代码里复制了至少 6 次——
|
||||
/// ConsTrade.InterestMarginModels(框架级)
|
||||
/// SwapEodPositionService.marginTypes(实例字段)
|
||||
/// SwapEodPositionService.premiumModes(局部变量)
|
||||
/// SwapEventEmailService.marginTypes
|
||||
/// EodClientBalanceCalc.marginTypes
|
||||
/// ClientBalanceUtility.marginTypes
|
||||
/// 任何一处漏改(如新增保证金形态)都会导致口径分裂。本类收敛到单一来源。
|
||||
///
|
||||
/// 注意:这里的"保证金 mode"是现有系统把保证金错误建模为计息腿的历史遗留。
|
||||
/// 按 Margin 限界上下文的设计方向,未来保证金不应用 InterestMode 标识,
|
||||
/// 但在历史代码完全迁出前,需要一个统一判断点。
|
||||
/// </summary>
|
||||
public static class MarginModes
|
||||
{
|
||||
/// <summary>所有属于保证金的 InterestMode(初始预付金 / 追加预付金)。</summary>
|
||||
public static readonly IReadOnlyCollection<int> All = new HashSet<int>
|
||||
{
|
||||
(int)InterestModeEnum.初始预付金,
|
||||
(int)InterestModeEnum.追加预付金,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// List 形态,供 EF Core LINQ 表达式用(HashSet.Contains 无法翻译成 SQL)。
|
||||
/// 替代 ConsTrade.InterestMarginModels。
|
||||
/// </summary>
|
||||
public static readonly List<int> ForLinq = new()
|
||||
{
|
||||
(int)InterestModeEnum.初始预付金,
|
||||
(int)InterestModeEnum.追加预付金,
|
||||
};
|
||||
|
||||
/// <summary>判断 mode 是否属于保证金(非 LINQ 场景用)。</summary>
|
||||
public static bool Contains(int interestMode) => All.Contains(interestMode);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.ReturnLegs;
|
||||
|
||||
/// <summary>
|
||||
/// 方向因子计算。把 PositionType / SwapDirection 转成 +1/-1 乘数。
|
||||
///
|
||||
/// 多空方向(Long/Short)和收付方向(收取/支付)在代码里反复写成三元表达式,
|
||||
/// 散落在 SwapEodPositionService 15处 + SwapDealService 多处。
|
||||
/// 收敛到统一方法, 消除 +1/-1 写反的风险。
|
||||
/// </summary>
|
||||
public static class DirectionRatio
|
||||
{
|
||||
/// <summary>多空方向因子。多头=+1, 空头=-1。</summary>
|
||||
public static int LongShort(int positionType)
|
||||
=> positionType == (int)PositionTypeFlag.Long ? 1 : -1;
|
||||
|
||||
/// <summary>收付方向因子。收取=+1, 支付=-1。</summary>
|
||||
public static int ReceivePay(int direction)
|
||||
=> direction == (int)SwapDirectionEnum.收取 ? 1 : -1;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace YLErp.Modules.SwapModule.ReturnLegs;
|
||||
|
||||
/// <summary>
|
||||
/// 标的端分红/票息的增值税计算。
|
||||
///
|
||||
/// 业务含义:TRS 标的如果是债券,标的端会收到债券票息(coupon);
|
||||
/// 如果是股票,会收到现金分红(cash dividend)。系统统一用 Dividend 字段链路表达。
|
||||
/// 本类只负责"增值税后金额"这一步纯数学,不涉及取价/归属/落库。
|
||||
///
|
||||
/// 税务口径:票息/分红属于金融服务应税收入,先除以(1+税率)还原不含税收入,
|
||||
/// 再乘以(1-税率)扣减税负。公式:afterTax = payment / (1+tax) × (1-tax)。
|
||||
/// 原代码在 SwapEodPositionService 4处 + SwapDealService 1处 重复此公式。
|
||||
///
|
||||
/// 命名保持 DividendCalc 而非改为 CashFlow/Coupon——
|
||||
/// 因为系统里"分红"字段(DividendIn/PosiDividendSum)是统一链路,同时覆盖债券票息和股票分红,
|
||||
/// 改名会制造新歧义且需同步改 DB 字段,收益不匹配。
|
||||
/// </summary>
|
||||
public static class DividendCalc
|
||||
{
|
||||
/// <summary>增值税后票息/分红,四舍五入到分(2位)。</summary>
|
||||
public static decimal AfterTax(decimal payment, decimal tax)
|
||||
=> Math.Round(payment / (1 + tax) * (1 - tax), 2);
|
||||
|
||||
/// <summary>增值税后票息/分红(不四舍五入,供中间计算用)。</summary>
|
||||
public static decimal AfterTaxRaw(decimal payment, decimal tax)
|
||||
=> payment / (1 + tax) * (1 - tax);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace YLErp.Modules.SwapModule.ReturnLegs;
|
||||
|
||||
/// <summary>
|
||||
/// 标的端盯市(Mark-to-Market)计算。
|
||||
///
|
||||
/// UnderlyingMarketValue = 标的价 × 数量 × 合约乘数 × 多空方向
|
||||
/// PosiMtmPnL = (标的价 - 成本全价) × 数量 × 合约乘数 × 多空方向 × 收付方向
|
||||
///
|
||||
/// 原代码在 SwapEodPositionService 4处重复 UnderlyingMarketValue 公式(1731/1813/1898/2101)。
|
||||
/// </summary>
|
||||
public static class MtmCalc
|
||||
{
|
||||
/// <summary>标的市值。多头为正、空头为负。shortRatio: 多头=1, 空头=-1。</summary>
|
||||
public static decimal MarketValue(decimal price, decimal qty, decimal contractSize, int shortRatio)
|
||||
=> price * qty * contractSize * shortRatio;
|
||||
|
||||
/// <summary>盯市未实现盈亏 = (标的价 - 成本全价) × 数量 × 合约乘数 × 多空 × 收付。</summary>
|
||||
/// <param name="shortRatio">多头=1, 空头=-1。</param>
|
||||
/// <param name="ratio">收取=1, 支付=-1。</param>
|
||||
public static decimal UnrealizedPnl(decimal price, decimal costGrossPrice, decimal qty, decimal contractSize, int shortRatio, int ratio)
|
||||
=> (price - costGrossPrice) * qty * contractSize * shortRatio * ratio;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace YLErp.Modules.SwapModule.ReturnLegs;
|
||||
|
||||
/// <summary>
|
||||
/// 标的端数量递推(Qty Rollforward)。
|
||||
///
|
||||
/// 逐日推进持仓数量:今日数量 = 上一日终数量 + 今日开仓 - 今日平仓 + 公司行为调整。
|
||||
///
|
||||
/// 公司行为调整(corpActionDeltaQty)默认 0,当前不影响行为;
|
||||
/// 预留给公司行为改造(送股/拆股/配股)——届时填入非零值。
|
||||
/// 原代码(SwapEodPositionService:1980)只有开仓/平仓两项,硬编码了"数量只因交易变动"假设。
|
||||
/// </summary>
|
||||
public static class QtyRollforward
|
||||
{
|
||||
/// <summary>
|
||||
/// 计算今日持仓数量。
|
||||
/// </summary>
|
||||
/// <param name="previousQty">上一日终持仓数量。</param>
|
||||
/// <param name="openQty">今日开仓数量。</param>
|
||||
/// <param name="unwindQty">今日平仓数量。</param>
|
||||
/// <param name="corpActionDeltaQty">公司行为导致数量变动(送股/拆股),默认 0。</param>
|
||||
/// <returns>今日持仓数量(不低于 0)。</returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
namespace YLErp.Modules.SwapModule.ReturnLegs;
|
||||
|
||||
/// <summary>
|
||||
/// 标的端(Total Return Leg)归档产出的汇总值。
|
||||
///
|
||||
/// 这些值由标的盯市+分红计算得出,传给融资端(FundingLeg)作为计息基数参考。
|
||||
/// 对应原 SwapPositionCompose 里 DealFloatPositions 的产出:
|
||||
/// posiLongNotional / posiShortNotional / closePosiNotional / grossPrice
|
||||
///
|
||||
/// 命名说明:这里叫 ReturnLeg(业界标准 Total Return Leg),不叫 FloatLeg——
|
||||
/// "float" 在金融里首要含义是"浮动利率"(如 FR007),用于标的端会产生歧义。
|
||||
/// 标的端 = 标的资产的总回报(价格涨跌 + 票息/分红),与浮动利率无关。
|
||||
/// </summary>
|
||||
public readonly struct ReturnLegSummary
|
||||
{
|
||||
/// <summary>多头剩余名义本金(= 数量 × 全价)。融资端用它算多头腿计息基数。</summary>
|
||||
public decimal LongNotional { get; }
|
||||
|
||||
/// <summary>空头剩余名义本金。</summary>
|
||||
public decimal ShortNotional { get; }
|
||||
|
||||
/// <summary>本次平仓名义本金(平仓数量 × 合约乘数 × 含费全价)。</summary>
|
||||
public decimal CloseNotional { get; }
|
||||
|
||||
/// <summary>标的含费全价(PosiGrossPrice / EntryDirtyPrice)。融资端 mode 9 衡泰路径折算用。</summary>
|
||||
public decimal GrossPrice { get; }
|
||||
|
||||
public ReturnLegSummary(decimal longNotional, decimal shortNotional, decimal closeNotional, decimal grossPrice)
|
||||
=> (LongNotional, ShortNotional, CloseNotional, GrossPrice)
|
||||
= (longNotional, shortNotional, closeNotional, grossPrice);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using YLErp.Derivatives.Interest;
|
||||
|
||||
namespace YLErp.Modules.SwapModule;
|
||||
|
||||
/// <summary>
|
||||
/// 把一个取价委托包装成 IIndexFixer。
|
||||
///
|
||||
/// 为什么不直接用 Fr007IndexFixer.Instance:SwapDealService.TryGetFloatRate 是
|
||||
/// protected virtual,测试 override 它注入 stub。静态 Instance 直接调
|
||||
/// EodPriceQueryService 会绕过这个接缝,导致测试失败。
|
||||
/// 本类接受取价委托(SwapDealService 传入 this.TryGetFloatRate 的包装),
|
||||
/// 既保留 virtual 接缝,又用上 IndexFixerBase,且无需访问 protected 成员。
|
||||
/// </summary>
|
||||
internal sealed class SwapDealIndexFixer : IndexFixerBase, IIndexFixer
|
||||
{
|
||||
private readonly Func<DateTime, string, (bool ok, double rate)> _tryGet;
|
||||
|
||||
internal SwapDealIndexFixer(Func<DateTime, string, (bool ok, double rate)> 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
/// <summary>FR007 取价器,委托 TryGetFloatRate(保留 virtual 接缝供测试 stub)。</summary>
|
||||
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<swap_position>();
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据计息模式计算名义本金
|
||||
/// 根据计息模式计算名义本金。
|
||||
///
|
||||
/// 现状(过渡期):
|
||||
/// - 融资腿(mode 1/2/9)已委托 FundingLegStrategyFactory,走新策略体系。
|
||||
/// - 保证金(mode 5/6)暂保留——待 Margin 独立计息入口建成后迁出。
|
||||
/// - 多空存续(mode 7/8)界面已禁用,保留 case 仅为防御性兜底。
|
||||
/// - 死代码(mode 3/4)走 default。
|
||||
///
|
||||
/// 待保证金(mode 5/6)迁入 Margin 上下文后,本方法可整体删除,
|
||||
/// 调用点直接走 FundingLegStrategyFactory.Get(mode)。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
/// <summary>
|
||||
/// 保证金腿的 orginPv 维度重映射。
|
||||
///
|
||||
/// 保证金腿被迫走融资腿的差分公式(dynomicPrincipal = TdInterestPrincipal + posiPrincipal - orginPv),
|
||||
/// 但 orginPv 对融资腿是"交易名义本金(千万~亿级)",对保证金腿必须是"保证金本金"——
|
||||
/// 否则维度不匹配会算出巨负值。本方法把保证金场景的 orginPv 对齐到"上一日保证金本金"。
|
||||
///
|
||||
/// 待迁入 Margin 模块:保证金独立计息入口建好后,此方法移入 MarginAccount/MarginService。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保证金腿的利息方向翻转。保证金利息是券商付给客户(方向与融资腿相反)。
|
||||
/// 待迁入 Margin 模块。
|
||||
/// </summary>
|
||||
private static int FlipMarginDirection(int direction)
|
||||
=> direction == (int)SwapDirectionEnum.收取
|
||||
? (int)SwapDirectionEnum.支付
|
||||
: (int)SwapDirectionEnum.收取;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化利息腿信息
|
||||
/// </summary>
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
public class SwapEodPositionService : SwapTradeBaseService
|
||||
{
|
||||
private List<int> marginTypes = new List<int>() { (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<string, bool> tradeDic = new Dictionary<string, bool>();
|
||||
@@ -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();
|
||||
|
||||
@@ -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<int> eventTypes = new List<int>() { (int)SwapFlowEventTypeEnum.开仓, (int)SwapFlowEventTypeEnum.平仓 };
|
||||
private List<int> marginTypes = new List<int>() { (int)InterestModeEnum.追加预付金, (int)InterestModeEnum.初始预付金 };
|
||||
private List<int> marginTypes = MarginModes.All.ToList();
|
||||
private decimal wan = 10000m;
|
||||
public SwapEventEmailService(OptUserInfo userInfo) : base(userInfo)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user