feat(trade): 新增真实除权日字段并完善基金公司行为处理 init
- 添加 EffectiveDate 字段用于标识真实除权生效日 - 实现公司行为价格系数和数量系数统一计算方法 - 增加基金除权回退和平仓基线恢复功能 - 完善除权日验证逻辑,确保真实除权日不早于股权登记日 - 重构平仓流程,支持按有效EOD基线重新计算损益和现金 - 添加基金拆合股和现金分红的特殊处理逻辑 - 增加单元测试验证各种公司行为场景下的正确性
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
[TestClass]
|
||||
public class FundCorporateActionRollbackAndUnwindTest
|
||||
{
|
||||
private static readonly DateTime ExDate = new(2026, 8, 17);
|
||||
|
||||
[TestMethod]
|
||||
public void FCA_RB_001_回退选择最近实际Eod并遵守除权日边界()
|
||||
{
|
||||
var friday = CreateEod(new DateTime(2026, 8, 14), 1000m, 100m);
|
||||
var exDate = CreateEod(ExDate, 2000m, 50m);
|
||||
var invalidSunday = CreateEod(new DateTime(2026, 8, 16), 9999m, 1m);
|
||||
invalidSunday.Invalid = true;
|
||||
var snapshots = new[] { friday, invalidSunday, exDate };
|
||||
|
||||
var rollbackToExDate = SwapEodPositionService.SelectLatestEodPositionsBefore(
|
||||
snapshots,
|
||||
ExDate);
|
||||
var rollbackAfterExDate = SwapEodPositionService.SelectLatestEodPositionsBefore(
|
||||
snapshots,
|
||||
ExDate.AddDays(1));
|
||||
|
||||
Assert.AreEqual(friday.ValueDate, rollbackToExDate.Single().ValueDate,
|
||||
"回退到除权日应恢复除权前最近实际 EOD,不能用周日自然日或除权日自身");
|
||||
Assert.AreEqual(1000m, rollbackToExDate.Single().PosiQuantity);
|
||||
Assert.AreEqual(exDate.ValueDate, rollbackAfterExDate.Single().ValueDate,
|
||||
"回退到除权日之后应保留已经生效的除权 EOD");
|
||||
Assert.AreEqual(2000m, rollbackAfterExDate.Single().PosiQuantity);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FCA_UW_001_最近FundEod恢复价格数量且重复恢复不重复除权()
|
||||
{
|
||||
var realtime = CreateRealtimeFundPosition();
|
||||
var eod = CreateEod(ExDate, 2000m, 50m);
|
||||
|
||||
Assert.IsTrue(SwapEodPositionService.RestoreFundPositionFromEod(realtime, eod));
|
||||
Assert.AreEqual(2000m, realtime.PosiQuantity);
|
||||
Assert.AreEqual(50m, realtime.PosiGrossPrice);
|
||||
Assert.AreEqual(100000m, realtime.PosiNotionalValue);
|
||||
|
||||
Assert.IsTrue(SwapEodPositionService.RestoreFundPositionFromEod(realtime, eod));
|
||||
Assert.AreEqual(2000m, realtime.PosiQuantity,
|
||||
"恢复 EOD 是复制快照,不是再次套 10 送 10 系数,不能变成 4000");
|
||||
Assert.AreEqual(50m, realtime.PosiGrossPrice,
|
||||
"重复恢复不能把价格再次调整为 25");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FCA_UW_002_非Fund和最新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);
|
||||
|
||||
var td = SwapDealTestFactory.CreateTrade();
|
||||
var realtime = CreateRealtimeFundPosition();
|
||||
realtime.PosiQuantity = 1500m;
|
||||
realtime.PosiGrossPrice = 50m;
|
||||
var service = CreateService(td, realtime, eod, hasCompletedFlow: true);
|
||||
var unwindData = CreateFullCloseUnwindData();
|
||||
|
||||
Assert.IsFalse(service.RestoreEffectiveFundPositionForTest(unwindData, ExDate.AddDays(1)));
|
||||
Assert.AreEqual(1500m, realtime.PosiQuantity,
|
||||
"EOD 后已有部分平仓流水时不能用 2000 份 EOD 覆盖实时剩余 1500 份");
|
||||
Assert.AreEqual(1000m, unwindData.CloseQty,
|
||||
"未恢复基线时不得擅自改写前端请求,沿用既有当日实时流程");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FCA_UW_005_生效日盘中恢复前一Eod后再套除权()
|
||||
{
|
||||
var recordDate = new DateTime(2026, 8, 14);
|
||||
var realtime = CreateRealtimeFundPosition();
|
||||
var eod = CreateEod(recordDate, 1000m, 100m);
|
||||
var service = CreateService(SwapDealTestFactory.CreateTrade(), realtime, eod, hasCompletedFlow: false);
|
||||
service.ExDividendInfos.Add(new ex_dividend_info
|
||||
{
|
||||
UnderlyingCode = "FUND.TEST",
|
||||
ExDividendDate = recordDate,
|
||||
EffectiveDate = ExDate,
|
||||
GiveShareAmount = 10m,
|
||||
ValidStatus = true
|
||||
});
|
||||
var unwindData = CreateFullCloseUnwindData();
|
||||
|
||||
Assert.IsTrue(service.RestoreEffectiveFundPositionForTest(unwindData, ExDate));
|
||||
|
||||
Assert.AreEqual(2000m, realtime.PosiQuantity,
|
||||
"8 月 17 日盘中应先从 8 月 14 日 EOD 恢复,再按 10 送 10 变为 2000 份");
|
||||
Assert.AreEqual(50m, realtime.PosiGrossPrice,
|
||||
"真实除权生效日盘中应使用 50 元基准,不能继续使用登记日 100 元");
|
||||
Assert.AreEqual(2000m, unwindData.CloseQty);
|
||||
Assert.AreEqual(50m, unwindData.FlowEvents.Single().PosiGrossPrice);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FCA_UW_006_登记日盘中平仓不提前应用除权()
|
||||
{
|
||||
var recordDate = new DateTime(2026, 8, 14);
|
||||
var realtime = CreateRealtimeFundPosition();
|
||||
// 8 月 14 日盘中尚未生成当日 EOD,最近可用快照应是 8 月 13 日。
|
||||
var eod = CreateEod(recordDate.AddDays(-1), 1000m, 100m);
|
||||
var service = CreateService(SwapDealTestFactory.CreateTrade(), realtime, eod, hasCompletedFlow: false);
|
||||
service.ExDividendInfos.Add(new ex_dividend_info
|
||||
{
|
||||
UnderlyingCode = "FUND.TEST",
|
||||
ExDividendDate = recordDate,
|
||||
EffectiveDate = ExDate,
|
||||
GiveShareAmount = 10m,
|
||||
ValidStatus = true
|
||||
});
|
||||
var unwindData = CreateFullCloseUnwindData();
|
||||
unwindData.ValueDate = recordDate;
|
||||
unwindData.UnwindDate = recordDate.AddDays(1);
|
||||
|
||||
service.SwapUnwind(unwindData);
|
||||
|
||||
Assert.AreEqual(1000m, unwindData.PositionQty,
|
||||
"登记日仍使用除权前 EOD 基线,不能提前变为 2000 份");
|
||||
Assert.AreEqual(1000m, unwindData.CloseQty);
|
||||
Assert.AreEqual(100m, unwindData.FlowEvents.Single().PosiGrossPrice,
|
||||
"登记日盘中平仓价格仍应为 100 元,除权生效日才切换为 50 元");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FCA_UW_007_基金直接拆合股比例零点零一_平仓按新数量价格()
|
||||
{
|
||||
var recordDate = new DateTime(2026, 8, 14);
|
||||
var realtime = CreateRealtimeFundPosition();
|
||||
var eod = CreateEod(recordDate, 1000m, 100m);
|
||||
var td = SwapDealTestFactory.CreateTrade();
|
||||
td.StockEqvNotional = 100000d;
|
||||
td.TradeAmount = 1000d;
|
||||
var service = CreateService(td, realtime, eod, hasCompletedFlow: false);
|
||||
service.ExDividendInfos.Add(new ex_dividend_info
|
||||
{
|
||||
UnderlyingCode = "FUND.TEST",
|
||||
ExDividendDate = recordDate,
|
||||
EffectiveDate = ExDate,
|
||||
// 上游 splitratio=0.01 必须先转换为 10 * (0.01 - 1)=-9.9;
|
||||
// 直接写 0.01 会按当前字段公式得到 1.001 倍,无法表达缩小为 0.01 倍。
|
||||
GiveShareAmount = -9.9m,
|
||||
ValidStatus = true
|
||||
});
|
||||
var unwindData = CreateFullCloseUnwindData();
|
||||
unwindData.ValueDate = ExDate;
|
||||
unwindData.UnwindDate = ExDate.AddDays(1);
|
||||
|
||||
service.SwapUnwind(unwindData);
|
||||
|
||||
Assert.AreEqual(10m, unwindData.PositionQty,
|
||||
"Fund splitratio=0.01 时,有效平仓基线应为 1000 * 0.01 = 10 份");
|
||||
Assert.AreEqual(10m, unwindData.CloseQty);
|
||||
Assert.AreEqual(10000m, unwindData.FlowEvents.Single().PosiGrossPrice,
|
||||
"Fund 份额缩小为 0.01 倍时,直接平仓期初价应为 100 / 0.01 = 10000");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FCA_UW_003_正式平仓按FundEod基线重算PnL和现金()
|
||||
{
|
||||
var td = SwapDealTestFactory.CreateTrade();
|
||||
td.StockEqvNotional = 100000d;
|
||||
td.TradeAmount = 1000d;
|
||||
var realtime = CreateRealtimeFundPosition();
|
||||
var eod = CreateEod(ExDate, 2000m, 50m);
|
||||
var service = CreateService(td, realtime, eod, hasCompletedFlow: false);
|
||||
var unwindData = CreateFullCloseUnwindData();
|
||||
var floatEvent = unwindData.FlowEvents.Single();
|
||||
|
||||
service.SwapUnwind(unwindData);
|
||||
|
||||
Assert.AreEqual(2000m, unwindData.PositionQty);
|
||||
Assert.AreEqual(2000m, unwindData.CloseQty);
|
||||
Assert.AreEqual(100000m, unwindData.CloseNotionalValue);
|
||||
Assert.AreEqual(50m, floatEvent.PosiGrossPrice);
|
||||
Assert.AreEqual(20000m, floatEvent.MarkClosePnl,
|
||||
"平仓价 60 - 除权后期初价 50,乘 2000 份,应为 20000");
|
||||
Assert.AreEqual(20000m, unwindData.SwapRealizedPnL);
|
||||
Assert.AreEqual(-20000d, service.ClientCashCalls.Single().amount, 0.001d,
|
||||
"客户现金必须使用后台按有效 EOD 重算后的平仓金额");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FCA_UW_004_现金分红后部分平仓从Eod名义本金扣减()
|
||||
{
|
||||
var td = SwapDealTestFactory.CreateTrade();
|
||||
td.StockEqvNotional = 100000d;
|
||||
td.TradeAmount = 1000d;
|
||||
var realtime = CreateRealtimeFundPosition();
|
||||
var eod = CreateEod(ExDate, 1000m, 99m);
|
||||
var service = CreateService(td, realtime, eod, hasCompletedFlow: false);
|
||||
var unwindData = SwapDealTestFactory.CreateUnwindData(
|
||||
swapRealizedPnL: -500m,
|
||||
closeMethod: (int)CloseMethodEnum.部分平仓,
|
||||
closePercent: 0.5m,
|
||||
closeQty: 500m,
|
||||
closeNotionalValue: 50000m,
|
||||
positionQty: 1000m);
|
||||
unwindData.NotionalValue = 100000m;
|
||||
unwindData.PosiNotionalValue = 100000m;
|
||||
unwindData.FlowEvents.Add(new swap_flow_event
|
||||
{
|
||||
PositionId = 101,
|
||||
EventType = (int)SwapEventTypeEnum.平仓,
|
||||
UnderlyingCode = "FUND.TEST",
|
||||
UnderlyingInstrumentType = ConsGlobal.InstrumentType.Fund,
|
||||
PositionType = (int)PositionTypeFlag.Long,
|
||||
PayDirection = 1,
|
||||
PosiGrossPrice = 100m,
|
||||
PosiNetPrice = 100m,
|
||||
TradingAmountAvg = 99m,
|
||||
Quantity = 500m,
|
||||
PositionQty = 500m,
|
||||
ContractSize = 1m,
|
||||
MarkClosePnl = -500m
|
||||
});
|
||||
|
||||
service.SwapUnwind(unwindData);
|
||||
|
||||
Assert.AreEqual(99000m, unwindData.PosiNotionalValue);
|
||||
Assert.AreEqual(49500m, unwindData.CloseNotionalValue);
|
||||
Assert.AreEqual(0m, unwindData.SwapRealizedPnL,
|
||||
"市场价和除权后期初价同为 99 时不应产生额外盯市损益");
|
||||
Assert.AreEqual(49500d, td.StockEqvNotional, 0.001d,
|
||||
"应从 EOD 有效名义本金 99000 扣除 49500,不能从旧 trade 值 100000 扣减");
|
||||
Assert.AreEqual(500d, td.TradeAmount, 0.001d);
|
||||
}
|
||||
|
||||
private static TestableSwapDealService CreateService(
|
||||
trade td,
|
||||
swap_position realtime,
|
||||
eod_swap_position eod,
|
||||
bool hasCompletedFlow)
|
||||
{
|
||||
return new TestableSwapDealService(td)
|
||||
{
|
||||
RealtimeFloatPosition = realtime,
|
||||
LatestFundEodPosition = eod,
|
||||
HasCompletedFlowAfterLatestFundEod = hasCompletedFlow,
|
||||
ActiveSwapPositions = new List<swap_position> { realtime }
|
||||
};
|
||||
}
|
||||
|
||||
private static swap_position CreateRealtimeFundPosition()
|
||||
{
|
||||
return new swap_position
|
||||
{
|
||||
SwapTradeId = SwapDealTestFactory.SwapTradeId,
|
||||
PositionId = 101,
|
||||
IsInitial = false,
|
||||
PosiDirection = 1,
|
||||
PositionType = (int)PositionTypeFlag.Long,
|
||||
UnderlyingCode = "FUND.TEST",
|
||||
UnderlyingInstrumentType = ConsGlobal.InstrumentType.Fund,
|
||||
PosiQuantity = 1000m,
|
||||
PosiGrossPrice = 100m,
|
||||
PosiNetPrice = 100m,
|
||||
PosiNetFeePrice = 100m,
|
||||
PosiNetNoFeePrice = 100m,
|
||||
PosiNotionalValue = 100000m,
|
||||
ContractSize = 1m
|
||||
};
|
||||
}
|
||||
|
||||
private static eod_swap_position CreateEod(DateTime valueDate, decimal quantity, decimal price)
|
||||
{
|
||||
return new eod_swap_position
|
||||
{
|
||||
SwapTradeId = SwapDealTestFactory.SwapTradeId,
|
||||
PositionId = 101,
|
||||
ValueDate = valueDate,
|
||||
PosiDirection = 1,
|
||||
PositionType = (int)PositionTypeFlag.Long,
|
||||
UnderlyingCode = "FUND.TEST",
|
||||
UnderlyingInstrumentType = ConsGlobal.InstrumentType.Fund,
|
||||
PosiQuantity = quantity,
|
||||
PosiGrossPrice = price,
|
||||
PosiNetPrice = price,
|
||||
PosiNetFeePrice = price,
|
||||
PosiNetNoFeePrice = price,
|
||||
UnderlyingPrice = price,
|
||||
PosiNotionalValue = quantity * price,
|
||||
ContractSize = 1m
|
||||
};
|
||||
}
|
||||
|
||||
private static UnwindData CreateFullCloseUnwindData()
|
||||
{
|
||||
var data = SwapDealTestFactory.CreateUnwindData(
|
||||
swapRealizedPnL: -40000m,
|
||||
closeMethod: (int)CloseMethodEnum.全部平仓,
|
||||
closePercent: 1m,
|
||||
closeQty: 1000m,
|
||||
closeNotionalValue: 100000m,
|
||||
positionQty: 1000m);
|
||||
data.NotionalValue = 100000m;
|
||||
data.PosiNotionalValue = 100000m;
|
||||
data.FlowEvents.Add(new swap_flow_event
|
||||
{
|
||||
PositionId = 101,
|
||||
EventType = (int)SwapEventTypeEnum.平仓,
|
||||
UnderlyingCode = "FUND.TEST",
|
||||
UnderlyingInstrumentType = ConsGlobal.InstrumentType.Fund,
|
||||
PositionType = (int)PositionTypeFlag.Long,
|
||||
PayDirection = 1,
|
||||
PosiGrossPrice = 100m,
|
||||
PosiNetPrice = 100m,
|
||||
TradingAmountAvg = 60m,
|
||||
Quantity = 1000m,
|
||||
PositionQty = 0m,
|
||||
ContractSize = 1m,
|
||||
MarkClosePnl = -40000m
|
||||
});
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.9,1000 份应调整为 10 份");
|
||||
Assert.AreEqual(10000m, actual.PosiGrossPrice,
|
||||
"上游 splitratio=0.01 映射为 GiveShareAmount=-9.9,期初价格应反向放大 100 倍");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 场景4:未收盘抛异常
|
||||
// ================================================================
|
||||
|
||||
@@ -24,6 +24,13 @@ namespace YLErp.Modules.SwapModule
|
||||
public int SaveAllChangesCount;
|
||||
public int CloseReCheckCallCount;
|
||||
|
||||
/// <summary>Fund 盤中基线测试输入;生产服务通过数据库查询同名 seam。</summary>
|
||||
public swap_position RealtimeFloatPosition { get; set; }
|
||||
public eod_swap_position LatestFundEodPosition { get; set; }
|
||||
public bool HasCompletedFlowAfterLatestFundEod { get; set; }
|
||||
public List<swap_position> ActiveSwapPositions { get; set; } = new();
|
||||
public List<ex_dividend_info> ExDividendInfos { get; } = new();
|
||||
|
||||
public TestableSwapDealService(trade td,
|
||||
Dictionary<int, swap_event> swapEvents = null,
|
||||
Dictionary<long, List<swap_flow_event>> flowEventsByEventId = null)
|
||||
@@ -36,6 +43,33 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
protected override trade FindTrade(int tradeId) => tradeId == _trade.id ? _trade : null;
|
||||
|
||||
protected override List<swap_position> FindActiveSwapPositions(int tradeId)
|
||||
=> ActiveSwapPositions;
|
||||
|
||||
protected override swap_position FindRealtimeFloatPosition(UnwindData unwindData)
|
||||
=> RealtimeFloatPosition;
|
||||
|
||||
protected override eod_swap_position FindLatestFundEodPosition(int tradeId, long positionId, DateTime valueDate)
|
||||
=> LatestFundEodPosition;
|
||||
|
||||
protected override bool HasCompletedFlowAfterFundEod(int tradeId, long positionId, DateTime eodDate, DateTime valueDate)
|
||||
=> HasCompletedFlowAfterLatestFundEod;
|
||||
|
||||
protected override ex_dividend_info FindFundCorporateAction(string underlyingCode, DateTime valueDate)
|
||||
=> ExDividendInfos.FirstOrDefault(x => x.ValidStatus
|
||||
&& x.UnderlyingCode == underlyingCode
|
||||
&& x.EffectiveDate == valueDate.Date);
|
||||
|
||||
protected override decimal GetFundCorporateActionClosePrice(
|
||||
ex_dividend_info dividendInfo,
|
||||
decimal fallbackPrice)
|
||||
=> fallbackPrice;
|
||||
|
||||
protected override decimal GetFundDividendTaxRate() => 0m;
|
||||
|
||||
public bool RestoreEffectiveFundPositionForTest(UnwindData unwindData, DateTime valueDate)
|
||||
=> TryRestoreEffectiveFundPosition(unwindData, valueDate);
|
||||
|
||||
protected override int AddClientCash(trade td, double amount, string action, DateTime valueDate)
|
||||
{
|
||||
ClientCashCalls.Add((amount, action, valueDate));
|
||||
|
||||
@@ -42,6 +42,9 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <summary>AddClientCash 调用记录(金额, 操作)</summary>
|
||||
public List<(double amount, string action)> ClientCashCalls { get; } = new();
|
||||
|
||||
/// <summary>SwapPositionCompose 使用的公司行为内存数据;默认空,避免测试访问数据库。</summary>
|
||||
public List<ex_dividend_info> ExDividendInfos { get; } = new();
|
||||
|
||||
/// <summary>自增 id 模拟器(新增 eod 时分配 id)</summary>
|
||||
private int _nextId = 1;
|
||||
|
||||
@@ -78,6 +81,25 @@ 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 decimal GetFundCorporateActionClosePrice(
|
||||
ex_dividend_info dividendInfo,
|
||||
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