feat(bond): 支持债券与股票基金公司行为现金流计算的差异化处理 - init2
- 修改 CalcPayment 方法添加 useBondPriceScale 参数区分债券和股票/基金的金额计算口径 - 债券利息按每100元面值票息通过BondPriceConverter转为入库金额,股票基金分红直接计算 - 在BondPaymentService中添加详细的参数说明文档注释 - 更新SwapDealService中分红计算逻辑,根据标的类型自动选择合适的金额转换方式 - 新增CorporateActionEventLifecycleTest单元测试验证公司行为事件生命周期管理 - 添加SplitCorporateActionTddTest测试验证拆合股功能 - 优化FundCorporateActionRollbackAndUnwindTest扩展到股票类型测试 - 更新前端OperationHistory页面表格列宽和显示格式支持更长的说明信息
This commit is contained in:
@@ -69,7 +69,10 @@ namespace YLErp.Modules.SwapModule
|
||||
public List<(DateTime valueDate, int eventType, string reason, UnwindData data)> SwapEvents { get; } = new();
|
||||
|
||||
/// <summary>捕获落库的互换流水明细</summary>
|
||||
public List<swap_flow_event> PersistedFlowEvents { get; } = new();
|
||||
public List<swap_flow_event> PersistedFlowEvents => DbContext.swap_flow_event.Local.ToList();
|
||||
|
||||
/// <summary>捕获资金流水的金额、操作类型和发生日</summary>
|
||||
public List<(double amount, string action, DateTime valueDate)> ClientCashCallDetails { get; } = new();
|
||||
|
||||
public AutoSwapEodService(
|
||||
List<trade> trades, List<swap_position> positions,
|
||||
@@ -112,6 +115,13 @@ namespace YLErp.Modules.SwapModule
|
||||
protected override void ClearSwapPositionsForCompose(trade td, DateTime tradeDate, List<int> eventTypes) { }
|
||||
public override void ClearSwapPositions(trade td, DateTime valueDate, List<int> eventTypes, bool delAfter) { }
|
||||
|
||||
public override int AddClientCashInCashOut(OtcTradeBase td, double amount, string action, DateTime valueDate)
|
||||
{
|
||||
ClientCashCalls.Add((amount, action));
|
||||
ClientCashCallDetails.Add((amount, action, valueDate));
|
||||
return ClientCashCalls.Count;
|
||||
}
|
||||
|
||||
protected override swap_event AddSwapEvent(DateTime tradeDate, int swapTradeId, int eventType,
|
||||
string data, int clientCashId, bool save, string reason)
|
||||
{
|
||||
@@ -509,6 +519,9 @@ namespace YLErp.Modules.SwapModule
|
||||
$"分红支付日({actualPayDate:yyyy-MM-dd})不应早于结算日({PayDate:yyyy-MM-dd})");
|
||||
Assert.IsFalse(QdpModule.QdpCalendarHelper.IsHoliday(actualPayDate),
|
||||
$"分红支付日({actualPayDate:yyyy-MM-dd})必须落在非假日");
|
||||
Assert.AreEqual(1, svc.ClientCashCallDetails.Count, "应生成 1 条分红资金流水");
|
||||
Assert.AreEqual(actualPayDate, svc.ClientCashCallDetails[0].valueDate,
|
||||
"资金发生日应使用分红支付日");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
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, "BeforeQuantity=1000");
|
||||
StringAssert.Contains(reason, "AfterQuantity=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_FiltersPendingCorporateActionOnly()
|
||||
{
|
||||
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 visible = SwapEventService.FilterOperationHistory(events);
|
||||
|
||||
Assert.AreEqual(2, visible.Count);
|
||||
CollectionAssert.DoesNotContain(visible.Select(x => x.id).ToList(), 1L);
|
||||
CollectionAssert.Contains(visible.Select(x => x.id).ToList(), 2L);
|
||||
CollectionAssert.Contains(visible.Select(x => x.id).ToList(), 3L);
|
||||
}
|
||||
|
||||
[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.ApplyFundCorporateActionToPosition(
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ 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]
|
||||
@@ -51,15 +53,15 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FCA_UW_002_非Fund和最新Eod后已有完成流水时保持实时持仓()
|
||||
public void FCA_UW_002_股票与最新Eod后已有完成流水时保持实时持仓()
|
||||
{
|
||||
var nonFund = CreateRealtimeFundPosition();
|
||||
nonFund.UnderlyingInstrumentType = ConsGlobal.InstrumentType.Stock;
|
||||
var eod = CreateEod(ExDate, 2000m, 50m);
|
||||
|
||||
Assert.IsFalse(SwapEodPositionService.RestoreFundPositionFromEod(nonFund, eod));
|
||||
Assert.AreEqual(1000m, nonFund.PosiQuantity);
|
||||
Assert.AreEqual(100m, nonFund.PosiGrossPrice);
|
||||
Assert.IsTrue(SwapEodPositionService.RestoreFundPositionFromEod(nonFund, eod));
|
||||
Assert.AreEqual(2000m, nonFund.PosiQuantity);
|
||||
Assert.AreEqual(50m, nonFund.PosiGrossPrice);
|
||||
|
||||
var td = SwapDealTestFactory.CreateTrade();
|
||||
var realtime = CreateRealtimeFundPosition();
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
using System.Reflection;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
using YLErp.Modules.TradeModule.DealModule;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
[TestClass]
|
||||
public class SplitCorporateActionTddTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void SplitTenScalesQuantityAndPriceByTen()
|
||||
{
|
||||
var position = CreateFundPosition();
|
||||
var info = CreateCorporateAction(split: 10m);
|
||||
|
||||
Assert.IsTrue(SwapEodPositionService.ApplyFundCorporateActionToPosition(
|
||||
position, info, 100m, 0m));
|
||||
|
||||
Assert.AreEqual(1000m, position.PosiQuantity);
|
||||
Assert.AreEqual(10m, position.PosiGrossPrice);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SplitPointOneScalesQuantityAndPriceByPointOne()
|
||||
{
|
||||
var position = CreateFundPosition();
|
||||
var info = CreateCorporateAction(split: 0.1m);
|
||||
|
||||
Assert.IsTrue(SwapEodPositionService.ApplyFundCorporateActionToPosition(
|
||||
position, info, 100m, 0m));
|
||||
|
||||
Assert.AreEqual(10m, position.PosiQuantity);
|
||||
Assert.AreEqual(1000m, position.PosiGrossPrice);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GiveShareTenWithNullSplitUsesCompatibleFactorTwo()
|
||||
{
|
||||
var position = CreateFundPosition();
|
||||
var info = CreateCorporateAction(giveShare: 10m, split: null);
|
||||
|
||||
Assert.IsTrue(SwapEodPositionService.ApplyFundCorporateActionToPosition(
|
||||
position, info, 100m, 0m));
|
||||
|
||||
Assert.AreEqual(200m, position.PosiQuantity);
|
||||
Assert.AreEqual(50m, position.PosiGrossPrice);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GiveShareFiveAndSplitTwoHaveCombinedFactorThree()
|
||||
{
|
||||
var position = CreateFundPosition();
|
||||
var info = CreateCorporateAction(giveShare: 5m, split: 2m);
|
||||
|
||||
// (1 + 5 / 10) * 2 = 3:100 份/100 元变为 300 份/约 33.333333333 元。
|
||||
Assert.IsTrue(SwapEodPositionService.ApplyFundCorporateActionToPosition(
|
||||
position, info, 100m, 0m));
|
||||
|
||||
Assert.AreEqual(300m, position.PosiQuantity);
|
||||
Assert.IsTrue(Math.Abs(position.PosiGrossPrice - 33.333333333m) < 0.000000001m);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CashAmountDoesNotChangeTrsFundInitialPriceFactor()
|
||||
{
|
||||
var position = CreateFundPosition();
|
||||
var info = CreateCorporateAction(cash: 10m);
|
||||
|
||||
Assert.IsTrue(SwapEodPositionService.ApplyFundCorporateActionToPosition(
|
||||
position, info, 100m, 0m));
|
||||
|
||||
Assert.AreEqual(100m, position.PosiQuantity);
|
||||
Assert.AreEqual(100m, position.PosiGrossPrice);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RationedSharesUseExcelPriceRatioForTrsQuantity()
|
||||
{
|
||||
var position = CreateFundPosition();
|
||||
var info = CreateCorporateAction(
|
||||
rationedSharesAmount: 1m,
|
||||
rationedSharesPrice: 50m);
|
||||
|
||||
Assert.IsTrue(SwapEodPositionService.ApplyFundCorporateActionToPosition(
|
||||
position, info, 100m, 0m));
|
||||
|
||||
// Excel L-N:L=(100*10+1*50)/(10+1)=95.4545...,M=100/L;
|
||||
// 因此数量和价格分别按 Q'=Q*M、P'=P/M 调整。
|
||||
Assert.IsTrue(Math.Abs(position.PosiQuantity - 104.761904761905m) < 0.000000000001m);
|
||||
Assert.IsTrue(Math.Abs(position.PosiGrossPrice - 95.454545455m) < 0.000000001m);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ZeroSplitIsRejected()
|
||||
{
|
||||
var info = CreateCorporateAction(split: 0m);
|
||||
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() =>
|
||||
SwapEodPositionService.ApplyFundCorporateActionToPosition(
|
||||
CreateFundPosition(), info, 100m, 0m));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NegativeSplitIsRejected()
|
||||
{
|
||||
var info = CreateCorporateAction(split: -1m);
|
||||
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() =>
|
||||
SwapEodPositionService.ApplyFundCorporateActionToPosition(
|
||||
CreateFundPosition(), info, 100m, 0m));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MissingSplitDoesNotClearExistingSplitDuringMerge()
|
||||
{
|
||||
var target = CreateCorporateAction(split: 10m);
|
||||
var source = CreateCorporateAction(split: null);
|
||||
|
||||
InvokeMerge(target, source);
|
||||
|
||||
Assert.AreEqual(10m, GetSplit(target));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ExplicitSplitOneOverridesExistingSplitDuringMerge()
|
||||
{
|
||||
var target = CreateCorporateAction(split: 10m);
|
||||
var source = CreateCorporateAction(split: 1m);
|
||||
|
||||
InvokeMerge(target, source);
|
||||
|
||||
Assert.AreEqual(1m, GetSplit(target));
|
||||
}
|
||||
|
||||
private static ex_dividend_info CreateCorporateAction(
|
||||
decimal cash = 0m,
|
||||
decimal giveShare = 0m,
|
||||
decimal? split = null,
|
||||
decimal rationedSharesAmount = 0m,
|
||||
decimal rationedSharesPrice = 0m)
|
||||
{
|
||||
var info = new ex_dividend_info
|
||||
{
|
||||
UnderlyingCode = "FUND.TEST",
|
||||
ExDividendDate = new DateTime(2026, 8, 14),
|
||||
EffectiveDate = new DateTime(2026, 8, 17),
|
||||
GiveCashAmount = cash,
|
||||
GiveShareAmount = giveShare,
|
||||
RationedSharesAmount = rationedSharesAmount,
|
||||
RationedSharesPrice = rationedSharesPrice,
|
||||
ValidStatus = true
|
||||
};
|
||||
SetSplit(info, split);
|
||||
return info;
|
||||
}
|
||||
|
||||
private static swap_position CreateFundPosition()
|
||||
{
|
||||
return new swap_position
|
||||
{
|
||||
PosiDirection = 1,
|
||||
UnderlyingInstrumentType = ConsGlobal.InstrumentType.Fund,
|
||||
UnderlyingCode = "FUND.TEST",
|
||||
PosiQuantity = 100m,
|
||||
PosiGrossPrice = 100m,
|
||||
PosiNetPrice = 100m,
|
||||
PosiNetFeePrice = 100m,
|
||||
PosiNetNoFeePrice = 100m,
|
||||
ContractSize = 1m
|
||||
};
|
||||
}
|
||||
|
||||
private static void SetSplit(ex_dividend_info info, decimal? value)
|
||||
{
|
||||
var property = typeof(ex_dividend_info).GetProperty("Split");
|
||||
Assert.IsNotNull(property, "ex_dividend_info.Split 尚未实现");
|
||||
property.SetValue(info, value);
|
||||
}
|
||||
|
||||
private static decimal? GetSplit(ex_dividend_info info)
|
||||
{
|
||||
var property = typeof(ex_dividend_info).GetProperty("Split");
|
||||
Assert.IsNotNull(property, "ex_dividend_info.Split 尚未实现");
|
||||
return (decimal?)property.GetValue(info);
|
||||
}
|
||||
|
||||
private static void InvokeMerge(ex_dividend_info target, ex_dividend_info source)
|
||||
{
|
||||
var method = typeof(DividendService).GetMethod(
|
||||
"MergeNonZeroDividendValues",
|
||||
BindingFlags.Static | BindingFlags.NonPublic);
|
||||
Assert.IsNotNull(method, "公司行为存量合并方法不存在");
|
||||
method.Invoke(null, new object[] { target, source });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -332,7 +332,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SPC_FUND_002_现金分红_收盘时记入已实现分红()
|
||||
public void SPC_FUND_002_现金分红_登记日不直接入账()
|
||||
{
|
||||
var td = CreateTrade();
|
||||
var position = CreateFloatPosition(1, 1000m);
|
||||
@@ -351,16 +351,18 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
var actual = service.CreatedEodPositions.Single(x => x.PositionId == 1);
|
||||
|
||||
// 现金分红改由同步任务写入 bond_payment_info,并以 EffectiveDate 进入债券付息
|
||||
// 链路;登记日 EOD 不直接读取 ex_dividend_info,因此此处不应提前产生现金。
|
||||
Assert.AreEqual(1000m, actual.PosiQuantity);
|
||||
Assert.AreEqual(0m, actual.TdChangedQty);
|
||||
Assert.AreEqual(99m, actual.PosiGrossPrice);
|
||||
Assert.AreEqual(1000m, actual.TdPosiDividend);
|
||||
Assert.AreEqual(100m, actual.PosiGrossPrice);
|
||||
Assert.AreEqual(0m, actual.TdPosiDividend);
|
||||
Assert.AreEqual(0m, actual.PosiDividendSum);
|
||||
Assert.AreEqual(99000m, actual.PosiNotionalValue);
|
||||
Assert.AreEqual(100000m, actual.PosiNotionalValue);
|
||||
Assert.AreEqual(0m, actual.PosiMtmPnL);
|
||||
Assert.AreEqual(0m, actual.PosiProfitSum);
|
||||
Assert.AreEqual(1000m, actual.RealizedDividend);
|
||||
Assert.AreEqual(1000m, actual.RealizedPnl);
|
||||
Assert.AreEqual(0m, actual.RealizedDividend);
|
||||
Assert.AreEqual(0m, actual.RealizedPnl);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
||||
@@ -45,6 +45,9 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <summary>SwapPositionCompose 使用的公司行为内存数据;默认空,避免测试访问数据库。</summary>
|
||||
public List<ex_dividend_info> ExDividendInfos { get; } = new();
|
||||
|
||||
/// <summary>捕获公司行为生命周期事件,避免事件测试访问真实 swap_event 表。</summary>
|
||||
public List<swap_event> CorporateActionEvents { get; } = new();
|
||||
|
||||
/// <summary>自增 id 模拟器(新增 eod 时分配 id)</summary>
|
||||
private int _nextId = 1;
|
||||
|
||||
@@ -90,6 +93,32 @@ namespace YLErp.Modules.SwapModule
|
||||
.ToList();
|
||||
}
|
||||
|
||||
protected override List<ex_dividend_info> FindCorporateActionInfos(DateTime settleDate)
|
||||
{
|
||||
return ExDividendInfos
|
||||
.Where(x => x.ValidStatus
|
||||
&& (x.ExDividendDate?.Date == settleDate.Date
|
||||
|| x.EffectiveDate?.Date == settleDate.Date))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
protected override List<ex_dividend_info> FindRegistrationExDividendInfos(DateTime settleDate)
|
||||
{
|
||||
return ExDividendInfos
|
||||
.Where(x => x.ValidStatus
|
||||
&& x.ExDividendDate.HasValue
|
||||
&& x.ExDividendDate.Value.Date == settleDate.Date)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
protected override List<swap_event> FindCorporateActionEvents(int swapTradeId)
|
||||
{
|
||||
return CorporateActionEvents
|
||||
.Where(x => x.SwapTradeId == swapTradeId && !x.Invalid
|
||||
&& x.EventType == (int)SwapEventTypeEnum.公司行为)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
protected override decimal GetFundCorporateActionClosePrice(
|
||||
ex_dividend_info dividendInfo,
|
||||
decimal fallbackPrice)
|
||||
|
||||
Reference in New Issue
Block a user