feat(swap): 实现预付金利息腿实时剩余本金计算功能

- 新增 ResolveInterestLegPositionsAsOf 方法处理预付金利息腿实时本金计算
- 添加 FindCompletedFlowEvents 方法查询已完成的现金流事件
- 在日终持仓服务中集成实时剩余本金计算逻辑
- 移除日终重复扣减预付金本金的逻辑
- 优化利息端估值计算方式
- 添加部分平仓后预付金日终按实时剩余本金计息的测试用例
- 增加平仓日预付金日终不得重复扣减实时剩余本金的验证
- 完善历史回放场景下的本金调整功能
This commit is contained in:
张名锐
2026-08-06 14:32:38 +08:00
parent 6bbe9b1c13
commit 9ef45650c7
3 changed files with 233 additions and 6 deletions
@@ -36,6 +36,7 @@ namespace YLErp.Modules.SwapModule
// 输出别名(转发到基类捕获属性)
public List<eod_swap_position> CreatedEodPositions => PersistedPositions;
public List<swap_position> LastInterestCalculationPositions { get; private set; }
public TestableSwapEodService(
List<trade> trades, List<swap_position> positions,
@@ -80,7 +81,17 @@ namespace YLErp.Modules.SwapModule
decimal posiNotionalValue, decimal posiLongNotionalValue, decimal posiShortNotionalValue,
decimal closePosiNotionalValue, decimal closePrecent, int eventType, bool tdClose, bool needPrice,
decimal grossPrice, decimal orginPv, bool add = false, bool settment = true, bool newCalcLast = false,
List<swap_flow_event> closeList = null) => new List<swap_flow_event>();
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();
}
public void ExecuteSwapPositionCompose(DateTime settleDate, DateTime preSettleDate)
=> SwapPositionCompose(settleDate, preSettleDate, null);
@@ -250,5 +261,154 @@ namespace YLErp.Modules.SwapModule
Assert.IsTrue(ex.Message.Contains("未收盘"), $"异常消息应含'未收盘',实际:{ex.Message}");
Console.WriteLine($"SPC_004: 抛异常'{ex.Message}' ✅");
}
[TestMethod]
public void SPC_005_部分平仓后_预付金日终按实时剩余本金计息()
{
const long initialPrepayId = 2;
var td = CreateTrade();
var initialPrepay = new swap_position
{
id = initialPrepayId, SwapTradeId = SwapTradeId, PosiDirection = 0,
InterestDirection = (int)SwapDirectionEnum.,
InterestMode = (int)InterestModeEnum.,
InterestPrincipalFix = 1000m, IsInitial = true, Invalid = false,
PosiStartDate = SettleDate.AddDays(-1), PosiMatuirityDate = td.ExerciseDate.Value,
InterestSwapInterval = "[]"
};
var realPrepay = new swap_position
{
id = 3, PositionId = initialPrepayId, SwapTradeId = SwapTradeId, PosiDirection = 0,
InterestDirection = (int)SwapDirectionEnum.,
InterestMode = (int)InterestModeEnum.,
InterestPrincipalFix = 700m, IsInitial = false, Invalid = false
};
var prepayEod = new eod_swap_position
{
id = 200, SwapTradeId = SwapTradeId, PositionId = initialPrepayId,
ValueDate = PreSettleDate, PosiDirection = 0,
InterestDirection = (int)SwapDirectionEnum.,
InterestMode = (int)InterestModeEnum.,
InterestPrincipalFix = 700m, TdInterestPrincipal = 700m
};
var service = new TestableSwapEodService(
new List<trade> { td },
new List<swap_position> { CreateFloatPosition(1, 1000), initialPrepay, realPrepay },
new List<eod_swap_position> { CreateFloatEodPosition(1, 1000, 1.0020m), prepayEod },
new List<eod_swap> { new eod_swap { SwapTradeId = SwapTradeId, ValueDate = PreSettleDate } },
new List<trade_extend> { CreateExtend() }, new List<swap_flow_event>());
service.ExecuteSwapPositionCompose(SettleDate, PreSettleDate);
var calculatedPrepay = service.LastInterestCalculationPositions
.Single(x => x.id == initialPrepayId);
Assert.AreEqual(700m, calculatedPrepay.InterestPrincipalFix);
Assert.AreEqual(initialPrepayId, calculatedPrepay.id);
}
[TestMethod]
public void SPC_006_平仓日_预付金日终不得重复扣减实时剩余本金()
{
const long initialPrepayId = 2;
var td = CreateTrade();
var initialPrepay = new swap_position
{
id = initialPrepayId, SwapTradeId = SwapTradeId, PosiDirection = 0,
InterestDirection = (int)SwapDirectionEnum.,
InterestMode = (int)InterestModeEnum.,
InterestPrincipalFix = 1000m, IsInitial = true, Invalid = false,
IsAnnualized = true,
PosiStartDate = SettleDate.AddDays(-1), PosiMatuirityDate = td.ExerciseDate.Value,
InterestSwapInterval = "[]"
};
var realPrepay = new swap_position
{
id = 3, PositionId = initialPrepayId, SwapTradeId = SwapTradeId, PosiDirection = 0,
InterestDirection = (int)SwapDirectionEnum.,
InterestMode = (int)InterestModeEnum.,
InterestPrincipalFix = 700m, IsInitial = false, Invalid = false
};
var prepayEod = new eod_swap_position
{
id = 200, SwapTradeId = SwapTradeId, PositionId = initialPrepayId,
ValueDate = PreSettleDate, PosiDirection = 0,
InterestDirection = (int)SwapDirectionEnum.,
InterestMode = (int)InterestModeEnum.,
InterestPrincipalFix = 1000m, TdInterestPrincipal = 1000m
};
var closeFlow = CreateCloseFlowEvent(1, 300);
closeFlow.InterestRate = 0.01m;
var service = new TestableSwapEodService(
new List<trade> { td },
new List<swap_position> { CreateFloatPosition(1, 1000), initialPrepay, realPrepay },
new List<eod_swap_position> { CreateFloatEodPosition(1, 1000, 1.0020m), prepayEod },
new List<eod_swap> { new eod_swap { SwapTradeId = SwapTradeId, ValueDate = PreSettleDate } },
new List<trade_extend> { CreateExtend() },
new List<swap_flow_event> { closeFlow });
service.ExecuteSwapPositionCompose(SettleDate, PreSettleDate);
var persistedPrepay = service.CreatedEodPositions
.Single(x => x.PositionId == initialPrepayId);
Assert.AreEqual(700m, persistedPrepay.InterestPrincipalFix,
"实时腿已经扣减到700,日终不得再次按平仓比例扣减");
Assert.AreEqual(700m, persistedPrepay.TdInterestPrincipal,
"平仓日预付金计息本金应立即切换为实时剩余本金");
Assert.AreEqual(700m * 0.01m / 365m, persistedPrepay.TdInterestIncome,
"平仓日新增利息应按实时剩余本金计算");
}
[TestMethod]
public void SPC_007_HistoricalReplayUsesAsOfPrincipal()
{
const long originalPositionId = 2;
var original = new swap_position
{
id = originalPositionId, PosiDirection = 0,
InterestDirection = (int)SwapDirectionEnum.,
InterestMode = (int)InterestModeEnum.,
InterestPrincipalFix = 10000m
};
var realtime = new swap_position
{
PositionId = originalPositionId,
InterestMode = (int)InterestModeEnum.,
InterestPrincipalFix = 7000m
};
var close = new swap_flow_event
{
PositionId = originalPositionId,
PositionType = 0,
EventType = (int)SwapEventTypeEnum.,
EventDate = new DateTime(2026, 7, 9),
InterestMode = (int)InterestModeEnum.,
InterestPrincipal = 3000m
};
var floatClose = new swap_flow_event
{
PositionId = 1,
PositionType = 1,
EventType = (int)SwapEventTypeEnum.,
EventDate = new DateTime(2026, 7, 9),
TradingAmount = 3000000m
};
var originalWithFloat = new List<swap_position>
{
original,
new swap_position { id = 1, PosiDirection = 1, PosiNotionalValue = 10000000m }
};
var beforeClose = SwapDealService.ResolveInterestLegPositionsAsOf(
originalWithFloat, new List<swap_position> { realtime },
new[] { close, floatClose }, new DateTime(2026, 7, 8))
.Single(x => x.id == originalPositionId);
var onCloseDate = SwapDealService.ResolveInterestLegPositionsAsOf(
originalWithFloat, new List<swap_position> { realtime },
new[] { close, floatClose }, new DateTime(2026, 7, 9))
.Single(x => x.id == originalPositionId);
Assert.AreEqual(10000m, beforeClose.InterestPrincipalFix);
Assert.AreEqual(7000m, onCloseDate.InterestPrincipalFix);
}
}
}