Merge remote-tracking branch 'origin/glms/feature/1.4.2' into glms/feature/1.4.2-margin
This commit is contained in:
@@ -65,6 +65,13 @@ namespace YLErp.DBModels
|
||||
[DisplayName("实际付息(兑付)日")]
|
||||
[Column("pay_date_act")]
|
||||
public DateTime? payment_date { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 债权登记日(除息/归属截止日)——票息归属按此判定,而非支付日
|
||||
/// </summary>
|
||||
[DisplayName("债权登记日")]
|
||||
[Column("reg_date")]
|
||||
public DateTime? reg_date { get; set; }
|
||||
/// <summary>
|
||||
/// 每张兑付利息额
|
||||
/// </summary>
|
||||
|
||||
@@ -205,7 +205,7 @@ namespace YLErp.DBModels
|
||||
[DataChange]
|
||||
public int InterestDirection { get; set; }
|
||||
/// <summary>
|
||||
/// 计息基本类型 1:固定值,2:合约名义本金规模,3:持仓名义本金,4:持仓市值,5:初始预付金,6:追加预付金
|
||||
/// 计息基本类型 1:固定值,2:合约名义本金规模,5:初始预付金,6:追加预付金,9:标的期初全价
|
||||
/// </summary>
|
||||
[DisplayName("计息基本类型")]
|
||||
[DataChange]
|
||||
|
||||
@@ -280,7 +280,7 @@ namespace YLErp.DBModels
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 计息方式 1:固定值,2:合约名义本金规模,3:持仓名义本金,4:持仓市值
|
||||
/// 计息方式 1:固定值,2:合约名义本金规模,5:初始预付金,6:追加预付金,9:标的期初全价
|
||||
/// </summary>
|
||||
[DisplayName("计息方式")]
|
||||
[DataChange]
|
||||
|
||||
@@ -163,7 +163,7 @@ namespace YLErp.DBModels
|
||||
/// </summary>
|
||||
public string FloatRateUnderlyingCode { get; set; }
|
||||
/// <summary>
|
||||
/// 计息基本类型 1:固定值,2:合约名义本金规模,3:持仓名义本金,4:持仓市值,5:初始预付金,6:追加预付金
|
||||
/// 计息基本类型 1:固定值,2:合约名义本金规模,5:初始预付金,6:追加预付金,9:标的期初全价
|
||||
/// </summary>
|
||||
[DisplayName("计息基本类型")]
|
||||
[DataChange]
|
||||
|
||||
@@ -18,7 +18,7 @@ public readonly struct AccrualContext
|
||||
/// <summary>年化天数(365 / 360)。</summary>
|
||||
public int AnnualDays { get; }
|
||||
|
||||
/// <summary>舍入精度位数。默认 11(保证金腿);资金腿调用方应显式传 FundingLegPrecision=12。</summary>
|
||||
/// <summary>舍入精度位数。默认 11(仅未接线的 MarginAccount.AccrueInterest 走此默认;生产融资腿/保证金腿均显式用 FundingLegPrecision=12)。</summary>
|
||||
public int Precision { get; }
|
||||
|
||||
/// <summary>可选 trace 收集器;为 null 时不记录(纯计算场景直接传 null,与开关无关)。</summary>
|
||||
|
||||
@@ -53,7 +53,7 @@ public readonly struct InterestRate
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see cref="Compounding.Simple"/>:decimal 精确运算。</description></item>
|
||||
/// <item><description><see cref="Compounding.Compounded"/> / <see cref="Compounding.Continuous"/>:闭式(double 计算后回 decimal),
|
||||
/// 满足通用定价 / 保证金场景;若要 decimal 精度的离散重置日复利,请用 Simple 按段计息并滚动本金。</description></item>
|
||||
/// 满足通用定价;若要 decimal 精度的离散重置日复利,请用 Simple 按段计息并滚动本金。</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public decimal CompoundFactor(decimal t)
|
||||
|
||||
@@ -106,11 +106,11 @@ public readonly struct InterestResult
|
||||
/// </summary>
|
||||
public static class SwapInterest
|
||||
{
|
||||
/// <summary>系统统一价格精度位数(保证金腿)。</summary>
|
||||
/// <summary>默认舍入精度位数(历史值;生产融资腿与保证金腿均用 FundingLegPrecision=12)。</summary>
|
||||
public const int Precision = 11;
|
||||
|
||||
/// <summary>资金腿计息精度(生产口径)。资金腿所有落库/对账均以 12 位为准,
|
||||
/// 与保证金腿的 Precision=11 不同。提升至公共常量,消除 SwapDealService 与 FundingLegAccrual 的重复定义。</summary>
|
||||
/// <summary>资金腿与保证金腿的生产计息精度(落库/对账均以 12 位为准)。
|
||||
/// 提升至公共常量,消除 SwapDealService 与 SimpleInterestAccrual 的重复定义。</summary>
|
||||
public const int FundingLegPrecision = 12;
|
||||
|
||||
/// <summary>年化天数常量(合约字段存的是 int,故不用 enum)。</summary>
|
||||
|
||||
@@ -13,7 +13,7 @@ using YLErp.Core.Interest;
|
||||
namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
{
|
||||
/// <summary>
|
||||
/// 影子测试:CalcDailyCompoundInterestByEod(旧逐日循环)vs FundingLegAccrual.AccrueCompoundEod(新纯函数)。
|
||||
/// 影子测试:CalcDailyCompoundInterestByEod(旧逐日循环)vs CompoundInterestAccrual.AccrueEod(新纯函数)。
|
||||
/// 构造同一组参数,两套实现并行跑,断言结果一致(到分)。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
@@ -92,14 +92,14 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
decimal oldInterest = 0, oldTd = 0;
|
||||
var svc = new StubSvc();
|
||||
svc.CalcDailyCompoundInterestByEod(preEod, EodDate, TradeDate, position,
|
||||
Notional, Notional, flowEvent, AnnualDays, false, 0m, 1m, Notional,
|
||||
Notional, Notional, flowEvent, AnnualDays, 0m, 1m,
|
||||
ref oldInterest, ref oldTd);
|
||||
|
||||
// 新方法
|
||||
var rate = FundingLegRate.Fixed(FixedRate);
|
||||
var policy = new AccrualPolicy(AccrualBoundary.Both, true, 7, AnnualDays, true);
|
||||
var remainingPercent = Math.Max(0m, Math.Min(1m, Notional / Notional));
|
||||
var result = FundingLegAccrual.AccrueCompoundEod(
|
||||
var result = CompoundInterestAccrual.AccrueEod(
|
||||
50_000m, Notional, Notional, 1m, rate, policy,
|
||||
isResetDay: true, remainingPercent, EodDate);
|
||||
|
||||
@@ -125,13 +125,13 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
decimal oldInterest = 0, oldTd = 0;
|
||||
var svc = new StubSvc();
|
||||
svc.CalcDailyCompoundInterestByEod(preEod, nonResetDate, TradeDate, position,
|
||||
Notional, Notional, flowEvent, AnnualDays, false, 0m, 1m, Notional,
|
||||
Notional, Notional, flowEvent, AnnualDays, 0m, 1m,
|
||||
ref oldInterest, ref oldTd);
|
||||
|
||||
// 新方法
|
||||
var rate = FundingLegRate.Fixed(FixedRate);
|
||||
var policy = new AccrualPolicy(AccrualBoundary.Both, true, 7, AnnualDays, true);
|
||||
var result = FundingLegAccrual.AccrueCompoundEod(
|
||||
var result = CompoundInterestAccrual.AccrueEod(
|
||||
30_000m, Notional, Notional, 1m, rate, policy,
|
||||
isResetDay: false, 0m, nonResetDate);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ using YLErp.Modules.SwapModule.Accrual;
|
||||
namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
{
|
||||
/// <summary>
|
||||
/// 影子测试:CalcDailyCompoundInterest(旧逐日循环)vs FundingLegAccrual.AccrueCompoundPeriod(新分段纯函数)。
|
||||
/// 影子测试:CalcDailyCompoundInterest(旧逐日循环)vs CompoundInterestAccrual.AccruePeriod(新分段纯函数)。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class CompoundPeriodShadowTest
|
||||
@@ -74,7 +74,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
decimal oldI = 0, oldTd = 0;
|
||||
var svc = new StubSvc();
|
||||
svc.CalcDailyCompoundInterest(EndDate, position, Notional, flowEvent,
|
||||
AnnualDays, false, 0m, 1m, Notional, true, false,
|
||||
AnnualDays, 0m, 1m, true, false,
|
||||
ref oldI, ref oldTd);
|
||||
|
||||
// 新方法:固定利率全段相同,分段点 = PosiStartDate + k×7
|
||||
@@ -86,7 +86,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
(StartDate.AddDays(7), allInRate),
|
||||
(StartDate.AddDays(14), allInRate),
|
||||
};
|
||||
var result = FundingLegAccrual.AccrueCompoundPeriod(
|
||||
var result = CompoundInterestAccrual.AccruePeriod(
|
||||
notional: Notional,
|
||||
segmentRates: segRates,
|
||||
startDate: StartDate,
|
||||
@@ -122,7 +122,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
decimal oldI = 0, oldTd = 0;
|
||||
var svc = new StubSvc();
|
||||
svc.CalcDailyCompoundInterest(EndDate, position, Notional * closePct, flowEvent,
|
||||
AnnualDays, false, 0m, closePct, Notional, true, false,
|
||||
AnnualDays, 0m, closePct, true, false,
|
||||
ref oldI, ref oldTd, consumedInterest: consumed, resetCarryInterest: carry);
|
||||
|
||||
// 新方法
|
||||
@@ -133,7 +133,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
(StartDate.AddDays(7), allInRate),
|
||||
(StartDate.AddDays(14), allInRate),
|
||||
};
|
||||
var result = FundingLegAccrual.AccrueCompoundPeriod(
|
||||
var result = CompoundInterestAccrual.AccruePeriod(
|
||||
notional: Notional * closePct,
|
||||
segmentRates: segRates,
|
||||
startDate: StartDate,
|
||||
@@ -166,7 +166,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
decimal oldI = 0, oldTd = 0;
|
||||
var svc = new StubSvc();
|
||||
svc.CalcDailyCompoundInterest(EndDate, position, Notional, flowEvent,
|
||||
AnnualDays, false, 0m, 1m, Notional, true, true,
|
||||
AnnualDays, 0m, 1m, true, true,
|
||||
ref oldI, ref oldTd);
|
||||
|
||||
// 新方法
|
||||
@@ -177,7 +177,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
(StartDate.AddDays(7), allInRate),
|
||||
(StartDate.AddDays(14), allInRate),
|
||||
};
|
||||
var result = FundingLegAccrual.AccrueCompoundPeriod(
|
||||
var result = CompoundInterestAccrual.AccruePeriod(
|
||||
notional: Notional,
|
||||
segmentRates: segRates,
|
||||
startDate: StartDate,
|
||||
|
||||
@@ -12,7 +12,7 @@ using YLErp.Derivatives.Interest;
|
||||
namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
{
|
||||
/// <summary>
|
||||
/// 影子测试:CalcDailySimpleInterest(旧逐日循环)vs FundingLegAccrual.AccrueSimplePeriod(新分段纯函数)。
|
||||
/// 影子测试:CalcDailySimpleInterest(旧逐日循环)vs SimpleInterestAccrual.AccruePeriod(新分段纯函数)。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SimplePeriodShadowTest
|
||||
@@ -100,13 +100,13 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
decimal oldI = 0, oldTd = 0;
|
||||
var svc = new StubSvc();
|
||||
svc.CalcDailySimpleInterest(preEod, EndDate, position, Notional, flowEvent,
|
||||
AnnualDays, false, 0m, 1m, Notional, true, false, ref oldI, ref oldTd);
|
||||
AnnualDays, 0m, 1m, Notional, true, false, ref oldI, ref oldTd);
|
||||
|
||||
// 新方法:固定利率全段相同
|
||||
// 旧代码差分: dynomicPrincipal = preEod.TdInterestPrincipal(=0) + posiPrincipal - orginPv = 0
|
||||
var segRates = new List<(DateTime, decimal)> { (StartDate, Spread) };
|
||||
var accrualPrincipal = 0m + Notional - Notional; // 差分 = 0
|
||||
var result = FundingLegAccrual.AccrueSimplePeriod(
|
||||
var result = SimpleInterestAccrual.AccruePeriod(
|
||||
priorAccrued: 0m,
|
||||
notional: 0m,
|
||||
unwindFraction: 1m,
|
||||
@@ -146,13 +146,13 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
decimal oldI = 0, oldTd = 0;
|
||||
var svc = new StubSvc();
|
||||
svc.CalcDailySimpleInterest(preEod, EndDate, position, Notional, flowEvent,
|
||||
AnnualDays, false, 0m, 0.5m, Notional, true, false, ref oldI, ref oldTd);
|
||||
AnnualDays, 0m, 0.5m, Notional, true, false, ref oldI, ref oldTd);
|
||||
|
||||
// 新方法
|
||||
// 差分本金 = preEod.TdInterestPrincipal + posiPrincipal - orginPv
|
||||
var accrualPrincipal = Notional + Notional - Notional;
|
||||
var segRates = new List<(DateTime, decimal)> { (StartDate, Spread) };
|
||||
var result = FundingLegAccrual.AccrueSimplePeriod(
|
||||
var result = SimpleInterestAccrual.AccruePeriod(
|
||||
priorAccrued: 200_000m * 0.5m,
|
||||
notional: accrualPrincipal,
|
||||
unwindFraction: 0.5m,
|
||||
@@ -200,7 +200,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
decimal oldI = 0, oldTd = 0;
|
||||
var svc = new FloatStubSvc(new StubIndexFixer(fixingAtReset));
|
||||
svc.CalcDailySimpleInterest(preEod, EndDate, position, Notional, flowEvent,
|
||||
AnnualDays, false, floatRateIn, closePct, Notional, true, false, ref oldI, ref oldTd);
|
||||
AnnualDays, floatRateIn, closePct, Notional, true, false, ref oldI, ref oldTd);
|
||||
|
||||
// 新方法:手算 segmentRates(对齐旧代码取价循环的逻辑)
|
||||
// 4/21 <= preEodDate(4/30) → 跳过取价,currentFloat 保持入参 floatRateIn
|
||||
@@ -215,7 +215,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
|
||||
// 差分本金 = preEod.TdInterestPrincipal + posiPrincipal - orginPv
|
||||
var accrualPrincipal = Notional + Notional - Notional;
|
||||
var result = FundingLegAccrual.AccrueSimplePeriod(
|
||||
var result = SimpleInterestAccrual.AccruePeriod(
|
||||
priorAccrued: 200_000m * closePct,
|
||||
notional: accrualPrincipal,
|
||||
unwindFraction: closePct,
|
||||
|
||||
@@ -124,6 +124,17 @@ namespace YLErp.Modules.SwapModule
|
||||
return new swap_event { id = SwapEvents.Count };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 捕获 SaveAutoSwapDeal 落库的 flow_event(生产写 DbContext.swap_flow_event)。
|
||||
/// 同步到 PersistedFlowEvents 供 AS_009/010/011 断言;基类 FlowEvents 仍由它填充,
|
||||
/// 供 GetConsumedInterest 真实计算已结利息。
|
||||
/// </summary>
|
||||
protected override void PersistFlowEvent(swap_flow_event flowEvent)
|
||||
{
|
||||
base.PersistFlowEvent(flowEvent);
|
||||
PersistedFlowEvents.Add(flowEvent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 利息腿金额直接给定(付息金额),避免把 GetInterests 的计息细节混入本用例——
|
||||
/// 本文件关注的是「自动互换是否触发 / 几条 / 资金发生日 / 金额量级」,
|
||||
|
||||
@@ -509,7 +509,7 @@ namespace YLErp.Modules.SwapModule
|
||||
decimal tdInterestAmount = 0m;
|
||||
|
||||
service.CalcDailyCompoundInterestByEod(preEod, resetDate, startDate, position,
|
||||
principal, principal, flowEvent, AnnualDays, false, 0.013502m, 1m, principal,
|
||||
principal, principal, flowEvent, AnnualDays, 0.013502m, 1m,
|
||||
ref interestAmount, ref tdInterestAmount);
|
||||
|
||||
AssertDecimal(principal + pendingInterest, flowEvent.InterestPrincipal,
|
||||
|
||||
@@ -1724,15 +1724,15 @@ namespace YLErp.Modules.SwapModule
|
||||
decimal expectedAmountAtEnd = 0m;
|
||||
decimal expectedTdAmountAtEnd = 0m;
|
||||
dealService.CalcDailyCompoundInterest(
|
||||
finalCloseDate, position, remainingNotional, expectedEndFlow, AnnualDays, false,
|
||||
intermediateEod.FloatRate, 1m, originalNotional, true, false,
|
||||
finalCloseDate, position, remainingNotional, expectedEndFlow, AnnualDays,
|
||||
intermediateEod.FloatRate, 1m, true, false,
|
||||
ref expectedAmountAtEnd, ref expectedTdAmountAtEnd);
|
||||
var expectedPreviousFlow = new swap_flow_event { InterestRate = spread };
|
||||
decimal expectedAmountAtPreviousEod = 0m;
|
||||
decimal expectedTdAmountAtPreviousEod = 0m;
|
||||
dealService.CalcDailyCompoundInterest(
|
||||
intermediateDate, position, remainingNotional, expectedPreviousFlow, AnnualDays, false,
|
||||
intermediateEod.FloatRate, 1m, originalNotional, true, true,
|
||||
intermediateDate, position, remainingNotional, expectedPreviousFlow, AnnualDays,
|
||||
intermediateEod.FloatRate, 1m, true, true,
|
||||
ref expectedAmountAtPreviousEod, ref expectedTdAmountAtPreviousEod);
|
||||
var expectedFinalInterest = intermediateEod.InterestIncomeSum
|
||||
+ expectedAmountAtEnd - expectedAmountAtPreviousEod;
|
||||
|
||||
@@ -6,16 +6,14 @@ using YLErp.Modules.SwapModule.FundingLegs;
|
||||
namespace UnitTestProject.Modules.SwapModule.FundingLegs
|
||||
{
|
||||
/// <summary>
|
||||
/// 融资腿策略单测。验证每个策略的 CalcNotional 与现有 CalcNotionalByMode switch 完全一致。
|
||||
/// 这组测试是后续"迁移调用点"的安全网——迁移前后行为必须不变。
|
||||
/// 融资腿策略单测。验证每个 IFundingLegStrategy 实现的 CalcNotional 计息基数公式正确。
|
||||
/// 原 CalcNotionalByMode switch 已重构为策略类(见 FundingLegStrategyFactory)。
|
||||
/// </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)
|
||||
|
||||
@@ -23,7 +21,7 @@ namespace UnitTestProject.Modules.SwapModule.FundingLegs
|
||||
public void 固定值_部分平仓_计息基数恒等于Fix()
|
||||
{
|
||||
var leg = new FixedAmountLeg();
|
||||
var r = leg.CalcNotional(Fix, Notional, LongNotional, ShortNotional, 0.5m);
|
||||
var r = leg.CalcNotional(Fix, Notional, 0.5m);
|
||||
|
||||
Assert.AreEqual(Fix, r.ClosePrincipal, "平仓本金恒=Fix");
|
||||
Assert.AreEqual(Fix, r.PosiPrincipal, "持仓本金恒=Fix");
|
||||
@@ -34,7 +32,7 @@ namespace UnitTestProject.Modules.SwapModule.FundingLegs
|
||||
public void 固定值_全平_计息基数仍等于Fix()
|
||||
{
|
||||
var leg = new FixedAmountLeg();
|
||||
var r = leg.CalcNotional(Fix, Notional, LongNotional, ShortNotional, 1m);
|
||||
var r = leg.CalcNotional(Fix, Notional, 1m);
|
||||
Assert.AreEqual(Fix, r.ClosePrincipal);
|
||||
}
|
||||
|
||||
@@ -46,7 +44,7 @@ namespace UnitTestProject.Modules.SwapModule.FundingLegs
|
||||
public void 合约名义本金_部分平仓_本金按比例缩放()
|
||||
{
|
||||
var leg = new ContractNotionalLeg();
|
||||
var r = leg.CalcNotional(Fix, Notional, LongNotional, ShortNotional, 0.5m);
|
||||
var r = leg.CalcNotional(Fix, Notional, 0.5m);
|
||||
|
||||
Assert.AreEqual(50_000_000m, r.ClosePrincipal);
|
||||
Assert.AreEqual(Notional, r.PosiPrincipal);
|
||||
@@ -57,7 +55,7 @@ namespace UnitTestProject.Modules.SwapModule.FundingLegs
|
||||
public void 合约名义本金_全平_本金等于全额()
|
||||
{
|
||||
var leg = new ContractNotionalLeg();
|
||||
var r = leg.CalcNotional(Fix, Notional, LongNotional, ShortNotional, 1m);
|
||||
var r = leg.CalcNotional(Fix, Notional, 1m);
|
||||
Assert.AreEqual(Notional, r.ClosePrincipal);
|
||||
}
|
||||
|
||||
@@ -65,7 +63,7 @@ namespace UnitTestProject.Modules.SwapModule.FundingLegs
|
||||
public void 合约名义本金_零平仓_本金为零()
|
||||
{
|
||||
var leg = new ContractNotionalLeg();
|
||||
var r = leg.CalcNotional(Fix, Notional, LongNotional, ShortNotional, 0m);
|
||||
var r = leg.CalcNotional(Fix, Notional, 0m);
|
||||
|
||||
Assert.AreEqual(0m, r.ClosePrincipal);
|
||||
Assert.AreEqual(Notional, r.PosiPrincipal);
|
||||
@@ -79,7 +77,7 @@ namespace UnitTestProject.Modules.SwapModule.FundingLegs
|
||||
public void 标的期初全价_部分平仓_主路径公式同mode2()
|
||||
{
|
||||
var leg = new UnderlyingEntryFullPriceLeg();
|
||||
var r = leg.CalcNotional(Fix, Notional, LongNotional, ShortNotional, 0.5m);
|
||||
var r = leg.CalcNotional(Fix, Notional, 0.5m);
|
||||
|
||||
Assert.AreEqual(50_000_000m, r.ClosePrincipal);
|
||||
Assert.AreEqual(Notional, r.PosiPrincipal);
|
||||
@@ -90,7 +88,7 @@ namespace UnitTestProject.Modules.SwapModule.FundingLegs
|
||||
public void 标的期初全价_全平_本金等于全额()
|
||||
{
|
||||
var leg = new UnderlyingEntryFullPriceLeg();
|
||||
var r = leg.CalcNotional(Fix, Notional, LongNotional, ShortNotional, 1m);
|
||||
var r = leg.CalcNotional(Fix, Notional, 1m);
|
||||
Assert.AreEqual(Notional, r.ClosePrincipal);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
using YLErp.Modules.EodModule;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// GLMS-20260105-0006 回归:债券 TRS 登记日当天手动平仓/互换,分红收益应为 36160 而非 0。
|
||||
/// 根因双成因:
|
||||
/// A. BondPaymentService.GetBondPayments 用支付日(pay_date_PL/pay_date_act)而非债权登记日(reg_date)判定谁享有票息
|
||||
/// -> 登记日(4/3)当日 EOD 不计提,跨过支付日(4/6)才计提(巧合:4/4-4/5周末,下一交易日恰=支付日,掩盖缺陷)
|
||||
/// B. SwapDealService.GetPreEodDividendSum 用 ValueDate 严格小于 dealDate 读 T-1 EOD 快照
|
||||
/// -> 登记日当天手动平仓读不到当日 EOD,拿到 0
|
||||
/// 本文件用手工合成内存数据(不连 96 库),通过 virtual seam 注入,真实跑生产日期逻辑。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class GLMS20260105_0006_RegisterDateDividendTest
|
||||
{
|
||||
private const string BondCode = "230004.IB";
|
||||
private const int TradeId = 6006;
|
||||
private const long PositionId = 60061;
|
||||
private const decimal Qty = 20_000_000m;
|
||||
private const decimal PaymentPer100 = 0.1808m;
|
||||
private const decimal ExpectedDividend = 36_160m; // 20,000,000 × 0.1808 / 100
|
||||
|
||||
// 付息日历(截图):登记日 4/3,支付日 4/6
|
||||
private static readonly DateTime RegDate = new(2026, 4, 3);
|
||||
private static readonly DateTime PayDate = new(2026, 4, 6);
|
||||
private static readonly DateTime PreRegDate = new(2026, 4, 2);
|
||||
|
||||
#region 成因 A:日期口径 seam
|
||||
|
||||
private sealed class TestableBondPaymentService : BondPaymentService
|
||||
{
|
||||
private readonly List<BondPayment> _data;
|
||||
public TestableBondPaymentService(List<BondPayment> data) : base(OptUserInfo.UnitTestUser) { _data = data; }
|
||||
|
||||
protected override IQueryable<BondPayment> QueryBondPayments(string underlyingCode)
|
||||
=> _data.Where(x => x.underlyingCode == underlyingCode).AsQueryable();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CauseA_登记日当日EOD_应按登记日口径选中付息记录()
|
||||
{
|
||||
var record = new BondPayment
|
||||
{
|
||||
underlyingCode = BondCode,
|
||||
reg_date = RegDate, // 债权登记日 4/3(关键:分红归属按此判定)
|
||||
payment_date_pl = PayDate, // 理论付息日 4/6
|
||||
payment_date = PayDate, // 实际付息日 4/6
|
||||
payment_interest = PaymentPer100
|
||||
};
|
||||
var svc = new TestableBondPaymentService(new List<BondPayment> { record });
|
||||
|
||||
// 登记日当日的 EOD 计提区间 (4/2, 4/3]
|
||||
var payments = svc.GetBondPayments(BondCode, PreRegDate, RegDate);
|
||||
|
||||
// 修复前:用支付日(pay_date_PL=4/6)过滤 -> 4/6 不在 (4/2,4/3] -> 0 条(漏计分红)
|
||||
// 修复后:用债权登记日(reg_date=4/3)过滤 -> 4/3 落在区间 -> 1 条(GLMS-20260105-0006 已修复)
|
||||
Assert.AreEqual(1, payments.Count,
|
||||
"登记日(4/3)当日 EOD 应按债权登记日(reg_date)选中该笔付息;" +
|
||||
"当前按支付日(pay_date_PL=4/6)过滤会漏选->0条,导致分红不计提。");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 成因 B:T-1 快照 seam
|
||||
|
||||
private sealed class TestableSwapDealService : SwapDealService
|
||||
{
|
||||
private readonly List<eod_swap> _eodSwaps;
|
||||
private readonly List<eod_swap_position> _eodPositions;
|
||||
public TestableSwapDealService(List<eod_swap> eodSwaps, List<eod_swap_position> eodPositions)
|
||||
: base(OptUserInfo.UnitTestUser) { _eodSwaps = eodSwaps; _eodPositions = eodPositions; }
|
||||
|
||||
public decimal ExposeGetPreEodDividendSum(int tradeId, long positionId, DateTime dealDate)
|
||||
=> GetPreEodDividendSum(tradeId, positionId, dealDate);
|
||||
|
||||
protected override IQueryable<eod_swap> QueryPreEodSwaps(int tradeId)
|
||||
=> _eodSwaps.Where(x => x.SwapTradeId == tradeId).AsQueryable();
|
||||
|
||||
protected override eod_swap_position QueryPreEodPosition(int tradeId, long positionId, DateTime valueDate)
|
||||
=> _eodPositions.FirstOrDefault(x => x.SwapTradeId == tradeId && x.PositionId == positionId && x.ValueDate == valueDate);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CauseB_登记日当天手动平仓_应读到当日EOD分红36160()
|
||||
{
|
||||
// 4/2 EOD:累计分红 0;4/3 EOD(登记日):累计分红 36160(即登记日应有的状态)
|
||||
var eodSwaps = new List<eod_swap>
|
||||
{
|
||||
new eod_swap { SwapTradeId = TradeId, ValueDate = PreRegDate },
|
||||
new eod_swap { SwapTradeId = TradeId, ValueDate = RegDate }
|
||||
};
|
||||
var eodPositions = new List<eod_swap_position>
|
||||
{
|
||||
new eod_swap_position { SwapTradeId = TradeId, PositionId = PositionId, ValueDate = PreRegDate, PosiDividendSum = 0m, PosiQuantity = Qty },
|
||||
new eod_swap_position { SwapTradeId = TradeId, PositionId = PositionId, ValueDate = RegDate, PosiDividendSum = ExpectedDividend, PosiQuantity = Qty }
|
||||
};
|
||||
var svc = new TestableSwapDealService(eodSwaps, eodPositions);
|
||||
|
||||
// 登记日(4/3)当天手动平仓
|
||||
var dividend = svc.ExposeGetPreEodDividendSum(TradeId, PositionId, RegDate);
|
||||
|
||||
// 修复前:ValueDate 严格小于 dealDate 读 T-1(4/2) -> 0(漏读当日分红)
|
||||
// 修复后:ValueDate 小于等于 dealDate 读当日(4/3) -> 36160(GLMS-20260105-0006 已修复)
|
||||
Assert.AreEqual(ExpectedDividend, dividend, 0.01m,
|
||||
"登记日(4/3)当天手动平仓应读到当日 EOD 累计分红 36,160;" +
|
||||
"当前 GetPreEodDividendSum 用 ValueDate < dealDate 读 T-1 快照->0。");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -246,8 +246,19 @@ namespace YLErp.Modules.SwapModule
|
||||
var valueDate = DateTime.Today;
|
||||
var unwindDate = DateTime.Today;
|
||||
|
||||
var interests100 = new SwapDealService(user).GetUnwindInterests(valueDate, unwindDate, td.id, cp100_B, (int)SwapEventTypeEnum.平仓);
|
||||
var interests40 = new SwapDealService(user).GetUnwindInterests(valueDate, unwindDate, td.id, cp40_B, (int)SwapEventTypeEnum.平仓);
|
||||
// FR007 fixing 是外部数据依赖:非交易日/数据未发布时取价会抛 Exception。
|
||||
// 与"连不上库自动 Inconclusive"同语义——外部数据不可用不应判为测试失败。
|
||||
List<swap_flow_event> interests100, interests40;
|
||||
try
|
||||
{
|
||||
interests100 = new SwapDealService(user).GetUnwindInterests(valueDate, unwindDate, td.id, cp100_B, (int)SwapEventTypeEnum.平仓);
|
||||
interests40 = new SwapDealService(user).GetUnwindInterests(valueDate, unwindDate, td.id, cp40_B, (int)SwapEventTypeEnum.平仓);
|
||||
}
|
||||
catch (Exception ex) when (ex.Message.Contains("获取不到") && ex.Message.Contains("价格"))
|
||||
{
|
||||
Assert.Inconclusive($"FR007 fixing 数据不可用({valueDate:yyyy-MM-dd} 非交易日或数据未发布):{ex.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
PrintInterestComparison(interests100, interests40, cp100_B, cp40_B);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Reflection;
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
@@ -7,26 +6,10 @@ namespace YLErp.Modules.SwapModule
|
||||
public class InitUnwindTradingFeeTest
|
||||
{
|
||||
private static decimal InvokeCalcInitTradingFee(swap_position position, UnwindData unwindData)
|
||||
{
|
||||
var method = typeof(SwapDealService).GetMethod(
|
||||
"CalcInitTradingFee",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
|
||||
Assert.IsNotNull(method, "未找到 CalcInitTradingFee 私有静态方法");
|
||||
|
||||
return (decimal)method.Invoke(null, new object[] { position, unwindData });
|
||||
}
|
||||
=> TradingFeeCalc.CalcInitTradingFee(position, unwindData);
|
||||
|
||||
private static decimal InvokeCalcInitTradingFeePending(swap_position oriPosition, swap_position position, UnwindData unwindData)
|
||||
{
|
||||
var method = typeof(SwapDealService).GetMethod(
|
||||
"CalcInitTradingFeePending",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
|
||||
Assert.IsNotNull(method, "CalcInitTradingFeePending was not found");
|
||||
|
||||
return (decimal)method.Invoke(null, new object[] { oriPosition, position, unwindData });
|
||||
}
|
||||
=> TradingFeeCalc.CalcInitTradingFeePending(oriPosition, position, unwindData);
|
||||
|
||||
[TestMethod]
|
||||
public void 百分比模式_按平仓名义本金计算并四舍五入到两位()
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using YLErp;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
using YLErp.Modules.SwapModule;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
namespace UnitTestProject.Modules.SwapModule.Margin
|
||||
{
|
||||
/// <summary>
|
||||
/// 黄金回放验证(连真实测试库 192.168.2.96):对真实保证金交易逐日 EOD 比对
|
||||
/// GetInterests(settment=true,保证金分支现走 CalcMarginInterest) vs 直接调 CalcMarginInterest,
|
||||
/// 验证 GetInterests→CalcMarginInterest 接线的参数对齐(rate/posiPrincipal/preEod 等)正确。
|
||||
///
|
||||
/// 数据来自 96 库的真实保证金交易,覆盖追加预付金多行、多次部分平仓(InterestPrincipalFix 下台阶)、
|
||||
/// 跨 EOD 续接等单元测试够不到的边界。作为保证金计息迁移后的真实库回归守护。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class MarginInterestGoldenReplayTest
|
||||
{
|
||||
// 96 库里已确认含真实保证金腿的交易
|
||||
private static readonly string[] TradeNumbers =
|
||||
{
|
||||
"GLMS-20260701-0008",
|
||||
"GLMS-20260701-0013",
|
||||
"GLMS-20260701-0006",
|
||||
};
|
||||
|
||||
private sealed class StubSvc : SwapDealService
|
||||
{
|
||||
public StubSvc() : base(new OptUserInfo(0, nameof(MarginInterestGoldenReplayTest), OptUserFrom.UnitTest)) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 逐交易、逐 EOD 日,比对保证金腿新旧计息 InterestAmount/TdInterestAmount。
|
||||
/// 入参对齐口径(与 GetInterests 内部一致):
|
||||
/// posiPrincipal = InterestPrincipalFix;closePrincipal = Fix×closePercent(EOD=1);
|
||||
/// rate = oldEvt.InterestRate(严格取旧管线算出的 rate,消除 GetFixedRate 差异);
|
||||
/// 方向 = FlipDirection(position.InterestDirection)(GetInterests:742 对保证金翻转);
|
||||
/// preEod = 该 PositionId 上一日终 eod_swap_position;annualDays/calcFirst/calcLast 来自 trade_extend。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[TestCategory("DbDiagnose")]
|
||||
public void 保证金腿_真实库_EOD逐日新旧比对()
|
||||
{
|
||||
YLContext db;
|
||||
try { db = DbContextFactory.GetYLDbContext(); }
|
||||
catch (Exception ex) { Assert.Inconclusive($"无法连接测试库(192.168.2.96):{ex.Message}"); return; }
|
||||
|
||||
var svc = new StubSvc();
|
||||
int totalCompared = 0, mismatches = 0, skipped = 0;
|
||||
var diffLog = new StringBuilder();
|
||||
|
||||
foreach (var tradeNumber in TradeNumbers)
|
||||
{
|
||||
var td = db.trade.FirstOrDefault(t => t.TradeNumber == tradeNumber);
|
||||
if (td == null) { Console.WriteLine($"跳过:库无 {tradeNumber}"); skipped++; continue; }
|
||||
|
||||
var extend = db.trade_extend.FirstOrDefault(x => x.TradeId == td.id);
|
||||
int annualDays = extend?.ExtendObj.AnnualDays ?? 365;
|
||||
bool calcFirst = extend?.ExtendObj.InterestCalcMode?.StartsWith("1") ?? true;
|
||||
bool calcLast = extend?.ExtendObj.InterestCalcMode?.EndsWith("1") ?? true;
|
||||
|
||||
var marginPositions = db.swap_position
|
||||
.Where(p => p.SwapTradeId == td.id && !p.Invalid
|
||||
&& (p.InterestMode == (int)InterestModeEnum.初始预付金
|
||||
|| p.InterestMode == (int)InterestModeEnum.追加预付金))
|
||||
.ToList();
|
||||
if (marginPositions.Count == 0) { Console.WriteLine($"跳过:{tradeNumber} 无保证金腿"); skipped++; continue; }
|
||||
|
||||
// 该交易保证金腿的 EOD 日期序列
|
||||
var eodDates = db.eod_swap_position
|
||||
.Where(e => e.SwapTradeId == td.id && (e.InterestMode == 5 || e.InterestMode == 6))
|
||||
.Select(e => e.ValueDate).Distinct().OrderBy(d => d).ToList();
|
||||
|
||||
Console.WriteLine($"===== {tradeNumber} (id={td.id}):{marginPositions.Count} 条保证金腿,{eodDates.Count} 个 EOD 日 =====");
|
||||
|
||||
foreach (var valueDate in eodDates)
|
||||
{
|
||||
// 上一日终 preEod(取 eod_swap 最近 < valueDate 的日期)
|
||||
var preDate = db.eod_swap
|
||||
.Where(e => e.SwapTradeId == td.id && e.ValueDate < valueDate)
|
||||
.OrderByDescending(e => e.ValueDate)
|
||||
.Select(e => (DateTime?)e.ValueDate).FirstOrDefault();
|
||||
var preEods = preDate == null
|
||||
? new List<eod_swap_position>()
|
||||
: db.eod_swap_position
|
||||
.Where(e => e.SwapTradeId == td.id && e.ValueDate == preDate.Value
|
||||
&& (e.InterestMode == 5 || e.InterestMode == 6))
|
||||
.ToList();
|
||||
|
||||
// 旧管线:GetInterests(settment=true)。保证金分支不用 posiNotionalValue/closePosiNotionalValue/grossPrice/orginPv,传 0。
|
||||
List<swap_flow_event> oldList;
|
||||
try
|
||||
{
|
||||
oldList = svc.GetInterests(td, extend, valueDate, valueDate,
|
||||
preEods, marginPositions,
|
||||
0m, 0m, 0m, 0m, 1.0m,
|
||||
(int)SwapEventTypeEnum.自动互换, tdClose: false, needPrice: false,
|
||||
grossPrice: 0m, orginPv: 0m,
|
||||
add: false, settment: true, newCalcLast: false, closeList: null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($" {tradeNumber} @ {valueDate:yyyy-MM-dd} 旧管线异常:{ex.GetType().Name} {ex.Message}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 新方法:逐保证金腿
|
||||
foreach (var pos in marginPositions)
|
||||
{
|
||||
var oldEvt = oldList.FirstOrDefault(i => i.PositionId == pos.id);
|
||||
if (oldEvt == null) continue;
|
||||
|
||||
var preEod = preEods.FirstOrDefault(e => e.PositionId == pos.id) ?? new eod_swap_position { id = 0 };
|
||||
var posClone = pos.Clone();
|
||||
posClone.InterestDirection = MarginCalc.FlipDirection(pos.InterestDirection);
|
||||
decimal rate = oldEvt.InterestRate; // 严格对齐旧管线 rate(含 GetFixedRate + Round(12))
|
||||
|
||||
swap_flow_event newEvt;
|
||||
try
|
||||
{
|
||||
newEvt = svc.CalcMarginInterest(td, valueDate, valueDate, posClone, rate,
|
||||
pos.InterestPrincipalFix, pos.InterestPrincipalFix, 1.0m,
|
||||
annualDays, calcFirst, calcLast, preEod,
|
||||
(int)SwapEventTypeEnum.自动互换, add: false, settment: true, swap: false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
mismatches++;
|
||||
diffLog.AppendLine($"✗ {tradeNumber} PosId={pos.id} @ {valueDate:yyyy-MM-dd} 新方法异常:{ex.GetType().Name} {ex.Message}");
|
||||
continue;
|
||||
}
|
||||
|
||||
totalCompared++;
|
||||
decimal diffI = Math.Abs(newEvt.InterestAmount - oldEvt.InterestAmount);
|
||||
decimal diffTd = Math.Abs(newEvt.TdInterestAmount - oldEvt.TdInterestAmount);
|
||||
const decimal tol = 0.000001m;
|
||||
if (diffI > tol || diffTd > tol)
|
||||
{
|
||||
mismatches++;
|
||||
diffLog.AppendLine($"✗ {tradeNumber} PosId={pos.id} Mode={pos.InterestMode} @ {valueDate:yyyy-MM-dd}: " +
|
||||
$"旧 I={oldEvt.InterestAmount} Td={oldEvt.TdInterestAmount} | " +
|
||||
$"新 I={newEvt.InterestAmount} Td={newEvt.TdInterestAmount} | " +
|
||||
$"diffI={diffI} diffTd={diffTd} | " +
|
||||
$"preEod.id={preEod.id} TdIntPrin={preEod.TdInterestPrincipal} ProfitSum={preEod.InterestProfitSum} | " +
|
||||
$"Fix={pos.InterestPrincipalFix} rate={rate} IntType={pos.InterestType} IsAnnualized={pos.IsAnnualized}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"\n===== 比对汇总:共 {totalCompared} 条,不一致 {mismatches} 条,跳过 {skipped} 个交易 =====");
|
||||
if (diffLog.Length > 0) Console.WriteLine(diffLog.ToString());
|
||||
|
||||
Assert.IsTrue(mismatches == 0,
|
||||
$"保证金新旧管线 EOD 真实库比对有 {mismatches}/{totalCompared} 条不一致——提交2 前必须解决(详见输出)");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Newtonsoft.Json;
|
||||
using YLErp;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
using YLErp.Modules.SwapModule;
|
||||
using YLErp.Modules.SwapModule.Accrual;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
using YLErp.Derivatives.Interest;
|
||||
|
||||
namespace UnitTestProject.Modules.SwapModule.Margin
|
||||
{
|
||||
/// <summary>
|
||||
/// 影子对账:保证金腿方法 CalcMarginInterest(EOD 用昨日终本金、盘中用 accrualBasis 差分)vs
|
||||
/// 旧通用管线 CalcDailySimpleInterestByEod/CalcDailySimpleInterest。
|
||||
///
|
||||
/// 保证金是纯固定利率单利(FloatRateUnderlyingCode 恒空、InterestType 恒单利、SwapIntervalList 单段)。
|
||||
/// 本测试在生产切到 CalcMarginInterest 后作为回归守护,确认其 InterestAmount/TdInterestAmount
|
||||
/// 与旧纯函数(SimpleInterestAccrual)数值一致。覆盖 EOD 续接/首日、盘中全平/部分平仓/互换。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class MarginInterestShadowTest
|
||||
{
|
||||
private const decimal Principal = 2_000_000m; // 保证金本金(InterestPrincipalFix)
|
||||
private const decimal Rate = 0.03m; // 3% 年化固定利率
|
||||
private const int AnnualDays = 365;
|
||||
private static readonly DateTime StartDate = new(2026, 7, 1);
|
||||
private static readonly DateTime ExerciseDate = new(2027, 6, 30);
|
||||
|
||||
private sealed class StubSvc : SwapDealService
|
||||
{
|
||||
public StubSvc() : base(new OptUserInfo(0, nameof(MarginInterestShadowTest), OptUserFrom.UnitTest)) { }
|
||||
}
|
||||
|
||||
private static trade CreateTrade() => new trade
|
||||
{
|
||||
id = 1, TradeNumber = "UT-MARGIN-SHADOW", ClientId = 999998,
|
||||
TradeType = "收益互换", TradeDate = StartDate, StartDate = StartDate,
|
||||
ExerciseDate = ExerciseDate, TradeStatus = "确认成交", ValidState = "Valid",
|
||||
trade_extend = new trade_extend
|
||||
{
|
||||
TradeId = 1,
|
||||
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||||
{ AnnualDays = AnnualDays, InterestCalcMode = "10", SettlementRules = 0 })
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>保证金腿(初始预付金 mode 5):固定利率、单利、年化、无浮动标的。</summary>
|
||||
private static swap_position CreateMarginPosition() => new swap_position
|
||||
{
|
||||
id = 2001, SwapTradeId = 1, PosiDirection = 0,
|
||||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||||
InterestMode = (int)InterestModeEnum.初始预付金,
|
||||
InterestRateDefault = Rate, InterestPrincipalFix = Principal,
|
||||
PosiStartDate = StartDate, PosiMatuirityDate = ExerciseDate,
|
||||
IsInitial = true, Invalid = false,
|
||||
InterestType = (int)InterestTypeEnum.单利,
|
||||
IsAnnualized = true, interest_rest_days = 1, interest_rule = 0,
|
||||
FloatRateUnderlyingCode = null, InterestSwapInterval = "[]"
|
||||
};
|
||||
|
||||
/// <summary>构造昨日终 eod_swap_position(已含累计利息 InterestProfitSum 与昨日终本金)。</summary>
|
||||
private static eod_swap_position CreatePreEod(DateTime valueDate, decimal profitSum) => new eod_swap_position
|
||||
{
|
||||
id = 1, SwapTradeId = 1, PositionId = 2001,
|
||||
ValueDate = valueDate,
|
||||
TdInterestPrincipal = Principal, InterestPrincipalFix = Principal,
|
||||
InterestProfitSum = profitSum, PosiNotionalValue = Principal, FloatRate = 0m
|
||||
};
|
||||
|
||||
// ──────────────────────────── EOD 路径 ────────────────────────────
|
||||
|
||||
/// <summary>EOD 续接单日:有历史归档,notional=昨日终本金。</summary>
|
||||
[TestMethod]
|
||||
public void 影子_EOD续接单日_新旧一致()
|
||||
{
|
||||
var td = CreateTrade();
|
||||
var position = CreateMarginPosition();
|
||||
var valueDate = StartDate.AddDays(5);
|
||||
const decimal profitSum = 820m;
|
||||
|
||||
// 旧方法
|
||||
decimal oldI = 0, oldTd = 0;
|
||||
var svc = new StubSvc();
|
||||
svc.CalcDailySimpleInterestByEod(CreatePreEod(StartDate.AddDays(4), profitSum),
|
||||
valueDate, td.StartDate.Value, position, Principal, Principal,
|
||||
new swap_flow_event { InterestRate = Rate }, AnnualDays, 0m, 1m, ref oldI, ref oldTd);
|
||||
|
||||
// 新方法(独立 preEod,相同初始值)
|
||||
var newEvt = svc.CalcMarginInterest(td, valueDate, valueDate, position, Rate, Principal, Principal, 1m,
|
||||
AnnualDays, calcFirst: true, calcLast: true,
|
||||
CreatePreEod(StartDate.AddDays(4), profitSum), 0, add: false, settment: true, swap: false);
|
||||
|
||||
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
|
||||
Console.WriteLine($"新: I={newEvt.InterestAmount} Td={newEvt.TdInterestAmount} ClosePnL={newEvt.InterestClosePnL}");
|
||||
Assert.AreEqual(oldI, newEvt.InterestAmount, "InterestAmount 一致");
|
||||
Assert.AreEqual(oldTd, newEvt.TdInterestAmount, "TdInterestAmount 一致");
|
||||
}
|
||||
|
||||
/// <summary>EOD 首日(preEod.id==0):首日初始化 notional=posiPrincipal。</summary>
|
||||
[TestMethod]
|
||||
public void 影子_EOD首日_新旧一致()
|
||||
{
|
||||
var td = CreateTrade();
|
||||
var position = CreateMarginPosition();
|
||||
var valueDate = StartDate;
|
||||
|
||||
decimal oldI = 0, oldTd = 0;
|
||||
var svc = new StubSvc();
|
||||
svc.CalcDailySimpleInterestByEod(new eod_swap_position { id = 0 },
|
||||
valueDate, td.StartDate.Value, position, Principal, Principal,
|
||||
new swap_flow_event { InterestRate = Rate }, AnnualDays, 0m, 1m, ref oldI, ref oldTd);
|
||||
|
||||
var newEvt = svc.CalcMarginInterest(td, valueDate, valueDate, position, Rate, Principal, Principal, 1m,
|
||||
AnnualDays, calcFirst: true, calcLast: true,
|
||||
new eod_swap_position { id = 0 }, 0, add: false, settment: true, swap: false);
|
||||
|
||||
Assert.AreEqual(oldI, newEvt.InterestAmount, "InterestAmount 一致");
|
||||
Assert.AreEqual(oldTd, newEvt.TdInterestAmount, "TdInterestAmount 一致");
|
||||
}
|
||||
|
||||
// ──────────────────────────── 盘中路径 ────────────────────────────
|
||||
|
||||
/// <summary>盘中全平(closePercent=1):新方法 notional=posiPrincipal,旧方法差分 accrualBasis 恒=posiPrincipal。</summary>
|
||||
[TestMethod]
|
||||
public void 影子_盘中全平_新旧一致()
|
||||
{
|
||||
var td = CreateTrade();
|
||||
var position = CreateMarginPosition();
|
||||
var valueDate = StartDate.AddDays(5);
|
||||
const decimal profitSum = 820m;
|
||||
|
||||
// 旧方法:orginPv 经 PreviousBalance 对齐到昨日终保证金余额 → accrualBasis 恒= Principal
|
||||
decimal oldI = 0, oldTd = 0;
|
||||
var svc = new StubSvc();
|
||||
var preEodOld = CreatePreEod(StartDate.AddDays(4), profitSum);
|
||||
decimal orginPv = MarginCalc.PreviousBalance(preEodOld, Principal);
|
||||
svc.CalcDailySimpleInterest(preEodOld, valueDate, position, Principal,
|
||||
new swap_flow_event { InterestRate = Rate }, AnnualDays, 0m, 1m, orginPv,
|
||||
calcFirst: true, calcLast: false, ref oldI, ref oldTd);
|
||||
|
||||
// 新方法:notional = posiPrincipal(无差分、无 orginPv)
|
||||
var newEvt = svc.CalcMarginInterest(td, valueDate, valueDate, position, Rate, Principal, Principal, 1m,
|
||||
AnnualDays, calcFirst: true, calcLast: false,
|
||||
CreatePreEod(StartDate.AddDays(4), profitSum), 0, add: false, settment: false, swap: false);
|
||||
|
||||
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
|
||||
Console.WriteLine($"新: I={newEvt.InterestAmount} Td={newEvt.TdInterestAmount}");
|
||||
Assert.AreEqual(oldI, newEvt.InterestAmount, "InterestAmount 一致");
|
||||
Assert.AreEqual(oldTd, newEvt.TdInterestAmount, "TdInterestAmount 一致");
|
||||
}
|
||||
|
||||
/// <summary>盘中部分平仓(closePercent=0.5):缩放累计,新旧线性等价。</summary>
|
||||
[TestMethod]
|
||||
public void 影子_盘中部分平仓_新旧一致()
|
||||
{
|
||||
var td = CreateTrade();
|
||||
var position = CreateMarginPosition();
|
||||
var valueDate = StartDate.AddDays(5);
|
||||
const decimal profitSum = 820m;
|
||||
const decimal closePct = 0.5m;
|
||||
|
||||
decimal oldI = 0, oldTd = 0;
|
||||
var svc = new StubSvc();
|
||||
var preEodOld = CreatePreEod(StartDate.AddDays(4), profitSum);
|
||||
decimal orginPv = MarginCalc.PreviousBalance(preEodOld, Principal);
|
||||
svc.CalcDailySimpleInterest(preEodOld, valueDate, position, Principal,
|
||||
new swap_flow_event { InterestRate = Rate }, AnnualDays, 0m, closePct, orginPv,
|
||||
calcFirst: true, calcLast: false, ref oldI, ref oldTd);
|
||||
|
||||
var newEvt = svc.CalcMarginInterest(td, valueDate, valueDate, position, Rate,
|
||||
Principal * closePct, Principal, closePct,
|
||||
AnnualDays, calcFirst: true, calcLast: false,
|
||||
CreatePreEod(StartDate.AddDays(4), profitSum), 0, add: false, settment: false, swap: false);
|
||||
|
||||
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
|
||||
Console.WriteLine($"新: I={newEvt.InterestAmount} Td={newEvt.TdInterestAmount}");
|
||||
Assert.AreEqual(oldI, newEvt.InterestAmount, "InterestAmount 一致");
|
||||
Assert.AreEqual(oldTd, newEvt.TdInterestAmount, "TdInterestAmount 一致");
|
||||
}
|
||||
|
||||
/// <summary>互换事件(swap=true,盘中):利息应归零。</summary>
|
||||
[TestMethod]
|
||||
public void 影子_盘中互换_利息归零()
|
||||
{
|
||||
var td = CreateTrade();
|
||||
var position = CreateMarginPosition();
|
||||
var valueDate = StartDate.AddDays(5);
|
||||
|
||||
var svc = new StubSvc();
|
||||
var newEvt = svc.CalcMarginInterest(td, valueDate, valueDate, position, Rate, Principal, Principal, 1m,
|
||||
AnnualDays, calcFirst: true, calcLast: false,
|
||||
CreatePreEod(StartDate.AddDays(4), 820m), 0, add: false, settment: false, swap: true);
|
||||
|
||||
Assert.AreEqual(0m, newEvt.InterestAmount, "互换利息归零");
|
||||
Assert.AreEqual(0m, newEvt.TdInterestAmount, "互换 TdInterestAmount 归零");
|
||||
Assert.AreEqual(0m, newEvt.InterestClosePnL, "互换 InterestClosePnL 归零");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,12 @@ using YLErp.DBModels.Enums;
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 诊断测试:验证「浮动腿 fpositions 仍用 origPositions(orig 100M)」对本 deal 的
|
||||
/// 预付金/返回预付金结果是否产生影响。结论预期:本 deal 利息腿只有 mode 9(标的期初全价)
|
||||
/// 与 mode 5(初始预付金),CalcNotionalByMode 中 posiLong/posiShort 仅在「多头/空头存续名义本金」
|
||||
/// 分支被消费(L709-716),故本 deal 即便 fpositions 用 orig 100M,预付金腿结果也不受其影响。
|
||||
/// 本测试仅做诊断/验证,不改动任何生产代码;用反射调用 private CalcNotionalByMode 以直接证明
|
||||
/// “mode 9 / mode 5 的 closePrincipal 不依赖 posiLong/posiShort”。
|
||||
/// 诊断测试骨架:针对 GLMS 双轨持仓(orig/real)构造预付金腿(mode 5)与标的期初全价腿(mode 9),
|
||||
/// 用于验证“浮动腿 fpositions 用 origPositions 对预付金/标的端计息基数的影响”。
|
||||
/// 计息基数现由 FundingLegStrategyFactory + 各 IFundingLegStrategy 策略类计算
|
||||
/// (原 private CalcNotionalByMode 已重构移除);多空存续腿的 posiLong/posiShort 因界面禁用
|
||||
/// 已从策略接口删除,故预付金/标的端计息基数不依赖多空头寸。
|
||||
/// 注:当前仅含数据构造,反射诊断方法尚未实现(无 [TestMethod])。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SwapUnwindFloatingLegDiagnosticTdd
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// 【同日多次部分平仓 · unwind 基数滚动表征测试】
|
||||
/// ============================================================================
|
||||
/// 背景:unwind 计息基数公式 basis = priorNotional + notional - baseNotional
|
||||
/// (FundingLegAccrual / CalcDailyCompoundInterestByEod 同源),其中
|
||||
/// (CompoundInterestAccrual / CalcDailyCompoundInterestByEod 同源),其中
|
||||
/// - priorNotional = 上一日终归档 eod_swap_position.TdInterestPrincipal
|
||||
/// - baseNotional = orginPv = ResolveUnwindPreviousNotional(lastEod)(上一日终浮动端名义本金)
|
||||
/// - notional = 当前持仓名义本金(posiNotionalValue,来自实时持仓)
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// 捕获真实收盘产生的 swap_flow_event(生产写 DbContext.swap_flow_event)。
|
||||
/// 与 PersistEodSwapPosition 同理,这里只收集不写库,供 GetConsumedInterest 真实计算。
|
||||
/// </summary>
|
||||
protected void PersistFlowEvent(swap_flow_event flowEvent)
|
||||
protected override void PersistFlowEvent(swap_flow_event flowEvent)
|
||||
{
|
||||
if (flowEvent.id == 0) flowEvent.id = _nextId++;
|
||||
FlowEvents.Add(flowEvent);
|
||||
@@ -83,5 +83,16 @@ namespace YLErp.Modules.SwapModule
|
||||
ClientCashCalls.Add((amount, action));
|
||||
return _nextId++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AddClientCashInCashOut 生产实现会查 DataCacheProvider.GetClientDataSource().GetData(ClientId),
|
||||
/// 纯内存测试无客户缓存会抛"客户信息未找到"。与 AddClientCash 同构 no-op,
|
||||
/// 仅捕获调用记录,供断言使用。
|
||||
/// </summary>
|
||||
public override int AddClientCashInCashOut(OtcTradeBase td, double amount, string action, DateTime valueDate)
|
||||
{
|
||||
ClientCashCalls.Add((amount, action));
|
||||
return _nextId++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,10 +98,21 @@ namespace YLErp.Modules.EodModule
|
||||
/// <returns></returns>
|
||||
public List<BondPayment> GetBondPayments(string underlyingCode, DateTime startDate, DateTime endDate)
|
||||
{
|
||||
var result = DbContext.bondPayment.Where(x => x.underlyingCode == underlyingCode && x.payment_date > startDate && x.payment_date <= endDate).AsNoTracking().ToList();
|
||||
// GLMS-20260105-0006:票息归属按债权登记日(reg_date)判定,而非支付日(pay_date_PL/pay_date_act)。
|
||||
// 登记日当天 EOD 即应计提;原按支付日口径会让"登记日≠支付日"的债券漏计(二者恰差一工作日时缺陷被掩盖)。
|
||||
var result = QueryBondPayments(underlyingCode)
|
||||
.Where(x => x.reg_date > startDate && x.reg_date <= endDate)
|
||||
.AsNoTracking().ToList();
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可测性 seam:返回某债券的全部付息记录(未做日期过滤)。测试可 override 注入内存数据,
|
||||
/// 以验证日期口径(GLMS-20260105-0006:应按债权登记日 reg_date 而非支付日 pay_date_PL/pay_date_act 判定)。
|
||||
/// </summary>
|
||||
protected virtual IQueryable<BondPayment> QueryBondPayments(string underlyingCode)
|
||||
=> DbContext.bondPayment.Where(x => x.underlyingCode == underlyingCode);
|
||||
|
||||
|
||||
public List<BondPayment> GetTargetDatePayments(string underlyingCode, DateTime targetDate)
|
||||
{
|
||||
|
||||
@@ -29,4 +29,8 @@ public sealed class AccrualPolicy
|
||||
|
||||
public AccrualPolicy(AccrualBoundary convention, bool isCompound, int resetPeriodDays, int annualDays, bool isAnnualized = false)
|
||||
=> (Convention, IsCompound, ResetPeriodDays, AnnualDays, IsAnnualized) = (convention, isCompound, resetPeriodDays, annualDays, isAnnualized);
|
||||
|
||||
/// <summary>从 swap_position 构造 EOD 计息政策(算头算尾,重置周期取 interest_rest_days)。</summary>
|
||||
public static AccrualPolicy BuildEod(DBModels.swap_position position, int annualDays, bool isCompound)
|
||||
=> new(AccrualBoundary.Both, isCompound, position.interest_rest_days ?? 1, annualDays, position.IsAnnualized);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
using YLErp.Core.Interest;
|
||||
using YLErp.Derivatives.Interest;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
/// <summary>
|
||||
/// 复利计息纯函数——EOD 单日 + intraday 多日。
|
||||
/// 复利特征:每个重置日把累计利息并入本金(basis = notional + accrued)。
|
||||
/// </summary>
|
||||
public static class CompoundInterestAccrual
|
||||
{
|
||||
private const int Precision = SwapInterest.FundingLegPrecision;
|
||||
|
||||
/// <summary>复利日终计息基数(单一真相源,纯函数与调用方共用):
|
||||
/// 重置日 = notional + 累计利息×剩余比例(利息并入本金);非重置日 = priorNotional(昨日滚动基数)。
|
||||
/// remainingFraction 对齐 legacy 钳制到 [0,1]。</summary>
|
||||
public static decimal EodBasis(
|
||||
bool isResetDay, decimal notional, decimal priorAccrued, decimal remainingFraction, decimal priorNotional)
|
||||
=> isResetDay
|
||||
? notional + priorAccrued * Math.Max(0m, Math.Min(1m, remainingFraction))
|
||||
: priorNotional;
|
||||
|
||||
/// <summary>
|
||||
/// 复利日终计息(替换 CalcDailyCompoundInterestByEod 的纯数学部分)。
|
||||
/// 重置日:basis = notional + priorAccrued × remainingFraction(利息并入本金)。
|
||||
/// 非重置日:basis = priorNotional(昨日终滚动计息基数)。
|
||||
/// </summary>
|
||||
public static InterestResult AccrueEod(
|
||||
decimal priorAccrued,
|
||||
decimal priorNotional,
|
||||
decimal notional,
|
||||
decimal unwindFraction,
|
||||
FundingLegRate rate,
|
||||
AccrualPolicy policy,
|
||||
bool isResetDay,
|
||||
decimal remainingFraction,
|
||||
DateTime eodDate,
|
||||
AccrualTrace? trace = null)
|
||||
{
|
||||
var basis = EodBasis(isResetDay, notional, priorAccrued, remainingFraction, priorNotional);
|
||||
var displayBasis = basis * unwindFraction;
|
||||
|
||||
var allInRate = rate.AllInRate;
|
||||
trace?.EodContext(eodDate, isResetDay, unwindFraction, priorAccrued, priorNotional, notional, remainingFraction);
|
||||
var dayInterest = displayBasis * allInRate;
|
||||
var tdInterest = basis * allInRate;
|
||||
if (policy.IsAnnualized)
|
||||
{
|
||||
dayInterest /= policy.AnnualDays;
|
||||
tdInterest /= policy.AnnualDays;
|
||||
}
|
||||
|
||||
var totalAccrued = priorAccrued * unwindFraction + dayInterest;
|
||||
var result = new InterestResult(
|
||||
SwapInterest.Round(totalAccrued, Precision),
|
||||
SwapInterest.Round(tdInterest, Precision));
|
||||
|
||||
trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued);
|
||||
trace?.MarkEnd(result.Accrued, result.AccruedToday);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 复利多日计息(替换 CalcDailyCompoundInterest 的纯数学部分)。
|
||||
/// 从 startDate 到 endDate 全程重放,每个重置日把累计利息并入本金。
|
||||
/// </summary>
|
||||
public static InterestResult AccruePeriod(
|
||||
decimal notional,
|
||||
IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
AccrualBoundary boundary,
|
||||
int annualDays,
|
||||
bool isAnnualized,
|
||||
decimal resetCarryInterest,
|
||||
decimal realizedInterest,
|
||||
decimal unwindFraction,
|
||||
out decimal finalBasis,
|
||||
AccrualTrace? trace = null)
|
||||
{
|
||||
decimal accrualBasis = notional;
|
||||
decimal accrued = 0m;
|
||||
|
||||
trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized);
|
||||
|
||||
for (int si = 0; si < segmentRates.Count; si++)
|
||||
{
|
||||
var isLastSegment = si == segmentRates.Count - 1;
|
||||
var segEnd = isLastSegment
|
||||
? endDate
|
||||
: segmentRates[si + 1].StartDate;
|
||||
|
||||
// 重置日并本金
|
||||
accrualBasis = si == 0 ? notional : notional + accrued;
|
||||
|
||||
// 末日恰好是重置日且 carry 非零:用存量替代(旧代码 i%interestPeriod==0 && accrueDate==endDate)。
|
||||
var usedCarry = false;
|
||||
if (isLastSegment && si > 0 && resetCarryInterest != 0m
|
||||
&& segmentRates[si].StartDate == endDate)
|
||||
{
|
||||
accrualBasis = notional + resetCarryInterest;
|
||||
usedCarry = true;
|
||||
}
|
||||
|
||||
if (si > 0)
|
||||
trace?.Rollover(segmentRates[si].StartDate, usedCarry ? resetCarryInterest : accrued, accrualBasis);
|
||||
|
||||
var segIncludeStart = (si == 0) ? boundary.IncludeStart : true;
|
||||
var segIncludeEnd = isLastSegment ? boundary.IncludeEnd : false;
|
||||
var days = SwapInterest.AccrualDays(segmentRates[si].StartDate, segEnd,
|
||||
AccrualBoundary.Of(segIncludeStart, segIncludeEnd));
|
||||
if (days <= 0) continue;
|
||||
|
||||
var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate;
|
||||
var segInterest = accrualBasis * dailyRate * days;
|
||||
accrued += segInterest;
|
||||
trace?.Segment(si, segmentRates[si].StartDate, segEnd, days, segmentRates[si].Rate, accrualBasis, segInterest, accrued);
|
||||
}
|
||||
|
||||
finalBasis = accrualBasis;
|
||||
|
||||
if (realizedInterest != 0m)
|
||||
trace?.Unwind(endDate, unwindFraction, realizedInterest * unwindFraction, accrued - realizedInterest * unwindFraction);
|
||||
accrued -= realizedInterest * unwindFraction;
|
||||
|
||||
var result = new InterestResult(
|
||||
SwapInterest.Round(accrued, Precision),
|
||||
SwapInterest.Round(accrued, Precision));
|
||||
trace?.MarkEnd(result.Accrued, result.AccruedToday);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
using YLErp.Core.Interest;
|
||||
using YLErp.Derivatives.Interest;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
/// <summary>
|
||||
/// 融资腿计息编排层——纯数学部分(替换 SwapDealService 内 CalcDaily* 家族的纯计算)。
|
||||
///
|
||||
/// 命名规范(对齐 QuantLib / Strata):
|
||||
/// - notional → 计息名义本金(不用 principal,swap leg 用 notional 是业界标准)
|
||||
/// - accrued → 累计应计利息
|
||||
/// - unwindFraction → 平仓比例(0~1)
|
||||
/// - realizedInterest → 历史已结利息(legacy: consumedInterest)
|
||||
/// - priorNotional → 昨日终滚动计息基数(legacy: TdInterestPrincipal / dynomicPrincipal)
|
||||
/// </summary>
|
||||
public static class FundingLegAccrual
|
||||
{
|
||||
private const int Precision = SwapInterest.FundingLegPrecision;
|
||||
|
||||
/// <summary>复利日终计息基数(单一真相源,纯函数与调用方共用):
|
||||
/// 重置日 = notional + 累计利息×剩余比例(利息并入本金);非重置日 = priorNotional(昨日滚动基数)。
|
||||
/// remainingFraction 对齐 legacy 钳制到 [0,1]。</summary>
|
||||
public static decimal CompoundEodBasis(
|
||||
bool isResetDay, decimal notional, decimal priorAccrued, decimal remainingFraction, decimal priorNotional)
|
||||
=> isResetDay
|
||||
? notional + priorAccrued * Math.Max(0m, Math.Min(1m, remainingFraction))
|
||||
: priorNotional;
|
||||
|
||||
/// <summary>
|
||||
/// 单利日终计息(替换 CalcDailySimpleInterestByEod 的纯数学部分)。
|
||||
/// EOD 无差分:basis = priorNotional(昨日终滚动计息基数)。
|
||||
/// </summary>
|
||||
public static InterestResult AccrueSimpleEod(
|
||||
decimal priorAccrued,
|
||||
decimal priorNotional,
|
||||
decimal unwindFraction,
|
||||
FundingLegRate rate,
|
||||
AccrualPolicy policy,
|
||||
DateTime eodDate,
|
||||
AccrualTrace? trace = null)
|
||||
{
|
||||
var basis = priorNotional;
|
||||
var displayBasis = basis * unwindFraction;
|
||||
|
||||
var allInRate = rate.AllInRate;
|
||||
var dayInterest = displayBasis * allInRate;
|
||||
var tdInterest = basis * allInRate;
|
||||
if (policy.IsAnnualized)
|
||||
{
|
||||
dayInterest /= policy.AnnualDays;
|
||||
tdInterest /= policy.AnnualDays;
|
||||
}
|
||||
|
||||
var totalAccrued = priorAccrued + dayInterest;
|
||||
var result = new InterestResult(
|
||||
SwapInterest.Round(totalAccrued, Precision),
|
||||
SwapInterest.Round(tdInterest, Precision));
|
||||
|
||||
trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued);
|
||||
trace?.MarkEnd(result.Accrued, result.AccruedToday);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 复利日终计息(替换 CalcDailyCompoundInterestByEod 的纯数学部分)。
|
||||
/// 重置日:basis = notional + priorAccrued × remainingFraction(利息并入本金)。
|
||||
/// 非重置日:basis = priorNotional(昨日终滚动计息基数)。
|
||||
/// </summary>
|
||||
public static InterestResult AccrueCompoundEod(
|
||||
decimal priorAccrued,
|
||||
decimal priorNotional,
|
||||
decimal notional,
|
||||
decimal unwindFraction,
|
||||
FundingLegRate rate,
|
||||
AccrualPolicy policy,
|
||||
bool isResetDay,
|
||||
decimal remainingFraction,
|
||||
DateTime eodDate,
|
||||
AccrualTrace? trace = null)
|
||||
{
|
||||
var basis = CompoundEodBasis(isResetDay, notional, priorAccrued, remainingFraction, priorNotional);
|
||||
var displayBasis = basis * unwindFraction;
|
||||
|
||||
var allInRate = rate.AllInRate;
|
||||
trace?.EodContext(eodDate, isResetDay, unwindFraction, priorAccrued, priorNotional, notional, remainingFraction);
|
||||
var dayInterest = displayBasis * allInRate;
|
||||
var tdInterest = basis * allInRate;
|
||||
if (policy.IsAnnualized)
|
||||
{
|
||||
dayInterest /= policy.AnnualDays;
|
||||
tdInterest /= policy.AnnualDays;
|
||||
}
|
||||
|
||||
var totalAccrued = priorAccrued * unwindFraction + dayInterest;
|
||||
var result = new InterestResult(
|
||||
SwapInterest.Round(totalAccrued, Precision),
|
||||
SwapInterest.Round(tdInterest, Precision));
|
||||
|
||||
trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued);
|
||||
trace?.MarkEnd(result.Accrued, result.AccruedToday);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单利多日计息(替换 CalcDailySimpleInterest 的纯数学部分)。
|
||||
/// 本金全程恒定,按重置日分段取利率。
|
||||
/// Accrued = 缩放累计(InterestAmount),AccruedToday = 未缩放累计(TdInterestAmount)。
|
||||
/// </summary>
|
||||
public static InterestResult AccrueSimplePeriod(
|
||||
decimal priorAccrued,
|
||||
decimal notional,
|
||||
decimal unwindFraction,
|
||||
IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
DateTime priorValueDate,
|
||||
AccrualBoundary boundary,
|
||||
int annualDays,
|
||||
bool isAnnualized,
|
||||
AccrualTrace? trace = null)
|
||||
{
|
||||
var displayBasis = notional * unwindFraction;
|
||||
decimal accrued = priorAccrued; // 缩放累计 → InterestAmount
|
||||
decimal accruedUnscaled = priorAccrued; // 未缩放累计 → TdInterestAmount
|
||||
|
||||
trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized);
|
||||
|
||||
var segStart = startDate;
|
||||
|
||||
for (int si = 0; si < segmentRates.Count; si++)
|
||||
{
|
||||
var segEnd = si < segmentRates.Count - 1
|
||||
? segmentRates[si + 1].StartDate
|
||||
: endDate;
|
||||
|
||||
var effectiveStart = segStart > priorValueDate ? segStart : priorValueDate.AddDays(1);
|
||||
if (effectiveStart > segEnd) { segStart = segEnd; continue; }
|
||||
|
||||
// calcFirst 只跳过 startDate 本身;其余天(含重置日、ValueDate+1)只要 > ValueDate 恒纳入。
|
||||
// 与旧逐日循环一致:if (!calcFirst && accrueDate == startDate) continue 是唯一的首日跳过。
|
||||
// 中间段的 segIncludeStart 被 days<=0 跳过后误置 false,此处按 startDate 判定而非继承标记。
|
||||
var includeStart = effectiveStart == startDate ? boundary.IncludeStart : true;
|
||||
// calcLast 只影响 endDate 本身——只有真正的末段(si==Count-1)才算尾,
|
||||
// 不能用 segEnd==endDate 判断(interestPeriod=1 时中间段 segEnd 也可能==endDate)。
|
||||
var isLastSegment = si == segmentRates.Count - 1;
|
||||
var segBoundary = AccrualBoundary.Of(includeStart, isLastSegment && boundary.IncludeEnd);
|
||||
var days = SwapInterest.AccrualDays(effectiveStart, segEnd, segBoundary);
|
||||
if (days <= 0) { segStart = segEnd; continue; }
|
||||
|
||||
var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate;
|
||||
var segInterest = displayBasis * dailyRate * days;
|
||||
accrued += segInterest;
|
||||
accruedUnscaled += notional * dailyRate * days;
|
||||
trace?.Segment(si, effectiveStart, segEnd, days, segmentRates[si].Rate, displayBasis, segInterest, accrued);
|
||||
|
||||
segStart = segEnd;
|
||||
}
|
||||
|
||||
var result = new InterestResult(
|
||||
SwapInterest.Round(accrued, Precision),
|
||||
SwapInterest.Round(accruedUnscaled, Precision));
|
||||
trace?.MarkEnd(result.Accrued, result.AccruedToday);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 复利多日计息(替换 CalcDailyCompoundInterest 的纯数学部分)。
|
||||
/// 从 startDate 到 endDate 全程重放,每个重置日把累计利息并入本金。
|
||||
/// </summary>
|
||||
public static InterestResult AccrueCompoundPeriod(
|
||||
decimal notional,
|
||||
IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
AccrualBoundary boundary,
|
||||
int annualDays,
|
||||
bool isAnnualized,
|
||||
decimal resetCarryInterest,
|
||||
decimal realizedInterest,
|
||||
decimal unwindFraction,
|
||||
out decimal finalBasis,
|
||||
AccrualTrace? trace = null)
|
||||
{
|
||||
decimal accrualBasis = notional;
|
||||
decimal accrued = 0m;
|
||||
|
||||
trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized);
|
||||
|
||||
for (int si = 0; si < segmentRates.Count; si++)
|
||||
{
|
||||
var isLastSegment = si == segmentRates.Count - 1;
|
||||
var segEnd = isLastSegment
|
||||
? endDate
|
||||
: segmentRates[si + 1].StartDate;
|
||||
|
||||
// 重置日并本金
|
||||
accrualBasis = si == 0 ? notional : notional + accrued;
|
||||
|
||||
// 末日恰好是重置日且 carry 非零:用存量替代(旧代码 i%interestPeriod==0 && accrueDate==endDate)。
|
||||
// 注意:必须同时判断 startDate==endDate——endDate 非重置日时最后一段起点 < endDate,不应触发。
|
||||
var usedCarry = false;
|
||||
if (isLastSegment && si > 0 && resetCarryInterest != 0m
|
||||
&& segmentRates[si].StartDate == endDate)
|
||||
{
|
||||
accrualBasis = notional + resetCarryInterest;
|
||||
usedCarry = true;
|
||||
}
|
||||
|
||||
// 复利每段起点:记录并本金瞬间(非首段 = 利息滚入计息基数)
|
||||
if (si > 0)
|
||||
trace?.Rollover(segmentRates[si].StartDate, usedCarry ? resetCarryInterest : accrued, accrualBasis);
|
||||
|
||||
// 半开区间:重置日归下一段(旧代码逐日循环中重置日先更新本金再算息)
|
||||
var segIncludeStart = (si == 0) ? boundary.IncludeStart : true;
|
||||
var segIncludeEnd = isLastSegment ? boundary.IncludeEnd : false;
|
||||
var days = SwapInterest.AccrualDays(segmentRates[si].StartDate, segEnd,
|
||||
AccrualBoundary.Of(segIncludeStart, segIncludeEnd));
|
||||
if (days <= 0) continue;
|
||||
|
||||
var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate;
|
||||
var segInterest = accrualBasis * dailyRate * days;
|
||||
accrued += segInterest;
|
||||
trace?.Segment(si, segmentRates[si].StartDate, segEnd, days, segmentRates[si].Rate, accrualBasis, segInterest, accrued);
|
||||
}
|
||||
|
||||
finalBasis = accrualBasis;
|
||||
|
||||
// 扣除历史已结利息
|
||||
if (realizedInterest != 0m)
|
||||
trace?.Unwind(endDate, unwindFraction, realizedInterest * unwindFraction, accrued - realizedInterest * unwindFraction);
|
||||
accrued -= realizedInterest * unwindFraction;
|
||||
|
||||
var result = new InterestResult(
|
||||
SwapInterest.Round(accrued, Precision),
|
||||
SwapInterest.Round(accrued, Precision));
|
||||
trace?.MarkEnd(result.Accrued, result.AccruedToday);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
/// <summary>
|
||||
@@ -26,4 +28,10 @@ public readonly struct FundingLegRate
|
||||
/// <summary>构造浮动腿利率(all-in = 加点利差 + 指数定盘)。</summary>
|
||||
public static FundingLegRate Floating(decimal spread, decimal indexFixing)
|
||||
=> new(spread + indexFixing);
|
||||
|
||||
/// <summary>从 swap_position 构造:固定腿→Fixed(spread),浮动腿→Floating(spread+fixing)。</summary>
|
||||
public static FundingLegRate Build(swap_position position, decimal spread, decimal effectiveFloat)
|
||||
=> string.IsNullOrEmpty(position.FloatRateUnderlyingCode)
|
||||
? Fixed(spread)
|
||||
: Floating(spread, effectiveFloat);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
using YLErp.Core.Interest;
|
||||
using YLErp.Derivatives.Interest;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
/// <summary>
|
||||
/// 单利计息纯函数——EOD 单日 + intraday 多日。
|
||||
/// 单利特征:本金全程恒定(无并本金),按重置日分段取利率。
|
||||
/// </summary>
|
||||
public static class SimpleInterestAccrual
|
||||
{
|
||||
private const int Precision = SwapInterest.FundingLegPrecision;
|
||||
|
||||
/// <summary>
|
||||
/// 单利日终计息(替换 CalcDailySimpleInterestByEod 的纯数学部分)。
|
||||
/// EOD 无差分:basis = priorNotional(昨日终滚动计息基数)。
|
||||
/// </summary>
|
||||
public static InterestResult AccrueEod(
|
||||
decimal priorAccrued,
|
||||
decimal priorNotional,
|
||||
decimal unwindFraction,
|
||||
FundingLegRate rate,
|
||||
AccrualPolicy policy,
|
||||
DateTime eodDate,
|
||||
AccrualTrace? trace = null)
|
||||
{
|
||||
var basis = priorNotional;
|
||||
var displayBasis = basis * unwindFraction;
|
||||
|
||||
var allInRate = rate.AllInRate;
|
||||
var dayInterest = displayBasis * allInRate;
|
||||
var tdInterest = basis * allInRate;
|
||||
if (policy.IsAnnualized)
|
||||
{
|
||||
dayInterest /= policy.AnnualDays;
|
||||
tdInterest /= policy.AnnualDays;
|
||||
}
|
||||
|
||||
var totalAccrued = priorAccrued + dayInterest;
|
||||
var result = new InterestResult(
|
||||
SwapInterest.Round(totalAccrued, Precision),
|
||||
SwapInterest.Round(tdInterest, Precision));
|
||||
|
||||
trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued);
|
||||
trace?.MarkEnd(result.Accrued, result.AccruedToday);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单利多日计息(替换 CalcDailySimpleInterest 的纯数学部分)。
|
||||
/// 本金全程恒定,按重置日分段取利率。
|
||||
/// Accrued = 缩放累计(InterestAmount),AccruedToday = 未缩放累计(TdInterestAmount)。
|
||||
/// </summary>
|
||||
public static InterestResult AccruePeriod(
|
||||
decimal priorAccrued,
|
||||
decimal notional,
|
||||
decimal unwindFraction,
|
||||
IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
DateTime priorValueDate,
|
||||
AccrualBoundary boundary,
|
||||
int annualDays,
|
||||
bool isAnnualized,
|
||||
AccrualTrace? trace = null)
|
||||
{
|
||||
var displayBasis = notional * unwindFraction;
|
||||
decimal accrued = priorAccrued; // 缩放累计 → InterestAmount
|
||||
decimal accruedUnscaled = priorAccrued; // 未缩放累计 → TdInterestAmount
|
||||
|
||||
trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized);
|
||||
|
||||
var segStart = startDate;
|
||||
|
||||
for (int si = 0; si < segmentRates.Count; si++)
|
||||
{
|
||||
var segEnd = si < segmentRates.Count - 1
|
||||
? segmentRates[si + 1].StartDate
|
||||
: endDate;
|
||||
|
||||
var effectiveStart = segStart > priorValueDate ? segStart : priorValueDate.AddDays(1);
|
||||
if (effectiveStart > segEnd) { segStart = segEnd; continue; }
|
||||
|
||||
// calcFirst 只跳过 startDate 本身;其余天(含重置日、ValueDate+1)只要 > ValueDate 恒纳入。
|
||||
var includeStart = effectiveStart == startDate ? boundary.IncludeStart : true;
|
||||
var isLastSegment = si == segmentRates.Count - 1;
|
||||
var segBoundary = AccrualBoundary.Of(includeStart, isLastSegment && boundary.IncludeEnd);
|
||||
var days = SwapInterest.AccrualDays(effectiveStart, segEnd, segBoundary);
|
||||
if (days <= 0) { segStart = segEnd; continue; }
|
||||
|
||||
var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate;
|
||||
var segInterest = displayBasis * dailyRate * days;
|
||||
accrued += segInterest;
|
||||
accruedUnscaled += notional * dailyRate * days;
|
||||
trace?.Segment(si, effectiveStart, segEnd, days, segmentRates[si].Rate, displayBasis, segInterest, accrued);
|
||||
|
||||
segStart = segEnd;
|
||||
}
|
||||
|
||||
var result = new InterestResult(
|
||||
SwapInterest.Round(accrued, Precision),
|
||||
SwapInterest.Round(accruedUnscaled, Precision));
|
||||
trace?.MarkEnd(result.Accrued, result.AccruedToday);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.SwapModule;
|
||||
|
||||
/// <summary>
|
||||
/// 平仓比例(ClosePercent) 数学——占期初(A) / 占剩余(B) 两种口径的转换。
|
||||
/// 从 SwapDealService 提取为共享模块,两个 service 均可引用。
|
||||
/// </summary>
|
||||
public static class ClosePercentMath
|
||||
{
|
||||
/// <summary>
|
||||
/// 取上一日终的浮动端名义本金(orginPv 的来源)。
|
||||
/// 优先取浮动腿 PosiNotionalValue 之和,取不到用 eod_swap 多空绝对值之和,都没有用 currentNotional 兜底。
|
||||
/// </summary>
|
||||
public static decimal ResolveUnwindPreviousNotional(
|
||||
eod_swap lastEod,
|
||||
IEnumerable<eod_swap_position> lastEodPositions,
|
||||
decimal currentNotional)
|
||||
{
|
||||
var floatingPositions = lastEodPositions?.Where(x => x.PosiDirection > 0).ToList();
|
||||
decimal previousNotional;
|
||||
if (floatingPositions?.Count > 0)
|
||||
{
|
||||
previousNotional = floatingPositions.Sum(x => x.PosiNotionalValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
previousNotional = lastEod == null
|
||||
? currentNotional
|
||||
: Math.Abs(lastEod.NotionalValueLong) + Math.Abs(lastEod.NotionalValueShort);
|
||||
}
|
||||
|
||||
return previousNotional == 0m && currentNotional != 0m
|
||||
? currentNotional
|
||||
: previousNotional;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A(占期初) → B(占剩余),用于把前端传入的占期初比例换算成后端计算用的占剩余比例。
|
||||
/// </summary>
|
||||
public static decimal ToRemainingClosePercent(decimal originalClosePercent, decimal notionalValue, decimal posiNotionalValue)
|
||||
{
|
||||
if (posiNotionalValue <= 0) return originalClosePercent;
|
||||
var remaining = originalClosePercent * notionalValue / posiNotionalValue;
|
||||
return remaining > 1 ? 1 : remaining;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// B(占剩余) → A(占期初),用于落库 / 事件列表展示还原。见 ToRemainingClosePercent。
|
||||
/// </summary>
|
||||
public static decimal ToOriginalClosePercent(decimal remainingClosePercent, decimal notionalValue, decimal posiNotionalValue)
|
||||
{
|
||||
if (notionalValue <= 0) return remainingClosePercent;
|
||||
return remainingClosePercent * posiNotionalValue / notionalValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算 InitUnwind 默认占期初(A)平仓比例 = PosiNotionalValue / NotionalValue。
|
||||
/// 未平仓时 =1(平100%);部分平仓后自动变为剩余比例。
|
||||
/// </summary>
|
||||
public static decimal CalcDefaultInitClosePercent(decimal notionalValue, decimal posiNotionalValue)
|
||||
{
|
||||
return notionalValue > 0 ? posiNotionalValue / notionalValue : 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using YLErp;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
using YLErp.Modules.SwapModule.ReturnLegs;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 互换日终盈亏/精度计算纯函数集合。
|
||||
/// 自 SwapEodPositionService 抽出,支持无库单测;同类内部调用无需前缀。
|
||||
/// </summary>
|
||||
public static class EodPnlCalculator
|
||||
{
|
||||
// 日终利息待实现需跨日累计,按表设计保留 12 位;已实现结算仍按金额两位处理。
|
||||
private const int EodInterestStoragePrecision = 12;
|
||||
|
||||
internal static decimal RoundMoney(decimal value)
|
||||
{
|
||||
return Math.Round(value, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
internal static decimal RoundEodInterest(decimal value)
|
||||
{
|
||||
return Math.Round(value, EodInterestStoragePrecision, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仅在写入 eod_swap_position 前统一快照精度。
|
||||
/// 浮动腿收益最终以金额两位展示和存储;利息腿的待实现、计息基数及利率保留 12 位,
|
||||
/// 使部分结算后的尾差可继续参与后续计息。
|
||||
/// </summary>
|
||||
internal static void NormalizeEodPositionForStorage(eod_swap_position position)
|
||||
{
|
||||
if (string.IsNullOrEmpty(position.UnderlyingCode))
|
||||
{
|
||||
// 利息腿没有标的代码:待实现字段保留高精度,已实现结算字段收敛到金额两位。
|
||||
position.InterestPrincipalFix = RoundEodInterest(position.InterestPrincipalFix);
|
||||
position.InterestRateDefault = RoundEodInterest(position.InterestRateDefault);
|
||||
position.InterestFeePending = RoundEodInterest(position.InterestFeePending);
|
||||
position.TdInterestPrincipal = RoundEodInterest(position.TdInterestPrincipal);
|
||||
position.TdInterestRate = RoundEodInterest(position.TdInterestRate);
|
||||
position.TdInterestIncome = RoundEodInterest(position.TdInterestIncome);
|
||||
position.TdInterestFee = RoundEodInterest(position.TdInterestFee);
|
||||
position.InterestIncomeSum = RoundEodInterest(position.InterestIncomeSum);
|
||||
position.InterestFeeSum = RoundEodInterest(position.InterestFeeSum);
|
||||
position.InterestProfitSum = RoundEodInterest(position.InterestProfitSum);
|
||||
position.FloatRate = RoundEodInterest(position.FloatRate);
|
||||
position.SwapPositionValue = RoundEodInterest(position.SwapPositionValue);
|
||||
position.TdCloseInterest = RoundMoney(position.TdCloseInterest);
|
||||
position.TdCloseInterestFee = RoundMoney(position.TdCloseInterestFee);
|
||||
position.RealizedInterest = RoundMoney(position.RealizedInterest);
|
||||
position.RealizedInterestFee = RoundMoney(position.RealizedInterestFee);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 浮动腿有标的代码:其损益作为金额结果落库,统一按两位四舍五入。
|
||||
position.TdPosiDividend = RoundMoney(position.TdPosiDividend);
|
||||
position.PosiMtmPnL = RoundMoney(position.PosiMtmPnL);
|
||||
position.PosiDividendSum = RoundMoney(position.PosiDividendSum);
|
||||
position.PosiFeePending = RoundMoney(position.PosiFeePending);
|
||||
position.PosiProfitSum = RoundMoney(position.PosiProfitSum);
|
||||
position.TdCloseMtmPnl = RoundMoney(position.TdCloseMtmPnl);
|
||||
position.TdCloseDividend = RoundMoney(position.TdCloseDividend);
|
||||
position.TdCloseFee = RoundMoney(position.TdCloseFee);
|
||||
position.RealizedMtmPnL = RoundMoney(position.RealizedMtmPnL);
|
||||
position.RealizedDividend = RoundMoney(position.RealizedDividend);
|
||||
position.RealizedFee = RoundMoney(position.RealizedFee);
|
||||
position.SwapPositionValue = RoundMoney(position.SwapPositionValue);
|
||||
}
|
||||
position.RealizedPnl = RoundMoney(position.RealizedPnl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 浮动腿累计已实现盈亏由盯市、分红和费用三个已实现组成项汇总。
|
||||
/// 各组成项已经按本方视角落库,此处不再额外转换方向。
|
||||
/// </summary>
|
||||
internal static void SetFloatingRealizedPnl(eod_swap_position position)
|
||||
{
|
||||
position.RealizedPnl = position.RealizedMtmPnL
|
||||
+ position.RealizedDividend
|
||||
+ position.RealizedFee;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 汇总单条日终腿的我方已实现收益。
|
||||
/// 浮动腿及普通利息腿维持数据库记录的方向;初始/追加预付金腿的利息
|
||||
/// 则与保证金本金方向相反。这样“收取对手方保证金”产生的利息会作为
|
||||
/// 我方支付给对手方的成本计入,而不会错误增加框架合约已实现收益。
|
||||
/// 抽为静态纯函数以支持无库单测(marginTypes 等价于 ConsTrade.InterestMarginModels)。
|
||||
/// </summary>
|
||||
public static decimal CalculateSwapRealizedPnl(eod_swap_position position)
|
||||
{
|
||||
var interestRatio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
|
||||
|
||||
return position.RealizedMtmPnL
|
||||
+ position.RealizedDividend
|
||||
+ position.RealizedFee
|
||||
+ position.RealizedInterest * interestRatio
|
||||
+ position.RealizedInterestFee;
|
||||
}
|
||||
|
||||
/// <summary>填充框架合约的持仓腿汇总字段(多空名义本金/市值/浮动盈亏/dv01/平仓量)。
|
||||
/// SaveEodSwap 与 UpdateEodSwap 共用,消除 ~10 行重复。</summary>
|
||||
internal static void FillPositionLegSummary(eod_swap eod_Swap, List<eod_swap_position> positions)
|
||||
{
|
||||
eod_Swap.NotionalValueLong = Math.Round(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
eod_Swap.NotionalValueShort = Math.Round(-Math.Abs(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue)), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
eod_Swap.MarketValueLong = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.UnderlyingMarketValue);
|
||||
eod_Swap.MarketValueShort = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.UnderlyingMarketValue);
|
||||
eod_Swap.FloatingPnL = positions.Sum(s => s.PosiProfitSum);
|
||||
eod_Swap.dv01 = positions.Sum(s => s.dv01 ?? 0);
|
||||
eod_Swap.TdCloseQty = positions.Sum(s => s.TdCloseQty);
|
||||
}
|
||||
|
||||
/// <summary>利息腿 PnL 汇总(按方向比例 + 保证金翻转)。原 SaveEodSwap/UpdateEodSwap 各一段 ForEach。</summary>
|
||||
internal static decimal SumInterestPnL(List<eod_swap_position> interestPositions)
|
||||
{
|
||||
decimal interestPnL = 0;
|
||||
foreach (var x in interestPositions)
|
||||
interestPnL += x.InterestProfitSum * DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode);
|
||||
return interestPnL;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 风险报表符号归一化:把历史两种符号口径的 TdCloseInterest/RealizedInterest
|
||||
/// 统一按"绝对金额 × 业务方向"重写。普通利息腿收取为正、支付为负;
|
||||
/// 预付金腿利息方向与保证金本金方向相反。随后重算 RealizedPnl。
|
||||
/// 抽为 public static 纯函数以支持无库单测(见 SwapReportInterestSignNormalizeTest)。
|
||||
/// 仅当 InterestDirection > 0 时执行(与原内联逻辑等价)。
|
||||
/// </summary>
|
||||
public static void NormalizeInterestSignForReport(eod_swap_position position)
|
||||
{
|
||||
if (position.InterestDirection <= 0) return;
|
||||
|
||||
if (position.InterestMode == (int)InterestModeEnum.标的期初全价)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var interestRatio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
|
||||
position.TdCloseInterest = Math.Abs(position.TdCloseInterest) * interestRatio;
|
||||
position.RealizedInterest = Math.Abs(position.RealizedInterest) * interestRatio;
|
||||
// 兼容修复前已落库的利息腿:当时只累计了明细字段,未同步写入 RealizedPnl。
|
||||
position.RealizedPnl = position.RealizedInterest + position.RealizedInterestFee;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算预付金利率。多条初始/追加预付金腿按本金绝对值加权,
|
||||
/// 不按收付方向轧差,避免相反方向本金抵消后放大利率。
|
||||
/// </summary>
|
||||
internal static decimal CalculateWeightedMarginRate(IEnumerable<swap_position> margins)
|
||||
{
|
||||
var marginList = margins.ToList();
|
||||
var totalWeight = marginList.Sum(x => Math.Abs(x.InterestPrincipalFix));
|
||||
return totalWeight == 0
|
||||
? 0
|
||||
: marginList.Sum(x => x.InterestRateDefault * Math.Abs(x.InterestPrincipalFix)) / totalWeight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算预付金利息金额。InterestIncomeSum 已是各腿利息金额,
|
||||
/// 按收取为正、支付为负直接轧差求和,不做本金加权。
|
||||
/// 抽为 public static 纯函数以支持无库单测(见 SwapWeightedMarginInterestTest)。
|
||||
/// </summary>
|
||||
public static decimal CalculateWeightedMarginInterest(IEnumerable<eod_swap_position> margins)
|
||||
{
|
||||
return margins.Sum(x =>
|
||||
x.InterestIncomeSum * DirectionRatio.ReceivePay(x.InterestDirection));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 固定利息腿的累计已实现盈亏 = 累计已实现利息 + 累计已实现利息费用。
|
||||
/// 4 处 SaveAutoEodInterestPosition/SaveEodInterestPosition 路径口径一致,
|
||||
/// 抽为 public static 纯函数以支持无库单测(见 SwapFixedLegRealizedPnlTest),
|
||||
/// 并消除复制粘贴带来的笔误风险(如 L1296 历史双分号)。
|
||||
/// </summary>
|
||||
public static void SetFixedLegRealizedPnl(eod_swap_position position)
|
||||
{
|
||||
position.RealizedPnl = position.RealizedInterest + position.RealizedInterestFee;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,6 @@ public sealed class ContractNotionalLeg : IFundingLegStrategy
|
||||
{
|
||||
public InterestModeEnum Mode => InterestModeEnum.合约名义本金规模;
|
||||
|
||||
public NotionalResult CalcNotional(decimal fix, decimal posiNotional, decimal posiLong, decimal posiShort, decimal closePercent)
|
||||
public NotionalResult CalcNotional(decimal fix, decimal posiNotional, decimal closePercent)
|
||||
=> new(posiNotional * closePercent, posiNotional, closePercent);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@ public sealed class FixedAmountLeg : IFundingLegStrategy
|
||||
{
|
||||
public InterestModeEnum Mode => InterestModeEnum.固定值;
|
||||
|
||||
public NotionalResult CalcNotional(decimal fix, decimal posiNotional, decimal posiLong, decimal posiShort, decimal closePercent)
|
||||
public NotionalResult CalcNotional(decimal fix, decimal posiNotional, decimal closePercent)
|
||||
=> new(fix, fix, 1m);
|
||||
}
|
||||
|
||||
@@ -22,10 +22,8 @@ public interface IFundingLegStrategy
|
||||
/// </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);
|
||||
NotionalResult CalcNotional(decimal fix, decimal posiNotional, decimal closePercent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -14,6 +14,6 @@ public sealed class UnderlyingEntryFullPriceLeg : IFundingLegStrategy
|
||||
{
|
||||
public InterestModeEnum Mode => InterestModeEnum.标的期初全价;
|
||||
|
||||
public NotionalResult CalcNotional(decimal fix, decimal posiNotional, decimal posiLong, decimal posiShort, decimal closePercent)
|
||||
public NotionalResult CalcNotional(decimal fix, decimal posiNotional, decimal closePercent)
|
||||
=> new(posiNotional * closePercent, posiNotional, closePercent);
|
||||
}
|
||||
|
||||
@@ -4,14 +4,12 @@ using YLErp.Derivatives.Interest;
|
||||
namespace YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
/// <summary>
|
||||
/// 保证金账户。管理保证金余额的变动(追加/释放/返还),并提供计息入口。
|
||||
/// 保证金账户。管理保证金余额的变动(追加/释放/返还),并提供计息入口(预留抽象,尚未接线)。
|
||||
///
|
||||
/// 保证金是独立的资金管理概念(初始保证金/维持保证金/保证金余额/追保),
|
||||
/// 与融资腿(funding leg)完全无关。现有代码把保证金塞进 InterestMode==5/6
|
||||
/// 当计息腿处理是错误的,本类是正确建模的起点。
|
||||
///
|
||||
/// 利息计算委托 SwapInterest 纯函数(余额×利率×天数/年化),
|
||||
/// 保证金账户只提供余额和计息入口,不自己实现计息算法。
|
||||
/// 保证金是独立的资金管理概念(初始保证金/维持保证金/保证金余额/追保),与融资腿(funding leg)无关。
|
||||
/// 生产保证金计息入口为 SwapDealService.CalcMarginInterest(仍以 InterestMode 5/6 标识):
|
||||
/// EOD 用昨日终本金 preEod.TdInterestPrincipal(无差分);盘中用 accrualBasis 差分(orginPv 经 PreviousBalance)。
|
||||
/// 本类尚未被生产代码实例化——其扁平"余额×利率×天数"模型无法表达盘中差分与多行分段,留作未来简化抽象。
|
||||
/// </summary>
|
||||
public sealed class MarginAccount
|
||||
{
|
||||
@@ -31,7 +29,8 @@ public sealed class MarginAccount
|
||||
|
||||
/// <summary>
|
||||
/// 按当前余额计算保证金利息。委托 SwapInterest.AccrueSimple。
|
||||
/// 保证金利息是券商对客户保证金存款付息(方向与融资腿相反)。
|
||||
/// 注意:当前未被生产代码调用——生产保证金计息入口为 SwapDealService.CalcMarginInterest
|
||||
/// (处理 EOD 昨日终本金与盘中差分;本方法的扁平余额模型不覆盖盘中差分口径)。
|
||||
/// </summary>
|
||||
/// <param name="rate">保证金利率(年化,如 0.03 = 3%)。</param>
|
||||
/// <param name="startDate">计息开始日。</param>
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace YLErp.Modules.SwapModule.Margin;
|
||||
/// 保证金余额。现金、授信、担保等多种保证金形态的统一表达。
|
||||
///
|
||||
/// 保证金就是保证金——有余额、有利率、有利息,不存在"计息基数/Notional"概念。
|
||||
/// 余额随追加/释放/盈亏变动,利息由 SwapInterest 纯函数按 余额×利率×天数/年化 计算。
|
||||
/// 余额随追加/释放/盈亏变动,利息由计息层(SwapDealService.CalcMarginInterest)按 EOD 昨日终本金 / 盘中差分口径计算。
|
||||
/// </summary>
|
||||
public readonly struct MarginBalance
|
||||
{
|
||||
|
||||
@@ -41,4 +41,17 @@ public static class MarginModes
|
||||
|
||||
/// <summary>判断 mode 是否属于保证金(非 LINQ 场景用)。</summary>
|
||||
public static bool Contains(int interestMode) => All.Contains(interestMode);
|
||||
|
||||
/// <summary>固定值 + 保证金 mode 集合(固定值/初始预付金/追加预付金)。
|
||||
/// 用于 EOD 场景判断"计息基数取 InterestPrincipalFix 而非持仓名义本金"的腿。
|
||||
/// 替代 SwapEodPositionService 中 3 处内联 new List{固定值, 初始预付金, 追加预付金}。</summary>
|
||||
public static readonly IReadOnlyCollection<int> FixedAmountAndMargin = new HashSet<int>
|
||||
{
|
||||
(int)InterestModeEnum.固定值,
|
||||
(int)InterestModeEnum.初始预付金,
|
||||
(int)InterestModeEnum.追加预付金,
|
||||
};
|
||||
|
||||
/// <summary>判断 mode 是否为固定值或保证金。</summary>
|
||||
public static bool IsFixedAmountOrMargin(int interestMode) => FixedAmountAndMargin.Contains(interestMode);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.ReturnLegs;
|
||||
|
||||
@@ -19,4 +20,17 @@ public static class DirectionRatio
|
||||
/// <summary>收付方向因子。收取=+1, 支付=-1。</summary>
|
||||
public static int ReceivePay(int direction)
|
||||
=> direction == (int)SwapDirectionEnum.收取 ? 1 : -1;
|
||||
|
||||
/// <summary>利息腿 PnL 方向因子。收取=+1/支付=-1;保证金腿翻转(利息现金流与本金方向相反)。
|
||||
/// 原 7 处内联 `收取?1:-1; if(MarginModes) ratio=-ratio` 收口到此。</summary>
|
||||
public static int InterestLegPnl(int interestDirection, int interestMode)
|
||||
{
|
||||
var ratio = ReceivePay(interestDirection);
|
||||
return MarginModes.Contains(interestMode) ? -ratio : ratio;
|
||||
}
|
||||
|
||||
/// <summary>按收付方向选汇率类型。收取→Buy, 支付→Sell。
|
||||
/// 原 7 处内联 `收取 ? Buy : Sell` 收口到此。</summary>
|
||||
public static CurrencyRateType RateType(int direction)
|
||||
=> direction == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace YLErp.Modules.SwapModule.ReturnLegs;
|
||||
|
||||
/// <summary>
|
||||
/// EOD 利息腿展示值计算——轻量单日公式(非 SwapDealService 的分段计息引擎)。
|
||||
/// </summary>
|
||||
public static class InterestIncomeCalc
|
||||
{
|
||||
/// <summary>日应计利息 = 本金 × (固定利率 + 浮动利差) ÷ 年化天数(若年化)。
|
||||
/// 原 3 处内联 `principal*(rate+float); if(annualized) /=annualDays` 收口到此。</summary>
|
||||
public static decimal DailyAccrual(decimal principal, decimal rate, decimal floatRate, bool isAnnualized, int annualDays)
|
||||
{
|
||||
var amount = principal * (rate + floatRate);
|
||||
return isAnnualized ? amount / annualDays : amount;
|
||||
}
|
||||
|
||||
/// <summary>已实现利息滚存。(prev + today×ratio, prevFee + todayFee)。
|
||||
/// 原 4 处内联 2 行赋值收口到此。</summary>
|
||||
public static (decimal Interest, decimal Fee) RollRealized(
|
||||
decimal prevInterest, decimal prevFee, decimal closeInterest, decimal closeInterestFee, int ratio)
|
||||
=> (prevInterest + closeInterest * ratio, prevFee + closeInterestFee);
|
||||
}
|
||||
@@ -19,4 +19,12 @@ public static class MtmCalc
|
||||
/// <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;
|
||||
|
||||
/// <summary>浮动端总未实现盈亏 = 盯市盈亏 + 分红 + 待结费用。原 4 处内联收口到此。</summary>
|
||||
public static decimal ReturnLegProfitSum(decimal mtmPnl, decimal dividendSum, decimal feePending)
|
||||
=> mtmPnl + dividendSum + feePending;
|
||||
|
||||
/// <summary>加权均价混合:(昨日均价×昨日量 + 今日∑(量×均额)) / 总量。原 4 处内联收口到此。</summary>
|
||||
public static decimal BlendPrice(decimal prevPrice, decimal prevQty, decimal sumQtyTimesPrice, decimal totalQty)
|
||||
=> (prevPrice * prevQty + sumQtyTimesPrice) / totalQty;
|
||||
}
|
||||
|
||||
@@ -46,106 +46,17 @@ namespace YLErp.Modules.SwapModule
|
||||
/// 原 private 改 protected virtual,使测试 stub 可整体 override,规避内部 new SwapEventService 连库。</summary>
|
||||
protected virtual long SaveSwapDeal(UnwindData unwindData, int eventType, int clientCashId, string eventResason = "", bool approve = false)
|
||||
{
|
||||
NormalizeNotionalValues(unwindData);
|
||||
UnwindNormalizer.NormalizeNotionalValues(unwindData);
|
||||
return SaveSwapDealInternal(unwindData, eventType, clientCashId, eventResason, approve);
|
||||
}
|
||||
|
||||
private static void NormalizeNotionalValues(UnwindData unwindData)
|
||||
{
|
||||
unwindData.NotionalValue = Math.Round(unwindData.NotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
unwindData.PosiNotionalValue = Math.Round(unwindData.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
unwindData.CloseNotionalValue = Math.Round(unwindData.CloseNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
private static bool NormalizeFullCloseRequest(UnwindData unwindData)
|
||||
{
|
||||
if (unwindData.CloseMethod != (int)CloseMethodEnum.全部平仓
|
||||
&& unwindData.ClosePercent < 1
|
||||
&& !(unwindData.PositionQty > 0 && unwindData.CloseQty >= unwindData.PositionQty)
|
||||
&& !(unwindData.PosiNotionalValue > 0 && unwindData.CloseNotionalValue >= unwindData.PosiNotionalValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var closeQty = unwindData.CloseQty;
|
||||
var closeNotionalValue = unwindData.CloseNotionalValue;
|
||||
unwindData.ClosePercent = 1;
|
||||
if (unwindData.PositionQty > 0) unwindData.CloseQty = unwindData.PositionQty;
|
||||
if (unwindData.PosiNotionalValue > 0) unwindData.CloseNotionalValue = unwindData.PosiNotionalValue;
|
||||
return closeQty != unwindData.CloseQty || closeNotionalValue != unwindData.CloseNotionalValue;
|
||||
}
|
||||
|
||||
private static void RecalculateNormalizedUnwindAmounts(UnwindData unwindData)
|
||||
{
|
||||
var floatLeg = unwindData.FlowEvents.FirstOrDefault(x => !string.IsNullOrEmpty(x.UnderlyingCode));
|
||||
if (floatLeg == null || floatLeg.PosiGrossPrice == 0) return;
|
||||
|
||||
var input = new UnwindInput
|
||||
{
|
||||
Multiplier = ConsGlobal.InstrumentType.IsBond(floatLeg.UnderlyingInstrumentType) ? 100 : 1,
|
||||
PosiGrossPrice = floatLeg.PosiGrossPrice,
|
||||
TradingAmountAvg = floatLeg.TradingAmountAvg,
|
||||
CloseQty = unwindData.CloseQty,
|
||||
PositionQty = unwindData.PositionQty,
|
||||
ContractSize = floatLeg.ContractSize,
|
||||
CloseNotionalValue = unwindData.CloseNotionalValue,
|
||||
PayDirection = floatLeg.PayDirection,
|
||||
PositionType = floatLeg.PositionType,
|
||||
TradingFee = floatLeg.TradingFee.ToString(),
|
||||
TradingFeePending = floatLeg.TradingFeePending.ToString(),
|
||||
DividendIn = floatLeg.DividendIn.ToString()
|
||||
};
|
||||
foreach (var leg in unwindData.FlowEvents.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)))
|
||||
{
|
||||
var target = MarginModes.Contains(leg.InterestMode)
|
||||
? input.MarginLegs
|
||||
: input.InterestLegs;
|
||||
target.Add(new LegInput { InterestClosePnL = leg.InterestClosePnL });
|
||||
}
|
||||
|
||||
var result = FrontendCalcReference.CalcUnwind(input);
|
||||
floatLeg.MarkClosePnl = result.MarkClosePnl;
|
||||
unwindData.SwapCloseAmount = result.SwapCloseAmount;
|
||||
unwindData.SwapRealizedPnL = result.SwapRealizedPnL;
|
||||
unwindData.SwapMarginRebatePnl = result.SwapMarginRebatePnl;
|
||||
}
|
||||
|
||||
private static bool IsFullCloseAfterDeduction(UnwindData unwindData, double remainingNotional, double remainingQuantity)
|
||||
{
|
||||
return unwindData.ClosePercent == 1 || (remainingNotional == 0 && remainingQuantity == 0);
|
||||
}
|
||||
|
||||
// 待实现利息会进入 decimal(30,12) 日终快照;精度常量统一引用 SwapInterest.FundingLegPrecision,消除重复定义。
|
||||
private const int InterestCalculationPrecision = SwapInterest.FundingLegPrecision;
|
||||
|
||||
/// <summary>
|
||||
/// 手工平仓、手工互换及收益结算的利息事件按金额两位落库。
|
||||
/// 自动平仓保留原有计算与落库口径,不适用本阶段的手工结算规则。
|
||||
/// </summary>
|
||||
private static bool NormalizeSettledInterestAmounts(IEnumerable<swap_flow_event> flowEvents, int eventType, string eventReason)
|
||||
{
|
||||
if ((eventType != (int)SwapEventTypeEnum.平仓 && eventType != (int)SwapEventTypeEnum.互换)
|
||||
|| eventReason == "系统操作_自动平仓")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var flowEvent in flowEvents.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)))
|
||||
{
|
||||
// 只处理利息腿;浮动腿损益在日终快照入口统一按两位落库。
|
||||
flowEvent.InterestPrincipal = Math.Round(flowEvent.InterestPrincipal, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
flowEvent.InterestAmount = Math.Round(flowEvent.InterestAmount, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
flowEvent.TdInterestAmount = Math.Round(flowEvent.TdInterestAmount, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
flowEvent.InterestClosePnL = Math.Round(flowEvent.InterestClosePnL, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
flowEvent.InterestFee = Math.Round(flowEvent.InterestFee, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 客户现金在 SaveSwapDeal 之前创建,手工结算必须先收敛流水并重算汇总金额。
|
||||
private void NormalizeManualSettlementAmounts(UnwindData unwindData, int eventType, string eventReason)
|
||||
{
|
||||
if (!NormalizeSettledInterestAmounts(unwindData.FlowEvents, eventType, eventReason))
|
||||
if (!UnwindNormalizer.NormalizeSettledInterestAmounts(unwindData.FlowEvents, eventType, eventReason))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -372,7 +283,7 @@ namespace YLErp.Modules.SwapModule
|
||||
floatEvent.UnderlyingInstrumentType = position.UnderlyingInstrumentType;
|
||||
floatEvent.CloseFee = 0;
|
||||
floatEvent.BeforeCloseFee = oriPosition.PosiTradingFeePending;
|
||||
floatEvent.TradingFee = CalcInitTradingFee(oriPosition, unwindData);
|
||||
floatEvent.TradingFee = TradingFeeCalc.CalcInitTradingFee(oriPosition, unwindData);
|
||||
floatEvent.PosiTradingFeeUnit = oriPosition?.PosiTradingFeeUnit ?? 0;
|
||||
floatEvent.PosiFeeType = oriPosition?.PosiFeeType ?? 0;
|
||||
floatEvent.MarkClosePnl = 0;
|
||||
@@ -386,8 +297,8 @@ namespace YLErp.Modules.SwapModule
|
||||
floatEvent.PositionQty = 0;
|
||||
floatEvent.ContractSize = position.ContractSize;
|
||||
floatEvent.TradingAmount = floatEvent.Quantity * floatEvent.ContractSize;
|
||||
var ratio = position.PosiDirection == (int)SwapDirectionEnum.收取 ? -1m : 1m;
|
||||
floatEvent.TradingFeePending = CalcInitTradingFeePending(oriPosition, position, unwindData);
|
||||
var ratio = -DirectionRatio.ReceivePay(position.PosiDirection);
|
||||
floatEvent.TradingFeePending = TradingFeeCalc.CalcInitTradingFeePending(oriPosition, position, unwindData);
|
||||
floatEvent.DataState = (int)SwapFlowDateStateEnum.完成;
|
||||
floatEvent.InterestMode = position.InterestMode;
|
||||
floatEvent.ClientId = td.ClientId;
|
||||
@@ -397,37 +308,6 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
return unwindData;
|
||||
}
|
||||
private static decimal CalcInitTradingFee(swap_position oriPosition, UnwindData unwindData)
|
||||
{
|
||||
if (oriPosition == null || unwindData == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (oriPosition.PosiFeeType == 1)
|
||||
{
|
||||
return Math.Round(oriPosition.PosiTradingFeeUnit * unwindData.CloseQty, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
return Math.Round(oriPosition.PosiTradingFeeUnit / 100m * unwindData.CloseNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
private static decimal CalcInitTradingFeePending(swap_position oriPosition, swap_position position, UnwindData unwindData)
|
||||
{
|
||||
if (oriPosition == null || unwindData == null || oriPosition.PosiTradingFeeUnit == 0)
|
||||
{
|
||||
return position?.PosiTradingFeePending ?? 0;
|
||||
}
|
||||
|
||||
var closeBase = oriPosition.PosiFeeType == 1 ? unwindData.CloseQty : unwindData.CloseNotionalValue;
|
||||
var originalBase = oriPosition.PosiFeeType == 1 ? unwindData.NotionalQty : unwindData.NotionalValue;
|
||||
if (originalBase <= 0)
|
||||
{
|
||||
return position?.PosiTradingFeePending ?? 0;
|
||||
}
|
||||
|
||||
return Math.Round(oriPosition.PosiTradingFeePending * closeBase / originalBase, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
/// <summary>
|
||||
/// 校验上日是否收盘
|
||||
/// </summary>
|
||||
@@ -465,56 +345,6 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <param name="tradeId"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ServiceException"></exception>
|
||||
public UnwindData InitLongShortUnwind(int tradeId, SwapEventTypeEnum eventTypeEnum)
|
||||
{
|
||||
var td = DbContext.trade.Find(tradeId);
|
||||
if (td == null)
|
||||
{
|
||||
throw new ServiceException("未找到交易信息");
|
||||
}
|
||||
var positions = DbContext.swap_position.Where(x => x.SwapTradeId == tradeId && x.IsInitial && !x.Invalid);
|
||||
List<int> eventTyps = new List<int>() { (int)SwapEventTypeEnum.平仓, (int)SwapEventTypeEnum.互换, (int)SwapEventTypeEnum.自动互换 };
|
||||
var dealDate = valuedateBLL.ValueDate <= td.ExerciseDate.Value ? valuedateBLL.ValueDate : td.ExerciseDate.Value;
|
||||
//CheckLastEod(dealDate, td.TradeDate.Value, tradeId); //去掉平仓收盘限制
|
||||
var tradeExtend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == tradeId);
|
||||
td.trade_extend = tradeExtend;
|
||||
var preDealDate = GetPreDealDate(tradeId, dealDate, eventTyps);
|
||||
double stockEqvNotional = td.StockEqvNotional;//剩余名义本金
|
||||
var hasProcess = HasTradeProcess();
|
||||
swap_flow_event floatEvent = new swap_flow_event();
|
||||
UnwindData unwindData = new UnwindData();
|
||||
if (((valuedateBLL.SystemDate.CloseReCheck == 1) || (valuedateBLL.SystemDate.CloseReApprove == 1 && hasProcess)) && (td.TradeStatus == ConsTrade.平仓待复核 || td.TradeStatus == ConsTrade.互换待复核))
|
||||
{
|
||||
var swapEvent = GetSwapEvent(tradeId, (int)eventTypeEnum);
|
||||
if (swapEvent == null)
|
||||
{
|
||||
throw new Exception("该笔交易状态为平仓待复核,未找到相关记录,请检查该笔交易是否有效");
|
||||
}
|
||||
unwindData = swapEvent.unwindData;
|
||||
}
|
||||
else
|
||||
{
|
||||
unwindData.StartDate = td.TradeDate.Value;
|
||||
if (preDealDate.HasValue)
|
||||
{
|
||||
unwindData.StartDate = preDealDate.Value;
|
||||
}
|
||||
unwindData.ValueDate = dealDate;
|
||||
unwindData.UnwindDate = dealDate;
|
||||
unwindData.PayDate = QdpCalendarHelper.GetNonHoliday(dealDate.AddDays(td.trade_extend.ExtendObj.SettlementRules));
|
||||
unwindData.SwapTradeId = tradeId;
|
||||
unwindData.NotionalValue = Convert.ToDecimal(td.OriginalStockEqvNotional ?? 0);
|
||||
unwindData.NotionalQty = positions.Sum(s => s.PosiQuantity);
|
||||
unwindData.PosiNotionalValue = Convert.ToDecimal(stockEqvNotional);
|
||||
unwindData.PositionQty = 0;//平仓只做了结为0,互换用不上
|
||||
unwindData.AnnualDays = tradeExtend == null ? 365 : tradeExtend.ExtendObj.AnnualDays;
|
||||
if (eventTypeEnum == SwapEventTypeEnum.平仓)
|
||||
{
|
||||
unwindData.FlowEvents = GetUnwindInterests(dealDate, unwindData.UnwindDate.Value, tradeId, 1, (int)SwapEventTypeEnum.平仓);
|
||||
}
|
||||
}
|
||||
return unwindData;
|
||||
}
|
||||
/// <summary>
|
||||
/// 平仓初始化
|
||||
/// </summary>
|
||||
@@ -663,8 +493,8 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <para>根因(多次部分平仓预付金返还错误):预付金腿(初始/追加)的"当前剩余本金"存于实时持仓
|
||||
/// realPositions.InterestPrincipalFix,每次平仓由 UpdateInitalPosition 递减;而原始腿
|
||||
/// origPositions(IsInitial=1)的 InterestPrincipalFix 恒为初始值。GetInterests 算
|
||||
/// closePrincipal = Fix × closePercent 与预付金计息基数 orginPv(InitSwapDealInterest) 时都读
|
||||
/// position.InterestPrincipalFix,若沿用原始腿,会在多次部分平仓后仍返还/计算初始本金(如始终 99000)。</para>
|
||||
/// closePrincipal = Fix × closePercent 时读 position.InterestPrincipalFix,若沿用原始腿,
|
||||
/// 会在多次部分平仓后仍返还/计算初始本金(如始终 99000)。</para>
|
||||
/// <para>修复:迭代源仍用 origPositions(保留 orig.id → eod_swap_position.PositionId 的日终匹配,
|
||||
/// 全库实测 eod 均按 orig.id 归档;若换 realPositions 会破坏 preEod 匹配导致利息重算错误),仅对预付金腿
|
||||
/// Clone 覆盖其本金值为实时腿的剩余本金。real 与 orig 通过 real.PositionId == orig.id 精确 1:1 关联。
|
||||
@@ -745,24 +575,7 @@ namespace YLErp.Modules.SwapModule
|
||||
eod_swap lastEod,
|
||||
IEnumerable<eod_swap_position> lastEodPositions,
|
||||
decimal currentNotional)
|
||||
{
|
||||
var floatingPositions = lastEodPositions?.Where(x => x.PosiDirection > 0).ToList();
|
||||
decimal previousNotional;
|
||||
if (floatingPositions?.Count > 0)
|
||||
{
|
||||
previousNotional = floatingPositions.Sum(x => x.PosiNotionalValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
previousNotional = lastEod == null
|
||||
? currentNotional
|
||||
: Math.Abs(lastEod.NotionalValueLong) + Math.Abs(lastEod.NotionalValueShort);
|
||||
}
|
||||
|
||||
return previousNotional == 0m && currentNotional != 0m
|
||||
? currentNotional
|
||||
: previousNotional;
|
||||
}
|
||||
=> ClosePercentMath.ResolveUnwindPreviousNotional(lastEod, lastEodPositions, currentNotional);
|
||||
|
||||
/// <summary>
|
||||
/// 获取利息腿"已通过历史互换结出的累计利息"(用于复利重算时扣除,类比分红的 CalcConsumedDividend)。
|
||||
@@ -855,14 +668,22 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
// 融资腿(1/2/9): 走策略工厂
|
||||
var r = FundingLegStrategyFactory.Get(mode)
|
||||
.CalcNotional(position.InterestPrincipalFix, posiNotionalValue, posiLongNotionalValue, posiShortNotionalValue, closePrecent);
|
||||
.CalcNotional(position.InterestPrincipalFix, posiNotionalValue, closePrecent);
|
||||
closePrincipal = r.ClosePrincipal;
|
||||
posiPrincipal = r.PosiPrincipal;
|
||||
newClosePercent = r.ClosePercent;
|
||||
}
|
||||
// 根因位置:SwapEodPositionService.SaveAutoEodWithCloseInterestPosition 在平仓后收盘时传入
|
||||
// “收盘后剩余本金 + closePercent=1”,与盘中“平仓前本金 + 实际关闭比例”不是同一语义。
|
||||
// GetInterests 同时被盘中试算和 EOD 平仓后收盘调用:后者传入的
|
||||
// posiNotionalValue 是收盘后的剩余本金,closePosiNotionalValue 才是本次实际平掉的本金。
|
||||
// 例如平仓前 100、平掉 30、收盘后剩余 70 时,EOD 传入 posi=70、close=30、closePercent=1。
|
||||
// 模式2(合约名义本金规模)的本次结息本金必须始终是实际平仓额,因此无条件覆盖,
|
||||
// 否则会错误地用剩余 70 结算本次平掉的 30。模式9(标的期初全价)的部分平仓
|
||||
// 仍保留既有的剩余/复利动态本金承接逻辑;仅最终全平时 posi=0,才覆盖以避免结息本金为 0。
|
||||
if ((InterestModeEnum)position.InterestMode == InterestModeEnum.合约名义本金规模
|
||||
|| (InterestModeEnum)position.InterestMode == InterestModeEnum.标的期初全价
|
||||
&& posiNotionalValue == 0m)
|
||||
|| ((InterestModeEnum)position.InterestMode == InterestModeEnum.标的期初全价
|
||||
&& posiNotionalValue == 0m))
|
||||
{
|
||||
closePrincipal = closePosiNotionalValue;
|
||||
}
|
||||
@@ -876,7 +697,13 @@ namespace YLErp.Modules.SwapModule
|
||||
decimal floatRate = GetFloatRate(position, preEodPosition, td.StartDate.Value, endDate, interestPeriod, swap, positionClone);
|
||||
|
||||
// 根据场景计算利息
|
||||
if (settment)
|
||||
if (MarginModes.Contains(position.InterestMode))
|
||||
{
|
||||
// 保证金腿(5/6):专属计息,notional 直接取保证金余额,无融资腿差分公式与 orginPv 维度 hack
|
||||
interests.Add(CalcMarginInterest(td, valueDate, endDate, positionClone, rate, closePrincipal, posiPrincipal,
|
||||
newClosePercent, annualDays, calcFirst, calcLast||newCalcLast, preEodPosition, eventType, add, settment, swap));
|
||||
}
|
||||
else if (settment)
|
||||
{
|
||||
// 收盘归档场景,使用 CalcEodInterest
|
||||
interests.Add(CalcEodInterest(td, valueDate, positionClone, rate, floatRate, closePrincipal, posiPrincipal, annualDays, calcFirst, calcLast, preEodPosition, eventType, add));
|
||||
@@ -934,32 +761,19 @@ namespace YLErp.Modules.SwapModule
|
||||
/// 分母为 0(无持仓等异常场景)时原样返回,避免除零。
|
||||
/// </summary>
|
||||
public static decimal ToRemainingClosePercent(decimal originalClosePercent, decimal notionalValue, decimal posiNotionalValue)
|
||||
{
|
||||
if (posiNotionalValue <= 0) return originalClosePercent;
|
||||
var remaining = originalClosePercent * notionalValue / posiNotionalValue;
|
||||
return remaining > 1 ? 1 : remaining;
|
||||
}
|
||||
=> ClosePercentMath.ToRemainingClosePercent(originalClosePercent, notionalValue, posiNotionalValue);
|
||||
|
||||
/// <summary>
|
||||
/// B(占剩余) → A(占期初),用于落库 / 事件列表展示还原。见 ToRemainingClosePercent。
|
||||
/// </summary>
|
||||
public static decimal ToOriginalClosePercent(decimal remainingClosePercent, decimal notionalValue, decimal posiNotionalValue)
|
||||
{
|
||||
if (notionalValue <= 0) return remainingClosePercent;
|
||||
return remainingClosePercent * posiNotionalValue / notionalValue;
|
||||
}
|
||||
=> ClosePercentMath.ToOriginalClosePercent(remainingClosePercent, notionalValue, posiNotionalValue);
|
||||
|
||||
/// <summary>
|
||||
/// 计算 InitUnwind 默认占期初(A)平仓比例 = "平掉剩余全部持仓"对应的占期初比例。
|
||||
/// 即:ClosePercent(A) = PosiNotionalValue / NotionalValue。
|
||||
/// 未平仓时 PosiNotionalValue==NotionalValue → 1(平100%);
|
||||
/// 部分平仓后自动变为剩余比例(如已平 30% 则默认 0.7)。
|
||||
/// 与互换/提前终止 InitIncome 保持一致。抽出为纯函数以支持无库单测。
|
||||
/// 计算 InitUnwind 默认占期初(A)平仓比例 = PosiNotionalValue / NotionalValue。
|
||||
/// </summary>
|
||||
public static decimal CalcDefaultInitClosePercent(decimal notionalValue, decimal posiNotionalValue)
|
||||
{
|
||||
return notionalValue > 0 ? posiNotionalValue / notionalValue : 1;
|
||||
}
|
||||
=> ClosePercentMath.CalcDefaultInitClosePercent(notionalValue, posiNotionalValue);
|
||||
|
||||
/// <summary>
|
||||
/// 读取"上一收盘日"浮动腿的待实现分红(eod_swap_position.PosiDividendSum),
|
||||
@@ -992,16 +806,34 @@ namespace YLErp.Modules.SwapModule
|
||||
/// </remarks>
|
||||
protected virtual decimal GetPreEodDividendSum(int tradeId, long positionId, DateTime dealDate)
|
||||
{
|
||||
var lastEod = DbContext.eod_swap
|
||||
.Where(x => x.ValueDate < dealDate && x.SwapTradeId == tradeId)
|
||||
.OrderByDescending(o => o.ValueDate).FirstOrDefault();
|
||||
var preEodDate = lastEod == null ? dealDate.AddDays(-1) : lastEod.ValueDate;
|
||||
var preEod = new SwapEodPositionService(this)
|
||||
.GetPreEodPositions(tradeId, preEodDate)
|
||||
.FirstOrDefault(x => x.PositionId == positionId);
|
||||
var preEod = GetPreEodPositionByDate(tradeId, positionId, dealDate);
|
||||
return preEod == null ? 0m : preEod.PosiDividendSum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取 dealDate 对应"上一收盘日"持仓的累计分红快照。
|
||||
/// GLMS-20260105-0006:登记日当天手动平仓/互换时,当日 EOD 快照已含分红,应取到当日而非 T-1。
|
||||
/// 故由 ValueDate 严格小于 dealDate 改为 小于等于:当日 EOD 存在则读当日,否则回退上一收盘日(原口径不变)。
|
||||
/// </summary>
|
||||
protected virtual eod_swap_position GetPreEodPositionByDate(int tradeId, long positionId, DateTime dealDate)
|
||||
{
|
||||
var lastEod = QueryPreEodSwaps(tradeId)
|
||||
.Where(x => x.ValueDate <= dealDate)
|
||||
.OrderByDescending(o => o.ValueDate).FirstOrDefault();
|
||||
var preEodDate = lastEod == null ? dealDate.AddDays(-1) : lastEod.ValueDate;
|
||||
return QueryPreEodPosition(tradeId, positionId, preEodDate);
|
||||
}
|
||||
|
||||
/// <summary>可测性 seam:返回某交易的全部 eod_swap 行(不做日期过滤)。测试可 override 注入内存数据。</summary>
|
||||
protected virtual IQueryable<eod_swap> QueryPreEodSwaps(int tradeId)
|
||||
=> DbContext.eod_swap.Where(x => x.SwapTradeId == tradeId);
|
||||
|
||||
/// <summary>可测性 seam:取指定收盘日的持仓累计分红快照。测试可 override 注入内存数据。</summary>
|
||||
protected virtual eod_swap_position QueryPreEodPosition(int tradeId, long positionId, DateTime valueDate)
|
||||
=> new SwapEodPositionService(this)
|
||||
.GetPreEodPositions(tradeId, valueDate)
|
||||
.FirstOrDefault(x => x.PositionId == positionId);
|
||||
|
||||
/// <summary>
|
||||
/// 获取固定利率
|
||||
/// </summary>
|
||||
@@ -1088,12 +920,12 @@ namespace YLErp.Modules.SwapModule
|
||||
if (position.InterestType == (int)InterestTypeEnum.复利)
|
||||
{
|
||||
// 复利计算
|
||||
CalcDailyCompoundInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, false, eodFloatRate, 1m, posiPrincipal, ref interestAmount, ref tdInterestAmount);
|
||||
CalcDailyCompoundInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, eodFloatRate, 1m, ref interestAmount, ref tdInterestAmount);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 单利计算
|
||||
CalcDailySimpleInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, false, eodFloatRate, 1m, posiPrincipal, ref interestAmount, ref tdInterestAmount);
|
||||
CalcDailySimpleInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, eodFloatRate, 1m, ref interestAmount, ref tdInterestAmount);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1101,7 +933,125 @@ namespace YLErp.Modules.SwapModule
|
||||
interest.InterestAmount = Math.Round(interestAmount, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
|
||||
interest.TdInterestAmount = Math.Round(tdInterestAmount, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
|
||||
// 计算InterestClosePnL(方向:收取=1为正,支付=-1为负)
|
||||
var interestRatio = position.InterestDirection == 1 ? 1m : -1m;
|
||||
var interestRatio = DirectionRatio.ReceivePay(position.InterestDirection);
|
||||
interest.InterestClosePnL = interest.InterestAmount * interestRatio;
|
||||
|
||||
if (add) UpdateDbOption(interest);
|
||||
return interest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保证金腿(InterestMode 5/6)专属计息——替代 CalcEodInterest/CalcUnwindInterest 对保证金的处理。
|
||||
///
|
||||
/// 保证金是纯固定利率单利:浮动利率(FR007)/分段利率/复利对其均为死分支(前端无入口、
|
||||
/// 确认书不含、FundingLegRate.Build 对空 FloatRateUnderlyingCode 恒返回 Fixed)。故本方法直接用
|
||||
/// SimpleInterestAccrual 纯函数计息,本金取保证金余额:
|
||||
/// EOD = 昨日终本金 preEod.TdInterestPrincipal(与旧 CalcDailySimpleInterestByEod 同源,无差分)
|
||||
/// 盘中 = accrualBasis(preEod.TdInterestPrincipal + posiPrincipal - orginPv)
|
||||
/// 盘中保留差分是必要的:posiPrincipal 是否经 ResolveInterestLegPositions 对齐到实时剩余是路径相关的
|
||||
/// (生产对齐 / 诊断测试用原始腿),单一本金变量无法覆盖两种状态,差分经 orginPv 自适应。orginPv 在
|
||||
/// 本方法内部按保证金维度计算(PreviousBalance),消除原 InitSwapDealInterest 的外部维度 hack
|
||||
/// (融资腿 orginPv=浮动端名义本金)。保留累计语义(priorAccrued + 增量),满足下游字段契约。
|
||||
/// </summary>
|
||||
/// <param name="settment">true=收盘归档(EOD),false=盘中平仓/互换。</param>
|
||||
/// <param name="swap">互换事件(仅盘中生效,true 时利息归零,同 InitSwapDealInterest)。</param>
|
||||
public swap_flow_event CalcMarginInterest(
|
||||
trade td, DateTime valueDate, DateTime endDate, swap_position position, decimal rate,
|
||||
decimal closePrincipal, decimal posiPrincipal, decimal closePercent,
|
||||
int annualDays, bool calcFirst, bool calcLast,
|
||||
eod_swap_position preEod, int eventType, bool add, bool settment, bool swap)
|
||||
{
|
||||
// 当日是否计息(算头算尾)——同 CalcEodInterest
|
||||
bool calcToday = true;
|
||||
if (!calcFirst && valueDate == td.StartDate.Value) calcToday = false;
|
||||
if (!calcLast && valueDate == td.ExerciseDate.Value) calcToday = false;
|
||||
if (valueDate < position.PosiStartDate) calcToday = false;
|
||||
|
||||
// 首日初始化 preEod——同 CalcEodInterest
|
||||
if (preEod.id == 0)
|
||||
{
|
||||
preEod.FloatRate = 0m;
|
||||
preEod.TdInterestPrincipal = posiPrincipal;
|
||||
preEod.PosiNotionalValue = posiPrincipal;
|
||||
}
|
||||
|
||||
// 字段映射(保证金 FloatRate 恒 0;方向 position.InterestDirection 已由 GetInterests 翻转)
|
||||
var interest = new swap_flow_event
|
||||
{
|
||||
SwapTradeId = td.id,
|
||||
SwapTradeNo = td.TradeNumber,
|
||||
EventType = eventType,
|
||||
EventReason = "交易",
|
||||
EventDate = valueDate,
|
||||
PositionId = position.id,
|
||||
InterestDirection = position.InterestDirection,
|
||||
InterestRate = rate,
|
||||
InterestPrincipal = closePrincipal,
|
||||
InterestSwapInterval = position.InterestSwapInterval,
|
||||
InterestMode = position.InterestMode,
|
||||
FloatRate = 0m,
|
||||
DataState = (int)SwapFlowDateStateEnum.完成,
|
||||
ClientId = td.ClientId,
|
||||
UnwindDate = settment ? valueDate : endDate
|
||||
};
|
||||
|
||||
// 互换事件:利息归零(同 InitSwapDealInterest)
|
||||
if (swap && !settment)
|
||||
{
|
||||
interest.InterestAmount = 0m;
|
||||
interest.TdInterestAmount = 0m;
|
||||
interest.InterestClosePnL = 0m;
|
||||
if (add) UpdateDbOption(interest);
|
||||
return interest;
|
||||
}
|
||||
|
||||
decimal interestAmount = 0m;
|
||||
decimal tdInterestAmount = 0m;
|
||||
var legRate = FundingLegRate.Fixed(rate); // 保证金纯固定(无浮动)
|
||||
|
||||
if (calcToday)
|
||||
{
|
||||
if (settment)
|
||||
{
|
||||
// EOD:单日增量,累计 = 昨日累计 + 今日增量;notional = 昨日终本金(无差分)
|
||||
var policy = AccrualPolicy.BuildEod(position, annualDays, isCompound: false);
|
||||
var r = SimpleInterestAccrual.AccrueEod(
|
||||
priorAccrued: preEod.InterestProfitSum,
|
||||
priorNotional: preEod.TdInterestPrincipal,
|
||||
unwindFraction: 1m,
|
||||
rate: legRate, policy: policy, eodDate: valueDate);
|
||||
interestAmount = r.Accrued;
|
||||
tdInterestAmount = r.AccruedToday;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 盘中:accrualBasis 自适应"实时剩余本金"——posiPrincipal 已对齐(ResolveInterestLegPositions)
|
||||
// 时 = posiPrincipal;未对齐的原始腿经 orginPv(=PreviousBalance 昨日终) 修正回昨日终剩余。
|
||||
// 单一本金变量无法覆盖两种 position 状态,故保留差分(与 EOD 直接用 preEod.TdInterestPrincipal 不同)。
|
||||
// orginPv 在此内部按保证金维度计算,消除原 InitSwapDealInterest 的外部维度 hack。
|
||||
var orginPv = MarginCalc.PreviousBalance(preEod, posiPrincipal);
|
||||
var accrualBasis = preEod.TdInterestPrincipal + posiPrincipal - orginPv;
|
||||
var segmentRates = new List<(DateTime, decimal)> { (position.PosiStartDate, rate) };
|
||||
var r = SimpleInterestAccrual.AccruePeriod(
|
||||
priorAccrued: preEod.InterestProfitSum * closePercent,
|
||||
notional: accrualBasis,
|
||||
unwindFraction: closePercent,
|
||||
segmentRates: segmentRates,
|
||||
startDate: position.PosiStartDate,
|
||||
endDate: endDate,
|
||||
priorValueDate: preEod.ValueDate,
|
||||
boundary: AccrualBoundary.Of(calcFirst, calcLast),
|
||||
annualDays: annualDays,
|
||||
isAnnualized: position.IsAnnualized);
|
||||
interestAmount = r.Accrued;
|
||||
tdInterestAmount = r.AccruedToday;
|
||||
interest.InterestPrincipal = accrualBasis * closePercent; // 同 CalcDailySimpleInterest:1304
|
||||
}
|
||||
}
|
||||
|
||||
interest.InterestAmount = Math.Round(interestAmount, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
|
||||
interest.TdInterestAmount = Math.Round(tdInterestAmount, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
|
||||
var interestRatio = DirectionRatio.ReceivePay(position.InterestDirection);
|
||||
interest.InterestClosePnL = interest.InterestAmount * interestRatio;
|
||||
|
||||
if (add) UpdateDbOption(interest);
|
||||
@@ -1130,15 +1080,6 @@ namespace YLErp.Modules.SwapModule
|
||||
orginPv, calcFirst, calcLast, consumedInterest);
|
||||
}
|
||||
/// <summary>
|
||||
/// 保证金腿的 orginPv 维度重映射。
|
||||
///
|
||||
/// 保证金腿被迫走融资腿的差分公式(dynomicPrincipal = TdInterestPrincipal + posiPrincipal - orginPv),
|
||||
/// 但 orginPv 对融资腿是"交易名义本金(千万~亿级)",对保证金腿必须是"保证金本金"——
|
||||
/// 否则维度不匹配会算出巨负值。本方法把保证金场景的 orginPv 对齐到"上一日保证金本金"。
|
||||
///
|
||||
/// 待迁入 Margin 模块:保证金独立计息入口建好后,此方法移入 MarginAccount/MarginService。
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// 写入保证金的资金记录:应付预付金(SwapMarginAmount)和预付金返息(SwapMarginRebatePnl)。
|
||||
/// 依赖实例方法 AddClientCash/AddClientCashInCashOut,暂留此处。
|
||||
/// </summary>
|
||||
@@ -1215,11 +1156,8 @@ namespace YLErp.Modules.SwapModule
|
||||
interest.ClientId = td.ClientId;
|
||||
interest.UnwindDate = endDate;
|
||||
|
||||
// 保证金腿的 orginPv 对齐到保证金本金维度,避免差分公式维度不匹配算出巨负值
|
||||
if (MarginModes.Contains(position.InterestMode))
|
||||
{
|
||||
orginPv = MarginCalc.PreviousBalance(preEodPosition, position.InterestPrincipalFix);
|
||||
}
|
||||
// 保证金腿已走 CalcMarginInterest(不经过本方法),orginPv 维度重映射不再需要;
|
||||
// orginPv 此处仅对融资腿生效(差分公式 accrualBasis = TdInterestPrincipal + posiPrincipal - orginPv)。
|
||||
|
||||
if (swap)
|
||||
{
|
||||
@@ -1232,7 +1170,7 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
decimal InterestAmount = 0;
|
||||
decimal TdInterestAmount = 0;
|
||||
var interestRatio = position.InterestDirection == 1 ? 1m : -1m;
|
||||
var interestRatio = DirectionRatio.ReceivePay(position.InterestDirection);
|
||||
var floateRate = preEodPosition.FloatRate;
|
||||
if (position.InterestType == (int)InterestTypeEnum.复利)
|
||||
{
|
||||
@@ -1263,8 +1201,8 @@ namespace YLErp.Modules.SwapModule
|
||||
// 把上日尚未实现的的利息 按本次平掉的这部分计息基数分给本次平仓 并在重置日并入计息基数
|
||||
// 它只在当前 endDate 恰好为重置日时使用,避免把同一笔历史利息重复资本化。
|
||||
var resetCarryInterest = preEodPosition.InterestIncomeSum * remainingPercent;
|
||||
CalcDailyCompoundInterest(endDate, position, closePosiNotionalValue, interest, annualDays, needPrice,
|
||||
floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount,
|
||||
CalcDailyCompoundInterest(endDate, position, closePosiNotionalValue, interest, annualDays,
|
||||
floateRate, closePrecent, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount,
|
||||
consumedInterest, resetCarryInterest);
|
||||
if (preEodPosition.id != 0 && closePrecent == 1m)
|
||||
{
|
||||
@@ -1291,7 +1229,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
// 计算截至本次平仓日的累计利息 amountAtEnd
|
||||
CalcDailyCompoundInterest(replayEndDate, position, closePosiNotionalValue,
|
||||
interestAtEnd, annualDays, needPrice, floateRate, closePrecent, orginPv,
|
||||
interestAtEnd, annualDays, floateRate, closePrecent,
|
||||
calcFirst, calcLast, ref amountAtEnd, ref tdAmountAtEnd, consumedInterest);
|
||||
var interestAtPreviousEod = new swap_flow_event { InterestRate = rate };
|
||||
decimal amountAtPreviousEod = 0m;
|
||||
@@ -1300,7 +1238,7 @@ namespace YLErp.Modules.SwapModule
|
||||
// 因此此处按闭区间包含上一日终当天,避免算头不算尾时重复加入该日利息。
|
||||
// 计算截至上一日终累积的利息 amountAtPreviousEod
|
||||
CalcDailyCompoundInterest(preEodPosition.ValueDate, position, closePosiNotionalValue,
|
||||
interestAtPreviousEod, annualDays, needPrice, floateRate, closePrecent, orginPv,
|
||||
interestAtPreviousEod, annualDays, floateRate, closePrecent,
|
||||
calcFirst, true, ref amountAtPreviousEod, ref tdAmountAtPreviousEod, consumedInterest);
|
||||
// 例如 0004:5/18 待实现 -118631.261797,加 5/19 新增约 -4648.912760,
|
||||
// 得到最终应结 -123280.174557,按金额两位落为 Excel BN 的 -123280.17。
|
||||
@@ -1312,7 +1250,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
else
|
||||
{
|
||||
CalcDailySimpleInterest(preEodPosition, endDate, position, posiNotionalValue, interest, annualDays, needPrice, floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount);
|
||||
CalcDailySimpleInterest(preEodPosition, endDate, position, posiNotionalValue, interest, annualDays, floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount);
|
||||
}
|
||||
|
||||
interest.InterestAmount = Math.Round(InterestAmount, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
|
||||
@@ -1326,6 +1264,19 @@ namespace YLErp.Modules.SwapModule
|
||||
return interest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按 interest_rule 取 FR007 定盘价。无浮动标的时返回 fallback;取不到抛异常。
|
||||
/// EOD 单日取率 + BuildSegmentRates 多日取率共用此方法,FR007 定盘逻辑收口到一处。
|
||||
/// </summary>
|
||||
private decimal ResolveFloatRate(swap_position position, DateTime date, decimal fallback)
|
||||
{
|
||||
if (string.IsNullOrEmpty(position.FloatRateUnderlyingCode)) return fallback;
|
||||
var fixingDate = IndexFixerBase.GetFixingDate(date, position.interest_rule);
|
||||
if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing))
|
||||
return fixing != 0m ? fixing : fallback;
|
||||
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按重置周期切分利率段,每段记录 all-in 利率(spread+fixing)。返回 (分段列表, 末段浮动利率)。
|
||||
/// fetchAfterDate: 仅该日期之后的重置日才取 FR007(单利传 ValueDate,复利传 null 全程取)。
|
||||
@@ -1341,19 +1292,8 @@ namespace YLErp.Modules.SwapModule
|
||||
for (int i = 0; i <= calcDays; i += interestPeriod)
|
||||
{
|
||||
var resetDate = startDate.AddDays(i);
|
||||
if ((fetchAfterDate == null || resetDate > fetchAfterDate.Value)
|
||||
&& !string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
|
||||
{
|
||||
var fixingDate = IndexFixerBase.GetFixingDate(resetDate, position.interest_rule);
|
||||
if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing))
|
||||
{
|
||||
if (fixing != 0m) currentFloat = fixing;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
|
||||
}
|
||||
}
|
||||
if (fetchAfterDate == null || resetDate > fetchAfterDate.Value)
|
||||
currentFloat = ResolveFloatRate(position, resetDate, currentFloat);
|
||||
rates.Add((resetDate, spread + currentFloat));
|
||||
}
|
||||
return (rates, currentFloat);
|
||||
@@ -1371,7 +1311,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <param name="annualDays">年化天数</param>
|
||||
/// <returns></returns>
|
||||
public void CalcDailyCompoundInterest(DateTime endDate, swap_position position, decimal principal, swap_flow_event flowEvent,
|
||||
int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast,
|
||||
int annualDays, decimal floateRate, decimal closePercent, bool calcFirst, bool calcLast,
|
||||
ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m, decimal resetCarryInterest = 0m)
|
||||
{
|
||||
var startDate = position.PosiStartDate;
|
||||
@@ -1384,7 +1324,7 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
// 纯函数复利计息:分段重置日并本金 + resetCarryInterest + 扣 consumedInterest
|
||||
var interestTrace = new AccrualTrace();
|
||||
var result = FundingLegAccrual.AccrueCompoundPeriod(
|
||||
var result = CompoundInterestAccrual.AccruePeriod(
|
||||
notional: principal,
|
||||
segmentRates: segmentRates,
|
||||
startDate: startDate,
|
||||
@@ -1410,12 +1350,12 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <summary>
|
||||
/// 计算单利 盘中(按重置天数分段,每段使用对应浮动利率)
|
||||
/// </summary>
|
||||
public void CalcDailySimpleInterest(eod_swap_position preEodPosition, DateTime endDate, swap_position position, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m)
|
||||
public void CalcDailySimpleInterest(eod_swap_position preEodPosition, DateTime endDate, swap_position position, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m)
|
||||
{
|
||||
var startDate = position.PosiStartDate;
|
||||
int interestPeriod = position.interest_rest_days ?? 1;
|
||||
|
||||
// orginPv 是路径相关参考本金(资金腿=上一日终浮动端名义本金;保证金腿=上一日终保证金余额)。
|
||||
// orginPv 是路径相关参考本金(资金腿=上一日终浮动端名义本金)。保证金腿已走 CalcMarginInterest,不经此方法。
|
||||
// 单利差分:accrualBasis 全程恒定 = 昨日终滚动基数 + 当日名义本金 - 参考本金。
|
||||
var accrualBasis = preEodPosition.TdInterestPrincipal + posiPrincipal - orginPv;
|
||||
|
||||
@@ -1426,7 +1366,7 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
// 纯函数计息:Accrued=缩放累计(InterestAmount),AccruedToday=未缩放累计(TdInterestAmount)
|
||||
var interestTrace = new AccrualTrace();
|
||||
var result = FundingLegAccrual.AccrueSimplePeriod(
|
||||
var result = SimpleInterestAccrual.AccruePeriod(
|
||||
priorAccrued: preEodPosition.InterestProfitSum * closePercent,
|
||||
notional: accrualBasis,
|
||||
unwindFraction: closePercent,
|
||||
@@ -1460,26 +1400,14 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <param name="isAnnualized">是否年化</param>
|
||||
/// <param name="annualDays">年化天数</param>
|
||||
/// <returns></returns>
|
||||
public void CalcDailyCompoundInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, ref decimal InterestAmount, ref decimal TdInterestAmount)
|
||||
public void CalcDailyCompoundInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, decimal floateRate, decimal closePercent, ref decimal InterestAmount, ref decimal TdInterestAmount)
|
||||
{
|
||||
int interestPeriod = position.interest_rest_days ?? 1;
|
||||
var isResetDay = (endDate - tradeDate).Days % interestPeriod == 0;
|
||||
|
||||
// 重置日按 interest_rule 重新定盘浮动利率(GLMS-JIATT-20260805 根因——
|
||||
// 重置日=平仓日必须用新利率,否则沿用旧周期利率并污染后续 EOD)。非重置日沿用 floateRate。
|
||||
decimal effectiveFloat = floateRate;
|
||||
if (isResetDay && !string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
|
||||
{
|
||||
var fixingDate = IndexFixerBase.GetFixingDate(endDate, position.interest_rule);
|
||||
if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing))
|
||||
{
|
||||
if (fixing != 0m) effectiveFloat = fixing;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
|
||||
}
|
||||
}
|
||||
var effectiveFloat = isResetDay ? ResolveFloatRate(position, endDate, floateRate) : floateRate;
|
||||
flowEvent.FloatRate = effectiveFloat;
|
||||
|
||||
// remainingFraction:重置日把上一日终待实现利息按本次平仓基数分摊(EOD 全量为 1)。
|
||||
@@ -1487,21 +1415,13 @@ namespace YLErp.Modules.SwapModule
|
||||
? Math.Max(0m, Math.Min(1m, principal / posiPrincipal))
|
||||
: 1m;
|
||||
|
||||
// 纯数学下沉至 FundingLegAccrual.AccrueCompoundEod(DDD 命名 + 末位生产精度 12 舍入)。
|
||||
var isFixedLeg = string.IsNullOrEmpty(position.FloatRateUnderlyingCode);
|
||||
var legRate = isFixedLeg
|
||||
? FundingLegRate.Fixed(flowEvent.InterestRate)
|
||||
: FundingLegRate.Floating(flowEvent.InterestRate, effectiveFloat);
|
||||
var accrualPolicy = new AccrualPolicy(
|
||||
convention: AccrualBoundary.Both,
|
||||
isCompound: true,
|
||||
resetPeriodDays: interestPeriod,
|
||||
annualDays: annualDays,
|
||||
isAnnualized: position.IsAnnualized);
|
||||
// 纯数学下沉至 CompoundInterestAccrual.AccrueEod(DDD 命名 + 末位生产精度 12 舍入)。
|
||||
var legRate = FundingLegRate.Build(position, flowEvent.InterestRate, effectiveFloat);
|
||||
var accrualPolicy = AccrualPolicy.BuildEod(position, annualDays, isCompound: true);
|
||||
|
||||
// 完整计息 trace:前后日期/基数/利率/重置标志全过程,经 SwapCalcTrace 常驻落盘(关键路径日志)。
|
||||
var interestTrace = new AccrualTrace();
|
||||
var result = FundingLegAccrual.AccrueCompoundEod(
|
||||
var result = CompoundInterestAccrual.AccrueEod(
|
||||
priorAccrued: preEodPosition.InterestProfitSum,
|
||||
priorNotional: preEodPosition.TdInterestPrincipal,
|
||||
notional: posiPrincipal,
|
||||
@@ -1516,7 +1436,7 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
// flowEvent.InterestPrincipal:当日计息基数(已按平仓比例缩放)——下游 EOD 用它播种次日 TdInterestPrincipal。
|
||||
// 复用 CompoundEodBasis 单一真相源(与 AccrueCompoundEod 内部同一公式)。
|
||||
flowEvent.InterestPrincipal = FundingLegAccrual.CompoundEodBasis(
|
||||
flowEvent.InterestPrincipal = CompoundInterestAccrual.EodBasis(
|
||||
isResetDay, posiPrincipal, preEodPosition.InterestProfitSum, remainingFraction,
|
||||
preEodPosition.TdInterestPrincipal) * closePercent;
|
||||
|
||||
@@ -1527,7 +1447,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <summary>
|
||||
/// 计算单利 收盘(按重置天数分段,每段使用对应浮动利率)
|
||||
/// </summary>
|
||||
public void CalcDailySimpleInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, ref decimal InterestAmount, ref decimal TdInterestAmount)
|
||||
public void CalcDailySimpleInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, decimal floateRate, decimal closePercent, ref decimal InterestAmount, ref decimal TdInterestAmount)
|
||||
{
|
||||
// 首次操作(preEod.id == 0):计息基数按存量本金初始化——保留旧行为(含对 preEod 的就地修正)。
|
||||
if (preEodPosition.id == 0)
|
||||
@@ -1537,39 +1457,18 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
// 取率:重置日按 interest_rule 重新定盘浮动利率(GLMS-JIATT-20260805 根因——
|
||||
// 重置日=平仓日必须用新利率,否则沿用旧周期利率并污染后续 EOD)。
|
||||
decimal effectiveFloat = floateRate;
|
||||
int interestPeriod = position.interest_rest_days ?? 1;
|
||||
if ((endDate - tradeDate).Days % interestPeriod == 0
|
||||
&& !string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
|
||||
{
|
||||
var fixingDate = IndexFixerBase.GetFixingDate(endDate, position.interest_rule);
|
||||
if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing))
|
||||
{
|
||||
if (fixing != 0m) effectiveFloat = fixing;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
|
||||
}
|
||||
}
|
||||
var isResetDay = (endDate - tradeDate).Days % interestPeriod == 0;
|
||||
var effectiveFloat = isResetDay ? ResolveFloatRate(position, endDate, floateRate) : floateRate;
|
||||
|
||||
flowEvent.FloatRate = effectiveFloat;
|
||||
|
||||
// 纯数学下沉至 FundingLegAccrual(DDD 命名 + 末位生产精度 12 舍入),行为与上版逐字对齐。
|
||||
// 利率构成按腿型封装:固定腿 → FixedRate;浮动腿 → Spread + IndexFixing(沿用旧实现 InterestRate+浮动利率 的口径)。
|
||||
var isFixedLeg = string.IsNullOrEmpty(position.FloatRateUnderlyingCode);
|
||||
var legRate = isFixedLeg
|
||||
? FundingLegRate.Fixed(flowEvent.InterestRate)
|
||||
: FundingLegRate.Floating(flowEvent.InterestRate, effectiveFloat);
|
||||
var accrualPolicy = new AccrualPolicy(
|
||||
convention: AccrualBoundary.Both,
|
||||
isCompound: false,
|
||||
resetPeriodDays: position.interest_rest_days ?? 1,
|
||||
annualDays: annualDays,
|
||||
isAnnualized: position.IsAnnualized);
|
||||
// 纯数学下沉至 SimpleInterestAccrual(末位生产精度 12 舍入)。
|
||||
var legRate = FundingLegRate.Build(position, flowEvent.InterestRate, effectiveFloat);
|
||||
var accrualPolicy = AccrualPolicy.BuildEod(position, annualDays, isCompound: false);
|
||||
// 完整计息 trace:收集器由适配器创建,随后经 SwapCalcTrace 常驻落盘(关键路径日志,无条件)。
|
||||
var interestTrace = new AccrualTrace();
|
||||
var result = FundingLegAccrual.AccrueSimpleEod(
|
||||
var result = SimpleInterestAccrual.AccrueEod(
|
||||
priorAccrued: preEodPosition.InterestProfitSum,
|
||||
priorNotional: preEodPosition.TdInterestPrincipal,
|
||||
unwindFraction: closePercent,
|
||||
@@ -1594,16 +1493,16 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
throw new ServiceException("未找到交易信息");
|
||||
}
|
||||
NormalizeEventUnwindDate(unwindData);
|
||||
NormalizeNotionalValues(unwindData);
|
||||
UnwindNormalizer.NormalizeEventUnwindDate(unwindData);
|
||||
UnwindNormalizer.NormalizeNotionalValues(unwindData);
|
||||
NormalizeManualSettlementAmounts(unwindData, (int)SwapEventTypeEnum.平仓, "系统操作_平仓");
|
||||
//CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); //去掉平仓收盘限制
|
||||
// 前端按"占期初(original)"语义传 ClosePercent(A);后端全链路按"占剩余(remaining)"语义(B)消费。
|
||||
// 入口统一转换为 B,落库展示用的 A 由 SaveSwapDealInternal 还原。
|
||||
unwindData.ClosePercent = ToRemainingClosePercent(unwindData.ClosePercent, unwindData.NotionalValue, unwindData.PosiNotionalValue);
|
||||
if (NormalizeFullCloseRequest(unwindData))
|
||||
if (UnwindNormalizer.NormalizeFullCloseRequest(unwindData))
|
||||
{
|
||||
RecalculateNormalizedUnwindAmounts(unwindData);
|
||||
UnwindNormalizer.RecalculateNormalizedUnwindAmounts(unwindData);
|
||||
}
|
||||
ValidateFrontendPnL(unwindData, isIncome: false); // 只读校验告警,不阻断交易
|
||||
bool cofirm = false;
|
||||
@@ -1616,7 +1515,7 @@ namespace YLErp.Modules.SwapModule
|
||||
var eventId = SaveSwapDeal(unwindData, (int)SwapEventTypeEnum.平仓, clientCashId, "系统操作_平仓");
|
||||
var remainingStockEqvNotional = Math.Round(td.StockEqvNotional - Convert.ToDouble(unwindData.CloseNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
var remainingTradeAmount = td.TradeAmount - Convert.ToDouble(unwindData.CloseQty);
|
||||
var isFullClose = IsFullCloseAfterDeduction(unwindData, remainingStockEqvNotional, remainingTradeAmount);
|
||||
var isFullClose = UnwindNormalizer.IsFullCloseAfterDeduction(unwindData, remainingStockEqvNotional, remainingTradeAmount);
|
||||
if (isFullClose)
|
||||
{
|
||||
td.TradeStatus = "已平仓";
|
||||
@@ -1978,84 +1877,6 @@ namespace YLErp.Modules.SwapModule
|
||||
unwindData.SwapRealizedPnL = Math.Round(unwindData.SwapRealizedPnL, 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
/// <summary>
|
||||
/// 多空组合平仓
|
||||
/// </summary>
|
||||
/// <param name="unwindData"></param>
|
||||
/// <exception cref="ServiceException"></exception>
|
||||
public void SwapLongShortUnwind(UnwindData unwindData)
|
||||
{
|
||||
var td = DbContext.trade.Find(unwindData.SwapTradeId);
|
||||
if (td == null)
|
||||
{
|
||||
throw new ServiceException("未找到交易信息");
|
||||
}
|
||||
NormalizeEventUnwindDate(unwindData);
|
||||
unwindData.SwapRealizedPnL = unwindData.SwapCloseAmount;
|
||||
NormalizeManualSettlementAmounts(unwindData, (int)SwapEventTypeEnum.平仓, "系统操作_平仓");
|
||||
var trans = DbContext.Database.BeginTransaction();
|
||||
try
|
||||
{
|
||||
int clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(unwindData.SwapCloseAmount), ClientCashInCashOut.系统操作_平仓费, unwindData.ValueDate);
|
||||
RecordMarginCashFlow(td, unwindData);
|
||||
SaveSwapDeal(unwindData, (int)SwapEventTypeEnum.平仓, clientCashId, "系统操作_平仓");
|
||||
td.UnWindDate = unwindData.UnwindDate;
|
||||
td.StockEqvNotional = 0;
|
||||
td.TradeStatus = "已平仓";
|
||||
DbContext.SaveChanges();
|
||||
trans.Commit();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
trans.Rollback();
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
trans.Dispose();
|
||||
}
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
/// 多空组合互换
|
||||
/// </summary>
|
||||
/// <param name="swap_Deal"></param>
|
||||
/// <exception cref="ServiceException"></exception>
|
||||
public void SwapLongShort(UnwindData unwindData)
|
||||
{
|
||||
var td = DbContext.trade.Find(unwindData.SwapTradeId);
|
||||
if (td == null)
|
||||
{
|
||||
throw new ServiceException("未找到交易信息");
|
||||
}
|
||||
NormalizeEventUnwindDate(unwindData);
|
||||
unwindData.SwapRealizedPnL = unwindData.SwapCloseAmount;
|
||||
NormalizeManualSettlementAmounts(unwindData, (int)SwapEventTypeEnum.互换, "系统操作_互换");
|
||||
var trans = DbContext.Database.BeginTransaction();
|
||||
try
|
||||
{
|
||||
int clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(unwindData.SwapCloseAmount), ClientCashInCashOut.系统操作_互换, unwindData.ValueDate);
|
||||
SaveSwapDeal(unwindData, (int)SwapEventTypeEnum.互换, clientCashId, "系统操作_互换");
|
||||
td.UnWindDate = unwindData.UnwindDate;
|
||||
if (td.ExerciseDate <= unwindData.ValueDate)
|
||||
{
|
||||
td.Notional = 0;
|
||||
td.StockEqvNotional = 0;
|
||||
td.TradeStatus = "已到期";
|
||||
}
|
||||
DbContext.SaveChanges();
|
||||
trans.Commit();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
trans.Rollback();
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
trans.Dispose();
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 互换
|
||||
/// </summary>
|
||||
/// <param name="swap_Deal"></param>
|
||||
@@ -2067,7 +1888,7 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
throw new ServiceException("未找到交易信息");
|
||||
}
|
||||
NormalizeEventUnwindDate(unwindData);
|
||||
UnwindNormalizer.NormalizeEventUnwindDate(unwindData);
|
||||
ValidateIncomeValueDate(unwindData, td);
|
||||
NormalizeManualSettlementAmounts(unwindData, (int)SwapEventTypeEnum.互换, "系统操作_互换");
|
||||
//CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); //去掉平仓收盘限制
|
||||
@@ -2105,8 +1926,8 @@ namespace YLErp.Modules.SwapModule
|
||||
throw new Exception("该笔交易状态为平仓待复核,未找到相关记录,请检查该笔交易是否有效");
|
||||
}
|
||||
swapEvent.unwindData = JsonConvert.DeserializeObject<UnwindData>(swapEvent.EventData);
|
||||
NormalizeEventUnwindDate(swapEvent.unwindData);
|
||||
NormalizeNotionalValues(swapEvent.unwindData);
|
||||
UnwindNormalizer.NormalizeEventUnwindDate(swapEvent.unwindData);
|
||||
UnwindNormalizer.NormalizeNotionalValues(swapEvent.unwindData);
|
||||
// Stored events keep display ratio A; approval calculations consume remaining ratio B.
|
||||
swapEvent.unwindData.ClosePercent = ToRemainingClosePercent(
|
||||
swapEvent.unwindData.ClosePercent,
|
||||
@@ -2121,9 +1942,9 @@ namespace YLErp.Modules.SwapModule
|
||||
swapEvent.unwindData.FlowEvents = flowList;
|
||||
if (eventType == (int)SwapEventTypeEnum.平仓)
|
||||
{
|
||||
if (NormalizeFullCloseRequest(swapEvent.unwindData))
|
||||
if (UnwindNormalizer.NormalizeFullCloseRequest(swapEvent.unwindData))
|
||||
{
|
||||
RecalculateNormalizedUnwindAmounts(swapEvent.unwindData);
|
||||
UnwindNormalizer.RecalculateNormalizedUnwindAmounts(swapEvent.unwindData);
|
||||
}
|
||||
}
|
||||
if (eventType == (int)SwapEventTypeEnum.互换)
|
||||
@@ -2152,7 +1973,7 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
var remainingStockEqvNotional = Math.Round(td.StockEqvNotional - Convert.ToDouble(swapEvent.unwindData.CloseNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
var remainingTradeAmount = td.TradeAmount - Convert.ToDouble(swapEvent.unwindData.CloseQty);
|
||||
var isFullClose = IsFullCloseAfterDeduction(swapEvent.unwindData, remainingStockEqvNotional, remainingTradeAmount);
|
||||
var isFullClose = UnwindNormalizer.IsFullCloseAfterDeduction(swapEvent.unwindData, remainingStockEqvNotional, remainingTradeAmount);
|
||||
if (isFullClose)
|
||||
{
|
||||
td.TradeStatus = "已平仓";
|
||||
@@ -2197,7 +2018,7 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
throw new ServiceException("未找到交易信息");
|
||||
}
|
||||
NormalizeEventUnwindDate(unwindData);
|
||||
UnwindNormalizer.NormalizeEventUnwindDate(unwindData);
|
||||
if (eventType == (int)SwapEventTypeEnum.互换)
|
||||
{
|
||||
ValidateIncomeValueDate(unwindData, td);
|
||||
@@ -2211,9 +2032,9 @@ namespace YLErp.Modules.SwapModule
|
||||
unwindData.ClosePercent = ToRemainingClosePercent(unwindData.ClosePercent, unwindData.NotionalValue, unwindData.PosiNotionalValue);
|
||||
if (eventType == (int)SwapEventTypeEnum.平仓)
|
||||
{
|
||||
if (NormalizeFullCloseRequest(unwindData))
|
||||
if (UnwindNormalizer.NormalizeFullCloseRequest(unwindData))
|
||||
{
|
||||
RecalculateNormalizedUnwindAmounts(unwindData);
|
||||
UnwindNormalizer.RecalculateNormalizedUnwindAmounts(unwindData);
|
||||
}
|
||||
}
|
||||
string action = eventType == (int)SwapEventTypeEnum.互换 ? ClientCashInCashOut.系统操作_互换 : ClientCashInCashOut.系统操作_平仓费;
|
||||
@@ -2246,11 +2067,6 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
}
|
||||
|
||||
private static void NormalizeEventUnwindDate(UnwindData unwindData)
|
||||
{
|
||||
unwindData.UnwindDate = unwindData.ValueDate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存平仓/互换事件
|
||||
/// </summary>
|
||||
@@ -2264,7 +2080,7 @@ namespace YLErp.Modules.SwapModule
|
||||
throw new ServiceException("未找到交易信息");
|
||||
}
|
||||
var flowList = new List<swap_flow_event>(unwindData.FlowEvents);
|
||||
NormalizeSettledInterestAmounts(flowList, eventType, eventResason);
|
||||
UnwindNormalizer.NormalizeSettledInterestAmounts(flowList, eventType, eventResason);
|
||||
unwindData.FlowEvents.Clear();
|
||||
// 落库展示用"占期初(original)"语义(A);计算链(费用递减/全平判定)用"占剩余(remaining)"语义(B)。
|
||||
// 序列化前把 ClosePercent 还原为 A,序列化后立即还原回 B 供后续使用。
|
||||
|
||||
@@ -48,72 +48,13 @@ namespace YLErp.Modules.SwapModule
|
||||
: ConsGlobal.SwapDeliveryPriceRound;
|
||||
}
|
||||
|
||||
// 日终利息待实现需跨日累计,按表设计保留 12 位;已实现结算仍按金额两位处理。
|
||||
private const int EodInterestStoragePrecision = 12;
|
||||
|
||||
private static decimal RoundMoney(decimal value)
|
||||
{
|
||||
return Math.Round(value, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
private static decimal RoundEodInterest(decimal value)
|
||||
{
|
||||
return Math.Round(value, EodInterestStoragePrecision, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仅在写入 eod_swap_position 前统一快照精度。
|
||||
/// 浮动腿收益最终以金额两位展示和存储;利息腿的待实现、计息基数及利率保留 12 位,
|
||||
/// 使部分结算后的尾差可继续参与后续计息。
|
||||
/// </summary>
|
||||
private static void NormalizeEodPositionForStorage(eod_swap_position position)
|
||||
{
|
||||
if (string.IsNullOrEmpty(position.UnderlyingCode))
|
||||
{
|
||||
// 利息腿没有标的代码:待实现字段保留高精度,已实现结算字段收敛到金额两位。
|
||||
position.InterestPrincipalFix = RoundEodInterest(position.InterestPrincipalFix);
|
||||
position.InterestRateDefault = RoundEodInterest(position.InterestRateDefault);
|
||||
position.InterestFeePending = RoundEodInterest(position.InterestFeePending);
|
||||
position.TdInterestPrincipal = RoundEodInterest(position.TdInterestPrincipal);
|
||||
position.TdInterestRate = RoundEodInterest(position.TdInterestRate);
|
||||
position.TdInterestIncome = RoundEodInterest(position.TdInterestIncome);
|
||||
position.TdInterestFee = RoundEodInterest(position.TdInterestFee);
|
||||
position.InterestIncomeSum = RoundEodInterest(position.InterestIncomeSum);
|
||||
position.InterestFeeSum = RoundEodInterest(position.InterestFeeSum);
|
||||
position.InterestProfitSum = RoundEodInterest(position.InterestProfitSum);
|
||||
position.FloatRate = RoundEodInterest(position.FloatRate);
|
||||
position.SwapPositionValue = RoundEodInterest(position.SwapPositionValue);
|
||||
position.TdCloseInterest = RoundMoney(position.TdCloseInterest);
|
||||
position.TdCloseInterestFee = RoundMoney(position.TdCloseInterestFee);
|
||||
position.RealizedInterest = RoundMoney(position.RealizedInterest);
|
||||
position.RealizedInterestFee = RoundMoney(position.RealizedInterestFee);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 浮动腿有标的代码:其损益作为金额结果落库,统一按两位四舍五入。
|
||||
position.TdPosiDividend = RoundMoney(position.TdPosiDividend);
|
||||
position.PosiMtmPnL = RoundMoney(position.PosiMtmPnL);
|
||||
position.PosiDividendSum = RoundMoney(position.PosiDividendSum);
|
||||
position.PosiFeePending = RoundMoney(position.PosiFeePending);
|
||||
position.PosiProfitSum = RoundMoney(position.PosiProfitSum);
|
||||
position.TdCloseMtmPnl = RoundMoney(position.TdCloseMtmPnl);
|
||||
position.TdCloseDividend = RoundMoney(position.TdCloseDividend);
|
||||
position.TdCloseFee = RoundMoney(position.TdCloseFee);
|
||||
position.RealizedMtmPnL = RoundMoney(position.RealizedMtmPnL);
|
||||
position.RealizedDividend = RoundMoney(position.RealizedDividend);
|
||||
position.RealizedFee = RoundMoney(position.RealizedFee);
|
||||
position.SwapPositionValue = RoundMoney(position.SwapPositionValue);
|
||||
}
|
||||
position.RealizedPnl = RoundMoney(position.RealizedPnl);
|
||||
}
|
||||
|
||||
#region 可测试化接缝(Seams)——override 这些虚方法可在测试中替换 DB/外部调用,生产代码行为不变
|
||||
|
||||
/// <summary>持久化 eod 持仓记录(生产: DbContext.Add;测试: 收集到列表)</summary>
|
||||
protected virtual void PersistEodSwapPosition(eod_swap_position position)
|
||||
{
|
||||
// 所有新增或更新的日终持仓都经过此入口,避免不同日终分支出现精度差异。
|
||||
NormalizeEodPositionForStorage(position);
|
||||
EodPnlCalculator.NormalizeEodPositionForStorage(position);
|
||||
var storagePriceRound = GetStorageDeliveryPriceRound(position.UnderlyingInstrumentType, position.UnderlyingCode);
|
||||
position.PosiGrossPrice = Math.Round(position.PosiGrossPrice, storagePriceRound, MidpointRounding.AwayFromZero);
|
||||
position.UnderlyingPrice = Math.Round(position.UnderlyingPrice, storagePriceRound, MidpointRounding.AwayFromZero);
|
||||
@@ -193,6 +134,12 @@ namespace YLErp.Modules.SwapModule
|
||||
return new SwapEventService(this).AddSwapEventDate(tradeDate, swapTradeId, eventType, data, clientCashId, save, reason);
|
||||
}
|
||||
|
||||
/// <summary>持久化互换流水事件(生产: DbContext.swap_flow_event.Add;测试: 收集到列表)</summary>
|
||||
protected virtual void PersistFlowEvent(swap_flow_event flowEvent)
|
||||
{
|
||||
DbContext.swap_flow_event.Add(flowEvent);
|
||||
}
|
||||
|
||||
/// <summary>在事务中执行(生产: BeginTransaction/Commit/Rollback;测试: 直接执行不包事务)</summary>
|
||||
protected virtual void ExecuteInTransaction(Action action)
|
||||
{
|
||||
@@ -662,7 +609,7 @@ namespace YLErp.Modules.SwapModule
|
||||
decimal premiumTotal = 0;
|
||||
premiumInterests.ForEach(x =>
|
||||
{
|
||||
var ratio = x.InterestDirection == (int)SwapDirectionEnum.收取 ? -1 : 1;
|
||||
var ratio = -DirectionRatio.ReceivePay(x.InterestDirection);
|
||||
premiumTotal += x.InterestClosePnL * ratio;
|
||||
});
|
||||
unwindData.SwapMarginRebatePnl = premiumTotal;
|
||||
@@ -670,7 +617,7 @@ namespace YLErp.Modules.SwapModule
|
||||
decimal interestTotal = 0;
|
||||
interestLegs.ForEach(x =>
|
||||
{
|
||||
var ratio = x.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;
|
||||
var ratio = DirectionRatio.ReceivePay(x.InterestDirection);
|
||||
interestTotal += x.InterestClosePnL * ratio;
|
||||
});
|
||||
unwindData.SwapCloseAmount = interestTotal ;
|
||||
@@ -797,24 +744,26 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
unwindData.ClientCashIds = clientCashIds;
|
||||
string data = JsonConvert.SerializeObject(unwindData);
|
||||
var swapEvent = new SwapEventService(this).AddSwapEventDate(unwindData.ValueDate, unwindData.SwapTradeId, (int)SwapEventTypeEnum.自动互换, data, clientCashId, true, "系统操作-自动互换");//将互换总额存入事件
|
||||
// 走虚方法 AddSwapEvent(与 ComposePage:800 一致),让测试可 override 捕获事件;
|
||||
// 默认实现仍是 new SwapEventService(this).AddSwapEventDate,生产行为不变。
|
||||
var swapEvent = AddSwapEvent(unwindData.ValueDate, unwindData.SwapTradeId, (int)SwapEventTypeEnum.自动互换, data, clientCashId, true, "系统操作-自动互换");//将互换总额存入事件
|
||||
if (flowEvents!=null)
|
||||
{
|
||||
flowEvents.ForEach(x =>
|
||||
{
|
||||
x.EventId = swapEvent.id;
|
||||
DbContext.swap_flow_event.Add(x);
|
||||
PersistFlowEvent(x);
|
||||
});
|
||||
UpdateInitalPostion(flowEvents, td.id);
|
||||
}
|
||||
|
||||
|
||||
// 保存分红事件
|
||||
if (dividendEvents != null)
|
||||
{
|
||||
dividendEvents.ForEach(x =>
|
||||
{
|
||||
x.EventId = swapEvent.id;
|
||||
DbContext.swap_flow_event.Add(x);
|
||||
PersistFlowEvent(x);
|
||||
});
|
||||
UpdateInitalPostion(dividendEvents, td.id);
|
||||
}
|
||||
@@ -1041,11 +990,7 @@ namespace YLErp.Modules.SwapModule
|
||||
eodPayPosition.PosiMatuirityDate = td.ExerciseDate.Value;
|
||||
}
|
||||
var tradeExtend = td.trade_extend.ExtendObj;
|
||||
decimal ratio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;//收取为正,支付为负
|
||||
if (MarginModes.Contains(position.InterestMode))
|
||||
{
|
||||
ratio = -ratio;
|
||||
}
|
||||
var ratio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
|
||||
if (newEodPayPosition == null)
|
||||
{
|
||||
newEodPayPosition = new eod_swap_position();
|
||||
@@ -1085,18 +1030,16 @@ namespace YLErp.Modules.SwapModule
|
||||
newEodPayPosition.TdCloseInterest = flowEvents.Sum(x => x.InterestAmount);
|
||||
newEodPayPosition.TdCloseInterestFee = newEodPayPosition.TdInterestFee;
|
||||
//持仓内容-利息腿-损益统计(本方视角)
|
||||
var intersetAcmount = newEodPayPosition.TdInterestPrincipal * (newEodPayPosition.TdInterestRate + newEodPayPosition.FloatRate);
|
||||
if (position.IsAnnualized)
|
||||
{
|
||||
intersetAcmount /= tradeExtend.AnnualDays;
|
||||
}
|
||||
var intersetAcmount = InterestIncomeCalc.DailyAccrual(
|
||||
newEodPayPosition.TdInterestPrincipal, newEodPayPosition.TdInterestRate, newEodPayPosition.FloatRate,
|
||||
newEodPayPosition.IsAnnualized, tradeExtend.AnnualDays);
|
||||
newEodPayPosition.TdInterestIncome = intersetAcmount;// 要算一下当天产生的利息
|
||||
var interestIncomeBeforeSettlement = eodPayPosition.InterestIncomeSum + newEodPayPosition.TdInterestIncome;
|
||||
var interestFeeBeforeSettlement = eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee;
|
||||
var isMaturityFinalSettlement = valueDate.Date >= td.ExerciseDate.Value.Date
|
||||
&& flowEvents.Any()
|
||||
&& RoundMoney(interestIncomeBeforeSettlement) == RoundMoney(newEodPayPosition.TdCloseInterest)
|
||||
&& RoundMoney(interestFeeBeforeSettlement) == RoundMoney(newEodPayPosition.TdCloseInterestFee);
|
||||
&& EodPnlCalculator.RoundMoney(interestIncomeBeforeSettlement) == EodPnlCalculator.RoundMoney(newEodPayPosition.TdCloseInterest)
|
||||
&& EodPnlCalculator.RoundMoney(interestFeeBeforeSettlement) == EodPnlCalculator.RoundMoney(newEodPayPosition.TdCloseInterestFee);
|
||||
|
||||
if (isMaturityFinalSettlement)
|
||||
{
|
||||
@@ -1107,19 +1050,20 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
else
|
||||
{
|
||||
newEodPayPosition.InterestIncomeSum = RoundEodInterest(interestIncomeBeforeSettlement - newEodPayPosition.TdCloseInterest);
|
||||
newEodPayPosition.InterestFeeSum = RoundEodInterest(interestFeeBeforeSettlement - newEodPayPosition.TdCloseInterestFee);
|
||||
newEodPayPosition.InterestIncomeSum = EodPnlCalculator.RoundEodInterest(interestIncomeBeforeSettlement - newEodPayPosition.TdCloseInterest);
|
||||
newEodPayPosition.InterestFeeSum = EodPnlCalculator.RoundEodInterest(interestFeeBeforeSettlement - newEodPayPosition.TdCloseInterestFee);
|
||||
}
|
||||
newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum;
|
||||
//持仓价值
|
||||
newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio);
|
||||
|
||||
//累计已实现
|
||||
newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio;
|
||||
newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee;
|
||||
var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio);
|
||||
newEodPayPosition.RealizedInterest = rolled.Interest;
|
||||
newEodPayPosition.RealizedInterestFee = rolled.Fee;
|
||||
SetFixedLegRealizedPnl(newEodPayPosition);
|
||||
var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true,
|
||||
position.InterestDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell);
|
||||
DirectionRatio.RateType(position.InterestDirection));
|
||||
newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate);
|
||||
PersistEodSwapPosition(newEodPayPosition);
|
||||
}
|
||||
@@ -1176,11 +1120,7 @@ namespace YLErp.Modules.SwapModule
|
||||
var tradeExtend = td.trade_extend.ExtendObj;
|
||||
decimal posiNotionalValue = posiLongNotional + posiShortNational;
|
||||
decimal closePercent = 1;
|
||||
decimal ratio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;//收取为正,支付为负
|
||||
if (MarginModes.Contains(position.InterestMode))
|
||||
{
|
||||
ratio = -ratio;
|
||||
}
|
||||
var ratio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
|
||||
if (eodPayPosition == null)
|
||||
{
|
||||
eodPayPosition = new eod_swap_position();
|
||||
@@ -1198,7 +1138,7 @@ namespace YLErp.Modules.SwapModule
|
||||
positions.Add(position);
|
||||
List<eod_swap_position> preEodPositions = new List<eod_swap_position>();
|
||||
preEodPositions.Add(eodPayPosition);
|
||||
var interestModes = new List<int>() { (int)InterestModeEnum.固定值, (int)InterestModeEnum.初始预付金, (int)InterestModeEnum.追加预付金 };
|
||||
var interestModes = MarginModes.FixedAmountAndMargin;
|
||||
if (interestModes.Contains(position.InterestMode))
|
||||
{
|
||||
orginPv = eodPayPosition.InterestPrincipalFix;
|
||||
@@ -1211,8 +1151,8 @@ namespace YLErp.Modules.SwapModule
|
||||
// 日终快照仍使用上面的高精度应结金额计算待实现尾差,避免把舍入差提前丢掉。
|
||||
interests.ForEach(x =>
|
||||
{
|
||||
x.InterestAmount = RoundMoney(x.InterestAmount);
|
||||
x.InterestClosePnL = RoundMoney(x.InterestClosePnL);
|
||||
x.InterestAmount = EodPnlCalculator.RoundMoney(x.InterestAmount);
|
||||
x.InterestClosePnL = EodPnlCalculator.RoundMoney(x.InterestClosePnL);
|
||||
});
|
||||
decimal settledInterestAmount = interests.Sum(x => x.InterestAmount);
|
||||
|
||||
@@ -1249,20 +1189,21 @@ namespace YLErp.Modules.SwapModule
|
||||
// 到期自动互换是最后一次自动结算:两位实际金额已落流水/资金,待实现不再滚入下一日。
|
||||
newEodPayPosition.InterestIncomeSum = isMaturityFinalAutoSettlement
|
||||
? 0
|
||||
: RoundEodInterest(interestAmountBeforeSettlement - settledInterestAmount);
|
||||
: EodPnlCalculator.RoundEodInterest(interestAmountBeforeSettlement - settledInterestAmount);
|
||||
newEodPayPosition.InterestFeeSum = isMaturityFinalAutoSettlement
|
||||
? 0
|
||||
: RoundEodInterest(eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee - newEodPayPosition.TdCloseInterestFee);
|
||||
: EodPnlCalculator.RoundEodInterest(eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee - newEodPayPosition.TdCloseInterestFee);
|
||||
newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum;
|
||||
//持仓价值
|
||||
newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio);
|
||||
|
||||
//累计已实现
|
||||
newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio;
|
||||
newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee;
|
||||
var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio);
|
||||
newEodPayPosition.RealizedInterest = rolled.Interest;
|
||||
newEodPayPosition.RealizedInterestFee = rolled.Fee;
|
||||
SetFixedLegRealizedPnl(newEodPayPosition);
|
||||
var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true,
|
||||
position.InterestDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell);
|
||||
DirectionRatio.RateType(position.InterestDirection));
|
||||
newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate);
|
||||
PersistEodSwapPosition(newEodPayPosition);
|
||||
Log.Info($"the last newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}");
|
||||
@@ -1293,14 +1234,15 @@ namespace YLErp.Modules.SwapModule
|
||||
var tradeExtend = td.trade_extend.ExtendObj;
|
||||
// oriPosiNotionalValue 是平仓前规模,posiNotionalValue 是收盘后剩余规模,closeNational 是本次关闭规模。
|
||||
// 例如 30% 平仓:303139117.80 = 212197382.46 + 90941735.34。
|
||||
// 注意:此处的 posiNotionalValue 与盘中 GetUnwindInterests 传给 GetInterests 的语义不同:
|
||||
// 盘中传平仓前的当前剩余本金,EOD 此处传平仓后的剩余本金;后面又以 closePercent=1
|
||||
// 调用共享计息器。因此策略的 "posiNotional × closePercent" 在本例会得到 212197382.46,
|
||||
// 而本次实际应结的平仓本金是 closeNational=90941735.34。该语义错位由
|
||||
// SwapDealService.GetInterests 的模式2无条件修正、模式9全平零值兜底分流处理,不能删除。
|
||||
decimal oriPosiNotionalValue = posiLongNotional + posiShortNational + closeNational;
|
||||
decimal posiNotionalValue = posiLongNotional + posiShortNational;
|
||||
// ratio 只负责把腿内原始金额转换为本方盈亏方向,不参与计息金额本身的计算。
|
||||
decimal ratio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;//收取为正,支付为负
|
||||
if (MarginModes.Contains(position.InterestMode))
|
||||
{
|
||||
ratio = -ratio;
|
||||
}
|
||||
var ratio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
|
||||
// 首次日终结算可能包含当日收盘,因此尚无先前的日终利息持仓。
|
||||
// 部分平仓仍要续接上一日日终:CalcUnwindInterest 会将 InterestProfitSum
|
||||
// 加入本次待实现,已实现字段也必须按日累计,不能从新建的临时对象重新开始。
|
||||
@@ -1334,7 +1276,7 @@ namespace YLErp.Modules.SwapModule
|
||||
newEodPayPosition = eodPayPosition.Clone();
|
||||
newEodPayPosition.id = 0;
|
||||
}
|
||||
var interestModes = new List<int>() { (int)InterestModeEnum.固定值, (int)InterestModeEnum.初始预付金, (int)InterestModeEnum.追加预付金 };
|
||||
var interestModes = MarginModes.FixedAmountAndMargin;
|
||||
if (interestModes.Contains(position.InterestMode))
|
||||
{
|
||||
orginPv = eodPayPosition.InterestPrincipalFix;
|
||||
@@ -1360,6 +1302,8 @@ namespace YLErp.Modules.SwapModule
|
||||
List<eod_swap_position> preEodPositions = new List<eod_swap_position>();
|
||||
preEodPositions.Add(eodPayPosition);
|
||||
var calcLast = tradeExtend?.InterestCalcMode?.EndsWith("1") ?? true;
|
||||
// 此处 closePercent=1 表示 EOD 计算本次事件时走全额结息;它不是 closeNational / oriPosiNotionalValue。
|
||||
// 与上方“收盘后剩余本金”同时传入会触发共享计息器的模式2/9本金修正,见 GetInterests。
|
||||
var interests = CalcSwapInterests(td, td.trade_extend, valueDate, valueDate, preEodPositions, positions, posiNotionalValue, posiLongNotional, posiShortNational, closeNational, 1, eventType, false, true, grossPrice, orginPv, true, settment: false, newCalcLast: autoSwap || calcLast);
|
||||
// TdInterestAmount:计息器返回的全腿当日/累计参考值,用于拆出 EOD 的当日新增。
|
||||
// interestAmountBeforeSettlement:本次事件发生前理论应结的高精度利息。
|
||||
@@ -1370,11 +1314,11 @@ namespace YLErp.Modules.SwapModule
|
||||
decimal autoSettledInterestAmount = 0m;
|
||||
if (autoSwap && interests.Count > 0)
|
||||
{
|
||||
autoSettledInterestAmount = RoundMoney(interestAmountBeforeSettlement - manualSettledInterestAmount);
|
||||
autoSettledInterestAmount = EodPnlCalculator.RoundMoney(interestAmountBeforeSettlement - manualSettledInterestAmount);
|
||||
var autoInterest = interests[0];
|
||||
autoInterest.InterestAmount = autoSettledInterestAmount;
|
||||
autoInterest.InterestClosePnL = autoSettledInterestAmount
|
||||
* (autoInterest.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m);
|
||||
* DirectionRatio.ReceivePay(autoInterest.InterestDirection);
|
||||
}
|
||||
newEodPayPosition.ValueDate = valueDate;
|
||||
newEodPayPosition.PositionId = position.id;
|
||||
@@ -1420,11 +1364,9 @@ namespace YLErp.Modules.SwapModule
|
||||
newEodPayPosition.TdCloseInterest = manualSettledInterestAmount + autoSettledInterestAmount;
|
||||
// intersetAcmount 是收盘后本金的一天应计展示值。算尾部分平仓时,下面的复利分支会改用
|
||||
// 平仓前全额本金重算当天新增,但跨日携带的 TdInterestPrincipal 仍只能是剩余本金。
|
||||
var intersetAcmount = newEodPayPosition.TdInterestPrincipal * (newEodPayPosition.TdInterestRate + newEodPayPosition.FloatRate);
|
||||
if (position.IsAnnualized)
|
||||
{
|
||||
intersetAcmount /= tradeExtend.AnnualDays;
|
||||
}
|
||||
var intersetAcmount = InterestIncomeCalc.DailyAccrual(
|
||||
newEodPayPosition.TdInterestPrincipal, newEodPayPosition.TdInterestRate, newEodPayPosition.FloatRate,
|
||||
newEodPayPosition.IsAnnualized, tradeExtend.AnnualDays);
|
||||
newEodPayPosition.TdInterestIncome = autoSwap
|
||||
? intersetAcmount
|
||||
: !hasPreviousEod
|
||||
@@ -1474,12 +1416,9 @@ namespace YLErp.Modules.SwapModule
|
||||
var accrualPrincipal = calcLast
|
||||
? fullPrincipal
|
||||
: newEodPayPosition.TdInterestPrincipal;
|
||||
newEodPayPosition.TdInterestIncome = accrualPrincipal
|
||||
* (newEodPayPosition.TdInterestRate + newEodPayPosition.FloatRate);
|
||||
if (position.IsAnnualized)
|
||||
{
|
||||
newEodPayPosition.TdInterestIncome /= tradeExtend.AnnualDays;
|
||||
}
|
||||
newEodPayPosition.TdInterestIncome = InterestIncomeCalc.DailyAccrual(
|
||||
accrualPrincipal, newEodPayPosition.TdInterestRate, newEodPayPosition.FloatRate,
|
||||
newEodPayPosition.IsAnnualized, tradeExtend.AnnualDays);
|
||||
}
|
||||
if (!autoSwap
|
||||
&& closePercent > 0m && closePercent < 1m
|
||||
@@ -1505,13 +1444,13 @@ namespace YLErp.Modules.SwapModule
|
||||
// InterestIncomeSum 是收盘后仍未结算的尾差/剩余利息。
|
||||
// 部分平仓:扣款前待实现 - TdCloseInterest;最终全平且两位金额已覆盖时直接清零。
|
||||
newEodPayPosition.InterestIncomeSum = closePercent == 1
|
||||
&& RoundMoney(pendingInterestBeforeSettlement) == RoundMoney(newEodPayPosition.TdCloseInterest)
|
||||
&& EodPnlCalculator.RoundMoney(pendingInterestBeforeSettlement) == EodPnlCalculator.RoundMoney(newEodPayPosition.TdCloseInterest)
|
||||
? 0m
|
||||
: RoundEodInterest(pendingInterestBeforeSettlement - newEodPayPosition.TdCloseInterest);
|
||||
: EodPnlCalculator.RoundEodInterest(pendingInterestBeforeSettlement - newEodPayPosition.TdCloseInterest);
|
||||
newEodPayPosition.InterestFeeSum = closePercent == 1
|
||||
&& RoundMoney(pendingInterestFeeBeforeSettlement) == RoundMoney(newEodPayPosition.TdCloseInterestFee)
|
||||
&& EodPnlCalculator.RoundMoney(pendingInterestFeeBeforeSettlement) == EodPnlCalculator.RoundMoney(newEodPayPosition.TdCloseInterestFee)
|
||||
? 0m
|
||||
: RoundEodInterest(pendingInterestFeeBeforeSettlement - newEodPayPosition.TdCloseInterestFee);
|
||||
: EodPnlCalculator.RoundEodInterest(pendingInterestFeeBeforeSettlement - newEodPayPosition.TdCloseInterestFee);
|
||||
//持仓内容-利息腿-损益统计(本方视角)
|
||||
// InterestProfitSum 是利息腿待实现总额,包含利息和费用;无费用时等于 InterestIncomeSum。
|
||||
newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum;
|
||||
@@ -1525,11 +1464,12 @@ namespace YLErp.Modules.SwapModule
|
||||
//累计已实现
|
||||
// RealizedInterest 只增不回滚:上日累计已实现 + 当日结息按方向后的金额。
|
||||
// 收取腿的 -37119.14 会把累计已实现更新为 -37119.14;后续普通 EOD 保持该值。
|
||||
newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio;
|
||||
newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee;
|
||||
var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio);
|
||||
newEodPayPosition.RealizedInterest = rolled.Interest;
|
||||
newEodPayPosition.RealizedInterestFee = rolled.Fee;
|
||||
SetFixedLegRealizedPnl(newEodPayPosition);
|
||||
var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true,
|
||||
position.InterestDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell);
|
||||
DirectionRatio.RateType(position.InterestDirection));
|
||||
newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate);
|
||||
Log.Info($"即将插入数据库的 newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}");
|
||||
PersistEodSwapPosition(newEodPayPosition);
|
||||
@@ -1549,7 +1489,7 @@ namespace YLErp.Modules.SwapModule
|
||||
Log.Info($"eodPayPosition is {JsonHelper.Serialize(eodPayPosition, false)},newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}");
|
||||
List<IntervalModel> intervals = position.SwapIntervalList;
|
||||
var tradeExtend = td.trade_extend.ExtendObj;
|
||||
var interestModes = new List<int>() { (int)InterestModeEnum.固定值, (int)InterestModeEnum.初始预付金, (int)InterestModeEnum.追加预付金 };
|
||||
var interestModes = MarginModes.FixedAmountAndMargin;
|
||||
if (eodPayPosition == null)
|
||||
{
|
||||
//if (position.PosiStartDate > valueDate)
|
||||
@@ -1604,11 +1544,7 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
closePercent = 1;
|
||||
}
|
||||
decimal ratio = eodPayPosition.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;//收取为正,支付为负
|
||||
if (MarginModes.Contains(position.InterestMode))
|
||||
{
|
||||
ratio = -ratio;
|
||||
}
|
||||
var ratio = DirectionRatio.InterestLegPnl(eodPayPosition.InterestDirection, position.InterestMode);
|
||||
List<swap_position> positions = new List<swap_position>
|
||||
{
|
||||
position
|
||||
@@ -1647,11 +1583,12 @@ namespace YLErp.Modules.SwapModule
|
||||
newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio);
|
||||
|
||||
//累计已实现
|
||||
newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio;
|
||||
newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee;
|
||||
var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio);
|
||||
newEodPayPosition.RealizedInterest = rolled.Interest;
|
||||
newEodPayPosition.RealizedInterestFee = rolled.Fee;
|
||||
SetFixedLegRealizedPnl(newEodPayPosition);
|
||||
var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true,
|
||||
eodPayPosition.InterestDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell);
|
||||
DirectionRatio.RateType(eodPayPosition.InterestDirection));
|
||||
newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate);
|
||||
Log.Info($"the last newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}");
|
||||
PersistEodSwapPosition(newEodPayPosition);
|
||||
@@ -1692,7 +1629,7 @@ namespace YLErp.Modules.SwapModule
|
||||
bool open)
|
||||
{
|
||||
payQty = Math.Abs(payQty);
|
||||
int ratio = eventFlow.PayDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;//收取为正,支付为负
|
||||
int ratio = DirectionRatio.ReceivePay(eventFlow.PayDirection);//收取为正,支付为负
|
||||
int shortRatio = DirectionRatio.LongShort(newEodPayPosition.PositionType);
|
||||
newEodPayPosition.ValueDate = eventFlow.PayDate.Value;
|
||||
newEodPayPosition.PositionId = eventFlow.PositionId;
|
||||
@@ -1740,7 +1677,7 @@ namespace YLErp.Modules.SwapModule
|
||||
newEodPayPosition.TdPosiDividend = Math.Round(dividendIn * ratio, 2);
|
||||
newEodPayPosition.PosiMtmPnL = MtmCalc.UnrealizedPnl(newEodPayPosition.UnderlyingPrice, newEodPayPosition.PosiGrossPrice, newEodPayPosition.PosiQuantity, newEodPayPosition.ContractSize, shortRatio, (int)ratio);
|
||||
newEodPayPosition.PosiDividendSum = Math.Round(newEodPayPosition.TdPosiDividend - newEodPayPosition.TdCloseDividend, 2);
|
||||
newEodPayPosition.PosiProfitSum = newEodPayPosition.PosiMtmPnL + newEodPayPosition.PosiDividendSum + newEodPayPosition.PosiFeePending;
|
||||
newEodPayPosition.PosiProfitSum = MtmCalc.ReturnLegProfitSum(newEodPayPosition.PosiMtmPnL, newEodPayPosition.PosiDividendSum, newEodPayPosition.PosiFeePending);
|
||||
|
||||
//持仓价值
|
||||
newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum);
|
||||
@@ -1748,7 +1685,7 @@ namespace YLErp.Modules.SwapModule
|
||||
newEodPayPosition.RealizedFee = closeFee;
|
||||
newEodPayPosition.RealizedMtmPnL = newEodPayPosition.TdCloseMtmPnl;
|
||||
newEodPayPosition.RealizedDividend = newEodPayPosition.TdCloseDividend;
|
||||
SetFloatingRealizedPnl(newEodPayPosition);
|
||||
EodPnlCalculator.SetFloatingRealizedPnl(newEodPayPosition);
|
||||
|
||||
newEodPayPosition.PosiStatus = payQty == 0 ? 1 : 0;
|
||||
UpdateDbOption(newEodPayPosition);
|
||||
@@ -1792,7 +1729,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
var dealDate = curretEod.ValueDate;
|
||||
int shortRatio = DirectionRatio.LongShort(eod.PositionType);
|
||||
int directionRatio = eod.PosiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;
|
||||
int directionRatio = DirectionRatio.ReceivePay(eod.PosiDirection);
|
||||
curretEod.PosiStatus = curretEod.PosiQuantity == 0 ? 1 : 0;
|
||||
var price = GetSwapValuationPrice(eod.UnderlyingCode, dealDate, out decimal vobp);
|
||||
curretEod.dv01 = Dv01Helper.CalcDv01(eod.UnderlyingCode, curretEod.PosiQuantity, eod.PosiDirection, eod.PositionType, vobp);
|
||||
@@ -1813,7 +1750,7 @@ namespace YLErp.Modules.SwapModule
|
||||
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;
|
||||
curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending);
|
||||
curretEod.TdCloseFee = 0;
|
||||
curretEod.TdCloseQty = 0;
|
||||
curretEod.TdCloseMtmPnl = 0;
|
||||
@@ -1822,9 +1759,9 @@ namespace YLErp.Modules.SwapModule
|
||||
curretEod.RealizedMtmPnL = eod.RealizedMtmPnL + curretEod.TdCloseMtmPnl;
|
||||
curretEod.RealizedDividend = eod.RealizedDividend + curretEod.TdCloseDividend;
|
||||
curretEod.RealizedFee = eod.RealizedFee + curretEod.TdCloseFee;
|
||||
SetFloatingRealizedPnl(curretEod);
|
||||
EodPnlCalculator.SetFloatingRealizedPnl(curretEod);
|
||||
var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, td.StartDate.Value
|
||||
, seekPreday: true, currencyRateType: curretEod.PosiDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell);
|
||||
, seekPreday: true, currencyRateType: DirectionRatio.RateType(curretEod.PosiDirection));
|
||||
curretEod.TdCurrency = Convert.ToDecimal(currencyRate);
|
||||
//持仓价值
|
||||
curretEod.SwapPositionValue = PositionValueCalc.Calc(curretEod.InterestProfitSum, curretEod.PosiProfitSum);
|
||||
@@ -1837,17 +1774,6 @@ namespace YLErp.Modules.SwapModule
|
||||
return curretEod;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 浮动腿累计已实现盈亏由盯市、分红和费用三个已实现组成项汇总。
|
||||
/// 各组成项已经按本方视角落库,此处不再额外转换方向。
|
||||
/// </summary>
|
||||
private static void SetFloatingRealizedPnl(eod_swap_position position)
|
||||
{
|
||||
position.RealizedPnl = position.RealizedMtmPnL
|
||||
+ position.RealizedDividend
|
||||
+ position.RealizedFee;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新虚拟交易费用
|
||||
/// </summary>
|
||||
@@ -1884,7 +1810,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
var dealDate = curretEod.ValueDate;
|
||||
int shortRatio = DirectionRatio.LongShort(eod.PositionType);
|
||||
int directionRatio = eod.PosiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;
|
||||
int directionRatio = DirectionRatio.ReceivePay(eod.PosiDirection);
|
||||
var price = GetSwapValuationPrice(eod.UnderlyingCode, dealDate, out decimal vobp);
|
||||
var todayConsumedDividend = CalcConsumedDividend(curretEod, unwindEvents);
|
||||
var originNotional = (decimal)td.OriginalStockEqvNotional / swapPosition.PosiNetPrice;
|
||||
@@ -1923,16 +1849,16 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
curretEod.PosiDividendSum = 0;
|
||||
}
|
||||
SetFloatingRealizedPnl(curretEod);
|
||||
EodPnlCalculator.SetFloatingRealizedPnl(curretEod);
|
||||
curretEod.SwapPositionValue -= curretEod.TdCloseDividend;
|
||||
|
||||
curretEod.PosiProfitSum = curretEod.PosiMtmPnL + curretEod.PosiDividendSum + curretEod.PosiFeePending;
|
||||
curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending);
|
||||
if (curretEod.PosiStatus == 1)
|
||||
{
|
||||
curretEod.PosiNotionalValue = 0;
|
||||
}
|
||||
var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, td.StartDate.Value
|
||||
, seekPreday: true, currencyRateType: curretEod.PosiDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell);
|
||||
, seekPreday: true, currencyRateType: DirectionRatio.RateType(curretEod.PosiDirection));
|
||||
curretEod.TdCurrency = Convert.ToDecimal(currencyRate);
|
||||
//持仓价值
|
||||
curretEod.SwapPositionValue = PositionValueCalc.Calc(curretEod.InterestProfitSum, curretEod.PosiProfitSum);
|
||||
@@ -1972,7 +1898,7 @@ namespace YLErp.Modules.SwapModule
|
||||
return;
|
||||
}
|
||||
int shortRatio = DirectionRatio.LongShort(eod.PositionType);
|
||||
int directionRatio = eod.PosiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;
|
||||
int directionRatio = DirectionRatio.ReceivePay(eod.PosiDirection);
|
||||
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);
|
||||
@@ -2005,16 +1931,16 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
posiQty = 0;
|
||||
}
|
||||
curretEod.PosiGrossPrice = (eod.PosiGrossPrice * eod.PosiQuantity + openFlowEvents.Sum(a => a.Quantity * a.TradingAmountAvg)) / (eod.PosiQuantity + openQty);
|
||||
curretEod.PosiGrossPrice = MtmCalc.BlendPrice(eod.PosiGrossPrice, eod.PosiQuantity, openFlowEvents.Sum(a => a.Quantity * a.TradingAmountAvg), eod.PosiQuantity + openQty);
|
||||
curretEod.PosiGrossPrice = Math.Round(
|
||||
curretEod.PosiGrossPrice,
|
||||
GetStorageDeliveryPriceRound(curretEod.UnderlyingInstrumentType, curretEod.UnderlyingCode),
|
||||
MidpointRounding.AwayFromZero);
|
||||
curretEod.PosiNetPrice = (eod.PosiNetPrice * eod.PosiQuantity + openFlowEvents.Sum(a => a.Quantity * a.TradingAmountFeeAvg)) / (eod.PosiQuantity + openQty);
|
||||
curretEod.PosiNetPrice = MtmCalc.BlendPrice(eod.PosiNetPrice, eod.PosiQuantity, openFlowEvents.Sum(a => a.Quantity * a.TradingAmountFeeAvg), eod.PosiQuantity + openQty);
|
||||
curretEod.PosiNetPrice = Math.Round(curretEod.PosiNetPrice, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
|
||||
curretEod.PosiNetNoFeePrice = (eod.PosiNetNoFeePrice * eod.PosiQuantity + openFlowEvents.Sum(a => a.Quantity * a.TradingAmountNetAvg)) / (eod.PosiQuantity + openQty);
|
||||
curretEod.PosiNetNoFeePrice = MtmCalc.BlendPrice(eod.PosiNetNoFeePrice ?? 0m, eod.PosiQuantity, openFlowEvents.Sum(a => a.Quantity * (a.TradingAmountNetAvg ?? 0m)), eod.PosiQuantity + openQty);
|
||||
curretEod.PosiNetNoFeePrice = Math.Round(curretEod.PosiNetNoFeePrice ?? 0, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
|
||||
curretEod.PosiNetFeePrice = (eod.PosiNetFeePrice * eod.PosiQuantity + openFlowEvents.Sum(a => a.Quantity * a.TradingAmountNetFeeAvg)) / (eod.PosiQuantity + openQty);
|
||||
curretEod.PosiNetFeePrice = MtmCalc.BlendPrice(eod.PosiNetFeePrice ?? 0m, eod.PosiQuantity, openFlowEvents.Sum(a => a.Quantity * (a.TradingAmountNetFeeAvg ?? 0m)), eod.PosiQuantity + openQty);
|
||||
curretEod.PosiNetFeePrice = Math.Round(curretEod.PosiNetFeePrice ?? 0, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
curretEod.PosiNotionalValue = curretEod.PosiGrossPrice * curretEod.PosiQuantity * curretEod.ContractSize;
|
||||
@@ -2053,7 +1979,7 @@ namespace YLErp.Modules.SwapModule
|
||||
curretEod.PositionId = position.id;
|
||||
curretEod.ClientId = td.ClientId;
|
||||
int shortRatio = DirectionRatio.LongShort(position.PositionType);
|
||||
int directionRatio = position.PosiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;
|
||||
int directionRatio = DirectionRatio.ReceivePay(position.PosiDirection);
|
||||
curretEod.PositionType = position.PositionType;
|
||||
var eod = new eod_swap_position()
|
||||
{
|
||||
@@ -2099,11 +2025,11 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
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.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending);
|
||||
curretEod.RealizedMtmPnL = curretEod.TdCloseMtmPnl;
|
||||
curretEod.RealizedDividend = curretEod.TdCloseDividend;
|
||||
curretEod.RealizedFee = curretEod.TdCloseFee;
|
||||
SetFloatingRealizedPnl(curretEod);
|
||||
EodPnlCalculator.SetFloatingRealizedPnl(curretEod);
|
||||
curretEod.PosiStatus = curretEod.PosiQuantity == 0 ? 1 : 0;
|
||||
if (curretEod.PosiStatus == 1)
|
||||
{
|
||||
@@ -2112,7 +2038,7 @@ namespace YLErp.Modules.SwapModule
|
||||
//持仓价值
|
||||
curretEod.SwapPositionValue = PositionValueCalc.Calc(curretEod.InterestProfitSum, curretEod.PosiProfitSum);
|
||||
var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, td.StartDate.Value
|
||||
, seekPreday: true, currencyRateType: curretEod.PosiDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell);
|
||||
, seekPreday: true, currencyRateType: DirectionRatio.RateType(curretEod.PosiDirection));
|
||||
curretEod.TdCurrency = Convert.ToDecimal(currencyRate);
|
||||
UpdateDbOption(curretEod);
|
||||
curretEod.Invalid = false;
|
||||
@@ -2187,8 +2113,6 @@ namespace YLErp.Modules.SwapModule
|
||||
var positions = eodSwapPositions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//持仓腿
|
||||
// 框架合约的方向约定:多头为正、空头为负;总名义本金取交易原始规模,
|
||||
// 不能直接用多空腿相加,否则会把对冲方向误当成合约规模变化。
|
||||
eod_Swap.NotionalValueLong = Math.Round(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
eod_Swap.NotionalValueShort = Math.Round(-Math.Abs(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue)), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
eod_Swap.NotionalValue = Math.Round(Convert.ToDecimal(td.OriginalStockEqvNotional ?? td.StockEqvNotional), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
eod_Swap.SwapTradeId = td.id;
|
||||
eod_Swap.SwapTradeNo = td.TradeNumber;
|
||||
@@ -2196,23 +2120,8 @@ namespace YLErp.Modules.SwapModule
|
||||
eod_Swap.BookId = td.AssetId;
|
||||
eod_Swap.ValueDate = settleDate;
|
||||
eod_Swap.StructureType = td.StructureType;
|
||||
eod_Swap.MarketValueLong = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.UnderlyingMarketValue);
|
||||
eod_Swap.MarketValueShort = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.UnderlyingMarketValue);
|
||||
eod_Swap.FloatingPnL = positions.Sum(s => s.PosiProfitSum);
|
||||
eod_Swap.dv01 = positions.Sum(s => s.dv01 ?? 0);
|
||||
decimal interestPnL = 0;
|
||||
// 利息腿按我方视角归集。保证金腿的利息现金流方向与普通利息腿相反,
|
||||
// 因此保证金腿需要额外反转符号,确保 InterestPnL 表示我方的合约利率端收益。
|
||||
interestPositions.ForEach(x =>
|
||||
{
|
||||
decimal ratio = x.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;//收取为正,支付为负
|
||||
if (MarginModes.Contains(x.InterestMode))
|
||||
{
|
||||
ratio = -ratio;
|
||||
}
|
||||
interestPnL += x.InterestProfitSum * ratio;
|
||||
});
|
||||
eod_Swap.InterestPnL = interestPnL;
|
||||
EodPnlCalculator.FillPositionLegSummary(eod_Swap, positions);
|
||||
eod_Swap.InterestPnL = EodPnlCalculator.SumInterestPnL(interestPositions);
|
||||
eod_Swap.PostionValue = eodSwapPositions.Sum(s => s.SwapPositionValue);
|
||||
// 保证金腿的利息现金流方向与保证金本金方向相反。
|
||||
// 不能直接汇总 RealizedPnl,否则“收取客户保证金”的腿会把应支付给客户的
|
||||
@@ -2265,33 +2174,15 @@ namespace YLErp.Modules.SwapModule
|
||||
var interestPositions = eodSwapPositions.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//利息腿
|
||||
var positions = eodSwapPositions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//持仓腿
|
||||
eod_Swap.NotionalValue = Math.Round(Convert.ToDecimal(td.OriginalStockEqvNotional ?? td.StockEqvNotional), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
eod_Swap.NotionalValueLong = Math.Round(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
eod_Swap.NotionalValueShort = Math.Round(-Math.Abs(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue)), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
eod_Swap.MarketValueLong = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.UnderlyingMarketValue);
|
||||
eod_Swap.MarketValueShort = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.UnderlyingMarketValue);
|
||||
eod_Swap.FloatingPnL = positions.Sum(s => s.PosiProfitSum);
|
||||
eod_Swap.dv01 = positions.Sum(s => s.dv01 ?? 0);
|
||||
interestPositions.ForEach(x =>
|
||||
{
|
||||
decimal ratio = x.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;//收取为正,支付为负
|
||||
if (MarginModes.Contains(x.InterestMode))
|
||||
{
|
||||
ratio = -ratio;
|
||||
}
|
||||
eod_Swap.InterestPnL += x.InterestProfitSum * ratio;
|
||||
});
|
||||
EodPnlCalculator.FillPositionLegSummary(eod_Swap, positions);
|
||||
eod_Swap.InterestPnL += EodPnlCalculator.SumInterestPnL(interestPositions);
|
||||
eodSwapPositions.ForEach(x =>
|
||||
{
|
||||
decimal ratio = x.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;//收取为正,支付为负
|
||||
if (MarginModes.Contains(x.InterestMode))
|
||||
{
|
||||
ratio = -ratio;
|
||||
}
|
||||
var ratio = DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode);
|
||||
eod_Swap.TdRealizedPnL += x.TdCloseMtmPnl + x.TdCloseDividend + x.TdCloseFee + x.TdCloseInterest * ratio + x.TdCloseInterestFee;
|
||||
});
|
||||
eod_Swap.PostionValue = eodSwapPositions.Sum(s => s.SwapPositionValue);
|
||||
eod_Swap.RealizedPnL = eodSwapPositions.Sum(CalculateSwapRealizedPnl);
|
||||
eod_Swap.TdCloseQty = positions.Sum(s => s.TdCloseQty);
|
||||
var tradeInitMarginObj = DbContext.trade_initial_margin.FirstOrDefault(x => x.TradeId == td.id);
|
||||
var initMarginList = interestPositions.Where(x => x.InterestMode == (int)InterestModeEnum.初始预付金 && x.HappenDate == settleDate).ToList();
|
||||
var addMarginList = interestPositions.Where(x => x.InterestMode == (int)InterestModeEnum.追加预付金 && x.HappenDate == settleDate).ToList();
|
||||
@@ -2310,20 +2201,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// 我方支付给对手方的成本计入,而不会错误增加框架合约已实现收益。
|
||||
/// 抽为静态纯函数以支持无库单测(marginTypes 等价于 ConsTrade.InterestMarginModels)。
|
||||
/// </summary>
|
||||
public static decimal CalculateSwapRealizedPnl(eod_swap_position position)
|
||||
{
|
||||
var interestRatio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;
|
||||
if (MarginModes.Contains(position.InterestMode))
|
||||
{
|
||||
interestRatio = -interestRatio;
|
||||
}
|
||||
|
||||
return position.RealizedMtmPnL
|
||||
+ position.RealizedDividend
|
||||
+ position.RealizedFee
|
||||
+ position.RealizedInterest * interestRatio
|
||||
+ position.RealizedInterestFee;
|
||||
}
|
||||
public static decimal CalculateSwapRealizedPnl(eod_swap_position position) => EodPnlCalculator.CalculateSwapRealizedPnl(position);
|
||||
|
||||
/// <summary>
|
||||
/// 风险报表符号归一化:把历史两种符号口径的 TdCloseInterest/RealizedInterest
|
||||
@@ -2332,24 +2210,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// 抽为 public static 纯函数以支持无库单测(见 SwapReportInterestSignNormalizeTest)。
|
||||
/// 仅当 InterestDirection > 0 时执行(与原内联逻辑等价)。
|
||||
/// </summary>
|
||||
public static void NormalizeInterestSignForReport(eod_swap_position position)
|
||||
{
|
||||
if (position.InterestDirection <= 0) return;
|
||||
|
||||
var interestRatio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;
|
||||
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;
|
||||
// 兼容修复前已落库的利息腿:当时只累计了明细字段,未同步写入 RealizedPnl。
|
||||
position.RealizedPnl = position.RealizedInterest + position.RealizedInterestFee;
|
||||
}
|
||||
public static void NormalizeInterestSignForReport(eod_swap_position position) => EodPnlCalculator.NormalizeInterestSignForReport(position);
|
||||
|
||||
/// <summary>
|
||||
/// 获取多空组合 平仓详细
|
||||
@@ -2838,11 +2699,11 @@ namespace YLErp.Modules.SwapModule
|
||||
var floatRateInterest = eodInterests.Where(x => !string.IsNullOrEmpty(x.FloatRateUnderlyingCode)).FirstOrDefault();
|
||||
item.position.FloatRateUnderlyingCode = floatRateInterest?.FloatRateUnderlyingCode;
|
||||
item.position.FloatRate = floatRateInterest?.FloatRate ?? 0;
|
||||
item.OpenMarginAmount = initialMargins.Sum(s => s.InterestPrincipalFix * (s.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1));
|
||||
item.OpenMarginRate = CalculateWeightedMarginRate(tradeMargins);
|
||||
item.AdditionalMarginAmount = additionalMargins.Sum(s => s.InterestPrincipalFix * (s.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1));
|
||||
item.OpenMarginAmount = initialMargins.Sum(s => s.InterestPrincipalFix * DirectionRatio.ReceivePay(s.InterestDirection));
|
||||
item.OpenMarginRate = EodPnlCalculator.CalculateWeightedMarginRate(tradeMargins);
|
||||
item.AdditionalMarginAmount = additionalMargins.Sum(s => s.InterestPrincipalFix * DirectionRatio.ReceivePay(s.InterestDirection));
|
||||
item.MarginInterestAmount = CalculateWeightedMarginInterest(eodMargins);
|
||||
item.InterestAmount = eodInterests.Sum(s => s.InterestIncomeSum * (s.InterestDirection == (int)SwapDirectionEnum.收取 ? -1 : 1));
|
||||
item.InterestAmount = eodInterests.Sum(s => s.InterestIncomeSum * (-DirectionRatio.ReceivePay(s.InterestDirection)));
|
||||
item.InterestRate = eodInterests.Sum(s => s.InterestRateDefault);
|
||||
// 到期轧差才把期间付息/分红并入净额结算;派息日支付已在现金流层独立结算,不能重复计入估值。
|
||||
var nettingDividend = (tradeExtend?.ExtendObj?.DividendPayDate ?? 1) == 0 ? pendingDividend : 0m;
|
||||
@@ -2864,29 +2725,12 @@ namespace YLErp.Modules.SwapModule
|
||||
return retListResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算预付金利率。多条初始/追加预付金腿按本金绝对值加权,
|
||||
/// 不按收付方向轧差,避免相反方向本金抵消后放大利率。
|
||||
/// </summary>
|
||||
private static decimal CalculateWeightedMarginRate(IEnumerable<swap_position> margins)
|
||||
{
|
||||
var marginList = margins.ToList();
|
||||
var totalWeight = marginList.Sum(x => Math.Abs(x.InterestPrincipalFix));
|
||||
return totalWeight == 0
|
||||
? 0
|
||||
: marginList.Sum(x => x.InterestRateDefault * Math.Abs(x.InterestPrincipalFix)) / totalWeight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算预付金利息金额。InterestIncomeSum 已是各腿利息金额,
|
||||
/// 按收取为正、支付为负直接轧差求和,不做本金加权。
|
||||
/// 抽为 public static 纯函数以支持无库单测(见 SwapWeightedMarginInterestTest)。
|
||||
/// </summary>
|
||||
public static decimal CalculateWeightedMarginInterest(IEnumerable<eod_swap_position> margins)
|
||||
{
|
||||
return margins.Sum(x =>
|
||||
x.InterestIncomeSum * (x.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1));
|
||||
}
|
||||
public static decimal CalculateWeightedMarginInterest(IEnumerable<eod_swap_position> margins) => EodPnlCalculator.CalculateWeightedMarginInterest(margins);
|
||||
|
||||
/// <summary>
|
||||
/// 固定利息腿的累计已实现盈亏 = 累计已实现利息 + 累计已实现利息费用。
|
||||
@@ -2894,10 +2738,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// 抽为 public static 纯函数以支持无库单测(见 SwapFixedLegRealizedPnlTest),
|
||||
/// 并消除复制粘贴带来的笔误风险(如 L1296 历史双分号)。
|
||||
/// </summary>
|
||||
public static void SetFixedLegRealizedPnl(eod_swap_position position)
|
||||
{
|
||||
position.RealizedPnl = position.RealizedInterest + position.RealizedInterestFee;
|
||||
}
|
||||
public static void SetFixedLegRealizedPnl(eod_swap_position position) => EodPnlCalculator.SetFixedLegRealizedPnl(position);
|
||||
/// <summary>
|
||||
/// 将数据库中以公司/交易簿记方向保存的日终字段转换为客户视角。
|
||||
/// 该转换必须在拆分浮动收益、费用和期间付息/分红之前完成,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Consts;
|
||||
|
||||
namespace YLErp.Modules.SwapModule;
|
||||
|
||||
/// <summary>
|
||||
/// 平仓手续费计算——纯 static,无 this 依赖。
|
||||
/// 从 SwapDealService 提取,零行为变更。
|
||||
/// </summary>
|
||||
public static class TradingFeeCalc
|
||||
{
|
||||
public static decimal CalcInitTradingFee(swap_position oriPosition, UnwindData unwindData)
|
||||
{
|
||||
if (oriPosition == null || unwindData == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (oriPosition.PosiFeeType == 1)
|
||||
{
|
||||
return Math.Round(oriPosition.PosiTradingFeeUnit * unwindData.CloseQty, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
return Math.Round(oriPosition.PosiTradingFeeUnit / 100m * unwindData.CloseNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
public static decimal CalcInitTradingFeePending(swap_position oriPosition, swap_position position, UnwindData unwindData)
|
||||
{
|
||||
if (oriPosition == null || unwindData == null || oriPosition.PosiTradingFeeUnit == 0)
|
||||
{
|
||||
return position?.PosiTradingFeePending ?? 0;
|
||||
}
|
||||
|
||||
var closeBase = oriPosition.PosiFeeType == 1 ? unwindData.CloseQty : unwindData.CloseNotionalValue;
|
||||
var originalBase = oriPosition.PosiFeeType == 1 ? unwindData.NotionalQty : unwindData.NotionalValue;
|
||||
if (originalBase <= 0)
|
||||
{
|
||||
return position?.PosiTradingFeePending ?? 0;
|
||||
}
|
||||
|
||||
return Math.Round(oriPosition.PosiTradingFeePending * closeBase / originalBase, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using YLErp.Helpers;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
namespace YLErp.Modules.SwapModule;
|
||||
|
||||
/// <summary>
|
||||
/// 平仓数据(UnwindData)规范化——纯 static,无 this 依赖。
|
||||
/// 从 SwapDealService 提取,零行为变更。
|
||||
/// </summary>
|
||||
internal static class UnwindNormalizer
|
||||
{
|
||||
internal static void NormalizeNotionalValues(UnwindData unwindData)
|
||||
{
|
||||
unwindData.NotionalValue = Math.Round(unwindData.NotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
unwindData.PosiNotionalValue = Math.Round(unwindData.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
unwindData.CloseNotionalValue = Math.Round(unwindData.CloseNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
internal static bool NormalizeFullCloseRequest(UnwindData unwindData)
|
||||
{
|
||||
if (unwindData.CloseMethod != (int)CloseMethodEnum.全部平仓
|
||||
&& unwindData.ClosePercent < 1
|
||||
&& !(unwindData.PositionQty > 0 && unwindData.CloseQty >= unwindData.PositionQty)
|
||||
&& !(unwindData.PosiNotionalValue > 0 && unwindData.CloseNotionalValue >= unwindData.PosiNotionalValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var closeQty = unwindData.CloseQty;
|
||||
var closeNotionalValue = unwindData.CloseNotionalValue;
|
||||
unwindData.ClosePercent = 1;
|
||||
if (unwindData.PositionQty > 0) unwindData.CloseQty = unwindData.PositionQty;
|
||||
if (unwindData.PosiNotionalValue > 0) unwindData.CloseNotionalValue = unwindData.PosiNotionalValue;
|
||||
return closeQty != unwindData.CloseQty || closeNotionalValue != unwindData.CloseNotionalValue;
|
||||
}
|
||||
|
||||
internal static void RecalculateNormalizedUnwindAmounts(UnwindData unwindData)
|
||||
{
|
||||
var floatLeg = unwindData.FlowEvents.FirstOrDefault(x => !string.IsNullOrEmpty(x.UnderlyingCode));
|
||||
if (floatLeg == null || floatLeg.PosiGrossPrice == 0) return;
|
||||
|
||||
var input = new UnwindInput
|
||||
{
|
||||
Multiplier = ConsGlobal.InstrumentType.IsBond(floatLeg.UnderlyingInstrumentType) ? 100 : 1,
|
||||
PosiGrossPrice = floatLeg.PosiGrossPrice,
|
||||
TradingAmountAvg = floatLeg.TradingAmountAvg,
|
||||
CloseQty = unwindData.CloseQty,
|
||||
PositionQty = unwindData.PositionQty,
|
||||
ContractSize = floatLeg.ContractSize,
|
||||
CloseNotionalValue = unwindData.CloseNotionalValue,
|
||||
PayDirection = floatLeg.PayDirection,
|
||||
PositionType = floatLeg.PositionType,
|
||||
TradingFee = floatLeg.TradingFee.ToString(),
|
||||
TradingFeePending = floatLeg.TradingFeePending.ToString(),
|
||||
DividendIn = floatLeg.DividendIn.ToString()
|
||||
};
|
||||
foreach (var leg in unwindData.FlowEvents.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)))
|
||||
{
|
||||
var target = MarginModes.Contains(leg.InterestMode)
|
||||
? input.MarginLegs
|
||||
: input.InterestLegs;
|
||||
target.Add(new LegInput { InterestClosePnL = leg.InterestClosePnL });
|
||||
}
|
||||
|
||||
var result = FrontendCalcReference.CalcUnwind(input);
|
||||
floatLeg.MarkClosePnl = result.MarkClosePnl;
|
||||
unwindData.SwapCloseAmount = result.SwapCloseAmount;
|
||||
unwindData.SwapRealizedPnL = result.SwapRealizedPnL;
|
||||
unwindData.SwapMarginRebatePnl = result.SwapMarginRebatePnl;
|
||||
}
|
||||
|
||||
internal static bool IsFullCloseAfterDeduction(UnwindData unwindData, double remainingNotional, double remainingQuantity)
|
||||
{
|
||||
return unwindData.ClosePercent == 1 || (remainingNotional == 0 && remainingQuantity == 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手工平仓、手工互换及收益结算的利息事件按金额两位落库。
|
||||
/// 自动平仓保留原有计算与落库口径,不适用本阶段的手工结算规则。
|
||||
/// </summary>
|
||||
internal static bool NormalizeSettledInterestAmounts(IEnumerable<swap_flow_event> flowEvents, int eventType, string eventReason)
|
||||
{
|
||||
if ((eventType != (int)SwapEventTypeEnum.平仓 && eventType != (int)SwapEventTypeEnum.互换)
|
||||
|| eventReason == "系统操作_自动平仓")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var flowEvent in flowEvents.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)))
|
||||
{
|
||||
flowEvent.InterestPrincipal = Math.Round(flowEvent.InterestPrincipal, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
flowEvent.InterestAmount = Math.Round(flowEvent.InterestAmount, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
flowEvent.TdInterestAmount = Math.Round(flowEvent.TdInterestAmount, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
flowEvent.InterestClosePnL = Math.Round(flowEvent.InterestClosePnL, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
flowEvent.InterestFee = Math.Round(flowEvent.InterestFee, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static void NormalizeEventUnwindDate(UnwindData unwindData)
|
||||
{
|
||||
unwindData.UnwindDate = unwindData.ValueDate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
# 任务 4:盘中复利从 EOD 续接(而非 PosiStartDate 全程重放)
|
||||
|
||||
## 状态:待立项(高风险,需专项验证)
|
||||
|
||||
## 现状
|
||||
|
||||
`CalcDailyCompoundInterest` 从 `position.PosiStartDate` 全程重放到 `endDate`,每个重置日把累计利息并入本金(复利),最后扣 `consumedInterest * closePercent`。
|
||||
|
||||
**调用链**:`InitSwapDealInterest` → `CalcDailyCompoundInterest(endDate, PosiStartDate→endDate 全程重放)`
|
||||
|
||||
**问题**:交易存续期长(数月~数年)时,每次盘中平仓都从起息日重放,计算量随天数线性增长。
|
||||
|
||||
## 提议
|
||||
|
||||
改为从上一日终快照(`preEodPosition`)续接:
|
||||
- 起点 = `preEodPosition.ValueDate + 1`
|
||||
- 初始本金 = `preEodPosition.TdInterestPrincipal`(已含历史滚入利息)
|
||||
- 只算 `ValueDate+1` 到 `endDate` 的增量利息
|
||||
|
||||
## 风险分析(为什么不能直接改)
|
||||
|
||||
### 风险 1:并本金起点不同导致终值不等
|
||||
|
||||
| | 全程重放(当前) | EOD 续接(提议) |
|
||||
|---|---|---|
|
||||
| 起点 | `principal`(原始平仓名义本金) | `preEod.TdInterestPrincipal`(已滚利息) |
|
||||
| 滚法 | 每段 `basis = principal + accrued` | 每段 `basis = preEod.TdInterestPrincipal + segmentAccrued` |
|
||||
|
||||
两段路径在**中间重置日的四舍五入路径不同**(精度 12 的 Round 作用在不同的中间值上),终值**不一定逐分相等**。
|
||||
|
||||
### 风险 2:consumedInterest 语义翻转
|
||||
|
||||
- 全程重放:总利息 - consumedInterest × closePercent = 增量
|
||||
- EOD 续接:直接算增量,**不需要**扣 consumedInterest
|
||||
|
||||
如果 EOD 快照的 `InterestIncomeSum` 与 consumedInterest 口径不完全一致,直接去掉扣减会引入误差。
|
||||
|
||||
### 风险 3:resetCarryInterest 耦合
|
||||
|
||||
当前逻辑:`resetCarryInterest`(上一日终待实现 × remainingPercent)只在 `endDate` 恰好是重置日时并入本金。EOD 续接模式下,重置日的判定、remainingPercent 的计算、carry 的注入时机都不同。
|
||||
|
||||
### 风险 4:全平重放逻辑(lines 1293-1304)
|
||||
|
||||
`InitSwapDealInterest` 在 `closePrecent == 1m` 时做 **两次** `CalcDailyCompoundInterest` 重放(截至平仓日 + 截至上一日终),取差值。EOD 续接模式下这段逻辑需要完全重新设计。
|
||||
|
||||
## 验证方案(立项前提)
|
||||
|
||||
1. 构造测试用例:同一笔复利交易,跨越 ≥2 个重置周期,有 preEod 快照
|
||||
2. 用**旧全程重放**算出 `(InterestAmount, TdInterestAmount, finalBasis)`
|
||||
3. 用**新 EOD 续接**算出同样三个值
|
||||
4. 断言差额 < 0.01(到分)
|
||||
5. 覆盖场景:
|
||||
- 部分平仓(closePercent < 1)
|
||||
- 全平(closePercent == 1)
|
||||
- 平仓日 = 重置日
|
||||
- 平仓日 ≠ 重置日
|
||||
- 有/无 consumedInterest
|
||||
- 有/无 resetCarryInterest
|
||||
|
||||
## 建议排期
|
||||
|
||||
单独 sprint 处理,不混入日常重构。改动范围:
|
||||
- `CompoundInterestAccrual.AccruePeriod` 新增 `startBasis` 参数(或新方法 `AccrueFromEod`)
|
||||
- `CalcDailyCompoundInterest` wrapper 改为传 `preEod.TdInterestPrincipal` 作为起点
|
||||
- `InitSwapDealInterest` 全平重放逻辑简化(不再需要两次重放取差值)
|
||||
- `consumedInterest` 扣减逻辑移除或调整
|
||||
@@ -1536,11 +1536,6 @@ namespace YLErp.Modules.SystemModule
|
||||
Text = SwapTypeEnum.普通.ToString(),
|
||||
Value = SwapTypeEnum.普通.ToString()
|
||||
},
|
||||
new SelectItem
|
||||
{
|
||||
Text = SwapTypeEnum.多空组合.ToString(),
|
||||
Value = SwapTypeEnum.多空组合.ToString()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -352,36 +352,6 @@ namespace YLErp.Web.Controllers
|
||||
return View(model);
|
||||
}
|
||||
/// <summary>
|
||||
/// 收益互换 多空组合平仓
|
||||
/// </summary>
|
||||
/// <param name="enid"></param>
|
||||
/// <returns></returns>
|
||||
public ActionResult SwapLongShortUnwind(string enid, bool isUseApproval = false)
|
||||
{
|
||||
var intid = DecryptInt(enid);
|
||||
var model = new SwapDealService(CurUser).InitLongShortUnwind(intid, SwapEventTypeEnum.平仓);
|
||||
ViewBag.isUseApproval = isUseApproval;
|
||||
var hasProcess = new SwapDealService(CurUser).HasTradeProcess();
|
||||
//需要审批或者复核的交易都会显示行权审核提交按钮
|
||||
ViewBag.IsShowReCheckClose = (valuedateBLL.SystemDate.CloseReCheck == 1) || (valuedateBLL.SystemDate.CloseReApprove == 1 && hasProcess);
|
||||
return View(model);
|
||||
}
|
||||
/// <summary>
|
||||
/// 收益互换 多空组合互换
|
||||
/// </summary>
|
||||
/// <param name="enid"></param>
|
||||
/// <returns></returns>
|
||||
public ActionResult SwapLongShortSwap(string enid, bool isUseApproval = false)
|
||||
{
|
||||
var intid = DecryptInt(enid);
|
||||
var model = new SwapDealService(CurUser).InitLongShortUnwind(intid, SwapEventTypeEnum.互换);
|
||||
ViewBag.isUseApproval = isUseApproval;
|
||||
var hasProcess = new SwapDealService(CurUser).HasTradeProcess();
|
||||
//需要审批或者复核的交易都会显示行权审核提交按钮
|
||||
ViewBag.IsShowReCheckClose = (valuedateBLL.SystemDate.CloseReCheck == 1) || (valuedateBLL.SystemDate.CloseReApprove == 1 && hasProcess);
|
||||
return View(model);
|
||||
}
|
||||
/// <summary>
|
||||
/// 操作历史
|
||||
/// </summary>
|
||||
/// <param name="enid"></param>
|
||||
@@ -457,26 +427,6 @@ namespace YLErp.Web.Controllers
|
||||
return JsonSuccess("平仓成功");
|
||||
}
|
||||
/// <summary>
|
||||
///多空组合 平仓
|
||||
/// </summary>
|
||||
/// <param name="swap_Deal"></param>
|
||||
/// <returns></returns>
|
||||
public JsonResult SwapLongShortUnwindJson(UnwindData unwindData)
|
||||
{
|
||||
new SwapDealService(CurUser).SwapLongShortUnwind(unwindData);
|
||||
return JsonSuccess("平仓成功");
|
||||
}
|
||||
/// <summary>
|
||||
///多空组合 互换
|
||||
/// </summary>
|
||||
/// <param name="swap_Deal"></param>
|
||||
/// <returns></returns>
|
||||
public JsonResult SwapLongShortJson(UnwindData unwindData)
|
||||
{
|
||||
new SwapDealService(CurUser).SwapLongShort(unwindData);
|
||||
return JsonSuccess("互换成功");
|
||||
}
|
||||
/// <summary>
|
||||
/// 互换
|
||||
/// </summary>
|
||||
/// <param name="swap_Deal"></param>
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
@model UnwindData
|
||||
@{
|
||||
ViewBag.Title = "交易 | 收益结算";
|
||||
Layout = "~/Views/Shared/_InfoLayout.cshtml";
|
||||
bool isUseApproval = ViewBag.isUseApproval;
|
||||
bool isShowReCheckClose = ViewBag.IsShowReCheckClose;
|
||||
}
|
||||
@section CSS{
|
||||
<link rel="stylesheet" href="~/Style/Css/unwindSwapTrade.css?v=@(HtmlUtil.JsVersion)" />
|
||||
}
|
||||
@section JS
|
||||
{
|
||||
<script>
|
||||
var model = @Json.Serialize(Model);
|
||||
model.StartDate = model.StartDate ? model.StartDate.substr(0, 10) : "";
|
||||
model.ValueDate = model.ValueDate ? model.ValueDate.substr(0, 10) : "";
|
||||
var isUseApproval = "@isUseApproval" == "True";
|
||||
var g_isShowReCheckClose = "@isShowReCheckClose" == "True";
|
||||
</script>
|
||||
<script src="~/front/calendar?v=@(HtmlUtil.JsVersion)"></script>
|
||||
<script src="~/Scripts/fast/fastVue.components.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/front/swappriceprecision?v=@(HtmlUtil.JsVersion)"></script>
|
||||
<script src="~/Scripts/app/swaptrade/swapPricePrecisionHelper.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/Scripts/app/swaptrade/swapLongShort.js?v=@HtmlUtil.JsVersion"></script>
|
||||
}
|
||||
<div class="pb-3" id="vueDiv">
|
||||
<div class="yc-panel">
|
||||
<div id="commomArea" class="row" style="height:140px">
|
||||
<div class="form-group col-md-4">
|
||||
<label class="formlabel">成交名义本金</label>
|
||||
<input class="text-box" :value="formatAmount(deal.NotionalValue)" readonly="readonly" type="text" />
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="formlabel">持仓名义本金</label>
|
||||
<input class="text-box" :value="formatAmount(deal.PosiNotionalValue)" readonly="readonly" type="text" />
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="formlabel">起始日期</label>
|
||||
<input class="text-box" id="startDate" readonly="readonly" type="text" v-model="deal.StartDate" />
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="formlabel">收益结算日期</label>
|
||||
<vue-datepicker :maxdate="maxUnwindDate" :mindate="minStartDate" :holiday="1" v-model="deal.ValueDate" v-on:input="setValueDate" />
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="formlabel">年化天数</label>
|
||||
<input class="text-box" v-model="deal.AnnualDays" readonly="readonly" type="text" />
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="formlabel">平仓总额</label>
|
||||
<input class="text-box" :value="formatAmount(deal.SwapCloseAmount)" readonly="readonly" type="text" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="yc-panel">
|
||||
<div class="row no-gutters" style="display:block;">
|
||||
<div class="titleDiv">
|
||||
<div class="titleHead"></div>
|
||||
<div class="titleLabel"><label>预付金/预付金</label></div>
|
||||
</div>
|
||||
<div style="margin: 5px 0 0 12px">
|
||||
<table class="table table-bordered ratetable" style="width:750px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td width="116px">收支方向</td>
|
||||
<td>利息金额</td>
|
||||
<td>其他费用</td>
|
||||
<td>预付金/预付金平仓盈亏</td>
|
||||
</tr>
|
||||
<tr v-for="item in marginList">
|
||||
<td :class="item.InterestDirection==1?'swapget':'swappay'">{{item.InterestDirection==1?"收取":"支付"}}</td>
|
||||
<td>
|
||||
<vue-number-input v-model="item.InterestAmount" v-bind:format="inputFormatEqvNotional" v-on:input="changeInterestAmount(item)"></vue-number-input>
|
||||
</td>
|
||||
<td>
|
||||
<vue-number-input v-model="item.InterestFee" v-bind:format="inputFormatEqvNotional" v-on:input="changeInterestAmount(item)"></vue-number-input>
|
||||
</td>
|
||||
<td>{{formatAmount(item.InterestClosePnL)}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row no-gutters" style="display:block;">
|
||||
<div class="titleDiv">
|
||||
<div class="titleHead"></div>
|
||||
<div class="titleLabel"><label>利息端</label></div>
|
||||
</div>
|
||||
<div style="margin: 5px 0 0 12px">
|
||||
<table class="table table-bordered">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>收支方向</td>
|
||||
<td>利息金额</td>
|
||||
<td>其他费用</td>
|
||||
<td>利息端平仓盈亏</td>
|
||||
</tr>
|
||||
<tr v-for="item in interestList">
|
||||
<td :class="item.InterestDirection==1?'swapget':'swappay'">{{item.InterestDirection==1?"收取":"支付"}}</td>
|
||||
<td>
|
||||
<vue-number-input v-model="item.InterestAmount" v-bind:format="inputFormatEqvNotional" v-on:input="changeInterestAmount(item)"></vue-number-input>
|
||||
</td>
|
||||
<td>
|
||||
<vue-number-input v-model="item.InterestFee" v-bind:format="inputFormatEqvNotional" v-on:input="changeInterestAmount(item)"></vue-number-input>
|
||||
</td>
|
||||
<td>{{formatAmount(item.InterestClosePnL)}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
@if (isUseApproval)
|
||||
{
|
||||
<button class="btn btn-default swapclose" type="button" v-on:click="submitApproval('reject');">拒绝</button>
|
||||
<button class="btn btn-primary swapsave" type="button" v-on:click="submitApproval('pass')">审批通过</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="btn btn-default swapclose" type="button" onclick="layer.closeMe();">取消</button>
|
||||
<button class="btn btn-primary swapsave" type="button" v-on:click="incomeTrade">{{getSumbitText()}}</button>
|
||||
}
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,138 +0,0 @@
|
||||
@model UnwindData
|
||||
@{
|
||||
ViewBag.Title = "交易 | 交易平仓";
|
||||
Layout = "~/Views/Shared/_InfoLayout.cshtml";
|
||||
bool isUseApproval = ViewBag.isUseApproval;
|
||||
bool isShowReCheckClose = ViewBag.IsShowReCheckClose;
|
||||
}
|
||||
@section CSS{
|
||||
<link rel="stylesheet" href="~/Style/Css/unwindSwapTrade.css?v=@(HtmlUtil.JsVersion)" />
|
||||
}
|
||||
@section JS
|
||||
{
|
||||
<script>
|
||||
var model = @Json.Serialize(Model);
|
||||
model.StartDate = model.StartDate ? model.StartDate.substr(0, 10) : "";
|
||||
model.ValueDate = model.ValueDate ? model.ValueDate.substr(0, 10) : "";
|
||||
model.PayDate = model.PayDate ? model.PayDate.substr(0, 10) : "";
|
||||
var isUseApproval = "@isUseApproval" == "True";
|
||||
var g_isShowReCheckClose = "@isShowReCheckClose" == "True";
|
||||
</script>
|
||||
<script src="~/front/calendar?v=@(HtmlUtil.JsVersion)"></script>
|
||||
<script src="~/Scripts/fast/fastVue.components.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/front/swappriceprecision?v=@(HtmlUtil.JsVersion)"></script>
|
||||
<script src="~/Scripts/app/swaptrade/swapPricePrecisionHelper.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/Scripts/app/swaptrade/unwindLongShort.js?v=@HtmlUtil.JsVersion"></script>
|
||||
}
|
||||
<div class="pb-3" id="vueDiv">
|
||||
<div class="yc-panel">
|
||||
<div id="commomArea" class="row" style="height:140px">
|
||||
<div class="form-group col-md-3">
|
||||
<label class="formlabel">成交名义本金</label>
|
||||
<input class="text-box" :value="formatAmount(deal.NotionalValue)" readonly="readonly" type="text" />
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="formlabel">持仓名义本金</label>
|
||||
<input class="text-box" :value="formatAmount(deal.PosiNotionalValue)" readonly="readonly" type="text" />
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="formlabel">成交数量</label>
|
||||
<input class="text-box" :value="formatQuantity(deal.NotionalQty)" readonly="readonly" type="text" />
|
||||
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="formlabel">持仓数量</label>
|
||||
<input class="text-box" :value="formatQuantity(deal.PositionQty)" readonly="readonly" type="text" />
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="formlabel">起始日期</label>
|
||||
<input class="text-box" id="startDate" readonly="readonly" type="text" v-model="deal.StartDate" />
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="formlabel">平仓日期</label>
|
||||
<input class="text-box" id="DealDate" readonly="readonly" type="text" v-model="deal.ValueDate" />
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="formlabel">支付日期</label>
|
||||
<vue-datepicker :mindate="minStartDate" :holiday="1" v-model="deal.PayDate" />
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="formlabel">年化天数</label>
|
||||
<input class="text-box" v-model="deal.AnnualDays" readonly="readonly" type="text" />
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="formlabel">平仓总额</label>
|
||||
<input class="text-box" :value="formatAmount(deal.SwapCloseAmount)" readonly="readonly" type="text" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="yc-panel">
|
||||
<div class="row no-gutters" style="display:block;">
|
||||
<div class="titleDiv">
|
||||
<div class="titleHead"></div>
|
||||
<div class="titleLabel"><label>预付金/预付金</label></div>
|
||||
</div>
|
||||
<div style="margin: 5px 0 0 12px">
|
||||
<table class="table table-bordered ratetable" style="width:700px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td width="116px">收支方向</td>
|
||||
<td>资金类别</td>
|
||||
<td>应返还本金</td>
|
||||
<td>利息金额</td>
|
||||
<td>预付金/预付金平仓盈亏</td>
|
||||
</tr>
|
||||
<tr v-for="item in marginList">
|
||||
<td :class="item.InterestDirection==1?'swapget':'swappay'">{{item.InterestDirection==1?"收取":"支付"}}</td>
|
||||
<td>{{item.InterestModeStr}}</td>
|
||||
<td>
|
||||
<vue-number-input v-model="item.InterestPrincipal" v-bind:format="inputFormatEqvNotional" v-on:input="changeInterestAmount(item)"></vue-number-input>
|
||||
</td>
|
||||
<td>
|
||||
<vue-number-input v-model="item.InterestAmount" v-bind:format="inputFormatEqvNotional" v-on:input="changeInterestAmount(item)"></vue-number-input>
|
||||
</td>
|
||||
<td>{{formatAmount(item.InterestClosePnL)}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row no-gutters" style="display:block;">
|
||||
<div class="titleDiv">
|
||||
<div class="titleHead"></div>
|
||||
<div class="titleLabel"><label>利息端</label></div>
|
||||
</div>
|
||||
<div style="margin: 5px 0 0 12px">
|
||||
<table class="table table-bordered" style="width:750px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>收支方向</td>
|
||||
<td>利息金额</td>
|
||||
<td>利息端平仓盈亏</td>
|
||||
</tr>
|
||||
<tr v-for="item in interestList">
|
||||
<td :class="item.InterestDirection==1?'swapget':'swappay'">{{item.InterestDirection==1?"收取":"支付"}}</td>
|
||||
<td>
|
||||
<vue-number-input v-model="item.InterestAmount" v-bind:format="inputFormatEqvNotional" v-on:input="changeInterestAmount(item)"></vue-number-input>
|
||||
</td>
|
||||
<td>{{formatAmount(item.InterestClosePnL)}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
@if (isUseApproval)
|
||||
{
|
||||
<button class="btn btn-default swapclose" type="button" v-on:click="submitApproval('reject');">拒绝</button>
|
||||
<button class="btn btn-primary swapsave" type="button" v-on:click="submitApproval('pass')">审批通过</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="btn btn-default swapclose" type="button" onclick="layer.closeMe();">取消</button>
|
||||
<button class="btn btn-primary swapsave" type="button" v-on:click="closeTrade">{{getSumbitText()}}</button>
|
||||
}
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -141,10 +141,6 @@
|
||||
{
|
||||
@MyControls.Btn("平仓", string.Format("unWindSwapTrade('{0}')", tradeModel.EncryptId))
|
||||
}
|
||||
if (longShort && !hasPosition && tradeModel.CanSwapTradeUnwind() && CurUser.交易管理_交易平仓)
|
||||
{
|
||||
@MyControls.Btn("平仓", string.Format("unWindSwapLongShort('{0}')", tradeModel.EncryptId))
|
||||
}
|
||||
if (tradeModel.CanSwapTradeUnwind() && CurUser.交易管理_收益互换)
|
||||
{
|
||||
@MyControls.Btn("展期信息", string.Format("extensionTime('{0}')", tradeModel.EncryptId))
|
||||
@@ -153,10 +149,6 @@
|
||||
{
|
||||
@MyControls.Btn("收益结算", string.Format("unWindSwap('{0}')", tradeModel.EncryptId))
|
||||
}
|
||||
if (longShort && hasPosition && tradeModel.CanSwapTradeUnwind() && CurUser.交易管理_收益互换)
|
||||
{
|
||||
@MyControls.Btn("收益结算", string.Format("unWindLongShortSwap('{0}')", tradeModel.EncryptId))
|
||||
}
|
||||
@if (CurUser.交易管理_交易续作)
|
||||
{
|
||||
@MyControls.Btn("续作", string.Format("renewTrade('{0}')", tradeModel.EncryptId))
|
||||
|
||||
@@ -43,43 +43,5 @@
|
||||
"SwapMarginRebatePnl": 200,
|
||||
"SwapMarginAmount": 40000
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "LONGSHORT_UNWIND",
|
||||
"desc": "多空组合平仓(unwindLongShort):从0起,不含浮动盈亏(FloatPnlSum=0)",
|
||||
"eventType": 4,
|
||||
"deal": { "CloseQty": 0 },
|
||||
"floatPosition": { "FloatPnlSum": 0, "PositionType": 1, "PayDirection": 1, "TradingFee": 0 },
|
||||
"interestList": [],
|
||||
"marginList": [
|
||||
{ "InterestClosePnL": 150, "InterestDirection": 1, "InterestPrincipal": 20000 }
|
||||
],
|
||||
"expected": {
|
||||
"SwapCloseAmount": 150,
|
||||
"SwapRealizedPnL": 150,
|
||||
"SwapMarginRebatePnl": 150,
|
||||
"SwapMarginAmount": -20000
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "LONGSHORT_SWAP",
|
||||
"desc": "多空组合互换(swapLongShort):SwapMarginAmount 用裸 InterestPrincipal(无符号) —— 已知差异",
|
||||
"eventType": 5,
|
||||
"deal": { "CloseQty": 0 },
|
||||
"floatPosition": { "FloatPnlSum": 0, "PositionType": 1, "PayDirection": 1, "TradingFee": 0 },
|
||||
"interestList": [],
|
||||
"marginList": [
|
||||
{ "InterestClosePnL": 100, "InterestDirection": 1, "InterestPrincipal": 18000 }
|
||||
],
|
||||
"expected": {
|
||||
"SwapCloseAmount": 100,
|
||||
"SwapRealizedPnL": 100,
|
||||
"SwapMarginRebatePnl": 100,
|
||||
"SwapMarginAmount": 18000
|
||||
},
|
||||
"knownDiscrepancies": {
|
||||
"SwapMarginAmount": 36000
|
||||
}
|
||||
}
|
||||
]
|
||||
} ]
|
||||
}
|
||||
|
||||
@@ -163,8 +163,6 @@ describe('swap price precision common wiring', () => {
|
||||
edit: read('wwwroot/Scripts/app/swaptrade/swapTradeEdit.js'),
|
||||
income: read('wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js'),
|
||||
unwind: read('wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js'),
|
||||
longShort: read('wwwroot/Scripts/app/swaptrade/swapLongShort.js'),
|
||||
unwindLongShort: read('wwwroot/Scripts/app/swaptrade/unwindLongShort.js'),
|
||||
view: read('wwwroot/Scripts/app/swaptrade/swapTradeView.js'),
|
||||
flow: read('wwwroot/Scripts/app/swaptrade/SwapflowList.js'),
|
||||
helper: precisionHelperSrc
|
||||
|
||||
@@ -2000,9 +2000,6 @@ const subView = (function (jqGridMgr) {
|
||||
|
||||
if (tradeType === "收益互换") {
|
||||
srcurl = "/swaptrade2/SwapUnwind/?enid=" + encryptId;
|
||||
if (StructureType=="多空组合") {
|
||||
srcurl = "/swaptrade2/SwapLongShortUnwind/?enid=" + encryptId;
|
||||
}
|
||||
area = ["1300px", "720px"];
|
||||
}
|
||||
if (tradeType.indexOf("远期") >= 0) {
|
||||
|
||||
@@ -112,7 +112,7 @@ const vue = new Vue({
|
||||
this.multiplier,
|
||||
isUseApproval);
|
||||
this.interestList = model.FlowEvents.filter((item) => {
|
||||
return item.InterestMode == 1 || item.InterestMode == 2 || item.InterestMode == 7 || item.InterestMode == 8 || item.InterestMode == 9;
|
||||
return item.InterestMode == 1 || item.InterestMode == 2 || item.InterestMode == 9;
|
||||
});
|
||||
this.marginList = model.FlowEvents.filter((item) => {
|
||||
return item.InterestMode == 5 || item.InterestMode == 6;
|
||||
@@ -255,7 +255,7 @@ const vue = new Vue({
|
||||
var postData = { ValueDate: thisObj.deal.ValueDate, unwindDate: thisObj.deal.UnwindDate, tradeId: thisObj.deal.SwapTradeId, closePercent: 1, eventType:3 }
|
||||
main.post("/swaptrade2/GetUnwindInterestList", postData, { async: true }).done(function (resp) {
|
||||
thisObj.interestList = resp.obj.filter((item) => {
|
||||
return item.InterestMode == 1 || item.InterestMode == 2 || item.InterestMode == 7 || item.InterestMode == 8 || item.InterestMode == 9;
|
||||
return item.InterestMode == 1 || item.InterestMode == 2 || item.InterestMode == 9;
|
||||
});
|
||||
thisObj.marginList = resp.obj.filter((item) => {
|
||||
return item.InterestMode == 5 || item.InterestMode == 6;
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
//otcformat禁止千分位分组
|
||||
window.otcformat.options.disableGrouping = true;
|
||||
const inputFormatEqvNotional = swapPricePrecision.getCommonInputFormat('amount', { append: '' });
|
||||
const swapInstrumentType = (model.FlowEvents || []).find(item => item && item.UnderlyingInstrumentType)?.UnderlyingInstrumentType || model.UnderlyingInstrumentType || '';
|
||||
const formatSwapAmount = value => swapPricePrecision.normalizeCommon('amount', value);
|
||||
const formatSwapQuantity = value => swapPricePrecision.normalizeCommon('quantity', value, swapInstrumentType);
|
||||
let ValueDate = model.ValueDate;
|
||||
const vue = new Vue({
|
||||
el: '#vueDiv',
|
||||
data: {
|
||||
deal: model,
|
||||
interestList: [],
|
||||
marginList: [],
|
||||
},
|
||||
computed: {
|
||||
maxUnwindDate() {
|
||||
return ValueDate;
|
||||
},
|
||||
minStartDate() {
|
||||
return this.deal.StartDate;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.initDeal();
|
||||
this.setValueDate();
|
||||
},
|
||||
methods: {
|
||||
formatAmount(value) {
|
||||
return swapPricePrecision.formatCommon('amount', value);
|
||||
},
|
||||
formatQuantity(value) {
|
||||
return swapPricePrecision.formatCommon('quantity', value, swapInstrumentType);
|
||||
},
|
||||
initDeal() {
|
||||
this.interestList = model.FlowEvents.filter((item) => {
|
||||
return item.InterestMode == 1 || item.InterestMode == 2 || item.InterestMode == 7 || item.InterestMode == 8 || item.InterestMode == 9;
|
||||
});
|
||||
this.marginList = model.FlowEvents.filter((item) => {
|
||||
return item.InterestMode == 5 || item.InterestMode == 6;
|
||||
});
|
||||
},
|
||||
setValueDate(e) {//修改平仓日期
|
||||
if (e) {
|
||||
this.deal.ValueDate = e;
|
||||
this.deal.UnwindDate = e;
|
||||
}
|
||||
if (!isUseApproval) {
|
||||
this.getInterestList();
|
||||
} else {
|
||||
this.dataFormat();
|
||||
}
|
||||
},
|
||||
dataFormat() {
|
||||
this.deal.NotionalValue = formatSwapAmount(this.deal.NotionalValue);
|
||||
this.deal.PosiNotionalValue = formatSwapAmount(this.deal.PosiNotionalValue);
|
||||
this.deal.NotionalQty = formatSwapQuantity(this.deal.NotionalQty);
|
||||
this.deal.PositionQty = formatSwapQuantity(this.deal.PositionQty);
|
||||
this.deal.SwapCloseAmount = formatSwapAmount(this.deal.SwapCloseAmount);
|
||||
this.interestList.forEach(x => {
|
||||
//x.Principal = formatSwapAmount(x.Principal);
|
||||
//x.Rate = otcformat.fixed6(x.Rate);
|
||||
x.InterestFee = formatSwapAmount(x.InterestFee);
|
||||
x.InterestAmount = formatSwapAmount(x.InterestAmount);
|
||||
x.InterestClosePnL = formatSwapAmount(x.InterestClosePnL);
|
||||
//x.InterestStartDate = x.InterestStartDate ? x.InterestStartDate.substr(0, 10) : "";
|
||||
//x.InterestEndDate = x.InterestEndDate ? x.InterestEndDate.substr(0, 10) : "";
|
||||
});
|
||||
this.marginList.forEach(x => {
|
||||
x.InterestFee = formatSwapAmount(x.InterestFee);
|
||||
x.InterestAmount = formatSwapAmount(x.InterestAmount);
|
||||
x.InterestClosePnL = formatSwapAmount(x.InterestClosePnL);
|
||||
});
|
||||
},
|
||||
changeInterestAmount(item) {//修改利息金额
|
||||
let interestRatio = item.InterestDirection == 1 ? 1 : -1;
|
||||
item.InterestClosePnL = formatSwapAmount(parseFloat(item.InterestAmount) * interestRatio + parseFloat(item.InterestFee));
|
||||
this.calcCloseAmount();
|
||||
},
|
||||
calcCloseAmount() {//计算平仓总额=浮动收取+利息收取-浮动支付-利息支付
|
||||
let thisObj = this;
|
||||
thisObj.deal.SwapCloseAmount = 0;
|
||||
thisObj.deal.SwapRealizedPnL = 0;
|
||||
thisObj.deal.SwapMarginRebatePnl = 0;
|
||||
thisObj.deal.SwapMarginAmount = 0;
|
||||
this.interestList.forEach(x => {
|
||||
/*let interestRatio = x.InterestDirection == 1 ? 1 : -1;*/
|
||||
let interestAmount = parseFloat(x.InterestClosePnL);
|
||||
thisObj.deal.SwapCloseAmount = parseFloat(thisObj.deal.SwapCloseAmount) + interestAmount;
|
||||
thisObj.deal.SwapRealizedPnL = parseFloat(thisObj.deal.SwapRealizedPnL) + interestAmount;
|
||||
});
|
||||
this.marginList.forEach(x => {
|
||||
let interestAmount = parseFloat(x.InterestClosePnL);
|
||||
thisObj.deal.SwapCloseAmount = parseFloat(thisObj.deal.SwapCloseAmount) + interestAmount;
|
||||
thisObj.deal.SwapMarginRebatePnl = parseFloat(thisObj.deal.SwapMarginRebatePnl) + interestAmount;
|
||||
thisObj.deal.SwapRealizedPnL = parseFloat(thisObj.deal.SwapRealizedPnL) + interestAmount;
|
||||
thisObj.deal.SwapMarginAmount = parseFloat(thisObj.deal.SwapMarginAmount) + parseFloat(x.InterestPrincipal);
|
||||
});
|
||||
thisObj.deal.SwapCloseAmount = formatSwapAmount(thisObj.deal.SwapCloseAmount);
|
||||
thisObj.deal.SwapRealizedPnL = formatSwapAmount(thisObj.deal.SwapRealizedPnL);
|
||||
thisObj.deal.SwapMarginRebatePnl = formatSwapAmount(thisObj.deal.SwapMarginRebatePnl);
|
||||
thisObj.deal.SwapMarginAmount = formatSwapAmount(thisObj.deal.SwapMarginAmount);
|
||||
},
|
||||
getInterestList() {//根据平仓日期获取利息腿信息
|
||||
var thisObj = this;
|
||||
var postData = { valueDate: thisObj.deal.ValueDate, unwindDate: thisObj.deal.UnwindDate, tradeId: thisObj.deal.SwapTradeId, closePercent: 1, eventType: 3 }
|
||||
main.post("/swaptrade2/GetUnwindInterestList", postData, { async: true }).done(function (resp) {
|
||||
thisObj.interestList = resp.obj.filter((item) => {
|
||||
return item.InterestMode == 1 || item.InterestMode == 2 || item.InterestMode == 7 || item.InterestMode == 8 || item.InterestMode == 9;
|
||||
});
|
||||
thisObj.marginList = resp.obj.filter((item) => {
|
||||
return item.InterestMode == 5 || item.InterestMode == 6;
|
||||
});
|
||||
thisObj.calcCloseAmount();
|
||||
thisObj.dataFormat();
|
||||
});
|
||||
},
|
||||
incomeTrade() {//互换
|
||||
var thisObj = this;
|
||||
if (main.isEmpty(thisObj.deal.ValueDate)) {
|
||||
main.message("请输入平仓日期");
|
||||
return;
|
||||
}
|
||||
let reqObj = _.cloneDeep(thisObj.deal);
|
||||
let marginCloneList = _.cloneDeep(thisObj.marginList);
|
||||
|
||||
reqObj.FlowEvents = _.cloneDeep(thisObj.interestList);
|
||||
marginCloneList.forEach((item) => {
|
||||
reqObj.FlowEvents.push(item);
|
||||
})
|
||||
var postData = { unwindData: reqObj };
|
||||
var msg = "确认提交收益结算?";
|
||||
var postUrl = "/swaptrade2/SwapLongShortJson";
|
||||
if (g_isShowReCheckClose) {
|
||||
msg = "确认提交收益结算审核?";
|
||||
postUrl = "/swaptrade2/ApplyUnwind";
|
||||
postData.eventType = 3;//互换3,平仓2
|
||||
}
|
||||
main.confirm(msg,
|
||||
function () {
|
||||
//重新计算百分比
|
||||
var thisObj2 = thisObj;
|
||||
main.post(postUrl, { unwindData: reqObj }).done(function (res) {
|
||||
if (res.success) {
|
||||
thisObj2.closetrade_cashWindow();
|
||||
}
|
||||
else {
|
||||
try {
|
||||
thisObj2.closetrade_cashWindow();
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
getSumbitText: function () {
|
||||
return g_isShowReCheckClose ? "审核提交" : "保存";
|
||||
},
|
||||
submitApproval(status) {
|
||||
var pop = '';
|
||||
if (status === 'pass') {
|
||||
pop = "确认通过审批?";
|
||||
}
|
||||
if (status === 'reject') {
|
||||
pop = "确认拒绝?";
|
||||
}
|
||||
var confirmFunc = function (additionalProcessing) {
|
||||
var pData = { tradeId: trade.id, status: status, text: "" };
|
||||
if (!main.isEmpty(additionalProcessing)) {
|
||||
pData.additionalProcessing = additionalProcessing;
|
||||
}
|
||||
var thisObj2 = thisObj;
|
||||
main.post("/processtradelog/UpdateTradeProcessLog", pData).done(
|
||||
function (data) {
|
||||
if (data.obj && data.obj.proccessType == "AdditionalProcessing") {
|
||||
if (data.obj.type == "LackOfMoney") {
|
||||
var htmlContent = `<div style="padding:10px">${data.obj.message}</div>`;
|
||||
var lackMoneyConfirmLayer = main.open2("提示",
|
||||
htmlContent,
|
||||
{
|
||||
area: ["430px", "175px"],
|
||||
btn: ['交易特批', '取消'],
|
||||
yes: function (index, layero) {
|
||||
var layerIndex = lackMoneyConfirmLayer;
|
||||
main.confirm("客户资金或授信不足,强制成交会导致本机构产生风险!要继续审批通过?", function () {
|
||||
layer.close(layerIndex);
|
||||
confirmFunc("LackOfMoney");
|
||||
});
|
||||
},
|
||||
cancel: function (index, layero) {
|
||||
if (window.parent && window.parent.reloadtrade) {
|
||||
thisObj2.closetrade_cashWindow();
|
||||
}
|
||||
(parent || window).layer.closeAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
(parent || window).main.message(data.msg);
|
||||
try { thisObj2.closetrade_cashWindow(); }
|
||||
catch (e) { }
|
||||
if (parent) {
|
||||
parent.layer.closeAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
main.confirm(pop, confirmFunc);
|
||||
},
|
||||
closetrade_cashWindow: function () {
|
||||
layer.closeMe('reloadData');
|
||||
},
|
||||
closeCurrentWindow: function () {
|
||||
try {
|
||||
if (window.parent && window.parent.reload) window.parent.reload();
|
||||
} catch (e) {
|
||||
}
|
||||
try {
|
||||
var layer = window.parent.layer;
|
||||
layer.close(layer.getFrameIndex(window.name));
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
'vue-datepicker': FastVue.vueDatePicker(),
|
||||
'vue-number-input': FastVue.vueNumberInput(),
|
||||
}
|
||||
});
|
||||
@@ -868,9 +868,6 @@ const vue = new Vue({
|
||||
}
|
||||
},
|
||||
changeInterestMode(item) {
|
||||
if (item.InterestMode == 7 || item.InterestMode == 8) {
|
||||
item.InterestType = 0;
|
||||
}
|
||||
},
|
||||
changeInterestType(item) {
|
||||
if (!item.interest_rest_days) {
|
||||
@@ -1462,28 +1459,7 @@ const vue = new Vue({
|
||||
},
|
||||
// 多空组合利息腿校验
|
||||
checkSwapRateList() {
|
||||
var thisObj = this;
|
||||
var longInterestModelCount = 0;
|
||||
var shortInterestModelCount = 0;
|
||||
var check = true;
|
||||
if (thisObj.getSwapList != null) {
|
||||
thisObj.getSwapList.forEach((val, num, arr) => {
|
||||
if (val.InterestMode==7) {
|
||||
longInterestModelCount++;
|
||||
}
|
||||
if (val.InterestMode == 8) {
|
||||
shortInterestModelCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (longInterestModelCount > 1) {
|
||||
check = false;
|
||||
main.message("计息基本类型为多头存续名义本金的利息腿只能有一条");
|
||||
}
|
||||
if (shortInterestModelCount > 1) {
|
||||
check = false;
|
||||
main.message("计息基本类型为空头存续名义本金的利息腿只能有一条");
|
||||
}
|
||||
return check;
|
||||
},
|
||||
//观察日起始日期跟交易起始日期检查
|
||||
@@ -1619,7 +1595,7 @@ const vue = new Vue({
|
||||
//初始化利息端列表
|
||||
initSwapRateList() {
|
||||
var thisObj = this;
|
||||
thisObj.getSwapList = thisObj.trade.swap_positions.filter(x => { if ((x.UnderlyingCode == null || x.UnderlyingCode.length == 0) && x.IsInitial && (x.InterestMode == 1 || x.InterestMode == 2 || x.InterestMode == 7 || x.InterestMode == 8 || x.InterestMode == 9)) return x; });
|
||||
thisObj.getSwapList = thisObj.trade.swap_positions.filter(x => { if ((x.UnderlyingCode == null || x.UnderlyingCode.length == 0) && x.IsInitial && (x.InterestMode == 1 || x.InterestMode == 2 || x.InterestMode == 9)) return x; });
|
||||
thisObj.getSwapList.forEach((val, num, arr) => {
|
||||
arr[num].index = num;
|
||||
arr[num].category_tag = arr[num].category_tag || '互换利率';
|
||||
|
||||
@@ -330,46 +330,6 @@ function unWindSwap(id) {
|
||||
}
|
||||
});
|
||||
}
|
||||
function unWindSwapLongShort(id) {
|
||||
var title = "交易平仓";
|
||||
var srcurl = "/swaptrade2/SwapLongShortUnwind/?enid=" + id;
|
||||
main.post("/swaptrade2/CheckEodTrade?enid=" + id).done(function (res) {
|
||||
if (res.success) {
|
||||
main.open(title,
|
||||
srcurl,
|
||||
{
|
||||
area: ["1300px", "780px"],
|
||||
end: function () {
|
||||
if (window.parent && window.parent.reloadtrade) {
|
||||
window.parent.reloadtrade();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
main.message(res.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
function unWindLongShortSwap(id) {
|
||||
var title = "期间互换";
|
||||
var srcurl = "/swaptrade2/SwapLongShortSwap/?enid=" + id;
|
||||
main.post("/swaptrade2/CheckEodTrade?enid=" + id).done(function (res) {
|
||||
if (res.success) {
|
||||
main.open(title,
|
||||
srcurl,
|
||||
{
|
||||
area: ["1300px", "780px"],
|
||||
end: function () {
|
||||
if (window.parent && window.parent.reloadtrade) {
|
||||
window.parent.reloadtrade();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
main.message(res.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
function extensionTime(id) {
|
||||
var title = "展期信息";
|
||||
var srcurl = "/swaptrade2/ExtensionTime/?enid=" + id;
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
//otcformat禁止千分位分组
|
||||
window.otcformat.options.disableGrouping = true;
|
||||
const inputFormatEqvNotional = swapPricePrecision.getCommonInputFormat('amount', { append: '' });
|
||||
const swapInstrumentType = (model.FlowEvents || []).find(item => item && item.UnderlyingInstrumentType)?.UnderlyingInstrumentType || model.UnderlyingInstrumentType || '';
|
||||
const formatSwapAmount = value => swapPricePrecision.normalizeCommon('amount', value);
|
||||
const formatSwapQuantity = value => swapPricePrecision.normalizeCommon('quantity', value, swapInstrumentType);
|
||||
let dealDate = model.DealDate;
|
||||
const vue = new Vue({
|
||||
el: '#vueDiv',
|
||||
data: {
|
||||
deal: model,
|
||||
interestList: [],
|
||||
marginList: [],
|
||||
oriClosePercent: model.ClosePercent
|
||||
},
|
||||
computed: {
|
||||
minStartDate() {
|
||||
return dealDate;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.initDeal();
|
||||
this.calcCloseAmount();
|
||||
this.dataFormat();
|
||||
},
|
||||
methods: {
|
||||
formatAmount(value) {
|
||||
return swapPricePrecision.formatCommon('amount', value);
|
||||
},
|
||||
formatQuantity(value) {
|
||||
return swapPricePrecision.formatCommon('quantity', value, swapInstrumentType);
|
||||
},
|
||||
initDeal() {
|
||||
this.interestList = model.FlowEvents.filter((item) => {
|
||||
return item.InterestMode == 1 || item.InterestMode == 2 || item.InterestMode == 7 || item.InterestMode == 8 || item.InterestMode == 9;
|
||||
});
|
||||
this.marginList = model.FlowEvents.filter((item) => {
|
||||
return item.InterestMode == 5 || item.InterestMode == 6;
|
||||
});
|
||||
},
|
||||
dataFormat() {
|
||||
this.deal.NotionalValue = formatSwapAmount(this.deal.NotionalValue);
|
||||
this.deal.PosiNotionalValue = formatSwapAmount(this.deal.PosiNotionalValue);
|
||||
this.deal.NotionalQty = formatSwapQuantity(this.deal.NotionalQty);
|
||||
this.deal.SwapCloseAmount = formatSwapAmount(this.deal.SwapCloseAmount);
|
||||
this.deal.PositionQty = formatSwapQuantity(this.deal.PositionQty);
|
||||
this.interestList.forEach(x => {
|
||||
//x.Principal = formatSwapAmount(x.Principal);
|
||||
//x.Rate = otcformat.fixed6(x.Rate);
|
||||
x.InterestAmount = formatSwapAmount(x.InterestAmount);
|
||||
x.InterestClosePnL = formatSwapAmount(x.InterestClosePnL);
|
||||
//x.InterestStartDate = x.InterestStartDate ? x.InterestStartDate.substr(0, 10) : "";
|
||||
//x.InterestEndDate = x.InterestEndDate ? x.InterestEndDate.substr(0, 10) : "";
|
||||
});
|
||||
this.marginList.forEach(x => {
|
||||
x.InterestAmount = formatSwapAmount(x.InterestAmount);
|
||||
x.InterestClosePnL = formatSwapAmount(x.InterestClosePnL);
|
||||
});
|
||||
},
|
||||
changeInterestAmount(item) {//修改利息金额
|
||||
let interestRatio = item.InterestDirection == 1 ? 1 : -1;
|
||||
item.InterestClosePnL = formatSwapAmount(parseFloat(item.InterestAmount) * interestRatio+ parseFloat(item.InterestFee));
|
||||
this.calcCloseAmount();
|
||||
},
|
||||
calcCloseAmount() {//计算平仓总额=浮动收取+利息收取-浮动支付-利息支付
|
||||
let thisObj = this;
|
||||
thisObj.deal.SwapCloseAmount = 0;
|
||||
thisObj.deal.SwapRealizedPnL = 0;
|
||||
thisObj.deal.SwapMarginRebatePnl = 0;
|
||||
thisObj.deal.SwapMarginAmount = 0;
|
||||
this.interestList.forEach(x => {
|
||||
/*let interestRatio = x.InterestDirection == 1 ? 1 : -1;*/
|
||||
let interestAmount = parseFloat(x.InterestClosePnL);
|
||||
thisObj.deal.SwapCloseAmount = parseFloat(thisObj.deal.SwapCloseAmount) + interestAmount;
|
||||
thisObj.deal.SwapRealizedPnL = parseFloat(thisObj.deal.SwapRealizedPnL) + interestAmount;
|
||||
});
|
||||
this.marginList.forEach(x => {
|
||||
let interestAmount = parseFloat(x.InterestClosePnL);
|
||||
let interestRatio = x.InterestDirection == 1 ? -1 : 1;
|
||||
thisObj.deal.SwapCloseAmount = parseFloat(thisObj.deal.SwapCloseAmount) + interestAmount;
|
||||
thisObj.deal.SwapMarginRebatePnl = parseFloat(thisObj.deal.SwapMarginRebatePnl) + interestAmount;
|
||||
thisObj.deal.SwapRealizedPnL = parseFloat(thisObj.deal.SwapRealizedPnL) + interestAmount;
|
||||
thisObj.deal.SwapMarginAmount = parseFloat(thisObj.deal.SwapMarginAmount) + parseFloat(x.InterestPrincipal) * interestRatio;
|
||||
});
|
||||
thisObj.deal.SwapCloseAmount = formatSwapAmount(thisObj.deal.SwapCloseAmount);
|
||||
thisObj.deal.SwapRealizedPnL = formatSwapAmount(thisObj.deal.SwapRealizedPnL);
|
||||
thisObj.deal.SwapMarginRebatePnl = formatSwapAmount(thisObj.deal.SwapMarginRebatePnl);
|
||||
thisObj.deal.SwapMarginAmount = formatSwapAmount(thisObj.deal.SwapMarginAmount);
|
||||
},
|
||||
closeTrade() {//平仓
|
||||
var thisObj = this;
|
||||
let reqObj = _.cloneDeep(thisObj.deal);
|
||||
let marginCloneList = _.cloneDeep(thisObj.marginList);
|
||||
reqObj.FlowEvents = _.cloneDeep(thisObj.interestList);
|
||||
marginCloneList.forEach((item) => {
|
||||
reqObj.FlowEvents.push(item);
|
||||
})
|
||||
var postData = { unwindData: reqObj };
|
||||
var msg = "确认提交平仓?";
|
||||
var postUrl = "/swaptrade2/SwapLongShortUnwindJson";
|
||||
if (g_isShowReCheckClose) {
|
||||
msg = "确认提交平仓审核?";
|
||||
postUrl = "/swaptrade2/ApplyUnwind";
|
||||
postData.eventType = 2;//互换3,平仓2
|
||||
}
|
||||
main.confirm(msg,
|
||||
function () {
|
||||
//重新计算百分比
|
||||
var thisObj2 = thisObj;
|
||||
main.post(postUrl, postData).done(function (res) {
|
||||
if (res.success) {
|
||||
thisObj2.closetrade_cashWindow();
|
||||
}
|
||||
else {
|
||||
try {
|
||||
thisObj2.closetrade_cashWindow();
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
getSumbitText: function () {
|
||||
return g_isShowReCheckClose ? "审核提交" : "保存";
|
||||
},
|
||||
submitApproval(status) {
|
||||
var pop = '';
|
||||
if (status === 'pass') {
|
||||
pop = "确认通过审批?";
|
||||
}
|
||||
if (status === 'reject') {
|
||||
pop = "确认拒绝?";
|
||||
}
|
||||
let thisObj = this;
|
||||
var confirmFunc = function (additionalProcessing) {
|
||||
var pData = { tradeId: thisObj.deal.SwapTradeId, status: status, text: "" };
|
||||
if (!main.isEmpty(additionalProcessing)) {
|
||||
pData.additionalProcessing = additionalProcessing;
|
||||
}
|
||||
var thisObj2 = thisObj;
|
||||
main.post("/processtradelog/UpdateTradeProcessLog", pData).done(
|
||||
function (data) {
|
||||
if (data.obj && data.obj.proccessType == "AdditionalProcessing") {
|
||||
if (data.obj.type == "LackOfMoney") {
|
||||
var htmlContent = `<div style="padding:10px">${data.obj.message}</div>`;
|
||||
var lackMoneyConfirmLayer = main.open2("提示",
|
||||
htmlContent,
|
||||
{
|
||||
area: ["430px", "175px"],
|
||||
btn: ['交易特批', '取消'],
|
||||
yes: function (index, layero) {
|
||||
var layerIndex = lackMoneyConfirmLayer;
|
||||
main.confirm("客户资金或授信不足,强制成交会导致本机构产生风险!要继续审批通过?", function () {
|
||||
layer.close(layerIndex);
|
||||
confirmFunc("LackOfMoney");
|
||||
});
|
||||
},
|
||||
cancel: function (index, layero) {
|
||||
if (window.parent && window.parent.reloadtrade) {
|
||||
thisObj2.closetrade_cashWindow();
|
||||
}
|
||||
(parent || window).layer.closeAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
(parent || window).main.message(data.msg);
|
||||
try { thisObj2.closetrade_cashWindow(); }
|
||||
catch (e) { }
|
||||
if (parent) {
|
||||
parent.layer.closeAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
main.confirm(pop, confirmFunc);
|
||||
},
|
||||
closetrade_cashWindow: function () {
|
||||
layer.closeMe('reloadData');
|
||||
},
|
||||
closeCurrentWindow: function () {
|
||||
try {
|
||||
if (window.parent && window.parent.reload) window.parent.reload();
|
||||
} catch (e) {
|
||||
}
|
||||
try {
|
||||
var layer = window.parent.layer;
|
||||
layer.close(layer.getFrameIndex(window.name));
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
'vue-datepicker': FastVue.vueDatePicker(),
|
||||
'vue-number-input': FastVue.vueNumberInput(),
|
||||
}
|
||||
});
|
||||
@@ -110,7 +110,7 @@ const vue = new Vue({
|
||||
this.floatPosition = positions[0];
|
||||
this.initPosiNetPrice = this.floatPosition.PosiGrossPrice;
|
||||
this.interestList = model.FlowEvents.filter((item) => {
|
||||
return item.InterestMode == 1 || item.InterestMode == 2 || item.InterestMode == 7 || item.InterestMode == 8 || item.InterestMode == 9;
|
||||
return item.InterestMode == 1 || item.InterestMode == 2 || item.InterestMode == 9;
|
||||
});
|
||||
this.marginList = model.FlowEvents.filter((item) => {
|
||||
return item.InterestMode == 5 || item.InterestMode == 6;
|
||||
@@ -375,7 +375,7 @@ const vue = new Vue({
|
||||
' closeNotionalValue=', thisObj.deal.CloseNotionalValue, ' posiNotionalValue=', thisObj.deal.PosiNotionalValue);
|
||||
main.post("/swaptrade2/GetUnwindInterestList", postData, { async: true }).done(function (resp) {
|
||||
thisObj.interestList = resp.obj.filter((item) => {
|
||||
return item.InterestMode == 1 || item.InterestMode == 2 || item.InterestMode == 7 || item.InterestMode == 8 || item.InterestMode == 9;
|
||||
return item.InterestMode == 1 || item.InterestMode == 2 || item.InterestMode == 9;
|
||||
});
|
||||
thisObj.marginList = resp.obj.filter((item) => {
|
||||
return item.InterestMode == 5 || item.InterestMode == 6;
|
||||
|
||||
@@ -731,9 +731,6 @@ function passorreinfo(eid, tradeType, status, isGroup, StructureType) {
|
||||
else if (tradeType === "收益互换") {
|
||||
weight = "95%";
|
||||
url = "/swaptrade2/SwapUnwind/?enid=" + eid + "&isUseApproval=" + true;
|
||||
if (StructureType == "多空组合") {
|
||||
url = "/swaptrade2/SwapLongShortUnwind/?enid=" + eid + "&isUseApproval=" + true;
|
||||
}
|
||||
height = "800px";
|
||||
}
|
||||
else {
|
||||
@@ -744,9 +741,6 @@ function passorreinfo(eid, tradeType, status, isGroup, StructureType) {
|
||||
weight = "95%";
|
||||
title = "互换审批";
|
||||
url = "/swaptrade2/SwapIncome/?enid=" + eid + "&isUseApproval=" + true;
|
||||
if (StructureType == "多空组合") {
|
||||
url = "/swaptrade2/SwapLongShortSwap/?enid=" + eid + "&isUseApproval=" + true;
|
||||
}
|
||||
height = "800px";
|
||||
}
|
||||
else if (status === "行权待复核") {
|
||||
@@ -883,9 +877,6 @@ function unWindSelect(id, tradeType, isReCheck, StructureType) {
|
||||
});
|
||||
} else if (tradeType == "收益互换") {
|
||||
var url = "/swaptrade2/SwapUnwind/?enid=" + id + "&isUseApproval=" + true;;
|
||||
if (StructureType == "多空组合") {
|
||||
url = "/swaptrade2/SwapLongShortUnwind/?enid=" + id + "&isUseApproval=" + true;;
|
||||
}
|
||||
main.open(title, url, {
|
||||
area: ["1300px", "720px"],
|
||||
end: function () {
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
# 缺陷分析:利息部分平仓尾差(业务场景3 / 业务场景4 浮动利率)
|
||||
|
||||
> 数据来源:`缺陷测试-利息20260807晚.xlsx`(独立手算 oracle,非代码 re-baseline)
|
||||
> 分析日期:2026-08-08 | 关联分支:`glms/feature/1.4.2`
|
||||
|
||||
## 1. 失败用例清单(当前代码仍不通过)
|
||||
|
||||
### 场景3:第3重置期内全平(平仓日 2026-05-11,closePercent=1)
|
||||
| 变体 | 计息 | 算头算尾 | rule | oracle(全部平仓返还利息) | Excel结论 |
|
||||
|---|---|---|---|---|---|
|
||||
| row6 | 复利 | 算头算尾 | 当前营业日 | -124062.54 | **不通过** |
|
||||
| row7 | 复利 | 算头算尾 | 当前营业日 | 280303.16 | **不通过** |
|
||||
| row8 | 复利 | 算头不算尾 | 当前营业日 | -117918.47 | 通过 |
|
||||
| row9 | 复利 | 算头不算尾 | 当前营业日 | 266674.35 | 通过 |
|
||||
| row10 | 复利 | 算头算尾 | 前一营业日 | -123730.45 | **不通过** |
|
||||
| row11 | 复利 | 算头算尾 | 前一营业日 | 279733.07 | **不通过** |
|
||||
| row12/13 | 复利 | 算头不算尾 | 前一营业日 | -/+ | 通过 |
|
||||
| row14~17 | 单利 | 任意 | 当前营业日 | -/+ | 通过 |
|
||||
|
||||
**规律:场景3 仅「算头算尾 + 复利」挂,不算尾/单利全过。**
|
||||
|
||||
### 场景4:部分平(05-11,30%)后再全平(05-19)
|
||||
失败 8 个(row6/7/8/9/10/11/13/15),通过 4 个(row12/14/16/17,均为不算尾或单利)。
|
||||
Excel 备注(row6)原文:
|
||||
> 不通过,部分平仓时,利息端平仓金额没有跟随平仓比例变化。全部平仓时,居然没有考虑已经过大支付了利息。
|
||||
|
||||
场景4 row6 量化:oracle 最终全平 = -124093.74,系统 = -124122.96,**系统多算 29.22**;
|
||||
部分用例偏差更大(row8 多算 3685.96,row15 多算 3961.58)。说明部分平仓后再全平的尾差链路在复利下整体脆弱。
|
||||
|
||||
## 2. 根因(代码实证)
|
||||
|
||||
### Bug A:部分平仓利息端未随平仓比例缩放
|
||||
`SwapDealService.cs` 盘中平仓(复利分支,`CalcDailyCompoundInterest` 内 `daysFromPreEod==1` 早退分支):
|
||||
```
|
||||
InterestAmount = preEodPosition.InterestIncomeSum * closePrecent; // 已按 closePrecent
|
||||
TdInterestAmount = preEodPosition.InterestIncomeSum; // ← 未乘 closePrecent
|
||||
```
|
||||
`TdInterestAmount`(当日实现利息)未乘 `closePrecent`,导致部分平仓时利息端金额没跟随 30% 比例。
|
||||
对应场景4 备注第一条「利息端平仓金额没有跟随平仓比例变化」。
|
||||
|
||||
### Bug B:全部平仓未扣减已部分平仓已付利息
|
||||
`SwapEodPositionService.cs` EOD 平仓结算:
|
||||
```
|
||||
TdCloseInterest = flowEvents.Sum(x => x.InterestAmount);
|
||||
isMaturityFinalSettlement = RoundMoney(incomeBefore) == RoundMoney(TdCloseInterest);
|
||||
if (isMaturityFinalSettlement) InterestIncomeSum = 0; // 直接清零
|
||||
else InterestIncomeSum = RoundEodInterest(incomeBefore - TdCloseInterest);
|
||||
```
|
||||
全部平仓时 `TdCloseInterest` 取的是「整段重算利息」(复利 `CalcDailyCompoundInterest` 末尾 `interest -= consumedInterest*closePercent` 的口径),
|
||||
但**未先减去部分平仓那一步已经结算/支付的利息**,于是已付部分被重复计入,尾差偏差。
|
||||
对应场景4 备注第二条「全部平仓时没考虑已大支付了利息」。
|
||||
|
||||
### Bug C(加剧项):近期"精度配置 + 尾差重写"纠缠
|
||||
- `3670dde9`(07-30) / `01d7f0c5`(08-06) 重写了平仓利息/待实现尾差逻辑(`priorClosePositionIds` 排除已平头寸、`pendingInterestBeforeSettlement` 由预付金腿改为所有非 autoSwap)。
|
||||
- 同期 `bff3e920`(07-29) `swappriceprecision.js`:`yield 6→4`、`price 11→9`;`a4906010` 净价/全价精度分开。
|
||||
- 尾差 = 高精度应结 − 结算(2位)。精度配置改变 → 舍入残差落点变 → 与重写后的尾差逻辑在"部分平后再全平"长链路(场景4)上交互出错。固定利率 4-2 路径短未触发,浮动 4-2 路径长直接爆。
|
||||
|
||||
## 3. 为什么现有测试没护住好代码
|
||||
1. **测试被 re-baseline 到代码**:`01d7f0c5` 把期望常量从 `0.006383561644` 改成 `-0.010438356164`,拿新代码输出当期望值 → 测试只是复述代码行为。
|
||||
2. **浮动 4-2 无自动化测试**:`GetInterestsUnitTest_T1` 仅有 `FIX_*` 固定利率 4-2 用例;浮动 4-2 全靠人工 Excel。
|
||||
3. **断言容差太松**:既有 `AssertInterestEqual` 用 `ConsGlobal.PriceRound-2` 容差(约 0.01),尾差差在 4~6 位小数全被放过。
|
||||
4. **真 oracle 躺在 Excel 未自动化**:「善洁方法二」30%/70% 守恒检查是极佳 golden,但人肉比对,CI 不响。
|
||||
|
||||
## 4. 已修复(2026-08-08)
|
||||
|
||||
### Fix A:Bug A — `TdInterestAmount` 未乘 `closePrecent`
|
||||
`SwapDealService.cs` `CalcDailyCompoundInterest` 内 `daysFromPreEod==1` 早退分支:
|
||||
```csharp
|
||||
// 修复前(Bug A):
|
||||
TdInterestAmount = preEodPosition.InterestIncomeSum;
|
||||
// 修复后:
|
||||
TdInterestAmount = preEodPosition.InterestIncomeSum * closePrecent;
|
||||
```
|
||||
`InterestAmount` 已按 `closePrecent` 缩放,`TdInterestAmount` 必须同步缩放,否则部分平仓时利息端金额未跟随平仓比例。
|
||||
|
||||
### Fix B:根因 — `resetCarryInterest` 使用 EOD 舍入快照导致精度偏差
|
||||
`SwapDealService.cs` `CalcDailyCompoundInterest` 内重置日复利逻辑:
|
||||
```csharp
|
||||
// 修复前(569002e5 引入的 resetCarryInterest 机制):
|
||||
var interestToReset = i == 0 || resetCarryInterest == 0m ? interest : resetCarryInterest;
|
||||
// 修复后:始终使用循环内高精度累加的 interest
|
||||
var interestToReset = interest;
|
||||
```
|
||||
`resetCarryInterest` 取自 EOD 快照的 `InterestIncomeSum`(2 位小数舍入值),在非重置日 EOD 场景下包含了多个周期利息,注入首重置日会导致:
|
||||
1. 精度损失(舍入值 vs 循环高精度累加值)
|
||||
2. 多周期利息错误注入(EOD 的 InterestIncomeSum 是整段累计,不是当前周期利息)
|
||||
|
||||
此修复与 `253a89b7` 对 `CalcDailyCompoundInterestByEod`(EOD 路径)的修复逻辑一致。
|
||||
|
||||
### Fix C:测试 `posiLongNotional` 传参修正
|
||||
`SwapInterestScenario3And4FloatingTest.cs` 中 `ExecuteClose` 调用的 `posiLongNotional` 应为**平仓后剩余**名义本金(非平仓前):
|
||||
- 场景3全平:`posiLongNotional = 0`(全平后无剩余)
|
||||
- 场景4部分平:`posiLongNotional = Notional - partialCloseNotional`(70% 剩余)
|
||||
- 场景4全平:`posiLongNotional = 0`(全平后无剩余)
|
||||
|
||||
这使得 `oriPosiNotionalValue = remaining_after + close = original`,`closePercent` 计算正确。
|
||||
|
||||
## 5. 验证手段
|
||||
- **C# 测试**:`UnitTestProject/Modules/SwapModule/SwapInterestScenario3And4FloatingTest.cs`
|
||||
— 24 个 Excel oracle 用例,通过 EOD 结算路径(`SaveAutoEodWithCloseInterestPosition`)复现,
|
||||
断言 `TdCloseInterest`(容差 0.01)。修复前 → RED(偏差 29~3961 元),修复后 → 预期 GREEN。
|
||||
⚠️ 需在 Windows + VS 运行验证。
|
||||
- **禁止 re-baseline**:今后任何 fix 改测试期望值常量,必须附注来源(本 Excel 手算 or 文档公式),否则评审红线。
|
||||
Reference in New Issue
Block a user