feat(bond): 支持债券与股票基金公司行为现金流计算的差异化处理 - init2
- 修改 CalcPayment 方法添加 useBondPriceScale 参数区分债券和股票/基金的金额计算口径 - 债券利息按每100元面值票息通过BondPriceConverter转为入库金额,股票基金分红直接计算 - 在BondPaymentService中添加详细的参数说明文档注释 - 更新SwapDealService中分红计算逻辑,根据标的类型自动选择合适的金额转换方式 - 新增CorporateActionEventLifecycleTest单元测试验证公司行为事件生命周期管理 - 添加SplitCorporateActionTddTest测试验证拆合股功能 - 优化FundCorporateActionRollbackAndUnwindTest扩展到股票类型测试 - 更新前端OperationHistory页面表格列宽和显示格式支持更长的说明信息
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user