Files
zszq-trs/UnitTestProject/Modules/SwapModule/CorporateActionEventLifecycleTest.cs
T
张名锐 aa87d69999 fix(swap): 修复公司行为事件生命周期管理问题
- 添加了股权登记日日期格式化显示功能
- 实现了公司行为类型的动态描述逻辑(配股、送股、拆分、分红)
- 新增了过滤待生效公司行为事件的操作历史机制
- 修正了操作历史中隐藏未应用公司行为事件的逻辑
- 完善了公司行为事件原因描述的构建方法
- 添加了针对不同公司行为类型的单元测试验证
2026-08-25 17:58:51 +08:00

334 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YLErp.DBModels;
using YLErp.DBModels.Consts;
namespace YLErp.Modules.SwapModule
{
[TestClass]
public class CorporateActionEventLifecycleTest
{
// 8/14 登记日只创建 Applied=false 的待生效事件;8/17 真实生效日补齐
// 同一事件的调整前后快照并标记 Applied=true。
private static readonly DateTime RecordDate = new DateTime(2026, 8, 14);
private static readonly DateTime EffectiveDate = new DateTime(2026, 8, 17);
[TestMethod]
public void RegistrationSnapshot_IsPending_AndKeepsBeforeFields()
{
var info = CreateAction(77, ConsGlobal.InstrumentType.Stock);
info.GiveShareAmount = 1m;
var before = CreateEodPosition(9, info.UnderlyingCode, 1000m, 100m);
var snapshot = SwapEodPositionService.BuildCorporateActionEventData(
info,
before,
null,
applied: false);
Assert.AreEqual(77, snapshot.ExDividendInfoId);
Assert.AreEqual(9L, snapshot.PositionId);
Assert.AreEqual(1000m, snapshot.BeforeQuantity);
Assert.AreEqual(100m, snapshot.BeforePrice);
Assert.AreEqual(100000m, snapshot.BeforeNotional);
Assert.AreEqual(0m, snapshot.AfterQuantity);
Assert.IsFalse(snapshot.Applied);
var reason = SwapEventService.BuildCorporateActionEventReason(snapshot);
StringAssert.Contains(reason, "股权登记日:2026-08-14 发生公司行为(送股)");
StringAssert.Contains(reason, "调整前:名义本金:100000 期初标的价格:100 持仓数量:1000");
StringAssert.Contains(reason, "调整后:名义本金:0 期初标的价格:0 持仓数量:0");
}
[TestMethod]
public void EffectiveSnapshot_ContainsAfterFields_AndSupportsStockAndFund()
{
var info = CreateAction(78, ConsGlobal.InstrumentType.Fund);
var before = CreateEodPosition(10, info.UnderlyingCode, 1000m, 100m);
var after = CreateEodPosition(10, info.UnderlyingCode, 2000m, 50m);
var snapshot = SwapEodPositionService.BuildCorporateActionEventData(
info,
before,
after,
applied: true);
Assert.AreEqual(1000m, snapshot.BeforeQuantity);
Assert.AreEqual(100m, snapshot.BeforePrice);
Assert.AreEqual(2000m, snapshot.AfterQuantity);
Assert.AreEqual(50m, snapshot.AfterPrice);
Assert.AreEqual(100000m, snapshot.AfterNotional);
Assert.IsTrue(snapshot.Applied);
Assert.IsTrue(SwapEodPositionService.IsCorporateActionInstrument(ConsGlobal.InstrumentType.Stock));
Assert.IsTrue(SwapEodPositionService.IsCorporateActionInstrument(ConsGlobal.InstrumentType.Fund));
Assert.IsFalse(SwapEodPositionService.IsCorporateActionInstrument(ConsGlobal.InstrumentType.TBonds));
}
[TestMethod]
public void CorporateActionReason_ShowsOneActionTypeOrActualCashDividend()
{
AssertActionDescription(
new CorporateActionEventData { ExDividendDate = RecordDate, RationedSharesAmount = 1m, GiveShareAmount = 1m, Split = 2m, GiveCashAmount = 10m, CashFlowChange = 1000m },
"发生公司行为(配股)");
AssertActionDescription(
new CorporateActionEventData { ExDividendDate = RecordDate, GiveShareAmount = 1m, Split = 2m, GiveCashAmount = 10m, CashFlowChange = 1000m },
"发生公司行为(送股)");
AssertActionDescription(
new CorporateActionEventData { ExDividendDate = RecordDate, Split = 2m, GiveCashAmount = 10m, CashFlowChange = 1000m },
"发生公司行为(拆分)");
AssertActionDescription(
new CorporateActionEventData { ExDividendDate = RecordDate, GiveCashAmount = 11m, CashFlowChange = 220000m },
"发生公司行为(产生分红:220000)");
}
[TestMethod]
public void Rerun_DoesNotCreateDuplicateCorporateActionEvent()
{
var info = CreateAction(79, ConsGlobal.InstrumentType.Stock);
var snapshot = SwapEodPositionService.BuildCorporateActionEventData(
info,
CreateEodPosition(11, info.UnderlyingCode, 1000m, 100m),
null,
applied: false);
var existing = new swap_event
{
SwapTradeId = 100,
EventType = (int)SwapEventTypeEnum.公司行为,
EventData = JsonConvert.SerializeObject(snapshot),
Invalid = false
};
Assert.IsFalse(SwapEodPositionService.ShouldCreateCorporateActionEvent(
new[] { existing },
info,
11L));
}
[TestMethod]
public void LegacyEventWithoutExDividendInfoId_DoesNotBlockCurrentEvent()
{
var info = CreateAction(79, ConsGlobal.InstrumentType.Stock);
var legacySnapshot = SwapEodPositionService.BuildCorporateActionEventData(
info,
CreateEodPosition(11, info.UnderlyingCode, 1000m, 100m),
null,
applied: false);
legacySnapshot.ExDividendInfoId = 0;
var legacyEvent = new swap_event
{
SwapTradeId = 100,
EventType = (int)SwapEventTypeEnum.公司行为,
EventData = JsonConvert.SerializeObject(legacySnapshot),
Invalid = false
};
Assert.IsTrue(SwapEodPositionService.ShouldCreateCorporateActionEvent(
new[] { legacyEvent },
info,
11L));
}
[TestMethod]
public void OperationHistory_HidesPendingCorporateActionUntilItIsApplied()
{
var info = CreateAction(80, ConsGlobal.InstrumentType.Stock);
var pendingData = SwapEodPositionService.BuildCorporateActionEventData(
info,
CreateEodPosition(12, info.UnderlyingCode, 1000m, 100m),
null,
applied: false);
var appliedData = SwapEodPositionService.BuildCorporateActionEventData(
info,
CreateEodPosition(13, info.UnderlyingCode, 1000m, 100m),
CreateEodPosition(13, info.UnderlyingCode, 2000m, 50m),
applied: true);
var events = new List<swap_event>
{
new swap_event { id = 1, EventType = (int)SwapEventTypeEnum.公司行为, EventData = JsonConvert.SerializeObject(pendingData) },
new swap_event { id = 2, EventType = (int)SwapEventTypeEnum.公司行为, EventData = JsonConvert.SerializeObject(appliedData) },
new swap_event { id = 3, EventType = (int)SwapEventTypeEnum.互换, EventData = "{}" }
};
var filter = typeof(SwapEventService).GetMethod(
"FilterOperationHistory",
BindingFlags.NonPublic | BindingFlags.Static);
Assert.IsNotNull(filter, "操作历史必须过滤登记日创建的待生效公司行为事件。");
var visibleEvents = (List<swap_event>)filter.Invoke(null, new object[] { events });
Assert.AreEqual(2, visibleEvents.Count);
Assert.IsFalse(visibleEvents.Any(x => x.id == 1));
Assert.IsTrue(visibleEvents.Any(x => x.id == 2));
Assert.IsTrue(visibleEvents.Any(x => x.id == 3));
}
[TestMethod]
public void EffectiveCorporateAction_AdjustsStockQuantityAndPrice()
{
var position = new swap_position
{
PositionId = 14,
PosiDirection = 1,
UnderlyingInstrumentType = ConsGlobal.InstrumentType.Stock,
UnderlyingCode = "STOCK.TEST",
PosiQuantity = 1000m,
PosiGrossPrice = 100m,
PosiNetPrice = 100m,
ContractSize = 1m
};
var info = CreateAction(81, ConsGlobal.InstrumentType.Stock);
info.GiveShareAmount = 10m;
var applied = SwapEodPositionService.ApplyCorporateActionToPosition(
position,
info,
100m,
0m);
Assert.IsTrue(applied);
Assert.AreEqual(2000m, position.PosiQuantity);
Assert.AreEqual(50m, position.PosiGrossPrice);
Assert.AreEqual(100000m, position.PosiNotionalValue);
}
[TestMethod]
public void Lifecycle_RegistrationIsIdempotent_ThenEffectiveUpdatesSameEvent()
{
var info = CreateAction(82, ConsGlobal.InstrumentType.Stock);
var before = CreateEodPosition(15, info.UnderlyingCode, 1000m, 100m);
var after = CreateEodPosition(15, info.UnderlyingCode, 2000m, 50m);
var service = new EventRecordingService();
var trade = new trade { id = 100 };
service.Record(
trade,
new[] { before },
Array.Empty<eod_swap_position>(),
new[] { info },
Array.Empty<ex_dividend_info>(),
RecordDate);
service.Record(
trade,
new[] { before },
Array.Empty<eod_swap_position>(),
new[] { info },
Array.Empty<ex_dividend_info>(),
RecordDate);
Assert.AreEqual(1, service.Events.Count);
Assert.AreEqual(1000m, before.PosiQuantity, "登记日不能改持仓数量");
Assert.AreEqual(100m, before.PosiGrossPrice, "登记日不能改持仓价格");
var pending = JsonConvert.DeserializeObject<CorporateActionEventData>(service.Events[0].EventData);
Assert.IsFalse(pending.Applied);
Assert.AreEqual(RecordDate, service.Events[0].ValueDate.Date);
service.Record(
trade,
new[] { after },
new[] { before },
Array.Empty<ex_dividend_info>(),
new[] { info },
EffectiveDate);
Assert.AreEqual(1, service.Events.Count, "生效日应更新原事件而非新增事件");
Assert.AreEqual(1, service.UpdateCount);
var applied = JsonConvert.DeserializeObject<CorporateActionEventData>(service.Events[0].EventData);
Assert.IsTrue(applied.Applied);
Assert.AreEqual(1000m, applied.BeforeQuantity);
Assert.AreEqual(2000m, applied.AfterQuantity);
Assert.AreEqual(50m, applied.AfterPrice);
Assert.AreEqual(RecordDate, service.Events[0].ValueDate.Date);
}
private sealed class EventRecordingService : TestableSwapEodPositionService
{
public List<swap_event> Events { get; } = new List<swap_event>();
public int UpdateCount { get; private set; }
public EventRecordingService()
: base(nameof(CorporateActionEventLifecycleTest))
{
}
protected override List<swap_event> FindCorporateActionEvents(int swapTradeId)
{
return Events;
}
protected override swap_event AddSwapEvent(
DateTime tradeDate,
int swapTradeId,
int eventType,
string data,
int clientCashId,
bool save,
string reason)
{
return new swap_event { id = Events.Count + 1 };
}
protected override void UpdateCorporateActionEventRecord(swap_event swapEvent)
{
UpdateCount++;
}
public void Record(
trade trade,
IReadOnlyCollection<eod_swap_position> current,
IReadOnlyCollection<eod_swap_position> previous,
IReadOnlyCollection<ex_dividend_info> registration,
IReadOnlyCollection<ex_dividend_info> effective,
DateTime settleDate)
{
RecordCorporateActionEvents(
trade,
current,
previous,
registration,
effective,
settleDate);
}
}
private static ex_dividend_info CreateAction(int id, string instrumentType)
{
return new ex_dividend_info
{
id = id,
UnderlyingCode = instrumentType == ConsGlobal.InstrumentType.Fund ? "FUND.TEST" : "STOCK.TEST",
ExDividendDate = RecordDate,
EffectiveDate = EffectiveDate,
GiveShareAmount = 0m,
GiveCashAmount = 0m,
ValidStatus = true
};
}
private static void AssertActionDescription(CorporateActionEventData data, string expected)
{
StringAssert.Contains(SwapEventService.BuildCorporateActionEventReason(data), expected);
}
private static eod_swap_position CreateEodPosition(long positionId, string code, decimal quantity, decimal price)
{
return new eod_swap_position
{
PositionId = positionId,
UnderlyingCode = code,
UnderlyingInstrumentType = ConsGlobal.InstrumentType.Stock,
PosiQuantity = quantity,
PosiGrossPrice = price,
PosiNotionalValue = quantity * price,
PosiNetPrice = price,
ContractSize = 1m,
PosiDirection = 1,
PositionType = 1
};
}
}
}