Files
zszq-trs/UnitTestProject/Modules/SwapModule/FundCorporateActionRollbackAndUnwindTest.cs
T
张名锐 ff7e540084 refactor(swap): 优化基金公司行为处理逻辑 - finished001
- 修改 FindFundCorporateActions 方法以支持查询跨日期范围的公司行为记录
- 实现按生效日期和ID顺序稳定排序的多条公司行为记录处理
- 更新平仓时基金基线恢复逻辑以正确处理跨多个生效日的场景
- 在测试类中重写 FindFundCorporateActions 方法以支持单元测试验证
- 添加完整的跨非交易日公司行为恢复功能测试用例
2026-08-20 16:10:56 +08:00

396 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using YLErp.DBModels;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.SwapModule
{
[TestClass]
public class FundCorporateActionRollbackAndUnwindTest
{
// 生产恢复范围已从原 Fund-only 扩展到 TRS Fund/Stock;本组继续使用 Fund 夹具,
// 验证共享的登记日/EffectiveDate 边界和回退、平仓基线。
private static readonly DateTime ExDate = new(2026, 8, 17);
[TestMethod]
public void FCA_RB_001_回退选择最近实际Eod并遵守除权日边界()
{
var friday = CreateEod(new DateTime(2026, 8, 14), 1000m, 100m);
var exDate = CreateEod(ExDate, 2000m, 50m);
var invalidSunday = CreateEod(new DateTime(2026, 8, 16), 9999m, 1m);
invalidSunday.Invalid = true;
var snapshots = new[] { friday, invalidSunday, exDate };
var rollbackToExDate = SwapEodPositionService.SelectLatestEodPositionsBefore(
snapshots,
ExDate);
var rollbackAfterExDate = SwapEodPositionService.SelectLatestEodPositionsBefore(
snapshots,
ExDate.AddDays(1));
Assert.AreEqual(friday.ValueDate, rollbackToExDate.Single().ValueDate,
"回退到除权日应恢复除权前最近实际 EOD,不能用周日自然日或除权日自身");
Assert.AreEqual(1000m, rollbackToExDate.Single().PosiQuantity);
Assert.AreEqual(exDate.ValueDate, rollbackAfterExDate.Single().ValueDate,
"回退到除权日之后应保留已经生效的除权 EOD");
Assert.AreEqual(2000m, rollbackAfterExDate.Single().PosiQuantity);
}
[TestMethod]
public void FCA_UW_001_最近FundEod恢复价格数量且重复恢复不重复除权()
{
var realtime = CreateRealtimeFundPosition();
var eod = CreateEod(ExDate, 2000m, 50m);
Assert.IsTrue(SwapEodPositionService.RestoreFundPositionFromEod(realtime, eod));
Assert.AreEqual(2000m, realtime.PosiQuantity);
Assert.AreEqual(50m, realtime.PosiGrossPrice);
Assert.AreEqual(100000m, realtime.PosiNotionalValue);
Assert.IsTrue(SwapEodPositionService.RestoreFundPositionFromEod(realtime, eod));
Assert.AreEqual(2000m, realtime.PosiQuantity,
"恢复 EOD 是复制快照,不是再次套 10 送 10 系数,不能变成 4000");
Assert.AreEqual(50m, realtime.PosiGrossPrice,
"重复恢复不能把价格再次调整为 25");
}
[TestMethod]
public void FCA_UW_002_股票与最新Eod后已有完成流水时保持实时持仓()
{
var nonFund = CreateRealtimeFundPosition();
nonFund.UnderlyingInstrumentType = ConsGlobal.InstrumentType.Stock;
var eod = CreateEod(ExDate, 2000m, 50m);
Assert.IsTrue(SwapEodPositionService.RestoreFundPositionFromEod(nonFund, eod));
Assert.AreEqual(2000m, nonFund.PosiQuantity);
Assert.AreEqual(50m, nonFund.PosiGrossPrice);
var td = SwapDealTestFactory.CreateTrade();
var realtime = CreateRealtimeFundPosition();
realtime.PosiQuantity = 1500m;
realtime.PosiGrossPrice = 50m;
var service = CreateService(td, realtime, eod, hasCompletedFlow: true);
var unwindData = CreateFullCloseUnwindData();
Assert.IsFalse(service.RestoreEffectiveFundPositionForTest(unwindData, ExDate.AddDays(1)));
Assert.AreEqual(1500m, realtime.PosiQuantity,
"EOD 后已有部分平仓流水时不能用 2000 份 EOD 覆盖实时剩余 1500 份");
Assert.AreEqual(1000m, unwindData.CloseQty,
"未恢复基线时不得擅自改写前端请求,沿用既有当日实时流程");
}
[TestMethod]
public void FCA_UW_008_股票TRS平仓恢复有效Eod基线()
{
var realtime = CreateRealtimeFundPosition();
realtime.UnderlyingCode = "STOCK.TEST";
realtime.UnderlyingInstrumentType = ConsGlobal.InstrumentType.Stock;
realtime.PosiQuantity = 1000m;
realtime.PosiGrossPrice = 100m;
realtime.PosiNetPrice = 100m;
realtime.PosiNetFeePrice = 100m;
realtime.PosiNetNoFeePrice = 100m;
realtime.PosiNotionalValue = 100000m;
var eod = CreateEod(ExDate, 2000m, 50m);
eod.UnderlyingCode = "STOCK.TEST";
eod.UnderlyingInstrumentType = ConsGlobal.InstrumentType.Stock;
var service = CreateService(SwapDealTestFactory.CreateTrade(), realtime, eod, hasCompletedFlow: false);
var unwindData = CreateFullCloseUnwindData();
unwindData.ValueDate = ExDate;
unwindData.UnwindDate = ExDate.AddDays(1);
Assert.IsTrue(service.RestoreEffectiveFundPositionForTest(unwindData, ExDate));
Assert.AreEqual(2000m, realtime.PosiQuantity,
"Stock TRS 生效日盘中平仓应使用有效 EOD 数量,不能继续使用除权前实时数量");
Assert.AreEqual(50m, realtime.PosiGrossPrice,
"Stock TRS 生效日盘中平仓应使用有效 EOD 价格");
Assert.AreEqual(2000m, unwindData.PositionQty);
Assert.AreEqual(2000m, unwindData.CloseQty);
Assert.AreEqual(50m, unwindData.FlowEvents.Single().PosiGrossPrice);
}
[TestMethod]
public void FCA_UW_005_生效日盘中恢复前一Eod后再套除权()
{
var recordDate = new DateTime(2026, 8, 14);
var realtime = CreateRealtimeFundPosition();
var eod = CreateEod(recordDate, 1000m, 100m);
var service = CreateService(SwapDealTestFactory.CreateTrade(), realtime, eod, hasCompletedFlow: false);
service.ExDividendInfos.Add(new ex_dividend_info
{
UnderlyingCode = "FUND.TEST",
ExDividendDate = recordDate,
EffectiveDate = ExDate,
GiveShareAmount = 10m,
ValidStatus = true
});
var unwindData = CreateFullCloseUnwindData();
Assert.IsTrue(service.RestoreEffectiveFundPositionForTest(unwindData, ExDate));
Assert.AreEqual(2000m, realtime.PosiQuantity,
"8 月 17 日盘中应先从 8 月 14 日 EOD 恢复,再按 10 送 10 变为 2000 份");
Assert.AreEqual(50m, realtime.PosiGrossPrice,
"真实除权生效日盘中应使用 50 元基准,不能继续使用登记日 100 元");
Assert.AreEqual(2000m, unwindData.CloseQty);
Assert.AreEqual(50m, unwindData.FlowEvents.Single().PosiGrossPrice);
}
[TestMethod]
public void FCA_UW_006_登记日盘中平仓不提前应用除权()
{
var recordDate = new DateTime(2026, 8, 14);
var realtime = CreateRealtimeFundPosition();
// 8 月 14 日盘中尚未生成当日 EOD,最近可用快照应是 8 月 13 日。
var eod = CreateEod(recordDate.AddDays(-1), 1000m, 100m);
var service = CreateService(SwapDealTestFactory.CreateTrade(), realtime, eod, hasCompletedFlow: false);
service.ExDividendInfos.Add(new ex_dividend_info
{
UnderlyingCode = "FUND.TEST",
ExDividendDate = recordDate,
EffectiveDate = ExDate,
GiveShareAmount = 10m,
ValidStatus = true
});
var unwindData = CreateFullCloseUnwindData();
unwindData.ValueDate = recordDate;
unwindData.UnwindDate = recordDate.AddDays(1);
service.SwapUnwind(unwindData);
Assert.AreEqual(1000m, unwindData.PositionQty,
"登记日仍使用除权前 EOD 基线,不能提前变为 2000 份");
Assert.AreEqual(1000m, unwindData.CloseQty);
Assert.AreEqual(100m, unwindData.FlowEvents.Single().PosiGrossPrice,
"登记日盘中平仓价格仍应为 100 元,除权生效日才切换为 50 元");
}
[TestMethod]
public void FCA_UW_007_基金直接拆合股比例零点零一_平仓按新数量价格()
{
var recordDate = new DateTime(2026, 8, 14);
var realtime = CreateRealtimeFundPosition();
var eod = CreateEod(recordDate, 1000m, 100m);
var td = SwapDealTestFactory.CreateTrade();
td.StockEqvNotional = 100000d;
td.TradeAmount = 1000d;
var service = CreateService(td, realtime, eod, hasCompletedFlow: false);
service.ExDividendInfos.Add(new ex_dividend_info
{
UnderlyingCode = "FUND.TEST",
ExDividendDate = recordDate,
EffectiveDate = ExDate,
// 上游 splitratio=0.01 必须先转换为 10 * (0.01 - 1)=-9.9
// 直接写 0.01 会按当前字段公式得到 1.001 倍,无法表达缩小为 0.01 倍。
GiveShareAmount = -9.9m,
ValidStatus = true
});
var unwindData = CreateFullCloseUnwindData();
unwindData.ValueDate = ExDate;
unwindData.UnwindDate = ExDate.AddDays(1);
service.SwapUnwind(unwindData);
Assert.AreEqual(10m, unwindData.PositionQty,
"Fund splitratio=0.01 时,有效平仓基线应为 1000 * 0.01 = 10 份");
Assert.AreEqual(10m, unwindData.CloseQty);
Assert.AreEqual(10000m, unwindData.FlowEvents.Single().PosiGrossPrice,
"Fund 份额缩小为 0.01 倍时,直接平仓期初价应为 100 / 0.01 = 10000");
}
[TestMethod]
public void FCA_UW_009_登记日跨非交易日到生效日按范围恢复基金基线()
{
var eodDate = new DateTime(2026, 7, 12);
var effectiveDate = new DateTime(2026, 7, 13);
var unwindDate = new DateTime(2026, 7, 17);
var realtime = CreateRealtimeFundPosition();
var eod = CreateEod(eodDate, 1000m, 100m);
var service = CreateService(SwapDealTestFactory.CreateTrade(), realtime, eod, hasCompletedFlow: false);
service.ExDividendInfos.Add(new ex_dividend_info
{
id = 1,
UnderlyingCode = "FUND.TEST",
// 7/10 登记,7/13 生效;7/11、7/12 虽无交易但仍可能存在未除权 EOD 快照。
ExDividendDate = new DateTime(2026, 7, 10),
EffectiveDate = effectiveDate,
// 生产数据口径:1 拆 2 直接存 Split=2GiveShareAmount 不参与该拆分。
GiveShareAmount = 0m,
Split = 2m,
ValidStatus = true
});
var unwindData = CreateFullCloseUnwindData();
unwindData.ValueDate = unwindDate;
unwindData.UnwindDate = unwindDate;
Assert.IsTrue(service.RestoreEffectiveFundPositionForTest(unwindData, unwindDate));
Assert.AreEqual(2000m, realtime.PosiQuantity,
"7 月 17 日平仓应补应用 7 月 13 日生效的 Split=2,公司行为不能只按平仓日命中");
Assert.AreEqual(50m, realtime.PosiGrossPrice);
Assert.AreEqual(2000m, unwindData.PositionQty);
Assert.AreEqual(2000m, unwindData.CloseQty);
Assert.AreEqual(50m, unwindData.FlowEvents.Single().PosiGrossPrice);
}
[TestMethod]
public void FCA_UW_003_正式平仓按FundEod基线重算PnL和现金()
{
var td = SwapDealTestFactory.CreateTrade();
td.StockEqvNotional = 100000d;
td.TradeAmount = 1000d;
var realtime = CreateRealtimeFundPosition();
var eod = CreateEod(ExDate, 2000m, 50m);
var service = CreateService(td, realtime, eod, hasCompletedFlow: false);
var unwindData = CreateFullCloseUnwindData();
var floatEvent = unwindData.FlowEvents.Single();
service.SwapUnwind(unwindData);
Assert.AreEqual(2000m, unwindData.PositionQty);
Assert.AreEqual(2000m, unwindData.CloseQty);
Assert.AreEqual(100000m, unwindData.CloseNotionalValue);
Assert.AreEqual(50m, floatEvent.PosiGrossPrice);
Assert.AreEqual(20000m, floatEvent.MarkClosePnl,
"平仓价 60 - 除权后期初价 50,乘 2000 份,应为 20000");
Assert.AreEqual(20000m, unwindData.SwapRealizedPnL);
Assert.AreEqual(-20000d, service.ClientCashCalls.Single().amount, 0.001d,
"客户现金必须使用后台按有效 EOD 重算后的平仓金额");
}
[TestMethod]
public void FCA_UW_004_现金分红后部分平仓从Eod名义本金扣减()
{
var td = SwapDealTestFactory.CreateTrade();
td.StockEqvNotional = 100000d;
td.TradeAmount = 1000d;
var realtime = CreateRealtimeFundPosition();
var eod = CreateEod(ExDate, 1000m, 99m);
var service = CreateService(td, realtime, eod, hasCompletedFlow: false);
var unwindData = SwapDealTestFactory.CreateUnwindData(
swapRealizedPnL: -500m,
closeMethod: (int)CloseMethodEnum.部分平仓,
closePercent: 0.5m,
closeQty: 500m,
closeNotionalValue: 50000m,
positionQty: 1000m);
unwindData.NotionalValue = 100000m;
unwindData.PosiNotionalValue = 100000m;
unwindData.FlowEvents.Add(new swap_flow_event
{
PositionId = 101,
EventType = (int)SwapEventTypeEnum.平仓,
UnderlyingCode = "FUND.TEST",
UnderlyingInstrumentType = ConsGlobal.InstrumentType.Fund,
PositionType = (int)PositionTypeFlag.Long,
PayDirection = 1,
PosiGrossPrice = 100m,
PosiNetPrice = 100m,
TradingAmountAvg = 99m,
Quantity = 500m,
PositionQty = 500m,
ContractSize = 1m,
MarkClosePnl = -500m
});
service.SwapUnwind(unwindData);
Assert.AreEqual(99000m, unwindData.PosiNotionalValue);
Assert.AreEqual(49500m, unwindData.CloseNotionalValue);
Assert.AreEqual(0m, unwindData.SwapRealizedPnL,
"市场价和除权后期初价同为 99 时不应产生额外盯市损益");
Assert.AreEqual(49500d, td.StockEqvNotional, 0.001d,
"应从 EOD 有效名义本金 99000 扣除 49500,不能从旧 trade 值 100000 扣减");
Assert.AreEqual(500d, td.TradeAmount, 0.001d);
}
private static TestableSwapDealService CreateService(
trade td,
swap_position realtime,
eod_swap_position eod,
bool hasCompletedFlow)
{
return new TestableSwapDealService(td)
{
RealtimeFloatPosition = realtime,
LatestFundEodPosition = eod,
HasCompletedFlowAfterLatestFundEod = hasCompletedFlow,
ActiveSwapPositions = new List<swap_position> { realtime }
};
}
private static swap_position CreateRealtimeFundPosition()
{
return new swap_position
{
SwapTradeId = SwapDealTestFactory.SwapTradeId,
PositionId = 101,
IsInitial = false,
PosiDirection = 1,
PositionType = (int)PositionTypeFlag.Long,
UnderlyingCode = "FUND.TEST",
UnderlyingInstrumentType = ConsGlobal.InstrumentType.Fund,
PosiQuantity = 1000m,
PosiGrossPrice = 100m,
PosiNetPrice = 100m,
PosiNetFeePrice = 100m,
PosiNetNoFeePrice = 100m,
PosiNotionalValue = 100000m,
ContractSize = 1m
};
}
private static eod_swap_position CreateEod(DateTime valueDate, decimal quantity, decimal price)
{
return new eod_swap_position
{
SwapTradeId = SwapDealTestFactory.SwapTradeId,
PositionId = 101,
ValueDate = valueDate,
PosiDirection = 1,
PositionType = (int)PositionTypeFlag.Long,
UnderlyingCode = "FUND.TEST",
UnderlyingInstrumentType = ConsGlobal.InstrumentType.Fund,
PosiQuantity = quantity,
PosiGrossPrice = price,
PosiNetPrice = price,
PosiNetFeePrice = price,
PosiNetNoFeePrice = price,
UnderlyingPrice = price,
PosiNotionalValue = quantity * price,
ContractSize = 1m
};
}
private static UnwindData CreateFullCloseUnwindData()
{
var data = SwapDealTestFactory.CreateUnwindData(
swapRealizedPnL: -40000m,
closeMethod: (int)CloseMethodEnum.全部平仓,
closePercent: 1m,
closeQty: 1000m,
closeNotionalValue: 100000m,
positionQty: 1000m);
data.NotionalValue = 100000m;
data.PosiNotionalValue = 100000m;
data.FlowEvents.Add(new swap_flow_event
{
PositionId = 101,
EventType = (int)SwapEventTypeEnum.平仓,
UnderlyingCode = "FUND.TEST",
UnderlyingInstrumentType = ConsGlobal.InstrumentType.Fund,
PositionType = (int)PositionTypeFlag.Long,
PayDirection = 1,
PosiGrossPrice = 100m,
PosiNetPrice = 100m,
TradingAmountAvg = 60m,
Quantity = 1000m,
PositionQty = 0m,
ContractSize = 1m,
MarkClosePnl = -40000m
});
return data;
}
}
}