- 债券付息查询/BondPayment/BondPaymentList这个界面 不再查询公司行为除权表 - 公司行为事件的说明 格式化一下变为:调整前:名义本金:XXXX 期初标的价格:XXXX 持仓数量:XXXX 调整后:名义本金:XXXX 期初标的价格:XXXX 持仓数量:XXXX - 报错信息修改:当某个标的的除权除息数据仍有交易引用时 则修改标的公司行为数据时则提示“不可修改,有交易【交易编号】使用了该条除权除息数据”。
304 lines
12 KiB
C#
304 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
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);
|
|
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, "调整前:名义本金: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 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_PreservesPendingCorporateActionForAudit()
|
|
{
|
|
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 = "{}" }
|
|
};
|
|
|
|
// 操作历史不再隐藏登记日待生效事件;Applied=false 是事件状态,不是展示过滤条件。
|
|
Assert.AreEqual(3, events.Count);
|
|
Assert.IsTrue(SwapEventService.TryDeserializeCorporateActionEventData(events[0], out var pendingSnapshot));
|
|
Assert.IsFalse(pendingSnapshot.Applied);
|
|
Assert.IsTrue(SwapEventService.TryDeserializeCorporateActionEventData(events[1], out var appliedSnapshot));
|
|
Assert.IsTrue(appliedSnapshot.Applied);
|
|
}
|
|
|
|
[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 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
|
|
};
|
|
}
|
|
}
|
|
}
|