feat(bond): 支持股票和基金现金分红纳入债券支付计算 - 收紧过度开发行为
- 实现股票/基金现金分红数据从 ex_dividend_info 同步到 BondPayment - 新增公司行为去重机制,避免镜像任务完成后重复计息 - 统一现金分红存储口径为"每 10 份派现金额",保持与同步任务一致性 - 修改 CalcPayment 方法,股票/基金分红需除以 10 转换实际现金金额 - 添加单元测试验证债券票息和股票/基金分红的不同计算方式 - 更新文档注释说明"每 10 份派现金额"存储规范 - 修复公司行为生效日处理逻辑,确保正确应用除权系数 - 扩展测试覆盖股票类证券的公司行为处理场景
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
using YLErp.DBModels;
|
||||
namespace YLErp.Modules.EodModule
|
||||
{
|
||||
[TestClass]
|
||||
public class BondPaymentServiceCalculationTest
|
||||
{
|
||||
private static BondPaymentService CreateService()
|
||||
{
|
||||
return new BondPaymentService(
|
||||
new OptUserInfo(0, nameof(BondPaymentServiceCalculationTest), OptUserFrom.UnitTest));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalcPayment_BondCoupon_KeepsPerHundredScale()
|
||||
{
|
||||
var payments = new List<BondPayment>
|
||||
{
|
||||
new BondPayment { payment_interest = 1m }
|
||||
};
|
||||
|
||||
// 债券票息 1 表示每 100 元面值付 1 元:1 * 1000 / 100 = 10。
|
||||
var actual = CreateService().CalcPayment(
|
||||
payments,
|
||||
qty: 1000m,
|
||||
longRatio: 1m,
|
||||
payDirection: 1m);
|
||||
|
||||
Assert.AreEqual(10m, actual);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalcPayment_StockOrFundDividend_DoesNotApplyBondScale()
|
||||
{
|
||||
var payments = new List<BondPayment>
|
||||
{
|
||||
new BondPayment { payment_interest = 10m }
|
||||
};
|
||||
|
||||
// GiveCashAmount=10(每 10 份派 10)时,payment_interest 直接存 10;
|
||||
// 持仓 1000 份的现金分红 = 10 * 1000 / 10 = 1000,不能再套债券报价的 /100 换算。
|
||||
var actual = CreateService().CalcPayment(
|
||||
payments,
|
||||
qty: 1000m,
|
||||
longRatio: 1m,
|
||||
payDirection: 1m,
|
||||
useBondPriceScale: false);
|
||||
|
||||
Assert.AreEqual(1000m, actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,7 +113,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void OperationHistory_FiltersPendingCorporateActionOnly()
|
||||
public void OperationHistory_PreservesPendingCorporateActionForAudit()
|
||||
{
|
||||
var info = CreateAction(80, ConsGlobal.InstrumentType.Stock);
|
||||
var pendingData = SwapEodPositionService.BuildCorporateActionEventData(
|
||||
@@ -133,12 +133,12 @@ namespace YLErp.Modules.SwapModule
|
||||
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);
|
||||
// 操作历史不再隐藏登记日待生效事件;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]
|
||||
@@ -158,7 +158,7 @@ namespace YLErp.Modules.SwapModule
|
||||
var info = CreateAction(81, ConsGlobal.InstrumentType.Stock);
|
||||
info.GiveShareAmount = 10m;
|
||||
|
||||
var applied = SwapEodPositionService.ApplyFundCorporateActionToPosition(
|
||||
var applied = SwapEodPositionService.ApplyCorporateActionToPosition(
|
||||
position,
|
||||
info,
|
||||
100m,
|
||||
|
||||
@@ -77,6 +77,38 @@ namespace YLErp.Modules.SwapModule
|
||||
"未恢复基线时不得擅自改写前端请求,沿用既有当日实时流程");
|
||||
}
|
||||
|
||||
[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后再套除权()
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace YLErp.Modules.SwapModule
|
||||
var position = CreateFundPosition();
|
||||
var info = CreateCorporateAction(split: 10m);
|
||||
|
||||
Assert.IsTrue(SwapEodPositionService.ApplyFundCorporateActionToPosition(
|
||||
Assert.IsTrue(SwapEodPositionService.ApplyCorporateActionToPosition(
|
||||
position, info, 100m, 0m));
|
||||
|
||||
Assert.AreEqual(1000m, position.PosiQuantity);
|
||||
@@ -27,7 +27,7 @@ namespace YLErp.Modules.SwapModule
|
||||
var position = CreateFundPosition();
|
||||
var info = CreateCorporateAction(split: 0.1m);
|
||||
|
||||
Assert.IsTrue(SwapEodPositionService.ApplyFundCorporateActionToPosition(
|
||||
Assert.IsTrue(SwapEodPositionService.ApplyCorporateActionToPosition(
|
||||
position, info, 100m, 0m));
|
||||
|
||||
Assert.AreEqual(10m, position.PosiQuantity);
|
||||
@@ -40,7 +40,7 @@ namespace YLErp.Modules.SwapModule
|
||||
var position = CreateFundPosition();
|
||||
var info = CreateCorporateAction(giveShare: 10m, split: null);
|
||||
|
||||
Assert.IsTrue(SwapEodPositionService.ApplyFundCorporateActionToPosition(
|
||||
Assert.IsTrue(SwapEodPositionService.ApplyCorporateActionToPosition(
|
||||
position, info, 100m, 0m));
|
||||
|
||||
Assert.AreEqual(200m, position.PosiQuantity);
|
||||
@@ -54,7 +54,7 @@ namespace YLErp.Modules.SwapModule
|
||||
var info = CreateCorporateAction(giveShare: 5m, split: 2m);
|
||||
|
||||
// (1 + 5 / 10) * 2 = 3:100 份/100 元变为 300 份/约 33.333333333 元。
|
||||
Assert.IsTrue(SwapEodPositionService.ApplyFundCorporateActionToPosition(
|
||||
Assert.IsTrue(SwapEodPositionService.ApplyCorporateActionToPosition(
|
||||
position, info, 100m, 0m));
|
||||
|
||||
Assert.AreEqual(300m, position.PosiQuantity);
|
||||
@@ -67,7 +67,7 @@ namespace YLErp.Modules.SwapModule
|
||||
var position = CreateFundPosition();
|
||||
var info = CreateCorporateAction(cash: 10m);
|
||||
|
||||
Assert.IsTrue(SwapEodPositionService.ApplyFundCorporateActionToPosition(
|
||||
Assert.IsTrue(SwapEodPositionService.ApplyCorporateActionToPosition(
|
||||
position, info, 100m, 0m));
|
||||
|
||||
Assert.AreEqual(100m, position.PosiQuantity);
|
||||
@@ -82,7 +82,7 @@ namespace YLErp.Modules.SwapModule
|
||||
rationedSharesAmount: 1m,
|
||||
rationedSharesPrice: 50m);
|
||||
|
||||
Assert.IsTrue(SwapEodPositionService.ApplyFundCorporateActionToPosition(
|
||||
Assert.IsTrue(SwapEodPositionService.ApplyCorporateActionToPosition(
|
||||
position, info, 100m, 0m));
|
||||
|
||||
// Excel L-N:L=(100*10+1*50)/(10+1)=95.4545...,M=100/L;
|
||||
@@ -97,7 +97,7 @@ namespace YLErp.Modules.SwapModule
|
||||
var info = CreateCorporateAction(split: 0m);
|
||||
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() =>
|
||||
SwapEodPositionService.ApplyFundCorporateActionToPosition(
|
||||
SwapEodPositionService.ApplyCorporateActionToPosition(
|
||||
CreateFundPosition(), info, 100m, 0m));
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ namespace YLErp.Modules.SwapModule
|
||||
var info = CreateCorporateAction(split: -1m);
|
||||
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() =>
|
||||
SwapEodPositionService.ApplyFundCorporateActionToPosition(
|
||||
SwapEodPositionService.ApplyCorporateActionToPosition(
|
||||
CreateFundPosition(), info, 100m, 0m));
|
||||
}
|
||||
|
||||
|
||||
@@ -97,19 +97,10 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
public void ExecuteFundCorporateActions(
|
||||
IReadOnlyCollection<eod_swap_position> positions,
|
||||
IReadOnlyCollection<eod_swap_position> previousEodPositions,
|
||||
IReadOnlyCollection<swap_flow_event> flowEvents,
|
||||
IReadOnlyCollection<ex_dividend_info> dividendInfos)
|
||||
{
|
||||
ApplyFundCorporateActions(
|
||||
ApplyCorporateActions(
|
||||
positions,
|
||||
previousEodPositions,
|
||||
flowEvents,
|
||||
dividendInfos.ToDictionary(x => x.UnderlyingCode, StringComparer.OrdinalIgnoreCase),
|
||||
SettleDate);
|
||||
ApplyFundCashDividends(
|
||||
positions,
|
||||
previousEodPositions,
|
||||
dividendInfos.ToDictionary(x => x.UnderlyingCode, StringComparer.OrdinalIgnoreCase),
|
||||
SettleDate);
|
||||
}
|
||||
@@ -315,8 +306,6 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
service.ExecuteFundCorporateActions(
|
||||
new[] { actual },
|
||||
new[] { previousEod },
|
||||
Array.Empty<swap_flow_event>(),
|
||||
service.ExDividendInfos);
|
||||
|
||||
Assert.AreEqual(2000m, actual.PosiQuantity);
|
||||
@@ -381,25 +370,24 @@ namespace YLErp.Modules.SwapModule
|
||||
new List<swap_flow_event>(),
|
||||
price: 100m);
|
||||
service.ExDividendInfos.Add(CreateFundCorporateAction(shareAmount: 10m));
|
||||
var todayEod = previousEod.Clone();
|
||||
todayEod.ValueDate = SettleDate;
|
||||
todayEod.UnderlyingPrice = 100m;
|
||||
// 生产重收盘每次都会从上一日 EOD clone 出新的当日基线,再应用一次公司行为;
|
||||
// 底层 ApplyCorporateActions 只负责处理调用方提供的未调整基线,不再承担恢复旧基线的测试兼容职责。
|
||||
var firstRunEod = previousEod.Clone();
|
||||
firstRunEod.ValueDate = SettleDate;
|
||||
firstRunEod.UnderlyingPrice = 100m;
|
||||
service.ExecuteFundCorporateActions(new[] { firstRunEod }, service.ExDividendInfos);
|
||||
|
||||
service.ExecuteFundCorporateActions(
|
||||
new[] { todayEod },
|
||||
new[] { previousEod },
|
||||
Array.Empty<swap_flow_event>(),
|
||||
service.ExDividendInfos);
|
||||
service.ExecuteFundCorporateActions(
|
||||
new[] { todayEod },
|
||||
new[] { previousEod },
|
||||
Array.Empty<swap_flow_event>(),
|
||||
service.ExDividendInfos);
|
||||
var rerunEod = previousEod.Clone();
|
||||
rerunEod.ValueDate = SettleDate;
|
||||
rerunEod.UnderlyingPrice = 100m;
|
||||
service.ExecuteFundCorporateActions(new[] { rerunEod }, service.ExDividendInfos);
|
||||
|
||||
Assert.AreEqual(2000m, todayEod.PosiQuantity);
|
||||
Assert.AreEqual(1000m, todayEod.TdChangedQty);
|
||||
Assert.AreEqual(50m, todayEod.PosiGrossPrice);
|
||||
Assert.AreEqual(100000m, todayEod.PosiNotionalValue);
|
||||
Assert.AreEqual(2000m, firstRunEod.PosiQuantity);
|
||||
Assert.AreEqual(1000m, firstRunEod.TdChangedQty);
|
||||
Assert.AreEqual(50m, firstRunEod.PosiGrossPrice);
|
||||
Assert.AreEqual(100000m, firstRunEod.PosiNotionalValue);
|
||||
Assert.AreEqual(firstRunEod.PosiQuantity, rerunEod.PosiQuantity);
|
||||
Assert.AreEqual(firstRunEod.PosiGrossPrice, rerunEod.PosiGrossPrice);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -427,8 +415,6 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
service.ExecuteFundCorporateActions(
|
||||
new[] { actual },
|
||||
new[] { previousEod },
|
||||
Array.Empty<swap_flow_event>(),
|
||||
service.ExDividendInfos);
|
||||
|
||||
Assert.AreEqual(1000m, actual.PosiQuantity);
|
||||
@@ -559,8 +545,6 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
service.ExecuteFundCorporateActions(
|
||||
new[] { actual },
|
||||
new[] { previousEod },
|
||||
Array.Empty<swap_flow_event>(),
|
||||
service.ExDividendInfos);
|
||||
|
||||
Assert.AreEqual(10m, actual.PosiQuantity,
|
||||
|
||||
@@ -65,8 +65,6 @@ namespace YLErp.Modules.SwapModule
|
||||
decimal fallbackPrice)
|
||||
=> fallbackPrice;
|
||||
|
||||
protected override decimal GetFundDividendTaxRate() => 0m;
|
||||
|
||||
public bool RestoreEffectiveFundPositionForTest(UnwindData unwindData, DateTime valueDate)
|
||||
=> TryRestoreEffectiveFundPosition(unwindData, valueDate);
|
||||
|
||||
|
||||
@@ -84,15 +84,6 @@ namespace YLErp.Modules.SwapModule
|
||||
return 1.0; // 本币,汇率=1
|
||||
}
|
||||
|
||||
protected override List<ex_dividend_info> FindExDividendInfos(DateTime settleDate)
|
||||
{
|
||||
return ExDividendInfos
|
||||
.Where(x => x.ValidStatus
|
||||
&& x.EffectiveDate.HasValue
|
||||
&& x.EffectiveDate.Value.Date == settleDate.Date)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
protected override List<ex_dividend_info> FindCorporateActionInfos(DateTime settleDate)
|
||||
{
|
||||
return ExDividendInfos
|
||||
@@ -102,15 +93,6 @@ namespace YLErp.Modules.SwapModule
|
||||
.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
|
||||
@@ -124,11 +106,6 @@ namespace YLErp.Modules.SwapModule
|
||||
decimal fallbackPrice)
|
||||
=> fallbackPrice;
|
||||
|
||||
protected override decimal GetDividendTaxRate()
|
||||
{
|
||||
return 0m;
|
||||
}
|
||||
|
||||
protected override int AddClientCash(trade td, double amount, string action, DateTime valueDate)
|
||||
{
|
||||
ClientCashCalls.Add((amount, action));
|
||||
|
||||
Reference in New Issue
Block a user