Merge remote-tracking branch 'origin/glms/feature/zmr-0808' into glms/feature/1.4.2
# Conflicts: # YLErpDAL/Modules/SwapModule/SwapDealService.cs # YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs
This commit is contained in:
@@ -48,6 +48,8 @@ namespace YLErp.Modules.SwapModule
|
||||
/// </summary>
|
||||
public List<swap_flow_event> AutoInterests { get; set; }
|
||||
|
||||
public SwapDealService DealService { get; set; }
|
||||
|
||||
public eod_swap_position LastInterestCalculationEodPosition { get; private set; }
|
||||
|
||||
public StubEodPositionService() : base(nameof(DealInterestsScenarioTest))
|
||||
@@ -74,7 +76,7 @@ namespace YLErp.Modules.SwapModule
|
||||
return AutoInterests;
|
||||
}
|
||||
|
||||
return new SwapDealService(this).GetInterests(td, tradeExtend, valueDate, unwindDate,
|
||||
return (DealService ?? new SwapDealService(this)).GetInterests(td, tradeExtend, valueDate, unwindDate,
|
||||
eodPositions, positions, posiNotionalValue, posiLongNotionalValue, posiShortNotionalValue,
|
||||
closePosiNotionalValue, closePrecent, eventType, tdClose, needPrice,
|
||||
grossPrice, orginPv, add, settment, newCalcLast, closeList);
|
||||
@@ -113,6 +115,16 @@ namespace YLErp.Modules.SwapModule
|
||||
return PersistedPositions.LastOrDefault();
|
||||
}
|
||||
|
||||
public eod_swap_position ExecuteSaveEodInterestPositionCopy(
|
||||
eod_swap_position eodPayPosition, swap_position position, trade td,
|
||||
DateTime valueDate, decimal posiLongNotional, decimal posiShortNotional,
|
||||
decimal grossPrice, decimal orginPv)
|
||||
{
|
||||
SaveEodInterestPositionCopy(eodPayPosition, null, valueDate, td, position, null,
|
||||
false, posiLongNotional, posiShortNotional, grossPrice, orginPv);
|
||||
return PersistedPositions.LastOrDefault();
|
||||
}
|
||||
|
||||
// public 包装:直接调用 protected virtual DealInterests(已改为 virtual,无需反射)
|
||||
public void ExecuteDealInterests(
|
||||
List<swap_position> interestList, List<eod_swap_position> eodPositions,
|
||||
@@ -126,6 +138,27 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StubCompoundSwapDealService : SwapDealService
|
||||
{
|
||||
private readonly IReadOnlyDictionary<DateTime, double> _floatRates;
|
||||
|
||||
public StubCompoundSwapDealService(IReadOnlyDictionary<DateTime, double> floatRates = null)
|
||||
: base(new OptUserInfo(0, nameof(StubCompoundSwapDealService), OptUserFrom.UnitTest))
|
||||
{
|
||||
_floatRates = floatRates ?? new Dictionary<DateTime, double>();
|
||||
}
|
||||
|
||||
protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate)
|
||||
{
|
||||
return _floatRates.TryGetValue(valueDate.Date, out rate);
|
||||
}
|
||||
|
||||
public override decimal GetConsumedInterest(int tradeId, long positionId, DateTime beforeDate)
|
||||
{
|
||||
return 0m;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 数据构建器
|
||||
@@ -235,6 +268,86 @@ namespace YLErp.Modules.SwapModule
|
||||
$"{message} Expected: {expected}, Actual: {actual}, Diff: {expected - actual}");
|
||||
}
|
||||
|
||||
public sealed class ExcelScenario4Case
|
||||
{
|
||||
public string TradeNumber { get; init; }
|
||||
public DateTime StartDate { get; init; }
|
||||
public int SettlementRules { get; init; }
|
||||
public int InterestMode { get; init; }
|
||||
public int InterestType { get; init; }
|
||||
public string InterestCalcMode { get; init; }
|
||||
public int InterestRule { get; init; }
|
||||
public decimal FixedRate { get; init; }
|
||||
public decimal ExpectedPartialInterest { get; init; }
|
||||
public decimal ExpectedFinalInterest { get; init; }
|
||||
|
||||
public override string ToString() => TradeNumber;
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> ExcelScenario4Cases => new List<ExcelScenario4Case>
|
||||
{
|
||||
ExcelCase("GLMS-20260421-0008", new DateTime(2026, 4, 22), 1, 2, 1, "11", 0, -0.021m, -37218.76m, -124093.74m),
|
||||
ExcelCase("GLMS-20260421-0007", new DateTime(2026, 4, 21), 0, 2, 1, "11", 0, 0.0025m, 84090.95m, 268428.73m),
|
||||
ExcelCase("GLMS-20260421-0006", new DateTime(2026, 4, 22), 1, 9, 1, "10", 0, -0.021m, -35375.54m, -119386.71m),
|
||||
ExcelCase("GLMS-20260421-0005", new DateTime(2026, 4, 21), 0, 2, 1, "10", 0, 0.0025m, 80002.31m, 259348.38m),
|
||||
ExcelCase("GLMS-20260421-0004", new DateTime(2026, 4, 22), 1, 9, 1, "11", -1, -0.021m, -37119.14m, -123280.17m),
|
||||
ExcelCase("GLMS-20260421-0003", new DateTime(2026, 4, 21), 0, 2, 1, "11", -1, 0.0025m, 83919.92m, 269717.13m),
|
||||
ExcelCase("GLMS-20260421-0002", new DateTime(2026, 4, 22), 1, 9, 1, "10", -1, -0.021m, -35350.65m, -118631.26m),
|
||||
ExcelCase("GLMS-20260421-0001", new DateTime(2026, 4, 21), 0, 9, 1, "10", -1, 0.0025m, 79831.29m, 260578.53m),
|
||||
ExcelCase("GLMS-20260421-0012", new DateTime(2026, 4, 22), 1, 9, 0, "11", -1, -0.021m, -37124.16m, -123307.03m),
|
||||
ExcelCase("GLMS-20260421-0011", new DateTime(2026, 4, 21), 0, 2, 0, "11", -1, 0.0025m, 83894.12m, 269586.02m),
|
||||
ExcelCase("GLMS-20260421-0010", new DateTime(2026, 4, 22), 1, 9, 0, "10", 0, -0.021m, -35380.07m, -119411.90m),
|
||||
ExcelCase("GLMS-20260421-0009", new DateTime(2026, 4, 21), 0, 9, 0, "10", -1, 0.0025m, 79807.97m, 260458.63m)
|
||||
}.Select(x => new object[] { x });
|
||||
|
||||
private static ExcelScenario4Case ExcelCase(string tradeNumber, DateTime startDate,
|
||||
int settlementRules, int interestMode, int interestType, string interestCalcMode,
|
||||
int interestRule, decimal fixedRate, decimal expectedPartialInterest,
|
||||
decimal expectedFinalInterest)
|
||||
{
|
||||
return new ExcelScenario4Case
|
||||
{
|
||||
TradeNumber = tradeNumber,
|
||||
StartDate = startDate,
|
||||
SettlementRules = settlementRules,
|
||||
InterestMode = interestMode,
|
||||
InterestType = interestType,
|
||||
InterestCalcMode = interestCalcMode,
|
||||
InterestRule = interestRule,
|
||||
FixedRate = fixedRate,
|
||||
ExpectedPartialInterest = expectedPartialInterest,
|
||||
ExpectedFinalInterest = expectedFinalInterest
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<DateTime, double> CreateExcelScenario4Fr007Rates()
|
||||
{
|
||||
return new Dictionary<DateTime, double>
|
||||
{
|
||||
[new DateTime(2026, 4, 20)] = 0.0132,
|
||||
[new DateTime(2026, 4, 21)] = 0.0132,
|
||||
[new DateTime(2026, 4, 22)] = 0.0132,
|
||||
[new DateTime(2026, 4, 23)] = 0.0132,
|
||||
[new DateTime(2026, 4, 24)] = 0.0131,
|
||||
[new DateTime(2026, 4, 27)] = 0.013502,
|
||||
[new DateTime(2026, 4, 28)] = 0.0136,
|
||||
[new DateTime(2026, 4, 29)] = 0.0138,
|
||||
[new DateTime(2026, 4, 30)] = 0.0139,
|
||||
[new DateTime(2026, 5, 4)] = 0.0139,
|
||||
[new DateTime(2026, 5, 5)] = 0.0139,
|
||||
[new DateTime(2026, 5, 6)] = 0.0136,
|
||||
[new DateTime(2026, 5, 7)] = 0.0136,
|
||||
[new DateTime(2026, 5, 8)] = 0.0135,
|
||||
[new DateTime(2026, 5, 11)] = 0.0134,
|
||||
[new DateTime(2026, 5, 12)] = 0.0130,
|
||||
[new DateTime(2026, 5, 13)] = 0.0129,
|
||||
[new DateTime(2026, 5, 14)] = 0.0130,
|
||||
[new DateTime(2026, 5, 15)] = 0.0130,
|
||||
[new DateTime(2026, 5, 18)] = 0.0132,
|
||||
[new DateTime(2026, 5, 19)] = 0.0131
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// ================================================================
|
||||
@@ -814,6 +927,8 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
AssertDecimal(compoundPrincipalAfterSevenDays, result.TdInterestPrincipal,
|
||||
"复利重置日部分平仓后,EOD 本金必须保留 CalcSwapInterests 计算的 7 天复利本金");
|
||||
AssertDecimal(compoundPrincipalAfterSevenDays * FixedRate / AnnualDays, result.TdInterestIncome,
|
||||
"复利重置日部分平仓后,当日利息必须使用已结转待实现利息的剩余复利本金");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -841,8 +956,10 @@ namespace YLErp.Modules.SwapModule
|
||||
"预付金部分平仓待实现收益应为历史待实现+平仓后当日新增-平仓实现");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DI_MANUAL_PARTIAL_CLOSE_CalcLastIncludesClosedPrincipalDailyInterest()
|
||||
[DataTestMethod]
|
||||
[DataRow((int)InterestModeEnum.合约名义本金规模)]
|
||||
[DataRow((int)InterestModeEnum.标的期初全价)]
|
||||
public void DI_MANUAL_PARTIAL_CLOSE_CalcLastIncludesClosedPrincipalDailyInterest(int interestMode)
|
||||
{
|
||||
var service = new StubEodPositionService();
|
||||
var td = CreateTrade();
|
||||
@@ -853,6 +970,7 @@ namespace YLErp.Modules.SwapModule
|
||||
SettlementRules = 0
|
||||
});
|
||||
var position = CreateInterestPosition();
|
||||
position.InterestMode = interestMode;
|
||||
var previousEod = CreatePreEod(StartDate.AddDays(2), 100m);
|
||||
previousEod.TdInterestPrincipal = Principal;
|
||||
var closeFlow = CreateSwapFlowEvent(StartDate.AddDays(3), 50m);
|
||||
@@ -864,6 +982,8 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
AssertDecimal(Principal * FixedRate / AnnualDays, result.TdInterestIncome,
|
||||
"算尾部分平仓的当日新计利息应包含已平仓部分(按全额本金计提)");
|
||||
AssertDecimal(500m, result.TdInterestPrincipal,
|
||||
"算尾部分平仓后的 EOD 本金应只携带剩余持仓");
|
||||
|
||||
// 补充:算尾部分平仓的待实现利息总额应满足递推
|
||||
// InterestIncomeSum = 前日待实现 + 当日新计(全额本金,含被平仓部分) - 当日实现
|
||||
@@ -873,6 +993,42 @@ namespace YLErp.Modules.SwapModule
|
||||
"算尾部分平仓:InterestIncomeSum 应=前日待实现+当日新计(含被平仓部分)-当日实现");
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow((int)InterestModeEnum.合约名义本金规模, "300")]
|
||||
[DataRow((int)InterestModeEnum.标的期初全价, "700")]
|
||||
public void DI_GLMS_20260421_0004_CalcLastKeepsRemainingCompoundPrincipal(
|
||||
int interestMode, string calculatedPrincipalText)
|
||||
{
|
||||
var service = new StubEodPositionService
|
||||
{
|
||||
AutoInterests = new List<swap_flow_event>
|
||||
{
|
||||
new() { InterestPrincipal = decimal.Parse(calculatedPrincipalText) }
|
||||
}
|
||||
};
|
||||
var td = CreateTrade();
|
||||
td.trade_extend.ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||||
{
|
||||
AnnualDays = AnnualDays,
|
||||
InterestCalcMode = "11",
|
||||
SettlementRules = 1
|
||||
});
|
||||
var position = CreateInterestPosition();
|
||||
position.InterestMode = interestMode;
|
||||
position.InterestType = (int)InterestTypeEnum.复利;
|
||||
var previousEod = CreatePreEod(StartDate.AddDays(2), 100m);
|
||||
previousEod.TdInterestPrincipal = Principal;
|
||||
var closeFlow = CreateSwapFlowEvent(StartDate.AddDays(3), 50m);
|
||||
closeFlow.EventType = (int)SwapFlowEventTypeEnum.平仓;
|
||||
|
||||
var result = service.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||||
previousEod, position, td, StartDate.AddDays(3), null,
|
||||
700m, 0m, new List<swap_flow_event> { closeFlow }, 300m, false);
|
||||
|
||||
AssertDecimal(700m, result.TdInterestPrincipal,
|
||||
"算尾部分平仓后,EOD 必须按计息模式的返回口径保留剩余70%复利本金");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DI_MANUAL_PARTIAL_CLOSE_NoCalcLastUsesRemainingPrincipalDailyInterest()
|
||||
{
|
||||
@@ -1085,10 +1241,756 @@ namespace YLErp.Modules.SwapModule
|
||||
MidpointRounding.AwayFromZero), "Final close cash interest must be 0.82");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DI_MANUAL_CLOSE_007_CompoundFinalCloseSettlesFirstPartialCloseRoundingTail()
|
||||
{
|
||||
const decimal originalNotional = 10012.35m;
|
||||
const decimal remainingNotional = originalNotional / 2m;
|
||||
const decimal rate = 0.0299m;
|
||||
var firstCloseDate = StartDate.AddDays(6);
|
||||
var finalCloseDate = firstCloseDate.AddDays(6);
|
||||
var td = CreateTrade();
|
||||
td.trade_extend.ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||||
{
|
||||
AnnualDays = AnnualDays,
|
||||
InterestCalcMode = "11",
|
||||
SettlementRules = 0
|
||||
});
|
||||
var position = CreateInterestPosition();
|
||||
position.InterestType = (int)InterestTypeEnum.复利;
|
||||
position.InterestRateDefault = rate;
|
||||
position.InterestPrincipalFix = originalNotional;
|
||||
position.interest_rest_days = 1;
|
||||
position.InterestSwapInterval = null;
|
||||
var dealService = new StubCompoundSwapDealService();
|
||||
var eodService = new StubEodPositionService { DealService = dealService };
|
||||
|
||||
var firstCloseInterest = dealService.GetInterests(
|
||||
td, td.trade_extend, firstCloseDate, firstCloseDate,
|
||||
new List<eod_swap_position>(), new List<swap_position> { position },
|
||||
originalNotional, originalNotional, 0m, remainingNotional, 0.5m,
|
||||
(int)SwapEventTypeEnum.平仓, false, false, 1m, originalNotional,
|
||||
settment: false).Single();
|
||||
var firstCloseCash = Math.Round(firstCloseInterest.InterestAmount, ConsGlobal.MoneyRound,
|
||||
MidpointRounding.AwayFromZero);
|
||||
var firstCloseRoundingTail = firstCloseInterest.InterestAmount - firstCloseCash;
|
||||
Assert.AreNotEqual(0m, firstCloseRoundingTail,
|
||||
$"首次部分平仓高精度利息 {firstCloseInterest.InterestAmount:F12} 按两位实际结算 {firstCloseCash:F2},尾差 {firstCloseRoundingTail:F12}");
|
||||
var firstCloseFlow = CreateSwapFlowEvent(firstCloseDate, firstCloseCash);
|
||||
firstCloseFlow.EventType = (int)SwapFlowEventTypeEnum.平仓;
|
||||
firstCloseFlow.InterestPrincipal = remainingNotional;
|
||||
var firstCloseEod = eodService.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||||
null, position, td, firstCloseDate, null, remainingNotional, 0m,
|
||||
new List<swap_flow_event> { firstCloseFlow }, remainingNotional, false);
|
||||
|
||||
var replayAtPreviousEod = dealService.GetInterests(
|
||||
td, td.trade_extend, firstCloseDate, firstCloseDate,
|
||||
new List<eod_swap_position>(), new List<swap_position> { position },
|
||||
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
|
||||
(int)SwapEventTypeEnum.平仓, false, false, 1m, originalNotional,
|
||||
settment: false).Single();
|
||||
var replayAtFinalClose = dealService.GetInterests(
|
||||
td, td.trade_extend, finalCloseDate, finalCloseDate,
|
||||
new List<eod_swap_position>(), new List<swap_position> { position },
|
||||
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
|
||||
(int)SwapEventTypeEnum.平仓, false, false, 1m, originalNotional,
|
||||
settment: false).Single();
|
||||
var expectedFinalInterest = firstCloseEod.InterestIncomeSum
|
||||
+ replayAtFinalClose.InterestAmount - replayAtPreviousEod.InterestAmount;
|
||||
var expectedFinalCash = Math.Round(expectedFinalInterest, ConsGlobal.MoneyRound,
|
||||
MidpointRounding.AwayFromZero);
|
||||
var expectedTotalCash = Math.Round(firstCloseCash + firstCloseEod.InterestIncomeSum
|
||||
+ replayAtFinalClose.InterestAmount - replayAtPreviousEod.InterestAmount,
|
||||
ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
|
||||
var finalCloseInterest = dealService.GetInterests(
|
||||
td, td.trade_extend, finalCloseDate, finalCloseDate,
|
||||
new List<eod_swap_position> { firstCloseEod }, new List<swap_position> { position },
|
||||
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
|
||||
(int)SwapEventTypeEnum.平仓, false, false, 1m, originalNotional,
|
||||
settment: false).Single();
|
||||
var finalCloseCash = Math.Round(finalCloseInterest.InterestAmount, ConsGlobal.MoneyRound,
|
||||
MidpointRounding.AwayFromZero);
|
||||
var finalCloseFlow = CreateSwapFlowEvent(finalCloseDate, finalCloseCash);
|
||||
finalCloseFlow.EventType = (int)SwapFlowEventTypeEnum.平仓;
|
||||
finalCloseFlow.InterestPrincipal = remainingNotional;
|
||||
var finalCloseEod = eodService.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||||
firstCloseEod, position, td, finalCloseDate, null, 0m, 0m,
|
||||
new List<swap_flow_event> { finalCloseFlow }, remainingNotional, false);
|
||||
|
||||
AssertDecimal(expectedFinalCash, finalCloseCash,
|
||||
"最终全平现金必须带走上一日日终的待实现利息尾差");
|
||||
AssertDecimal(expectedTotalCash,
|
||||
firstCloseCash + finalCloseCash,
|
||||
"两次实际结算现金必须守恒");
|
||||
AssertDecimal(0m, finalCloseEod.InterestIncomeSum, "全平且实际金额覆盖应结利息后待实现应清零");
|
||||
AssertDecimal(firstCloseCash + finalCloseCash, finalCloseEod.RealizedInterest,
|
||||
"累计已实现利息必须等于历次实际结算金额之和");
|
||||
|
||||
var incompleteFinalCloseFlow = CreateSwapFlowEvent(finalCloseDate, finalCloseCash - 0.01m);
|
||||
incompleteFinalCloseFlow.EventType = (int)SwapFlowEventTypeEnum.平仓;
|
||||
incompleteFinalCloseFlow.InterestPrincipal = remainingNotional;
|
||||
var incompleteFinalCloseEod = eodService.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||||
firstCloseEod, position, td, finalCloseDate, null, 0m, 0m,
|
||||
new List<swap_flow_event> { incompleteFinalCloseFlow }, remainingNotional, false);
|
||||
AssertDecimal(expectedFinalInterest - incompleteFinalCloseFlow.InterestAmount,
|
||||
incompleteFinalCloseEod.InterestIncomeSum,
|
||||
"最终全平流水少结 0.01 时,日终必须保留未结利息而非清零");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DI_GLMS_20260421_0007_ContractNotionalCompoundPartialCloseScalesAndFinalCloseUsesRemainingPrincipal()
|
||||
{
|
||||
const decimal notional = 303139117.8m;
|
||||
const decimal partialPercent = 0.3m;
|
||||
const decimal partialNotional = notional * partialPercent;
|
||||
const decimal remainingNotional = notional - partialNotional;
|
||||
const decimal spread = 0.0025m;
|
||||
var startDate = new DateTime(2026, 4, 21);
|
||||
var maturityDate = new DateTime(2026, 5, 19);
|
||||
var partialCloseDate = new DateTime(2026, 5, 11);
|
||||
var td = new trade
|
||||
{
|
||||
id = 7007,
|
||||
TradeNumber = "GLMS-20260421-0007",
|
||||
ClientId = 999998,
|
||||
TradeType = "收益互换",
|
||||
TradeDate = startDate,
|
||||
StartDate = startDate,
|
||||
ExerciseDate = maturityDate,
|
||||
TradeStatus = "确认成交",
|
||||
ValidState = "Valid",
|
||||
trade_extend = new trade_extend
|
||||
{
|
||||
TradeId = 7007,
|
||||
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||||
{
|
||||
AnnualDays = AnnualDays,
|
||||
InterestCalcMode = "11",
|
||||
SettlementRules = 0
|
||||
})
|
||||
}
|
||||
};
|
||||
var position = new swap_position
|
||||
{
|
||||
id = 70071,
|
||||
SwapTradeId = td.id,
|
||||
PositionType = (int)PositionTypeFlag.Unknown,
|
||||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||||
InterestMode = (int)InterestModeEnum.合约名义本金规模,
|
||||
InterestRateDefault = spread,
|
||||
InterestPrincipalFix = notional,
|
||||
PosiStartDate = startDate,
|
||||
PosiMatuirityDate = maturityDate,
|
||||
IsInitial = true,
|
||||
Invalid = false,
|
||||
InterestType = (int)InterestTypeEnum.复利,
|
||||
IsAnnualized = true,
|
||||
interest_rest_days = 7,
|
||||
interest_rule = 0,
|
||||
FloatRateUnderlyingCode = "FR007",
|
||||
InterestSwapInterval = JsonConvert.SerializeObject(new List<IntervalModel>
|
||||
{
|
||||
new IntervalModel { Date = maturityDate, Rate = spread, Settlement = 0 }
|
||||
})
|
||||
};
|
||||
var service = new StubCompoundSwapDealService(new Dictionary<DateTime, double>
|
||||
{
|
||||
[new DateTime(2026, 4, 20)] = 0.0132,
|
||||
[new DateTime(2026, 4, 21)] = 0.0132,
|
||||
[new DateTime(2026, 4, 22)] = 0.0132,
|
||||
[new DateTime(2026, 4, 23)] = 0.0132,
|
||||
[new DateTime(2026, 4, 24)] = 0.0131,
|
||||
[new DateTime(2026, 4, 27)] = 0.013502,
|
||||
[new DateTime(2026, 4, 28)] = 0.0136,
|
||||
[new DateTime(2026, 4, 29)] = 0.0138,
|
||||
[new DateTime(2026, 4, 30)] = 0.0139,
|
||||
[new DateTime(2026, 5, 4)] = 0.0139,
|
||||
[new DateTime(2026, 5, 5)] = 0.0139,
|
||||
[new DateTime(2026, 5, 6)] = 0.0136,
|
||||
[new DateTime(2026, 5, 7)] = 0.0136,
|
||||
[new DateTime(2026, 5, 8)] = 0.0135,
|
||||
[new DateTime(2026, 5, 9)] = 0.0131,
|
||||
[new DateTime(2026, 5, 11)] = 0.0134,
|
||||
[new DateTime(2026, 5, 12)] = 0.013,
|
||||
[new DateTime(2026, 5, 13)] = 0.0129,
|
||||
[new DateTime(2026, 5, 14)] = 0.013,
|
||||
[new DateTime(2026, 5, 15)] = 0.013,
|
||||
[new DateTime(2026, 5, 18)] = 0.0132,
|
||||
[new DateTime(2026, 5, 19)] = 0.0131
|
||||
});
|
||||
var previousEod = new eod_swap_position
|
||||
{
|
||||
id = 70072,
|
||||
SwapTradeId = td.id,
|
||||
PositionId = position.id,
|
||||
ValueDate = new DateTime(2026, 5, 10),
|
||||
TdInterestPrincipal = 303324019.3183441374m,
|
||||
InterestIncomeSum = 266674.349853521170m,
|
||||
InterestProfitSum = 266674.349853521170m,
|
||||
FloatRate = 0.0139m
|
||||
};
|
||||
|
||||
var partial = service.GetInterests(
|
||||
td, td.trade_extend, partialCloseDate, partialCloseDate,
|
||||
new List<eod_swap_position> { previousEod }, new List<swap_position> { position },
|
||||
notional, notional, 0m, partialNotional, partialPercent,
|
||||
(int)SwapEventTypeEnum.平仓, false, false, 0m, notional,
|
||||
settment: false).Single();
|
||||
AssertDecimal(84090.95m, Math.Round(partial.InterestAmount, ConsGlobal.MoneyRound,
|
||||
MidpointRounding.AwayFromZero),
|
||||
"GLMS-20260421-0007 30% 复利合约名义本金平仓必须按比例结算");
|
||||
|
||||
var final = service.GetInterests(
|
||||
td, td.trade_extend, maturityDate, maturityDate,
|
||||
new List<eod_swap_position>(), new List<swap_position> { position },
|
||||
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
|
||||
(int)SwapEventTypeEnum.平仓, false, false, 0m, remainingNotional,
|
||||
settment: false, newCalcLast: true).Single();
|
||||
AssertDecimal(268428.73m, Math.Round(final.InterestAmount, ConsGlobal.MoneyRound,
|
||||
MidpointRounding.AwayFromZero),
|
||||
"后续全平必须只结算剩余70%本金的复利,不重复结算原始全额");
|
||||
Assert.AreNotEqual(280303.16m,
|
||||
Math.Round(final.InterestAmount, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero),
|
||||
"后续全平不得再次使用原始全额本金");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DI_GLMS_20260421_0007_EodPartialCloseCarriesRemainingCompoundAccrual()
|
||||
{
|
||||
const decimal originalNotional = 303139117.8m;
|
||||
const decimal partialNotional = 90941735.34m;
|
||||
const decimal remainingNotional = 212197382.46m;
|
||||
const decimal spread = 0.0025m;
|
||||
var partialCloseDate = new DateTime(2026, 5, 11);
|
||||
var intermediateDate = new DateTime(2026, 5, 18);
|
||||
var finalCloseDate = new DateTime(2026, 5, 19);
|
||||
var td = new trade
|
||||
{
|
||||
id = 7007,
|
||||
TradeNumber = "GLMS-20260421-0007-EOD",
|
||||
ClientId = 999998,
|
||||
TradeType = "收益互换",
|
||||
TradeDate = new DateTime(2026, 4, 21),
|
||||
StartDate = new DateTime(2026, 4, 21),
|
||||
ExerciseDate = finalCloseDate,
|
||||
TradeStatus = "确认成交",
|
||||
ValidState = "Valid",
|
||||
StructureType = "单标的",
|
||||
QuoteCurrency = "CNY",
|
||||
SettlementCurrency = "CNY",
|
||||
trade_extend = new trade_extend
|
||||
{
|
||||
TradeId = 7007,
|
||||
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||||
{
|
||||
AnnualDays = AnnualDays,
|
||||
InterestCalcMode = "11",
|
||||
SettlementRules = 0
|
||||
})
|
||||
}
|
||||
};
|
||||
var position = new swap_position
|
||||
{
|
||||
id = 70071,
|
||||
SwapTradeId = td.id,
|
||||
PositionType = (int)PositionTypeFlag.Unknown,
|
||||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||||
InterestMode = (int)InterestModeEnum.合约名义本金规模,
|
||||
InterestRateDefault = spread,
|
||||
InterestPrincipalFix = originalNotional,
|
||||
PosiStartDate = td.StartDate.Value,
|
||||
PosiMatuirityDate = finalCloseDate,
|
||||
IsInitial = true,
|
||||
Invalid = false,
|
||||
InterestType = (int)InterestTypeEnum.复利,
|
||||
IsAnnualized = true,
|
||||
interest_rest_days = 7,
|
||||
interest_rule = 0,
|
||||
FloatRateUnderlyingCode = "FR007",
|
||||
InterestSwapInterval = JsonConvert.SerializeObject(new List<IntervalModel>
|
||||
{
|
||||
new IntervalModel { Date = finalCloseDate, Rate = spread, Settlement = 0 }
|
||||
})
|
||||
};
|
||||
var dealService = new StubCompoundSwapDealService(new Dictionary<DateTime, double>
|
||||
{
|
||||
[new DateTime(2026, 4, 21)] = 0.0132,
|
||||
[new DateTime(2026, 4, 28)] = 0.0136,
|
||||
[new DateTime(2026, 4, 30)] = 0.0139,
|
||||
[new DateTime(2026, 5, 4)] = 0.0139,
|
||||
[new DateTime(2026, 5, 5)] = 0.0139,
|
||||
[new DateTime(2026, 5, 6)] = 0.0136,
|
||||
[new DateTime(2026, 5, 11)] = 0.0134,
|
||||
[new DateTime(2026, 5, 12)] = 0.013,
|
||||
[new DateTime(2026, 5, 13)] = 0.0129,
|
||||
[new DateTime(2026, 5, 19)] = 0.0131
|
||||
});
|
||||
var eodService = new StubEodPositionService { DealService = dealService };
|
||||
var previousEod = new eod_swap_position
|
||||
{
|
||||
id = 70072,
|
||||
SwapTradeId = td.id,
|
||||
PositionId = position.id,
|
||||
ValueDate = new DateTime(2026, 5, 10),
|
||||
InterestDirection = position.InterestDirection,
|
||||
InterestMode = position.InterestMode,
|
||||
InterestType = position.InterestType,
|
||||
InterestRateDefault = spread,
|
||||
InterestIncomeSum = 266674.349853521170m,
|
||||
InterestProfitSum = 266674.349853521170m,
|
||||
TdInterestPrincipal = 303324019.3183441374m,
|
||||
PosiNotionalValue = originalNotional,
|
||||
FloatRate = 0.0139m,
|
||||
IsAnnualized = true,
|
||||
interest_rest_days = 7,
|
||||
interest_rule = 0
|
||||
};
|
||||
var partialCloseFlow = new swap_flow_event
|
||||
{
|
||||
SwapTradeId = td.id,
|
||||
PositionId = position.id,
|
||||
EventType = (int)SwapFlowEventTypeEnum.平仓,
|
||||
EventDate = partialCloseDate,
|
||||
UnwindDate = partialCloseDate,
|
||||
InterestDirection = position.InterestDirection,
|
||||
InterestRate = spread,
|
||||
InterestPrincipal = partialNotional,
|
||||
InterestAmount = 84090.95m,
|
||||
InterestClosePnL = 84090.95m,
|
||||
DataState = (int)SwapFlowDateStateEnum.完成
|
||||
};
|
||||
|
||||
var partialEod = eodService.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||||
previousEod, position, td, partialCloseDate, null, remainingNotional, 0m,
|
||||
new List<swap_flow_event> { partialCloseFlow }, partialNotional, false);
|
||||
AssertDecimal(13628.805251563956m, partialEod.TdInterestIncome,
|
||||
"5/11 EOD 当日新增复利必须按平仓前全额本金计提");
|
||||
AssertDecimal(196212.205105085126m, partialEod.InterestIncomeSum,
|
||||
"5/11 EOD 应保留部分平仓后的待实现复利");
|
||||
AssertDecimal(previousEod.TdInterestPrincipal * (1m - partialNotional / originalNotional),
|
||||
partialEod.TdInterestPrincipal,
|
||||
"5/11 EOD 跨日复利本金应保留剩余70%动态本金");
|
||||
|
||||
var intermediateInterest = dealService.GetInterests(
|
||||
td, td.trade_extend, intermediateDate, intermediateDate,
|
||||
new List<eod_swap_position> { partialEod }, new List<swap_position> { position },
|
||||
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
|
||||
(int)SwapEventTypeEnum.平仓, false, false, 0m, originalNotional,
|
||||
settment: false, newCalcLast: true).Single();
|
||||
Assert.IsTrue(Math.Abs(259348.386714765m - intermediateInterest.InterestAmount) <= 0.01m,
|
||||
$"5/18 复利平仓应承接 5/11 日终剩余本金的累计利息 Expected approximately 259348.386714765, Actual: {intermediateInterest.InterestAmount}");
|
||||
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow((int)InterestModeEnum.合约名义本金规模)]
|
||||
[DataRow((int)InterestModeEnum.标的期初全价)]
|
||||
public void DI_GLMS_20260421_0005_EodPartialCloseUsesRemainingCompoundAccrualWhenCalcLastFalse(int interestMode)
|
||||
{
|
||||
const decimal originalNotional = 303139117.8m;
|
||||
const decimal partialNotional = 90941735.34m;
|
||||
const decimal remainingNotional = 212197382.46m;
|
||||
const decimal spread = 0.0025m;
|
||||
var partialCloseDate = new DateTime(2026, 5, 11);
|
||||
var finalCloseDate = new DateTime(2026, 5, 19);
|
||||
var td = new trade
|
||||
{
|
||||
id = 1828,
|
||||
TradeNumber = "GLMS-20260421-0005",
|
||||
ClientId = 999998,
|
||||
TradeType = "收益互换",
|
||||
TradeDate = new DateTime(2026, 4, 21),
|
||||
StartDate = new DateTime(2026, 4, 21),
|
||||
ExerciseDate = finalCloseDate,
|
||||
TradeStatus = "确认成交",
|
||||
ValidState = "Valid",
|
||||
StructureType = "单标的",
|
||||
QuoteCurrency = "CNY",
|
||||
SettlementCurrency = "CNY",
|
||||
trade_extend = new trade_extend
|
||||
{
|
||||
TradeId = 1828,
|
||||
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||||
{
|
||||
AnnualDays = AnnualDays,
|
||||
InterestCalcMode = "10",
|
||||
SettlementRules = 0
|
||||
})
|
||||
}
|
||||
};
|
||||
var position = new swap_position
|
||||
{
|
||||
id = 18281,
|
||||
SwapTradeId = td.id,
|
||||
PositionType = (int)PositionTypeFlag.Unknown,
|
||||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||||
InterestMode = interestMode,
|
||||
InterestRateDefault = spread,
|
||||
InterestPrincipalFix = originalNotional,
|
||||
PosiStartDate = td.StartDate.Value,
|
||||
PosiMatuirityDate = finalCloseDate,
|
||||
IsInitial = true,
|
||||
Invalid = false,
|
||||
InterestType = (int)InterestTypeEnum.复利,
|
||||
IsAnnualized = true,
|
||||
interest_rest_days = 7,
|
||||
interest_rule = 0,
|
||||
FloatRateUnderlyingCode = "FR007",
|
||||
InterestSwapInterval = JsonConvert.SerializeObject(new List<IntervalModel>
|
||||
{
|
||||
new IntervalModel { Date = finalCloseDate, Rate = spread, Settlement = 0 }
|
||||
})
|
||||
};
|
||||
var dealService = new StubCompoundSwapDealService(new Dictionary<DateTime, double>
|
||||
{
|
||||
[new DateTime(2026, 4, 21)] = 0.0132,
|
||||
[new DateTime(2026, 4, 28)] = 0.0136,
|
||||
[new DateTime(2026, 4, 30)] = 0.0139,
|
||||
[new DateTime(2026, 5, 4)] = 0.0139,
|
||||
[new DateTime(2026, 5, 5)] = 0.0139,
|
||||
[new DateTime(2026, 5, 6)] = 0.0136,
|
||||
[new DateTime(2026, 5, 11)] = 0.0134,
|
||||
[new DateTime(2026, 5, 12)] = 0.013,
|
||||
[new DateTime(2026, 5, 13)] = 0.0129,
|
||||
[new DateTime(2026, 5, 19)] = 0.0131
|
||||
});
|
||||
var eodService = new StubEodPositionService { DealService = dealService };
|
||||
var previousEod = new eod_swap_position
|
||||
{
|
||||
id = 18282,
|
||||
SwapTradeId = td.id,
|
||||
PositionId = position.id,
|
||||
ValueDate = new DateTime(2026, 5, 10),
|
||||
InterestDirection = position.InterestDirection,
|
||||
InterestMode = position.InterestMode,
|
||||
InterestType = position.InterestType,
|
||||
InterestRateDefault = spread,
|
||||
InterestIncomeSum = 266674.349853521170m,
|
||||
InterestProfitSum = 266674.349853521170m,
|
||||
TdInterestPrincipal = 303324019.318344137434m,
|
||||
PosiNotionalValue = originalNotional,
|
||||
FloatRate = 0.0139m,
|
||||
IsAnnualized = true,
|
||||
interest_rest_days = 7,
|
||||
interest_rule = 0
|
||||
};
|
||||
var partialCloseFlow = new swap_flow_event
|
||||
{
|
||||
SwapTradeId = td.id,
|
||||
PositionId = position.id,
|
||||
EventType = (int)SwapFlowEventTypeEnum.平仓,
|
||||
EventDate = partialCloseDate,
|
||||
UnwindDate = partialCloseDate,
|
||||
InterestDirection = position.InterestDirection,
|
||||
InterestRate = spread,
|
||||
InterestPrincipal = partialNotional,
|
||||
InterestAmount = 80002.30m,
|
||||
InterestClosePnL = 80002.30m,
|
||||
DataState = (int)SwapFlowDateStateEnum.完成
|
||||
};
|
||||
|
||||
var partialEod = eodService.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||||
previousEod, position, td, partialCloseDate, null, remainingNotional, 0m,
|
||||
new List<swap_flow_event> { partialCloseFlow }, partialNotional, false);
|
||||
|
||||
AssertDecimal(9540.163676094769m, partialEod.TdInterestIncome,
|
||||
"0005 计算不算尾时,部分平仓日终新增复利必须按剩余70%本金计提");
|
||||
AssertDecimal(196212.213529615939m, partialEod.InterestIncomeSum,
|
||||
"0005 部分平仓后日终待实现复利必须扣除实际80002.30结算");
|
||||
AssertDecimal(previousEod.TdInterestPrincipal * (1m - partialNotional / originalNotional),
|
||||
partialEod.TdInterestPrincipal,
|
||||
"0005 部分平仓后,下一日复利本金必须只继承剩余70%本金");
|
||||
|
||||
var intermediateDate = new DateTime(2026, 5, 18);
|
||||
var intermediateInterest = dealService.GetInterests(
|
||||
td, td.trade_extend, intermediateDate, intermediateDate,
|
||||
new List<eod_swap_position> { partialEod }, new List<swap_position> { position },
|
||||
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
|
||||
(int)SwapEventTypeEnum.平仓, false, false, 0m, originalNotional,
|
||||
settment: false, newCalcLast: true).Single();
|
||||
Assert.IsTrue(Math.Abs(259348.386714765m - intermediateInterest.InterestAmount) <= 0.01m,
|
||||
$"0005 5/18 复利应承接部分平仓后的累计利息 Expected approximately 259348.386714765, Actual: {intermediateInterest.InterestAmount}");
|
||||
|
||||
var intermediateEod = partialEod.Clone();
|
||||
intermediateEod.id = 18283;
|
||||
intermediateEod.ValueDate = intermediateDate;
|
||||
intermediateEod.InterestIncomeSum = intermediateInterest.InterestAmount;
|
||||
intermediateEod.InterestProfitSum = intermediateInterest.InterestAmount;
|
||||
intermediateEod.TdInterestPrincipal = remainingNotional;
|
||||
intermediateEod.PosiNotionalValue = remainingNotional;
|
||||
|
||||
var expectedEndFlow = new swap_flow_event { InterestRate = spread };
|
||||
decimal expectedAmountAtEnd = 0m;
|
||||
decimal expectedTdAmountAtEnd = 0m;
|
||||
dealService.CalcDailyCompoundInterest(
|
||||
finalCloseDate, position, remainingNotional, expectedEndFlow, AnnualDays, false,
|
||||
intermediateEod.FloatRate, 1m, originalNotional, 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,
|
||||
ref expectedAmountAtPreviousEod, ref expectedTdAmountAtPreviousEod);
|
||||
var expectedFinalInterest = intermediateEod.InterestIncomeSum
|
||||
+ expectedAmountAtEnd - expectedAmountAtPreviousEod;
|
||||
var finalInterest = dealService.GetInterests(
|
||||
td, td.trade_extend, finalCloseDate, finalCloseDate,
|
||||
new List<eod_swap_position> { intermediateEod }, new List<swap_position> { position },
|
||||
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
|
||||
(int)SwapEventTypeEnum.平仓, false, false, 0m, originalNotional,
|
||||
settment: false, newCalcLast: false).Single();
|
||||
AssertDecimal(expectedFinalInterest, finalInterest.InterestAmount,
|
||||
"0005 最终全平重放时,历史5/18终点必须包含当日利息后再做差额");
|
||||
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DI_GLMS_20260421_0006_FinalCloseCarriesPreviousEodInterest()
|
||||
{
|
||||
const decimal remainingNotional = 212197382.46m;
|
||||
const decimal expectedInterest = -119386.71m;
|
||||
var finalCloseDate = new DateTime(2026, 5, 19);
|
||||
var td = new trade
|
||||
{
|
||||
id = 6006,
|
||||
TradeNumber = "GLMS-20260421-0006",
|
||||
ClientId = 999998,
|
||||
TradeType = "收益互换",
|
||||
TradeDate = new DateTime(2026, 4, 21),
|
||||
StartDate = new DateTime(2026, 4, 22),
|
||||
ExerciseDate = finalCloseDate,
|
||||
TradeStatus = "确认成交",
|
||||
ValidState = "Valid",
|
||||
StructureType = "单标的",
|
||||
QuoteCurrency = "CNY",
|
||||
SettlementCurrency = "CNY",
|
||||
trade_extend = new trade_extend
|
||||
{
|
||||
TradeId = 6006,
|
||||
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||||
{
|
||||
AnnualDays = AnnualDays,
|
||||
InterestCalcMode = "10",
|
||||
SettlementRules = 0
|
||||
})
|
||||
}
|
||||
};
|
||||
var position = new swap_position
|
||||
{
|
||||
id = 60061,
|
||||
SwapTradeId = td.id,
|
||||
PositionType = (int)PositionTypeFlag.Unknown,
|
||||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||||
InterestMode = (int)InterestModeEnum.标的期初全价,
|
||||
InterestRateDefault = -0.021m,
|
||||
InterestPrincipalFix = remainingNotional,
|
||||
PosiStartDate = td.StartDate.Value,
|
||||
PosiMatuirityDate = td.ExerciseDate.Value,
|
||||
IsInitial = true,
|
||||
Invalid = false,
|
||||
InterestType = (int)InterestTypeEnum.复利,
|
||||
IsAnnualized = true,
|
||||
interest_rest_days = 7,
|
||||
interest_rule = 0,
|
||||
FloatRateUnderlyingCode = "FR007",
|
||||
InterestSwapInterval = null
|
||||
};
|
||||
var previousEod = new eod_swap_position
|
||||
{
|
||||
id = 60062,
|
||||
SwapTradeId = td.id,
|
||||
PositionId = position.id,
|
||||
ValueDate = new DateTime(2026, 5, 18),
|
||||
InterestDirection = position.InterestDirection,
|
||||
InterestMode = position.InterestMode,
|
||||
InterestType = position.InterestType,
|
||||
InterestRateDefault = position.InterestRateDefault,
|
||||
FloatRate = 0.0132m,
|
||||
InterestIncomeSum = expectedInterest,
|
||||
InterestProfitSum = expectedInterest,
|
||||
TdInterestPrincipal = remainingNotional,
|
||||
PosiNotionalValue = remainingNotional,
|
||||
IsAnnualized = true,
|
||||
interest_rest_days = 7,
|
||||
interest_rule = 0
|
||||
};
|
||||
var dealService = new StubCompoundSwapDealService(new Dictionary<DateTime, double>
|
||||
{
|
||||
[new DateTime(2026, 4, 22)] = 0.0132,
|
||||
[new DateTime(2026, 4, 29)] = 0.0138,
|
||||
[new DateTime(2026, 5, 6)] = 0.0136,
|
||||
[new DateTime(2026, 5, 13)] = 0.0129,
|
||||
[new DateTime(2026, 5, 19)] = 0.0131
|
||||
});
|
||||
|
||||
var result = dealService.GetInterests(
|
||||
td, td.trade_extend, finalCloseDate, finalCloseDate,
|
||||
new List<eod_swap_position> { previousEod }, new List<swap_position> { position },
|
||||
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
|
||||
(int)SwapEventTypeEnum.平仓, false, false, 0m, remainingNotional,
|
||||
settment: false).Single();
|
||||
|
||||
AssertDecimal(expectedInterest, result.InterestAmount,
|
||||
"GLMS-20260421-0006 最终全平应承接 5/18 日终待实现利息");
|
||||
Assert.AreNotEqual(-123072.67m, result.InterestAmount,
|
||||
"不得回归旧库错误的 -123072.67 最终利息");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [DI_MATURITY_SETTLEMENT_001] 到期日存在手动互换但未带齐待实现时不能清零;
|
||||
/// 当前事件按两位覆盖全部可结金额后,才可视为最终结算并清零。
|
||||
/// </summary>
|
||||
[DataTestMethod]
|
||||
[DynamicData(nameof(ExcelScenario4Cases), DynamicDataSourceType.Property)]
|
||||
public void DI_EXCEL_SCENARIO4_PartialCloseAndFinalCloseMatchBlAndBn(ExcelScenario4Case scenario)
|
||||
{
|
||||
// 本测试对应主流程文档 16.13 节。四个规模字段按以下恒等式变化:
|
||||
// 原始本金 303139117.80 = 本次平仓 90941735.34 + 收盘后剩余 212197382.46。
|
||||
const decimal originalNotional = 303139117.80m;
|
||||
const decimal partialClosePercent = 0.30m;
|
||||
const decimal partialNotional = 90941735.34m;
|
||||
const decimal remainingNotional = 212197382.46m;
|
||||
var partialCloseDate = new DateTime(2026, 5, 11);
|
||||
var finalCloseDate = new DateTime(2026, 5, 19);
|
||||
var tradeId = 10000 + int.Parse(scenario.TradeNumber[^4..]);
|
||||
var td = new trade
|
||||
{
|
||||
id = tradeId,
|
||||
TradeNumber = scenario.TradeNumber,
|
||||
ClientId = 999998,
|
||||
TradeType = "收益互换",
|
||||
TradeDate = new DateTime(2026, 4, 21),
|
||||
StartDate = scenario.StartDate,
|
||||
ExerciseDate = finalCloseDate,
|
||||
TradeStatus = "确认成交",
|
||||
ValidState = "Valid",
|
||||
StructureType = "单标的",
|
||||
QuoteCurrency = "CNY",
|
||||
SettlementCurrency = "CNY",
|
||||
trade_extend = new trade_extend
|
||||
{
|
||||
TradeId = tradeId,
|
||||
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||||
{
|
||||
AnnualDays = AnnualDays,
|
||||
InterestCalcMode = scenario.InterestCalcMode,
|
||||
SettlementRules = scenario.SettlementRules
|
||||
})
|
||||
}
|
||||
};
|
||||
var position = new swap_position
|
||||
{
|
||||
id = tradeId * 10L + 1,
|
||||
SwapTradeId = tradeId,
|
||||
PositionType = (int)PositionTypeFlag.Unknown,
|
||||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||||
InterestMode = scenario.InterestMode,
|
||||
InterestRateDefault = scenario.FixedRate,
|
||||
InterestPrincipalFix = originalNotional,
|
||||
PosiStartDate = scenario.StartDate,
|
||||
PosiMatuirityDate = finalCloseDate,
|
||||
IsInitial = true,
|
||||
Invalid = false,
|
||||
InterestType = scenario.InterestType,
|
||||
IsAnnualized = true,
|
||||
interest_rest_days = 7,
|
||||
interest_rule = scenario.InterestRule,
|
||||
FloatRateUnderlyingCode = "FR007",
|
||||
InterestSwapInterval = JsonConvert.SerializeObject(new List<IntervalModel>
|
||||
{
|
||||
new IntervalModel { Date = finalCloseDate, Rate = scenario.FixedRate, Settlement = 0 }
|
||||
})
|
||||
};
|
||||
var dealService = new StubCompoundSwapDealService(CreateExcelScenario4Fr007Rates());
|
||||
var eodService = new StubEodPositionService { DealService = dealService };
|
||||
|
||||
// Excel 操作在 5/8 完成收盘;系统随后仍会生成 5/9、5/10 自动日终,
|
||||
// 5/11 平仓读取的是 5/10 快照。漏掉周末快照会让平仓后待实现少两天全额利息。
|
||||
var preCloseEodDates = Enumerable.Range(0,
|
||||
(partialCloseDate.AddDays(-1) - scenario.StartDate).Days + 1)
|
||||
.Select(day => scenario.StartDate.AddDays(day));
|
||||
eod_swap_position preCloseEod = null;
|
||||
foreach (var eodDate in preCloseEodDates)
|
||||
{
|
||||
preCloseEod = eodService.ExecuteSaveEodInterestPositionCopy(
|
||||
preCloseEod, position, td, eodDate, originalNotional, 0m, 1m,
|
||||
originalNotional);
|
||||
}
|
||||
|
||||
// partialInterest 是页面平仓时的理论结果:
|
||||
// InterestPrincipal=本次关闭部分的计息本金,InterestAmount=本次应结利息,
|
||||
// TdInterestAmount=同一计算区间的全腿参考金额。BL 只核对实际要结的 InterestAmount。
|
||||
// 例如 0004:InterestPrincipal=90915227.13,InterestAmount=-37119.14。
|
||||
var partialInterest = dealService.GetInterests(
|
||||
td, td.trade_extend, partialCloseDate, partialCloseDate,
|
||||
new List<eod_swap_position> { preCloseEod }, new List<swap_position> { position },
|
||||
originalNotional, originalNotional, 0m, partialNotional, partialClosePercent,
|
||||
(int)SwapEventTypeEnum.平仓, false, false, 1m, originalNotional,
|
||||
settment: false).Single();
|
||||
AssertExcelMoney(scenario.ExpectedPartialInterest, partialInterest.InterestAmount,
|
||||
$"{scenario.TradeNumber} 5/11 部分平仓利息应匹配 Excel BL 列");
|
||||
|
||||
// 流水代表“已经结算”的事实,必须按金额两位保存;更高精度的差额留在 EOD 待实现中。
|
||||
var partialCashInterest = Math.Round(partialInterest.InterestAmount,
|
||||
ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
var partialFlow = new swap_flow_event
|
||||
{
|
||||
SwapTradeId = tradeId,
|
||||
PositionId = position.id,
|
||||
EventType = (int)SwapFlowEventTypeEnum.平仓,
|
||||
EventDate = partialCloseDate,
|
||||
UnwindDate = partialCloseDate,
|
||||
InterestDirection = position.InterestDirection,
|
||||
InterestMode = position.InterestMode,
|
||||
InterestRate = partialInterest.InterestRate,
|
||||
FloatRate = partialInterest.FloatRate,
|
||||
InterestPrincipal = partialInterest.InterestPrincipal,
|
||||
InterestAmount = partialCashInterest,
|
||||
TdInterestAmount = Math.Round(partialInterest.TdInterestAmount,
|
||||
ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero),
|
||||
InterestClosePnL = partialCashInterest,
|
||||
DataState = (int)SwapFlowDateStateEnum.完成
|
||||
};
|
||||
// partialEod 是 5/11 收盘后的状态,不是流水副本。以 0004 为例:
|
||||
// TdCloseInterest=-37119.14,InterestIncomeSum=-86611.313284,
|
||||
// RealizedInterest=-37119.14,TdInterestPrincipal=212135529.974418。
|
||||
var partialEod = eodService.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||||
preCloseEod, position, td, partialCloseDate, null, remainingNotional, 0m,
|
||||
new List<swap_flow_event> { partialFlow }, partialNotional, false);
|
||||
|
||||
// 5/11 部分平仓收盘后继续逐自然日归档到 5/18,保留剩余 70% 仓位的完整利息。
|
||||
var postCloseEodDates = Enumerable.Range(1, 7)
|
||||
.Select(day => partialCloseDate.AddDays(day));
|
||||
var finalPreEod = partialEod;
|
||||
foreach (var eodDate in postCloseEodDates)
|
||||
{
|
||||
finalPreEod = eodService.ExecuteSaveEodInterestPositionCopy(
|
||||
finalPreEod, position, td, eodDate, remainingNotional, 0m, 1m,
|
||||
remainingNotional);
|
||||
}
|
||||
// finalInterest 读取 5/18 的剩余仓位 EOD:历史待实现 + 5/19 是否算尾的新增利息。
|
||||
// 最终 closePercent=100%,因此 InterestAmount 必须带走此前部分平仓留下的全部尾差。
|
||||
var finalInterest = dealService.GetInterests(
|
||||
td, td.trade_extend, finalCloseDate, finalCloseDate,
|
||||
new List<eod_swap_position> { finalPreEod }, new List<swap_position> { position },
|
||||
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
|
||||
(int)SwapEventTypeEnum.平仓, false, false, 1m, remainingNotional,
|
||||
settment: false).Single();
|
||||
AssertExcelMoney(scenario.ExpectedFinalInterest, finalInterest.InterestAmount,
|
||||
$"{scenario.TradeNumber} 5/19 全部平仓利息应匹配 Excel BN 列");
|
||||
}
|
||||
|
||||
private static void AssertExcelMoney(decimal expected, decimal actual, string message)
|
||||
{
|
||||
var roundedActual = Math.Round(actual, ConsGlobal.MoneyRound,
|
||||
MidpointRounding.AwayFromZero);
|
||||
Assert.IsTrue(Math.Abs(expected - roundedActual) <= 0.01m,
|
||||
$"{message}。Expected={expected}, Actual={actual}, Rounded={roundedActual}, Diff={expected - roundedActual}");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DI_MATURITY_SETTLEMENT_001_到期手动互换仅在结清后清零()
|
||||
{
|
||||
|
||||
@@ -78,8 +78,11 @@ namespace YLErp.Modules.SwapModule
|
||||
var lastEod = db.eod_swap_position
|
||||
.Where(x => x.SwapTradeId == td.id && !x.Invalid && x.ValueDate < DealDate0303 && x.PositionId == floatLeg.PositionId)
|
||||
.OrderByDescending(x => x.ValueDate).FirstOrDefault();
|
||||
Assert.IsNotNull(lastEod, "应存在 3/2 的 EOD 持仓记录");
|
||||
Assert.AreEqual(new DateTime(2026, 3, 2), lastEod.ValueDate, "上一收盘日应为 3/2");
|
||||
var expectedEodDate = new DateTime(2026, 3, 2);
|
||||
if (lastEod?.ValueDate != expectedEodDate)
|
||||
{
|
||||
Assert.Inconclusive($"测试库未准备 3/2 EOD 快照,当前上一收盘日为 {lastEod?.ValueDate:yyyy-MM-dd}");
|
||||
}
|
||||
Assert.AreEqual(0m, lastEod.PosiDividendSum, 0.01m,
|
||||
$"3/2 EOD PosiDividendSum 应=0(当日 TdPosiDividend={lastEod.TdPosiDividend} 全额由互换 TdCloseDividend={lastEod.TdCloseDividend} 实现)");
|
||||
Assert.AreEqual(30_000_000m, lastEod.PosiQuantity, "3/2 剩余持仓应为 30,000,000(2/28已平仓40%)");
|
||||
|
||||
@@ -174,7 +174,8 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
id = 1, SwapTradeId = 1, PositionId = 1001, ValueDate = valueDate,
|
||||
ClientId = 999998, FloatRate = floatRate, TdInterestPrincipal = tdPrincipal,
|
||||
PosiNotionalValue = tdPrincipal, InterestProfitSum = interestSum
|
||||
PosiNotionalValue = tdPrincipal, InterestIncomeSum = interestSum,
|
||||
InterestProfitSum = interestSum
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -250,6 +250,7 @@ namespace YLErp.Modules.SwapModule
|
||||
FloatRate = floatRate,
|
||||
TdInterestPrincipal = tdPrincipal,
|
||||
PosiNotionalValue = tdPrincipal,
|
||||
InterestIncomeSum = interestSum,
|
||||
InterestProfitSum = interestSum
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
using Newtonsoft.Json;
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 对话及缺陷表中的部分平仓后最终全平案例。
|
||||
/// 使用生产 GetInterests 计算,不连接数据库;数据库数值仅作为冻结输入快照。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SwapCloseConversationCasesRegressionTest
|
||||
{
|
||||
private const int AnnualDays = 365;
|
||||
private const decimal CentTolerance = 0.015m;
|
||||
|
||||
public sealed class CloseCase
|
||||
{
|
||||
public string TradeNumber { get; init; }
|
||||
public DateTime StartDate { get; init; }
|
||||
public DateTime CloseDate { get; init; }
|
||||
public string InterestCalcMode { get; init; }
|
||||
public int SettlementRules { get; init; }
|
||||
public int InterestMode { get; init; }
|
||||
public int InterestType { get; init; }
|
||||
public int ResetDays { get; init; }
|
||||
public int InterestRule { get; init; }
|
||||
public decimal FixedRate { get; init; }
|
||||
public decimal PreviousPrincipal { get; init; }
|
||||
public decimal PreviousPendingInterest { get; init; }
|
||||
public decimal PreviousFloatRate { get; init; }
|
||||
public decimal CloseFloatRate { get; init; }
|
||||
public decimal OriginalNotional { get; init; }
|
||||
public decimal RemainingNotional { get; init; }
|
||||
public decimal InitialQuantity { get; init; }
|
||||
public decimal PartialCloseQuantity { get; init; }
|
||||
public decimal PartialCloseInterest { get; init; }
|
||||
public decimal ExpectedFinalInterest { get; init; }
|
||||
|
||||
public override string ToString() => TradeNumber;
|
||||
}
|
||||
|
||||
private sealed class SnapshotSwapDealService : SwapDealService
|
||||
{
|
||||
private readonly double _floatRate;
|
||||
private readonly IReadOnlyDictionary<DateTime, double> _floatRates;
|
||||
|
||||
public SnapshotSwapDealService(decimal floatRate)
|
||||
: base(new OptUserInfo(0, nameof(SwapCloseConversationCasesRegressionTest), OptUserFrom.UnitTest))
|
||||
{
|
||||
_floatRate = (double)floatRate;
|
||||
_floatRates = BuildAprFloatRates();
|
||||
}
|
||||
|
||||
protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate)
|
||||
{
|
||||
if (_floatRates.TryGetValue(valueDate.Date, out rate))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
rate = _floatRate;
|
||||
return true;
|
||||
}
|
||||
|
||||
public override decimal GetConsumedInterest(int tradeId, long positionId, DateTime beforeDate)
|
||||
=> 0m;
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> ConversationCases => BuildCases().Select(x => new object[] { x });
|
||||
|
||||
[DataTestMethod]
|
||||
[DynamicData(nameof(ConversationCases), DynamicDataSourceType.Property)]
|
||||
public void FinalCloseMatchesConversationCase(CloseCase closeCase)
|
||||
{
|
||||
var trade = CreateTrade(closeCase);
|
||||
var position = CreatePosition(closeCase);
|
||||
var previousEod = CreatePreviousEod(closeCase, position);
|
||||
var service = new SnapshotSwapDealService(closeCase.CloseFloatRate);
|
||||
|
||||
var result = service.GetInterests(
|
||||
trade, trade.trade_extend, closeCase.CloseDate, closeCase.CloseDate,
|
||||
new List<eod_swap_position> { previousEod }, new List<swap_position> { position },
|
||||
closeCase.RemainingNotional, closeCase.RemainingNotional, 0m,
|
||||
closeCase.RemainingNotional, 1m, (int)SwapEventTypeEnum.平仓,
|
||||
false, false, 0m,
|
||||
closeCase.InterestType == 0 ? closeCase.RemainingNotional : closeCase.OriginalNotional,
|
||||
add: false, settment: false, newCalcLast: false).Single();
|
||||
|
||||
AssertAmount(closeCase.ExpectedFinalInterest, result.InterestAmount,
|
||||
$"{closeCase.TradeNumber} 最终全平利息");
|
||||
|
||||
if (closeCase.InterestCalcMode.EndsWith("0"))
|
||||
{
|
||||
AssertAmount(closeCase.PreviousPendingInterest, result.InterestAmount,
|
||||
$"{closeCase.TradeNumber} 不算尾时必须带走上日全部待实现利息");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.AreNotEqual(
|
||||
Math.Round(closeCase.PreviousPendingInterest, 2, MidpointRounding.AwayFromZero),
|
||||
Math.Round(result.InterestAmount, 2, MidpointRounding.AwayFromZero),
|
||||
$"{closeCase.TradeNumber} 算尾时必须包含最终平仓日新增利息");
|
||||
}
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DynamicData(nameof(ConversationCases), DynamicDataSourceType.Property)]
|
||||
public void PartialCloseSnapshotKeepsOriginalRatioAndRemainingTail(CloseCase closeCase)
|
||||
{
|
||||
var closePercentOfOriginal = closeCase.PartialCloseQuantity / closeCase.InitialQuantity;
|
||||
var expectedPercent = closeCase.TradeNumber.Contains("JIATT") ? 0.4m : 0.3m;
|
||||
|
||||
Assert.AreEqual(expectedPercent, closePercentOfOriginal,
|
||||
$"{closeCase.TradeNumber} 部分平仓比例必须按期初数量口径记录");
|
||||
Assert.AreNotEqual(0m, closeCase.PartialCloseInterest,
|
||||
$"{closeCase.TradeNumber} 5/11 或 8/4 部分平仓利息快照不得丢失");
|
||||
Assert.AreNotEqual(0m, closeCase.PreviousPendingInterest,
|
||||
$"{closeCase.TradeNumber} 最终平仓前待实现尾差不得提前清零");
|
||||
}
|
||||
|
||||
private static trade CreateTrade(CloseCase closeCase)
|
||||
{
|
||||
return new trade
|
||||
{
|
||||
id = 1,
|
||||
TradeNumber = closeCase.TradeNumber,
|
||||
TradeDate = closeCase.StartDate,
|
||||
StartDate = closeCase.StartDate,
|
||||
ExerciseDate = closeCase.CloseDate,
|
||||
TradeStatus = "确认成交",
|
||||
ValidState = "Valid",
|
||||
trade_extend = new trade_extend
|
||||
{
|
||||
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||||
{
|
||||
AnnualDays = AnnualDays,
|
||||
InterestCalcMode = closeCase.InterestCalcMode,
|
||||
SettlementRules = closeCase.SettlementRules
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static swap_position CreatePosition(CloseCase closeCase)
|
||||
{
|
||||
return new swap_position
|
||||
{
|
||||
id = 1,
|
||||
PositionType = 0,
|
||||
InterestDirection = 1,
|
||||
InterestMode = closeCase.InterestMode,
|
||||
InterestType = closeCase.InterestType,
|
||||
InterestRateDefault = closeCase.FixedRate,
|
||||
InterestPrincipalFix = closeCase.OriginalNotional,
|
||||
PosiStartDate = closeCase.StartDate,
|
||||
PosiMatuirityDate = closeCase.CloseDate,
|
||||
IsInitial = true,
|
||||
Invalid = false,
|
||||
IsAnnualized = true,
|
||||
interest_rest_days = closeCase.ResetDays,
|
||||
interest_rule = closeCase.InterestRule,
|
||||
FloatRateUnderlyingCode = "FR007",
|
||||
InterestSwapInterval = JsonConvert.SerializeObject(new List<IntervalModel>
|
||||
{
|
||||
new IntervalModel
|
||||
{
|
||||
Date = closeCase.CloseDate,
|
||||
Rate = closeCase.FixedRate,
|
||||
Settlement = 0
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
private static eod_swap_position CreatePreviousEod(CloseCase closeCase, swap_position position)
|
||||
{
|
||||
return new eod_swap_position
|
||||
{
|
||||
id = 1,
|
||||
PositionId = position.id,
|
||||
ValueDate = closeCase.CloseDate.AddDays(-1),
|
||||
InterestDirection = position.InterestDirection,
|
||||
InterestMode = position.InterestMode,
|
||||
InterestType = position.InterestType,
|
||||
InterestRateDefault = closeCase.FixedRate,
|
||||
InterestIncomeSum = closeCase.PreviousPendingInterest,
|
||||
InterestProfitSum = closeCase.PreviousPendingInterest,
|
||||
TdInterestPrincipal = closeCase.PreviousPrincipal,
|
||||
PosiNotionalValue = 0m,
|
||||
FloatRate = closeCase.PreviousFloatRate,
|
||||
IsAnnualized = true,
|
||||
interest_rest_days = closeCase.ResetDays,
|
||||
interest_rule = closeCase.InterestRule
|
||||
};
|
||||
}
|
||||
|
||||
private static void AssertAmount(decimal expected, decimal actual, string message)
|
||||
{
|
||||
Assert.IsTrue(Math.Abs(expected - actual) <= CentTolerance,
|
||||
$"{message}。Expected={expected}, Actual={actual}, Diff={expected - actual}");
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<DateTime, double> BuildAprFloatRates()
|
||||
{
|
||||
return new Dictionary<DateTime, double>
|
||||
{
|
||||
[new DateTime(2026, 4, 20)] = 0.0132,
|
||||
[new DateTime(2026, 4, 21)] = 0.0132,
|
||||
[new DateTime(2026, 4, 22)] = 0.0132,
|
||||
[new DateTime(2026, 4, 23)] = 0.0132,
|
||||
[new DateTime(2026, 4, 24)] = 0.0131,
|
||||
[new DateTime(2026, 4, 27)] = 0.013502,
|
||||
[new DateTime(2026, 4, 28)] = 0.0136,
|
||||
[new DateTime(2026, 4, 29)] = 0.0138,
|
||||
[new DateTime(2026, 4, 30)] = 0.0139,
|
||||
[new DateTime(2026, 5, 4)] = 0.0139,
|
||||
[new DateTime(2026, 5, 5)] = 0.0139,
|
||||
[new DateTime(2026, 5, 6)] = 0.0136,
|
||||
[new DateTime(2026, 5, 7)] = 0.0136,
|
||||
[new DateTime(2026, 5, 8)] = 0.0135,
|
||||
[new DateTime(2026, 5, 9)] = 0.0131,
|
||||
[new DateTime(2026, 5, 11)] = 0.0134,
|
||||
[new DateTime(2026, 5, 12)] = 0.0130,
|
||||
[new DateTime(2026, 5, 13)] = 0.0129,
|
||||
[new DateTime(2026, 5, 14)] = 0.0130,
|
||||
[new DateTime(2026, 5, 15)] = 0.0130,
|
||||
[new DateTime(2026, 5, 18)] = 0.0132,
|
||||
[new DateTime(2026, 5, 19)] = 0.0131
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyList<CloseCase> BuildCases()
|
||||
{
|
||||
var apr21Mode9NoLast = AprCase("", new DateTime(2026, 4, 21), "10", 0, 9, 1, -1,
|
||||
0.0025m, 212393195.604981356504m, 260578.522161724795m, 0.0134m, 0.0132m,
|
||||
79831.29m, 260578.53m);
|
||||
var apr21Mode2NoLast = AprCase("", new DateTime(2026, 4, 21), "10", 0, 2, 1, 0,
|
||||
0.0025m, 212393594.673529615939m, 259348.391672295294m, 0.0130m, 0.0131m,
|
||||
80002.30m, 259348.38m);
|
||||
var apr21Mode2WithLast = AprCase("", new DateTime(2026, 4, 21), "11", 0, 2, 1, 0,
|
||||
0.0025m, 212393594.665105085126m, 259348.383245260196m, 0.0130m, 0.0131m,
|
||||
84090.95m, 268428.73m);
|
||||
var apr22Mode9NoLast = AprCase("", new DateTime(2026, 4, 22), "10", 1, 9, 1, -1,
|
||||
-0.0210m, 212106644.672742434546m, -118631.263817268568m, 0.0130m, 0.0130m,
|
||||
-35350.65m, -118631.26m);
|
||||
var apr22Mode2WithLast = AprCase("", new DateTime(2026, 4, 22), "11", 1, 2, 1, 0,
|
||||
-0.0210m, 212106237.833444880927m, -119386.717400887353m, 0.0129m, 0.0129m,
|
||||
-37218.76m, -124093.74m);
|
||||
var apr21SimpleWithLast = AprCase("", new DateTime(2026, 4, 21), "11", 0, 2, 0, -1,
|
||||
0.0025m, 212197382.46m, 260458.629530704663m, 0.0134m, 0.0132m,
|
||||
83894.12m, 269586.02m);
|
||||
|
||||
return new List<CloseCase>
|
||||
{
|
||||
WithTradeNumber(apr21Mode2WithLast, "GLMS-20260421-0007"),
|
||||
WithTradeNumber(apr21Mode2NoLast, "GLMS-20260421-0005"),
|
||||
WithTradeNumber(apr21Mode9NoLast, "GLMS-20260421-0001"),
|
||||
WithTradeNumber(apr22Mode2WithLast, "GLMS-20260421-0008"),
|
||||
WithTradeNumber(apr21SimpleWithLast, "GLMS-20260421-0011"),
|
||||
WithTradeNumber(apr21Mode2WithLast, "GLMS-MARSK-20260421-FICC-01-180205IB"),
|
||||
WithTradeNumber(apr21Mode9NoLast, "GLMS-MARSK-20260421-FICC-02-180205IB"),
|
||||
WithTradeNumber(apr22Mode9NoLast, "GLMS-MARSK-20260421-FICC-03-180205IB"),
|
||||
WithTradeNumber(apr22Mode2WithLast, "GLMS-MARSK-20260421-FICC-04-180205IB"),
|
||||
WithTradeNumber(apr21SimpleWithLast, "GLMS-MARSK-20260421-FICC-05-180205IB"),
|
||||
JiattCase("GLMS-JIATT-20260805-FICC-01-2180120IB", 30041492.070122881942m,
|
||||
10019.043756537721m, 2970.02m, 10019.04105m),
|
||||
JiattCase("GLMS-JIATT-20260727-FICC-02-2180120IB", 30044833.3381m,
|
||||
13360.932596m, 5197.53m, 13360.93051m)
|
||||
};
|
||||
}
|
||||
|
||||
private static CloseCase AprCase(string tradeNumber, DateTime startDate, string calcMode,
|
||||
int settlementRules, int interestMode, int interestType, int interestRule,
|
||||
decimal fixedRate, decimal previousPrincipal, decimal previousPending,
|
||||
decimal previousFloatRate, decimal closeFloatRate, decimal partialInterest,
|
||||
decimal expectedFinal)
|
||||
{
|
||||
return new CloseCase
|
||||
{
|
||||
TradeNumber = tradeNumber,
|
||||
StartDate = startDate,
|
||||
CloseDate = new DateTime(2026, 5, 19),
|
||||
InterestCalcMode = calcMode,
|
||||
SettlementRules = settlementRules,
|
||||
InterestMode = interestMode,
|
||||
InterestType = interestType,
|
||||
ResetDays = 7,
|
||||
InterestRule = interestRule,
|
||||
FixedRate = fixedRate,
|
||||
PreviousPrincipal = previousPrincipal,
|
||||
PreviousPendingInterest = previousPending,
|
||||
PreviousFloatRate = previousFloatRate,
|
||||
CloseFloatRate = closeFloatRate,
|
||||
OriginalNotional = 303139117.80m,
|
||||
RemainingNotional = 212197382.46m,
|
||||
InitialQuantity = 300000000m,
|
||||
PartialCloseQuantity = 90000000m,
|
||||
PartialCloseInterest = partialInterest,
|
||||
ExpectedFinalInterest = expectedFinal
|
||||
};
|
||||
}
|
||||
|
||||
private static CloseCase JiattCase(string tradeNumber, decimal remainingPrincipal,
|
||||
decimal previousPending, decimal partialInterest, decimal expectedFinal)
|
||||
{
|
||||
return new CloseCase
|
||||
{
|
||||
TradeNumber = tradeNumber,
|
||||
StartDate = new DateTime(2026, 7, 28),
|
||||
CloseDate = new DateTime(2026, 8, 7),
|
||||
InterestCalcMode = "10",
|
||||
SettlementRules = 0,
|
||||
InterestMode = 9,
|
||||
InterestType = 1,
|
||||
ResetDays = 7,
|
||||
InterestRule = -1,
|
||||
FixedRate = 0.001234m,
|
||||
PreviousPrincipal = remainingPrincipal,
|
||||
PreviousPendingInterest = previousPending,
|
||||
PreviousFloatRate = 0.0213m,
|
||||
CloseFloatRate = 0.0213m,
|
||||
OriginalNotional = 50061728.39m,
|
||||
RemainingNotional = remainingPrincipal,
|
||||
InitialQuantity = 50000000m,
|
||||
PartialCloseQuantity = 20000000m,
|
||||
PartialCloseInterest = partialInterest,
|
||||
ExpectedFinalInterest = expectedFinal
|
||||
};
|
||||
}
|
||||
|
||||
private static CloseCase WithTradeNumber(CloseCase source, string tradeNumber)
|
||||
{
|
||||
return new CloseCase
|
||||
{
|
||||
TradeNumber = tradeNumber,
|
||||
StartDate = source.StartDate,
|
||||
CloseDate = source.CloseDate,
|
||||
InterestCalcMode = source.InterestCalcMode,
|
||||
SettlementRules = source.SettlementRules,
|
||||
InterestMode = source.InterestMode,
|
||||
InterestType = source.InterestType,
|
||||
ResetDays = source.ResetDays,
|
||||
InterestRule = source.InterestRule,
|
||||
FixedRate = source.FixedRate,
|
||||
PreviousPrincipal = source.PreviousPrincipal,
|
||||
PreviousPendingInterest = source.PreviousPendingInterest,
|
||||
PreviousFloatRate = source.PreviousFloatRate,
|
||||
CloseFloatRate = source.CloseFloatRate,
|
||||
OriginalNotional = source.OriginalNotional,
|
||||
RemainingNotional = source.RemainingNotional,
|
||||
InitialQuantity = source.InitialQuantity,
|
||||
PartialCloseQuantity = source.PartialCloseQuantity,
|
||||
PartialCloseInterest = source.PartialCloseInterest,
|
||||
ExpectedFinalInterest = source.ExpectedFinalInterest
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,13 +85,10 @@ namespace YLErp.Modules.SwapModule
|
||||
List<swap_flow_event> closeList = null)
|
||||
{
|
||||
LastInterestCalculationPositions = positions;
|
||||
return positions.Select(position => new swap_flow_event
|
||||
{
|
||||
PositionId = position.id,
|
||||
InterestPrincipal = 1000m,
|
||||
InterestRate = 0.01m,
|
||||
FloatRate = 0.01m
|
||||
}).ToList();
|
||||
return base.CalcSwapInterests(td, tradeExtend, valueDate, unwindDate,
|
||||
eodPositions, positions, posiNotionalValue, posiLongNotionalValue, posiShortNotionalValue,
|
||||
closePosiNotionalValue, closePrecent, eventType, tdClose, needPrice,
|
||||
grossPrice, orginPv, add, settment, newCalcLast, closeList);
|
||||
}
|
||||
|
||||
public void ExecuteSwapPositionCompose(DateTime settleDate, DateTime preSettleDate)
|
||||
@@ -328,7 +325,8 @@ namespace YLErp.Modules.SwapModule
|
||||
id = initialPrepayId, SwapTradeId = SwapTradeId, PosiDirection = 0,
|
||||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||||
InterestMode = (int)InterestModeEnum.初始预付金,
|
||||
InterestPrincipalFix = 1000m, IsInitial = true, Invalid = false,
|
||||
InterestPrincipalFix = 1000m, InterestRateDefault = 0.01m,
|
||||
IsInitial = true, Invalid = false,
|
||||
IsAnnualized = true,
|
||||
PosiStartDate = SettleDate.AddDays(-1), PosiMatuirityDate = td.ExerciseDate.Value,
|
||||
InterestSwapInterval = "[]"
|
||||
@@ -356,7 +354,8 @@ namespace YLErp.Modules.SwapModule
|
||||
EventType = (int)SwapEventTypeEnum.平仓,
|
||||
EventDate = SettleDate, DataState = (int)SwapFlowDateStateEnum.完成,
|
||||
InterestMode = (int)InterestModeEnum.初始预付金,
|
||||
InterestPrincipal = 300m
|
||||
InterestPrincipal = 300m,
|
||||
InterestRate = 0.01m
|
||||
};
|
||||
var service = new TestableSwapEodService(
|
||||
new List<trade> { td },
|
||||
@@ -374,7 +373,9 @@ namespace YLErp.Modules.SwapModule
|
||||
"实时腿已经扣减到700,日终不得再次按平仓比例扣减");
|
||||
Assert.AreEqual(700m, persistedPrepay.TdInterestPrincipal,
|
||||
"平仓日预付金计息本金应立即切换为实时剩余本金");
|
||||
Assert.AreEqual(700m * 0.01m / 365m, persistedPrepay.TdInterestIncome,
|
||||
var expectedDailyInterest = Math.Round(700m * 0.01m / 365m,
|
||||
12, MidpointRounding.AwayFromZero);
|
||||
Assert.AreEqual(expectedDailyInterest, persistedPrepay.TdInterestIncome,
|
||||
"平仓日新增利息应按实时剩余本金计算");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user