feat(trade): 新增真实除权日字段并完善基金公司行为处理 init

- 添加 EffectiveDate 字段用于标识真实除权生效日
- 实现公司行为价格系数和数量系数统一计算方法
- 增加基金除权回退和平仓基线恢复功能
- 完善除权日验证逻辑,确保真实除权日不早于股权登记日
- 重构平仓流程,支持按有效EOD基线重新计算损益和现金
- 添加基金拆合股和现金分红的特殊处理逻辑
- 增加单元测试验证各种公司行为场景下的正确性
This commit is contained in:
张名锐
2026-08-18 11:20:56 +08:00
parent 04f4468be5
commit aa1a13d7a3
11 changed files with 1582 additions and 18 deletions
@@ -57,6 +57,7 @@ namespace YLErp.Modules.SwapModule
protected override List<eod_swap> FindEodSwapsByDate(DateTime valueDate) => _eodSwaps;
protected override List<swap_flow_event> FindFlowEvents(int swapTradeId, DateTime settleDate) => _flowEvents;
protected override List<swap_flow_event> FindCompletedFlowEvents(List<int> tradeIds) => _flowEvents;
public override DateTime? GetPreDealDate(int tradeId, DateTime valueDate, List<int> eventTypes) => null;
protected override List<eod_swap_position> FindEodSwapPositions(int swapTradeId, DateTime preSettleDate)
=> _eodPositions.Where(x => x.SwapTradeId == swapTradeId && x.ValueDate >= preSettleDate).ToList();
protected override List<swap_position> FindSwapPositions(int swapTradeId)
@@ -93,6 +94,20 @@ namespace YLErp.Modules.SwapModule
public void ExecuteSwapPositionCompose(DateTime settleDate, DateTime preSettleDate)
=> SwapPositionCompose(settleDate, preSettleDate, null);
public void ExecuteFundCorporateActions(
IReadOnlyCollection<eod_swap_position> positions,
IReadOnlyCollection<eod_swap_position> previousEodPositions,
IReadOnlyCollection<swap_flow_event> flowEvents,
IReadOnlyCollection<ex_dividend_info> dividendInfos)
{
ApplyFundCorporateActions(
positions,
previousEodPositions,
flowEvents,
dividendInfos.ToDictionary(x => x.UnderlyingCode, StringComparer.OrdinalIgnoreCase),
SettleDate);
}
}
#endregion
@@ -144,7 +159,7 @@ namespace YLErp.Modules.SwapModule
PosiDirection = 1, PositionType = (int)PositionTypeFlag.Long, Invalid = false,
PosiQuantity = qty, PosiGrossPrice = grossPrice, PosiNetPrice = 1.0050m,
PosiNetFeePrice = 1.0030m, PosiNetNoFeePrice = 1.0000m,
UnderlyingCode = "220205.IB", ContractSize = 1m,
UnderlyingCode = "220205.IB", UnderlyingPrice = grossPrice, ContractSize = 1m,
InterestIncomeSum = 0m, InterestProfitSum = 0m, PosiNotionalValue = qty
};
}
@@ -161,6 +176,41 @@ namespace YLErp.Modules.SwapModule
};
}
private static ex_dividend_info CreateFundCorporateAction(
decimal cashAmount = 0m,
decimal shareAmount = 0m)
{
return new ex_dividend_info
{
UnderlyingCode = "FUND.TEST",
ExDividendDate = SettleDate,
EffectiveDate = SettleDate,
GiveCashAmount = cashAmount,
GiveShareAmount = shareAmount,
ValidStatus = true
};
}
private static void SetFundLeg(swap_position position, eod_swap_position previousEod)
{
position.UnderlyingCode = "FUND.TEST";
position.UnderlyingInstrumentType = ConsGlobal.InstrumentType.Fund;
position.PosiGrossPrice = 100m;
position.PosiNetPrice = 102m;
position.PosiNetFeePrice = 104m;
position.PosiNetNoFeePrice = 106m;
previousEod.UnderlyingCode = position.UnderlyingCode;
previousEod.UnderlyingInstrumentType = position.UnderlyingInstrumentType;
previousEod.PosiGrossPrice = position.PosiGrossPrice;
previousEod.PosiNetPrice = position.PosiNetPrice;
previousEod.PosiNetFeePrice = position.PosiNetFeePrice;
previousEod.PosiNetNoFeePrice = position.PosiNetNoFeePrice;
previousEod.PosiNotionalValue = previousEod.PosiGrossPrice
* previousEod.PosiQuantity
* previousEod.ContractSize;
}
#endregion
// ================================================================
@@ -238,6 +288,282 @@ namespace YLErp.Modules.SwapModule
Console.WriteLine($"SPC_003: PosiQuantity={floatEod.PosiQuantity}, TdCloseQty={floatEod.TdCloseQty} ✅");
}
[TestMethod]
public void SPC_FUND_001_送股除权_调整价格数量并重算持仓结果()
{
var td = CreateTrade();
var position = CreateFloatPosition(1, 1000m);
var previousEod = CreateFloatEodPosition(1, 1000m, 100m);
SetFundLeg(position, previousEod);
var service = new TestableSwapEodService(
new List<trade> { td },
new List<swap_position> { position },
new List<eod_swap_position> { previousEod },
new List<eod_swap>(),
new List<trade_extend> { CreateExtend() },
new List<swap_flow_event>(),
price: 100m);
service.ExDividendInfos.Add(CreateFundCorporateAction(shareAmount: 10m));
var actual = previousEod.Clone();
actual.ValueDate = SettleDate;
actual.UnderlyingPrice = 100m;
service.ExecuteFundCorporateActions(
new[] { actual },
new[] { previousEod },
Array.Empty<swap_flow_event>(),
service.ExDividendInfos);
Assert.AreEqual(2000m, actual.PosiQuantity);
Assert.AreEqual(1000m, actual.TdChangedQty);
Assert.AreEqual(50m, actual.PosiGrossPrice);
Assert.AreEqual(51m, actual.PosiNetPrice);
Assert.AreEqual(52m, actual.PosiNetFeePrice);
Assert.AreEqual(53m, actual.PosiNetNoFeePrice);
Assert.AreEqual(100000m, actual.PosiNotionalValue);
Assert.AreEqual(200000m, actual.UnderlyingMarketValue);
Assert.AreEqual(100000m, actual.PosiMtmPnL);
Assert.AreEqual(100000m, actual.PosiProfitSum);
}
[TestMethod]
public void SPC_FUND_002_现金分红_只调整价格不生成分红流水()
{
var td = CreateTrade();
var position = CreateFloatPosition(1, 1000m);
var previousEod = CreateFloatEodPosition(1, 1000m, 100m);
SetFundLeg(position, previousEod);
var service = new TestableSwapEodService(
new List<trade> { td },
new List<swap_position> { position },
new List<eod_swap_position> { previousEod },
new List<eod_swap>(),
new List<trade_extend> { CreateExtend() },
new List<swap_flow_event>(),
price: 100m);
service.ExDividendInfos.Add(CreateFundCorporateAction(cashAmount: 10m));
var actual = previousEod.Clone();
actual.ValueDate = SettleDate;
actual.UnderlyingPrice = 100m;
service.ExecuteFundCorporateActions(
new[] { actual },
new[] { previousEod },
Array.Empty<swap_flow_event>(),
service.ExDividendInfos);
Assert.AreEqual(1000m, actual.PosiQuantity);
Assert.AreEqual(0m, actual.TdChangedQty);
Assert.AreEqual(99m, actual.PosiGrossPrice);
Assert.AreEqual(0m, actual.TdPosiDividend);
Assert.AreEqual(0m, actual.PosiDividendSum);
Assert.AreEqual(99000m, actual.PosiNotionalValue);
}
[TestMethod]
public void SPC_FUND_003_同日重跑_从前日基线重算不重复除权()
{
var td = CreateTrade();
var position = CreateFloatPosition(1, 1000m);
var previousEod = CreateFloatEodPosition(1, 1000m, 100m);
SetFundLeg(position, previousEod);
var service = new TestableSwapEodService(
new List<trade> { td },
new List<swap_position> { position },
new List<eod_swap_position> { previousEod },
new List<eod_swap>(),
new List<trade_extend> { CreateExtend() },
new List<swap_flow_event>(),
price: 100m);
service.ExDividendInfos.Add(CreateFundCorporateAction(shareAmount: 10m));
var todayEod = previousEod.Clone();
todayEod.ValueDate = SettleDate;
todayEod.UnderlyingPrice = 100m;
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);
Assert.AreEqual(2000m, todayEod.PosiQuantity);
Assert.AreEqual(1000m, todayEod.TdChangedQty);
Assert.AreEqual(50m, todayEod.PosiGrossPrice);
Assert.AreEqual(100000m, todayEod.PosiNotionalValue);
}
[TestMethod]
public void SPC_FUND_004_非Fund标的_即使命中公司行为也不调整()
{
var td = CreateTrade();
var position = CreateFloatPosition(1, 1000m);
position.UnderlyingCode = "FUND.TEST";
position.PosiGrossPrice = 100m;
var previousEod = CreateFloatEodPosition(1, 1000m, 100m);
previousEod.UnderlyingCode = position.UnderlyingCode;
previousEod.UnderlyingInstrumentType = "TBonds";
var service = new TestableSwapEodService(
new List<trade> { td },
new List<swap_position> { position },
new List<eod_swap_position> { previousEod },
new List<eod_swap>(),
new List<trade_extend> { CreateExtend() },
new List<swap_flow_event>(),
price: 100m);
service.ExDividendInfos.Add(CreateFundCorporateAction(shareAmount: 10m));
var actual = previousEod.Clone();
actual.ValueDate = SettleDate;
actual.UnderlyingPrice = 100m;
service.ExecuteFundCorporateActions(
new[] { actual },
new[] { previousEod },
Array.Empty<swap_flow_event>(),
service.ExDividendInfos);
Assert.AreEqual(1000m, actual.PosiQuantity);
Assert.AreEqual(100m, actual.PosiGrossPrice);
Assert.AreEqual(0m, actual.TdChangedQty);
}
[TestMethod]
public void SPC_FUND_005_同日同代码多条有效记录_明确失败()
{
var service = new TestableSwapEodService(
new List<trade> { CreateTrade() },
new List<swap_position>(),
new List<eod_swap_position>(),
new List<eod_swap>(),
new List<trade_extend> { CreateExtend() },
new List<swap_flow_event>());
service.ExDividendInfos.Add(CreateFundCorporateAction(cashAmount: 1m));
service.ExDividendInfos.Add(CreateFundCorporateAction(shareAmount: 1m));
var exception = Assert.ThrowsException<InvalidOperationException>(() =>
service.ExecuteSwapPositionCompose(SettleDate, PreSettleDate));
StringAssert.Contains(exception.Message, "存在多条有效除权记录");
}
[TestMethod]
public void SPC_FUND_006_登记日Eod保持除权前数量价格_生效日才调整()
{
var recordDate = SettleDate;
var effectiveDate = recordDate.AddDays(3);
var td = CreateTrade();
var position = CreateFloatPosition(1, 1000m);
var previousEod = CreateFloatEodPosition(1, 1000m, 100m);
SetFundLeg(position, previousEod);
var service = new TestableSwapEodService(
new List<trade> { td },
new List<swap_position> { position },
new List<eod_swap_position> { previousEod },
new List<eod_swap>(),
new List<trade_extend> { CreateExtend() },
new List<swap_flow_event>(),
price: 100m);
service.ExDividendInfos.Add(new ex_dividend_info
{
UnderlyingCode = "FUND.TEST",
ExDividendDate = recordDate,
EffectiveDate = effectiveDate,
GiveShareAmount = 10m,
ValidStatus = true
});
service.ExecuteSwapPositionCompose(recordDate, PreSettleDate);
var recordEod = service.CreatedEodPositions.First(x => x.PositionId == 1);
Assert.AreEqual(1000m, recordEod.PosiQuantity,
"登记日 EOD 仍展示除权前数量,不能提前变成 2000");
Assert.AreEqual(100m, recordEod.PosiGrossPrice,
"登记日 EOD 仍展示除权前价格,不能提前变成 50");
}
[TestMethod]
public void SPC_FUND_007_生效日先以除权后基线处理平仓_1000平300得到1700份50元()
{
var recordDate = SettleDate;
var effectiveDate = recordDate.AddDays(3);
var td = CreateTrade();
var initialPosition = CreateFloatPosition(1, 1000m);
var realtimePosition = initialPosition.Clone();
realtimePosition.id = 2;
realtimePosition.IsInitial = false;
realtimePosition.PositionId = initialPosition.id;
var previousEod = CreateFloatEodPosition(1, 1000m, 100m);
previousEod.ValueDate = recordDate;
SetFundLeg(initialPosition, previousEod);
SetFundLeg(realtimePosition, previousEod);
var closeFlow = CreateCloseFlowEvent(initialPosition.id, 300m);
closeFlow.UnderlyingCode = "FUND.TEST";
closeFlow.UnderlyingInstrumentType = ConsGlobal.InstrumentType.Fund;
closeFlow.DividendIn = 0m;
var service = new TestableSwapEodService(
new List<trade> { td },
new List<swap_position> { initialPosition, realtimePosition },
new List<eod_swap_position> { previousEod },
new List<eod_swap> { new eod_swap { SwapTradeId = SwapTradeId, ValueDate = recordDate } },
new List<trade_extend> { CreateExtend() },
new List<swap_flow_event> { closeFlow },
price: 100m);
service.ExDividendInfos.Add(new ex_dividend_info
{
UnderlyingCode = "FUND.TEST",
ExDividendDate = recordDate,
EffectiveDate = effectiveDate,
GiveShareAmount = 10m,
ValidStatus = true
});
service.ExecuteSwapPositionCompose(effectiveDate, recordDate);
var effectiveEod = service.CreatedEodPositions.First(x => x.PositionId == 1);
Assert.AreEqual(1700m, effectiveEod.PosiQuantity,
"生效日先把 1000 份变为 2000 份,再平仓 300 份,应剩 1700 而非 1400");
Assert.AreEqual(50m, effectiveEod.PosiGrossPrice,
"10 送 10 后期初价格应为 50");
}
[TestMethod]
public void SPC_FUND_008_上游splitratio零点零一映射GiveShareAmount负九点九_Eod数量价格调整()
{
var td = CreateTrade();
var position = CreateFloatPosition(1, 1000m);
var previousEod = CreateFloatEodPosition(1, 1000m, 100m);
SetFundLeg(position, previousEod);
var service = new TestableSwapEodService(
new List<trade> { td },
new List<swap_position> { position },
new List<eod_swap_position> { previousEod },
new List<eod_swap>(),
new List<trade_extend> { CreateExtend() },
new List<swap_flow_event>(),
price: 100m);
// 上游 splitratio=sharesafter/sharesbefore=0.01,落库前按
// GiveShareAmount=10*(splitratio-1) 转换为 -9.9;现有公式因此得到 0.01 倍。
service.ExDividendInfos.Add(CreateFundCorporateAction(shareAmount: -9.9m));
var actual = previousEod.Clone();
actual.ValueDate = SettleDate;
actual.UnderlyingPrice = 100m;
service.ExecuteFundCorporateActions(
new[] { actual },
new[] { previousEod },
Array.Empty<swap_flow_event>(),
service.ExDividendInfos);
Assert.AreEqual(10m, actual.PosiQuantity,
"上游 splitratio=0.01 映射为 GiveShareAmount=-9.91000 份应调整为 10 份");
Assert.AreEqual(10000m, actual.PosiGrossPrice,
"上游 splitratio=0.01 映射为 GiveShareAmount=-9.9,期初价格应反向放大 100 倍");
}
// ================================================================
// 场景4:未收盘抛异常
// ================================================================