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));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using BaseOUDAL;
|
||||
using BaseOUDAL;
|
||||
using DocumentFormat.OpenXml.Bibliography;
|
||||
using ExcelDataReader.Log;
|
||||
using YLErp.DBModels;
|
||||
@@ -99,6 +99,52 @@ namespace YLErp.Modules.EodModule
|
||||
public List<BondPayment> GetBondPayments(string underlyingCode, DateTime startDate, DateTime endDate)
|
||||
{
|
||||
var result = DbContext.bondPayment.Where(x => x.underlyingCode == underlyingCode && x.payment_date > startDate && x.payment_date <= endDate).AsNoTracking().ToList();
|
||||
|
||||
// 让 Copy/Update EOD 始终只依赖 BondPaymentService,而不必在收盘链路直接累加 ex_dividend_info。
|
||||
// 口径约定:bond_payment_info.payment_interest 对 Stock/Fund 统一按“每 10 份派现金额”存储,
|
||||
// 即直接存 ex_dividend_info.GiveCashAmount 原值,不做 /10;
|
||||
// 最后的 /10 由 CalcPayment 非债券分支完成。
|
||||
// 去重:镜像任务完成后的正式记录带 jsid = -dividend.id;此处先按该负号标记,
|
||||
// 或用“同一实际付息日 + 同一派现金额”兜底去重,避免镜像完成后重复计息。
|
||||
var corporatePayments = (from dividend in DbContext.ex_dividend_info.AsNoTracking()
|
||||
join underlying in DbContext.underlying_manager.AsNoTracking()
|
||||
on dividend.UnderlyingCode equals underlying.UnderlyingCode
|
||||
where dividend.ValidStatus
|
||||
&& dividend.EffectiveDate.HasValue
|
||||
&& dividend.EffectiveDate.Value > startDate
|
||||
&& dividend.EffectiveDate.Value <= endDate
|
||||
&& dividend.GiveCashAmount != 0
|
||||
&& dividend.UnderlyingCode == underlyingCode
|
||||
&& (underlying.UnderlyingInstrumentType == ConsGlobal.InstrumentType.Stock
|
||||
|| underlying.UnderlyingInstrumentType == ConsGlobal.InstrumentType.Fund)
|
||||
select dividend).ToList();
|
||||
foreach (var dividend in corporatePayments)
|
||||
{
|
||||
// 按“每 10 份派现金额”口径,直接存 GiveCashAmount 原值,与同步任务/CalcPayment 保持一致。
|
||||
var paymentInterest = dividend.GiveCashAmount;
|
||||
var hasMirroredPayment = result.Any(payment =>
|
||||
payment.jsid == -dividend.id
|
||||
|| (payment.payment_date.HasValue
|
||||
&& payment.payment_date.Value.Date == dividend.EffectiveDate.Value.Date
|
||||
&& payment.payment_interest == paymentInterest));
|
||||
if (hasMirroredPayment)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(new BondPayment
|
||||
{
|
||||
underlyingCode = dividend.UnderlyingCode,
|
||||
payment_date_pl = dividend.EffectiveDate,
|
||||
payment_date = dividend.EffectiveDate,
|
||||
payment_interest = paymentInterest,
|
||||
paying_price = paymentInterest,
|
||||
channel_source = ExDividendDataSources.Manual,
|
||||
jsid = -dividend.id,
|
||||
create_time = dividend.OptDate,
|
||||
update_time = dividend.OptDate
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -142,8 +188,8 @@ namespace YLErp.Modules.EodModule
|
||||
/// <param name="useBondPriceScale">
|
||||
/// 是否按债券报价的百分比口径换算。债券的 payment_interest 是每 100 元面值的票息,
|
||||
/// 需要继续通过 BondPriceConverter 转成入库金额;Fund/Stock 的公司行为现金分红
|
||||
/// 在 bond_payment_info 中按每 10 份存储,payment_interest * qty 已经是实际现金,
|
||||
/// 不能再做一次 /100。默认 true 是为了保持所有历史债券调用方的原有口径。
|
||||
/// 在 bond_payment_info 中按“每 10 份派现金额”存储,payment_interest * qty / 10 才是实际现金,
|
||||
/// 不能再套债券的 /100。默认 true 是为了保持所有历史债券调用方的原有口径。
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
public decimal CalcPayment(
|
||||
@@ -156,11 +202,12 @@ namespace YLErp.Modules.EodModule
|
||||
var interest = payments.Sum(s => s.payment_interest ?? 0);
|
||||
var paymentAmount = interest * qty;
|
||||
// 债券:interest 为每 100 元面值的票息,×qty 后需 ÷100 转为实际金额。
|
||||
// Fund/Stock 公司行为:interest 已由【同步任务】写成 GiveCashAmount/10,
|
||||
// ×qty 就是“每 10 份派现额 × 持仓份额”,必须保留原金额,不能套债券的 /100。
|
||||
// Fund/Stock 公司行为:payment_interest 存的是“每 10 份派现金额”(GiveCashAmount 原值),
|
||||
// interest × qty 得到“每 10 份派现额 × 持仓份数”,需再 ÷10 才是实际现金;
|
||||
// 既不能套债券的 /100,也不能直接返回 paymentAmount(那样会放大 10 倍)。
|
||||
var actualAmount = useBondPriceScale
|
||||
? BondPriceConverter.ToStorage(paymentAmount)
|
||||
: paymentAmount;
|
||||
: paymentAmount / 10;
|
||||
return actualAmount * longRatio * payDirection;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,7 +285,7 @@ namespace YLErp.Modules.SwapModule
|
||||
return query.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>查询 valueDate 当日已经生效的最近有效 Fund EOD。</summary>
|
||||
/// <summary>查询 valueDate 当日已经生效的最近有效 Stock/Fund EOD。</summary>
|
||||
protected virtual eod_swap_position FindLatestFundEodPosition(
|
||||
int tradeId,
|
||||
long positionId,
|
||||
@@ -296,7 +296,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询 valueDate 当天真正生效的 Fund 公司行为。
|
||||
/// 查询 valueDate 当天真正生效的 Stock/Fund 公司行为。
|
||||
/// ExDividendDate 只是登记日,盘中基线不能按登记日提前切换;只有
|
||||
/// EffectiveDate == valueDate 时才把上一 EOD 的 Q/P 转成当日 BOD 的除权后 Q/P。
|
||||
/// </summary>
|
||||
@@ -311,7 +311,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 公司行为系数仍使用登记日收盘价,而不是生效日盘中/收盘价。
|
||||
/// Stock/Fund 公司行为系数仍使用登记日收盘价,而不是生效日盘中/收盘价。
|
||||
/// 测试可用 EOD 快照价格作为回退值;生产从登记日行情表取真实收盘价。
|
||||
/// </summary>
|
||||
protected virtual decimal GetFundCorporateActionClosePrice(
|
||||
@@ -328,10 +328,6 @@ namespace YLErp.Modules.SwapModule
|
||||
return Convert.ToDecimal(closePrice);
|
||||
}
|
||||
|
||||
/// <summary>读取 Fund 现金分红税率;单元测试可固定为 0,避免依赖系统日期配置。</summary>
|
||||
protected virtual decimal GetFundDividendTaxRate()
|
||||
=> new DividendService(this).GetDividendTaxRateDecimal();
|
||||
|
||||
/// <summary>
|
||||
/// 判断最新 EOD 之后是否已有同一浮动腿的完成流水。若有,说明当日实时持仓已发生部分平仓/互换,
|
||||
/// 不能再把较早 EOD 的数量覆盖回来,否则会抹掉当日成交结果。
|
||||
@@ -351,22 +347,22 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 恢复实时 Fund 浮动腿到截至指定日有效的 EOD 基线。
|
||||
/// 恢复实时 Stock/Fund 浮动腿到截至指定日有效的 EOD 基线。
|
||||
/// 这是唯一允许把 EOD 公司行为结果带入盘中平仓的入口:10 送 10 后 EOD 是 2000 份/50
|
||||
/// 时,下一日直接使用 2000/50,不再把前端可能传入的 1000/100 或已除权价格重复套系数。
|
||||
/// 若最新 EOD 后存在完成流水则保持实时腿原值,避免覆盖当日部分平仓;非 Fund、无 EOD
|
||||
/// 和固定/利息腿均返回 false,沿用原逻辑。
|
||||
/// 若最新 EOD 后存在完成流水则保持实时腿原值,避免覆盖当日部分平仓;非 Stock/Fund、无
|
||||
/// EOD 和固定/利息腿均返回 false,沿用原逻辑。
|
||||
/// </summary>
|
||||
protected virtual bool TryRestoreEffectiveFundPosition(
|
||||
swap_position position,
|
||||
DateTime valueDate)
|
||||
{
|
||||
// 只对收取方向的 Fund 浮动腿恢复 EOD;固定腿、利息腿和支付方向不应被公司行为改写。
|
||||
// 只对收取方向的 Stock/Fund 浮动腿恢复 EOD;固定腿、利息腿和支付方向不应被公司行为改写。
|
||||
// 无历史 EOD 或最新 EOD 后已有完成流水时返回 false,由调用方保持实时持仓原值,
|
||||
// 不伪造一份快照,也不把较早的 2000 份/50 覆盖掉当日已经部分平仓后的实时数量。
|
||||
if (position == null
|
||||
|| position.PosiDirection <= 0
|
||||
|| position.UnderlyingInstrumentType != ConsGlobal.InstrumentType.Fund)
|
||||
|| !SwapEodPositionService.IsCorporateActionInstrument(position.UnderlyingInstrumentType))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -405,15 +401,16 @@ namespace YLErp.Modules.SwapModule
|
||||
if (closePrice <= 0)
|
||||
{
|
||||
throw new ServiceException(
|
||||
$"Fund 标的【{position.UnderlyingCode}】登记日【{corporateAction.ExDividendDate:yyyy-MM-dd}】缺少有效收盘价,无法执行除权");
|
||||
$"Stock/Fund 标的【{position.UnderlyingCode}】登记日【{corporateAction.ExDividendDate:yyyy-MM-dd}】缺少有效收盘价,无法执行除权");
|
||||
}
|
||||
|
||||
var dividendTaxRate = GetFundDividendTaxRate();
|
||||
SwapEodPositionService.ApplyFundCorporateActionToPosition(
|
||||
// TODO: 现金模式不使用税率参与 Q/P 除权;价格调整模式启用后再根据需求 考虑接入该配置。
|
||||
// var dividendTaxRate = GetFundDividendTaxRate();
|
||||
SwapEodPositionService.ApplyCorporateActionToPosition(
|
||||
position,
|
||||
corporateAction,
|
||||
closePrice,
|
||||
dividendTaxRate);
|
||||
0m);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -445,7 +442,7 @@ namespace YLErp.Modules.SwapModule
|
||||
if (requestedQty < 0m || (!fullClose && requestedQty > effectiveQty))
|
||||
{
|
||||
throw new ServiceException(
|
||||
$"Fund 浮动腿平仓数量 {requestedQty} 超过截至 {valueDate:yyyy-MM-dd} 有效持仓 {effectiveQty}");
|
||||
$"Stock/Fund 浮动腿平仓数量 {requestedQty} 超过截至 {valueDate:yyyy-MM-dd} 有效持仓 {effectiveQty}");
|
||||
}
|
||||
|
||||
var closeQty = fullClose ? effectiveQty : requestedQty;
|
||||
@@ -550,10 +547,10 @@ namespace YLErp.Modules.SwapModule
|
||||
td.trade_extend = tradeExtend;
|
||||
var position = positions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode) && !x.IsInitial).FirstOrDefault();
|
||||
var oriPosition = positions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode) && x.IsInitial).FirstOrDefault();
|
||||
// Fund 的盘中平仓基线来自最近有效 EOD;10 送 10 后应直接使用 2000 份/50,
|
||||
// Stock/Fund 的盘中平仓基线来自最近有效 EOD;10 送 10 后应直接使用 2000 份/50,
|
||||
// 不能继续读取实时表中的 1000 份/100 再让前端重复套用除权系数。
|
||||
var restoredFundBaseline = TryRestoreEffectiveFundPosition(position, dealDate);
|
||||
// 恢复失败表示非 Fund、无历史 EOD,或 EOD 后已有完成流水;此时保留当前实时值,
|
||||
var restoredCorporateActionBaseline = TryRestoreEffectiveFundPosition(position, dealDate);
|
||||
// 恢复失败表示非 Stock/Fund、无历史 EOD,或 EOD 后已有完成流水;此时保留当前实时值,
|
||||
// 继续原有盘中流程,避免用不完整快照制造数量/价格。
|
||||
var preDealDate = GetPreDealDate(tradeId, dealDate, eventTyps);
|
||||
var hasProcess = HasTradeProcess();
|
||||
@@ -590,8 +587,8 @@ namespace YLErp.Modules.SwapModule
|
||||
unwindData.NotionalValue = Convert.ToDecimal(td.OriginalStockEqvNotional ?? 0);
|
||||
unwindData.NotionalQty = positions.Where(x => x.IsInitial).Sum(s => s.PosiQuantity);
|
||||
// 现金分红会调整 EOD 期初价但不改数量,因此持仓名义本金可能从 100000 变为 99000。
|
||||
// 只有 Fund EOD 基线恢复成功时才使用该值;其他品种继续沿用 trade 原口径。
|
||||
unwindData.PosiNotionalValue = restoredFundBaseline
|
||||
// 只有 Stock/Fund EOD 基线恢复成功时才使用该值;其他品种继续沿用 trade 原口径。
|
||||
unwindData.PosiNotionalValue = restoredCorporateActionBaseline
|
||||
? position.PosiNotionalValue
|
||||
: Convert.ToDecimal(td.StockEqvNotional);
|
||||
unwindData.PositionQty = position != null ? position.PosiQuantity : Convert.ToDecimal(td.TradeAmount);
|
||||
@@ -783,8 +780,8 @@ namespace YLErp.Modules.SwapModule
|
||||
var tradeExtend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == tradeId);
|
||||
td.trade_extend = tradeExtend;
|
||||
var position = positions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode) && !x.IsInitial).FirstOrDefault();
|
||||
// 收益结算与手工平仓共用 Fund 的有效 EOD 基线,避免仍返回除权前价格/数量。
|
||||
var restoredFundBaseline = TryRestoreEffectiveFundPosition(position, dealDate);
|
||||
// 收益结算与手工平仓共用 Stock/Fund 的有效 EOD 基线,避免仍返回除权前价格/数量。
|
||||
var restoredCorporateActionBaseline = TryRestoreEffectiveFundPosition(position, dealDate);
|
||||
// 若无法恢复(例如当日已有互换/平仓流水),这里故意沿用实时腿,不能把较早 EOD
|
||||
// 当作当日最终状态;收益结算的其余字段仍按原始实时口径组装。
|
||||
//var preSettleDate = CheckLastEod(dealDate, td.StartDate.Value, tradeId);//上一交易日期
|
||||
@@ -823,7 +820,7 @@ namespace YLErp.Modules.SwapModule
|
||||
unwindData.StructureType = td.StructureType;
|
||||
unwindData.NotionalValue = Convert.ToDecimal(td.OriginalStockEqvNotional ?? 0);
|
||||
unwindData.NotionalQty = positions.Where(x => x.IsInitial).Sum(s => s.PosiQuantity);
|
||||
unwindData.PosiNotionalValue = restoredFundBaseline
|
||||
unwindData.PosiNotionalValue = restoredCorporateActionBaseline
|
||||
? position.PosiNotionalValue
|
||||
: Convert.ToDecimal(td.StockEqvNotional);
|
||||
unwindData.PositionQty = position != null ? position.PosiQuantity : Convert.ToDecimal(td.TradeAmount);
|
||||
@@ -1829,13 +1826,13 @@ namespace YLErp.Modules.SwapModule
|
||||
throw new ServiceException("未找到交易信息");
|
||||
}
|
||||
NormalizeEventUnwindDate(unwindData);
|
||||
// 提交时再次从有效 EOD/实时腿复核基线,不能只相信前端缓存的数量和价格。
|
||||
var restoredFundBaseline = TryRestoreEffectiveFundPosition(unwindData, unwindData.ValueDate);
|
||||
// 这是直接提交路径的最后一道复核。若返回 false(非 Fund、无快照、或 EOD 后已有完成流水),
|
||||
// 提交时再次从有效 EOD/实时腿复核 Stock/Fund 基线,不能只相信前端缓存的数量和价格。
|
||||
var restoredCorporateActionBaseline = TryRestoreEffectiveFundPosition(unwindData, unwindData.ValueDate);
|
||||
// 这是直接提交路径的最后一道复核。若返回 false(非 Stock/Fund、无快照、或 EOD 后已有完成流水),
|
||||
// 不改写前端数据,沿用当日实时持仓;审批冻结事件和自动平仓入口不经过此复核,见下方说明。
|
||||
if (restoredFundBaseline)
|
||||
if (restoredCorporateActionBaseline)
|
||||
{
|
||||
// 正式提交必须让交易级余额与同一 Fund EOD 基线一致,再执行原有扣减。
|
||||
// 正式提交必须让交易级余额与同一 Stock/Fund EOD 基线一致,再执行原有扣减。
|
||||
// 例:派现后有效名义本金为 99000,平掉一半 49500 后应剩 49500;
|
||||
// 若仍从 trade 旧值 100000 扣减,会错误留下 50500。
|
||||
td.StockEqvNotional = Convert.ToDouble(unwindData.PosiNotionalValue);
|
||||
@@ -1902,7 +1899,7 @@ namespace YLErp.Modules.SwapModule
|
||||
var tradeExtend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == td.id);
|
||||
td.trade_extend = tradeExtend;
|
||||
var position = positions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode) && !x.IsInitial).FirstOrDefault();
|
||||
// 自动平仓由系统流水直接生成,当前入口沿用实时持仓和传入平仓数量,未重新读取 Fund EOD。
|
||||
// 自动平仓由系统流水直接生成,当前入口沿用实时持仓和传入平仓数量,未重新读取 Stock/Fund EOD。
|
||||
// 因此它不具备手工 SwapUnwind 的 EOD 复核保护,生产上需确保自动流水已在正确的 EOD 基线之后生成。
|
||||
var storagePriceRound = ConsGlobal.InstrumentType.IsBond(position?.UnderlyingInstrumentType)
|
||||
? ConsGlobal.PriceRound
|
||||
@@ -2007,7 +2004,7 @@ namespace YLErp.Modules.SwapModule
|
||||
int directionRatio = DirectionRatio.ReceivePay(flowEvent.PayDirection);
|
||||
var um = DataCacheProvider.GetUnderlyingDataSource().GetData(flowEvent.UnderlyingCode);
|
||||
// 债券付息按每百元票息存储,继续走 BondPriceConverter;Stock/Fund 的公司行为
|
||||
// 现金分红按每 10 份金额存储,实际现金就是 payment_interest * qty,不能 /100。
|
||||
// 现金分红按每 10 份金额存储,实际现金 = payment_interest * qty / 10,不能 /100。
|
||||
// 标的资料缺失时保持旧债券口径,避免未知标的的历史平仓金额被放大。
|
||||
var useBondPriceScale = um == null
|
||||
|| !SwapEodPositionService.IsCorporateActionInstrument(um.UnderlyingInstrumentType);
|
||||
@@ -2326,7 +2323,7 @@ namespace YLErp.Modules.SwapModule
|
||||
throw new ServiceException("未找到交易信息");
|
||||
}
|
||||
NormalizeEventUnwindDate(unwindData);
|
||||
// 正常页面先由 InitIncome 读取最近有效 Fund EOD;本提交方法本身不再重读快照,
|
||||
// 正常页面先由 InitIncome 读取最近有效 Stock/Fund EOD;本提交方法本身不再重读快照,
|
||||
// 直接使用调用方传入的数据。若数据来自待复核事件,则它是申请时冻结的快照,日期之后的除权
|
||||
// 不会在这里回写,属于审批链路的残余风险。
|
||||
ValidateIncomeValueDate(unwindData, td);
|
||||
@@ -2365,7 +2362,7 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
throw new Exception("该笔交易状态为平仓待复核,未找到相关记录,请检查该笔交易是否有效");
|
||||
}
|
||||
// 审批通过消费申请时序列化的 unwindData/流水,不重新按当前 Fund EOD 重建数量和价格。
|
||||
// 审批通过消费申请时序列化的 unwindData/流水,不重新按当前 Stock/Fund EOD 重建数量和价格。
|
||||
// 这是为了保持待复核事件可重放的一致性,但也意味着申请后发生除权时仍可能带入冻结的旧基线;
|
||||
// 直接提交路径的 EOD 复核不覆盖此审批路径。
|
||||
swapEvent.unwindData = JsonConvert.DeserializeObject<UnwindData>(swapEvent.EventData);
|
||||
@@ -2462,7 +2459,7 @@ namespace YLErp.Modules.SwapModule
|
||||
throw new ServiceException("未找到交易信息");
|
||||
}
|
||||
NormalizeEventUnwindDate(unwindData);
|
||||
// 进入审批申请时保存的是前端冻结的事件数据;当前路径不执行直接 SwapUnwind 的 Fund EOD 复核。
|
||||
// 进入审批申请时保存的是前端冻结的事件数据;当前路径不执行直接 SwapUnwind 的 Stock/Fund EOD 复核。
|
||||
// 因而申请发生在除权前、审批发生在除权后的场景,冻结数据仍是旧基线,需重新发起申请才能刷新。
|
||||
if (eventType == (int)SwapEventTypeEnum.互换)
|
||||
{
|
||||
|
||||
@@ -78,7 +78,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从已经确认的 Fund EOD 快照恢复实时浮动腿的有效基线。
|
||||
/// 从已经确认的 Stock/Fund EOD 快照恢复实时浮动腿的有效基线。
|
||||
/// 该方法只复制 EOD 已落库的数量、价格、名义本金及累计分红/待结费用,不再次计算
|
||||
/// 公司行动系数,因此是幂等的。例:原 1000 份、期初价 100,10 送 10 后 EOD 为
|
||||
/// 2000 份、50;下一日盘中直接恢复 2000/50,不能再变成 4000/25。
|
||||
@@ -112,7 +112,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询 valueDate 之前最近一份有效 Fund EOD 快照,作为盘中操作的日初基线。
|
||||
/// 查询 valueDate 之前最近一份有效 Stock/Fund EOD 快照,作为盘中操作的日初基线。
|
||||
/// 必须严格使用 < valueDate:试算日当天的 EOD 可能尚未完成,或是重收盘留下的待重建数据,
|
||||
/// 不能反向覆盖盘中实时持仓。例:D 日 10 送 10 后 EOD 为 2000 份/50,D+1 盘中读取 D;
|
||||
/// D 日盘中只读取 D-1,不会误把 D 日半成品当成已生效基线。Invalid 明细始终排除。
|
||||
@@ -446,25 +446,7 @@ namespace YLErp.Modules.SwapModule
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找结算日有效的公司行为记录;测试可替换为内存数据。
|
||||
/// settleDate 必须是收盘作业使用的日期边界(通常为 00:00:00),这里沿用完整
|
||||
/// DateTime 相等匹配;历史数据若带时分秒或为空,不会被静默归入当天,需在作业前
|
||||
/// 通过数据预检查处理,而不是让收盘在错误基线上继续计算。
|
||||
/// </summary>
|
||||
protected virtual List<ex_dividend_info> FindExDividendInfos(DateTime settleDate)
|
||||
{
|
||||
return DbContext.ex_dividend_info
|
||||
.Where(x => x.ValidStatus
|
||||
&& x.EffectiveDate.HasValue
|
||||
&& x.EffectiveDate.Value == settleDate.Date)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询登记日或真实生效日命中的公司行为。保留 FindExDividendInfos 这个
|
||||
/// 可替换入口,测试和历史调用方可以继续注入内存数据。
|
||||
/// </summary>
|
||||
/// <summary>查询登记日或真实生效日命中的有效公司行为。</summary>
|
||||
protected virtual List<ex_dividend_info> FindCorporateActionInfos(DateTime settleDate)
|
||||
{
|
||||
return DbContext.ex_dividend_info
|
||||
@@ -490,19 +472,6 @@ namespace YLErp.Modules.SwapModule
|
||||
UpdateDbOption(swapEvent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找登记日公司行为。登记日只创建待生效审计事件,不参与当日持仓系数计算;
|
||||
/// EffectiveDate 到达后才由 FindExDividendInfos 命中并改变 Stock/Fund 基线。
|
||||
/// </summary>
|
||||
protected virtual List<ex_dividend_info> FindRegistrationExDividendInfos(DateTime settleDate)
|
||||
{
|
||||
return FindCorporateActionInfos(settleDate)
|
||||
.Where(x => x.ValidStatus
|
||||
&& x.ExDividendDate.HasValue
|
||||
&& x.ExDividendDate.Value.Date == settleDate.Date)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取公司行为公式使用的收盘价。
|
||||
/// EffectiveDate 是真正切换持仓基线的日期,但除权系数的收盘价仍属于登记日
|
||||
@@ -523,16 +492,6 @@ namespace YLErp.Modules.SwapModule
|
||||
return Convert.ToDecimal(closePrice);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 公司行为现金分红使用的系统税率,小数形式。
|
||||
/// 系统配置按百分数存储(例如 13 表示 13%),公司行为公式需要 0.13;送股本身
|
||||
/// 不受该税率影响;此处只负责读取并换算,不在异常时擅自默认为 0。
|
||||
/// </summary>
|
||||
protected virtual decimal GetDividendTaxRate()
|
||||
{
|
||||
return new DividendService(this).GetDividendTaxRateDecimal();
|
||||
}
|
||||
|
||||
public static bool IsCorporateActionInstrument(string instrumentType)
|
||||
{
|
||||
// TRS 公司行为本期只覆盖 Stock/Fund。TBonds 等类型继续走原债券付息链路,
|
||||
@@ -700,7 +659,7 @@ namespace YLErp.Modules.SwapModule
|
||||
flowEvents);
|
||||
|
||||
// 现金分红不在登记日直接读取 ex_dividend_info 累加。
|
||||
// 同步任务会把 GiveCashAmount/10 写入 bond_payment_info,Copy/Update EOD 在
|
||||
// 同步任务会把 GiveCashAmount(每 10 份派现金额)写入 bond_payment_info,Copy/Update EOD 在
|
||||
// EffectiveDate 通过 CalcBondPayment 命中该行并生成 TdPosiDividend。
|
||||
// 这样登记日快照不提前变化,也不会与债券付息/平仓链路重复计算。
|
||||
RecordCorporateActionEvents(
|
||||
@@ -710,6 +669,9 @@ namespace YLErp.Modules.SwapModule
|
||||
registrationInfos,
|
||||
exDividendInfos,
|
||||
settleDate);
|
||||
// 登记日 EOD 仍保存除权前快照,但下一交易日开盘读取的实时浮动腿需要
|
||||
// 先切换到生效后的 Q/P。该更新基于当日 EOD 恢复后再套系数,重收盘不会重复放大。
|
||||
UpdateRealtimeCorporateActionPositions(td, curEodPosis, registrationInfos, exDividendInfos, settleDate);
|
||||
var posiLongNotional = curEodPosis.Where(s => s.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue);
|
||||
var posiShortNotional = curEodPosis.Where(s => s.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue);
|
||||
var closePosiNotional = curEodPosis.Where(s => s.TdCloseQty > 0).Sum(s => s.TdCloseQty * s.ContractSize * s.PosiGrossPrice);
|
||||
@@ -741,7 +703,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把上一实际 EOD 复制成“当日开盘基线”,并在需要时套用当日生效的 Fund 公司行为。
|
||||
/// 把上一实际 EOD 复制成“当日开盘基线”,并在需要时套用当日生效的 Stock/Fund 公司行为。
|
||||
/// 原始上一 EOD 只读保留在数据库中,确保登记日 EOD 报表仍展示除权前 Q/P。
|
||||
/// 例如 1000 份/100 元、10 送 10 的记录在 8 月 14 日 EOD 仍是 1000/100;
|
||||
/// 8 月 17 日处理当日流水前,内存基线先转为 2000/50,再平仓 300 份得到 1700/50。
|
||||
@@ -765,10 +727,8 @@ namespace YLErp.Modules.SwapModule
|
||||
.Select(x => x.Clone())
|
||||
.ToList();
|
||||
// 应用公司行为
|
||||
ApplyFundCorporateActions(
|
||||
ApplyCorporateActions(
|
||||
openingPositions,
|
||||
Array.Empty<eod_swap_position>(),
|
||||
Array.Empty<swap_flow_event>(),
|
||||
exDividendByCode,
|
||||
settleDate);
|
||||
return openingPositions;
|
||||
@@ -791,10 +751,8 @@ namespace YLErp.Modules.SwapModule
|
||||
/// EffectiveDate 再生成开盘基线。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
protected void ApplyFundCorporateActions(
|
||||
protected void ApplyCorporateActions(
|
||||
IEnumerable<eod_swap_position> positions,
|
||||
IReadOnlyCollection<eod_swap_position> previousEodPositions,
|
||||
IReadOnlyCollection<swap_flow_event> flowEvents,
|
||||
IReadOnlyDictionary<string, ex_dividend_info> exDividendByCode,
|
||||
DateTime settleDate)
|
||||
{
|
||||
@@ -803,8 +761,9 @@ namespace YLErp.Modules.SwapModule
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取系统税率
|
||||
var dividendTaxRate = GetDividendTaxRate();
|
||||
// TODO: 现金分红税率接入后,仅价格调整模式需要读取税率;TRS 现金模式下不参与除权系数。
|
||||
// var dividendTaxRate = GetDividendTaxRate();
|
||||
var dividendTaxRate = 0m;
|
||||
foreach (var position in positions)
|
||||
{
|
||||
if (position.PosiDirection <= 0
|
||||
@@ -824,41 +783,7 @@ namespace YLErp.Modules.SwapModule
|
||||
if (corporateActionClosePrice <= 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Fund 标的【{position.UnderlyingCode}】登记日【{dividendInfo.ExDividendDate:yyyy-MM-dd}】缺少有效收盘价,无法执行除权");
|
||||
}
|
||||
|
||||
// 当天已有 EOD 且该腿没有流水时,Copy 分支不会恢复价格字段。
|
||||
// 先还原前一日基线,避免同一结算日重跑时再次除权。
|
||||
var hasPositionFlow = flowEvents.Any(x => x.PositionId == position.PositionId);
|
||||
var previousPosition = previousEodPositions.FirstOrDefault(
|
||||
x => x.PositionId == position.PositionId);
|
||||
// 没有流水 且 有前一日持仓时,恢复前一日价格字段
|
||||
if (!hasPositionFlow && previousPosition != null)
|
||||
{
|
||||
position.PosiQuantity = previousPosition.PosiQuantity;
|
||||
position.PosiGrossPrice = previousPosition.PosiGrossPrice;
|
||||
position.PosiNetPrice = previousPosition.PosiNetPrice;
|
||||
position.PosiNetFeePrice = previousPosition.PosiNetFeePrice;
|
||||
position.PosiNetNoFeePrice = previousPosition.PosiNetNoFeePrice;
|
||||
}
|
||||
|
||||
var denominator = 10m + dividendInfo.GiveShareAmount + dividendInfo.RationedSharesAmount;
|
||||
if (denominator == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Fund 标的【{position.UnderlyingCode}】在【{settleDate:yyyy-MM-dd}】的除权份额参数导致除数为 0");
|
||||
}
|
||||
|
||||
// 计算除权系数
|
||||
var factors = DividendService.CalculateCorporateActionFactors(
|
||||
dividendInfo,
|
||||
corporateActionClosePrice,
|
||||
dividendTaxRate,
|
||||
adjustCashDividendPrice: false);
|
||||
if (factors.PriceRatio <= 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Fund 标的【{position.UnderlyingCode}】在【{settleDate:yyyy-MM-dd}】计算得到无效除权系数");
|
||||
$"Stock/Fund 标的【{position.UnderlyingCode}】登记日【{dividendInfo.ExDividendDate:yyyy-MM-dd}】缺少有效收盘价,无法执行除权");
|
||||
}
|
||||
|
||||
// Excel 公式 口径:PriceRatio 是“登记日收盘价 / 除权参考价”,
|
||||
@@ -866,37 +791,24 @@ namespace YLErp.Modules.SwapModule
|
||||
// 配股已经进入 价格参考价,所以即使没有送股,配股也会调整 TRS 数量;
|
||||
// 现金分红不影响 TRS Stock/Fund 期初价格,现金权益由独立分红字段处理。
|
||||
var originalQuantity = position.PosiQuantity;
|
||||
position.PosiQuantity = Math.Round(
|
||||
originalQuantity * factors.PriceRatio,
|
||||
12,
|
||||
MidpointRounding.AwayFromZero);
|
||||
// 计算公司行为发生后的 Q/P
|
||||
var adjusted = CalculateCorporateActionValues(
|
||||
position.PosiQuantity,
|
||||
position.PosiGrossPrice,
|
||||
position.PosiNetPrice,
|
||||
position.PosiNetFeePrice,
|
||||
position.PosiNetNoFeePrice,
|
||||
dividendInfo,
|
||||
corporateActionClosePrice,
|
||||
dividendTaxRate,
|
||||
GetStorageDeliveryPriceRound(position.UnderlyingInstrumentType, position.UnderlyingCode));
|
||||
position.PosiQuantity = adjusted.Quantity;
|
||||
position.TdChangedQty = position.PosiQuantity - originalQuantity;
|
||||
|
||||
var storagePriceRound = GetStorageDeliveryPriceRound(
|
||||
position.UnderlyingInstrumentType,
|
||||
position.UnderlyingCode);
|
||||
position.PosiGrossPrice = Math.Round(
|
||||
position.PosiGrossPrice / factors.PriceRatio,
|
||||
storagePriceRound,
|
||||
MidpointRounding.AwayFromZero);
|
||||
position.PosiNetPrice = Math.Round(
|
||||
position.PosiNetPrice / factors.PriceRatio,
|
||||
ConsGlobal.PriceRound,
|
||||
MidpointRounding.AwayFromZero);
|
||||
if (position.PosiNetFeePrice.HasValue)
|
||||
{
|
||||
position.PosiNetFeePrice = Math.Round(
|
||||
position.PosiNetFeePrice.Value / factors.PriceRatio,
|
||||
ConsGlobal.PriceRound,
|
||||
MidpointRounding.AwayFromZero);
|
||||
}
|
||||
if (position.PosiNetNoFeePrice.HasValue)
|
||||
{
|
||||
position.PosiNetNoFeePrice = Math.Round(
|
||||
position.PosiNetNoFeePrice.Value / factors.PriceRatio,
|
||||
ConsGlobal.PriceRound,
|
||||
MidpointRounding.AwayFromZero);
|
||||
}
|
||||
position.PosiGrossPrice = adjusted.GrossPrice;
|
||||
position.PosiNetPrice = adjusted.NetPrice;
|
||||
position.PosiNetFeePrice = adjusted.NetFeePrice;
|
||||
position.PosiNetNoFeePrice = adjusted.NetNoFeePrice;
|
||||
|
||||
var shortRatio = DirectionRatio.LongShort(position.PositionType);
|
||||
var directionRatio = DirectionRatio.ReceivePay(position.PosiDirection);
|
||||
@@ -998,16 +910,79 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
var closePrice = GetFundCorporateActionClosePrice(info, position.PosiGrossPrice);
|
||||
ApplyFundCorporateActionToPosition(
|
||||
ApplyCorporateActionToPosition(
|
||||
position,
|
||||
info,
|
||||
closePrice,
|
||||
GetDividendTaxRate());
|
||||
0m);
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 同步公司行为后的实时浮动腿。
|
||||
/// 登记日只更新下一交易日 BOD 使用的实时 Q/P,不改当日已落库的 EOD;
|
||||
/// 生效日则把已调整的 EOD 复制到实时腿。每次都先从当日 EOD 恢复,保证重跑幂等。
|
||||
/// </summary>
|
||||
private void UpdateRealtimeCorporateActionPositions(
|
||||
trade td,
|
||||
IReadOnlyCollection<eod_swap_position> currentEodPositions,
|
||||
IReadOnlyCollection<ex_dividend_info> registrationInfos,
|
||||
IReadOnlyCollection<ex_dividend_info> effectiveInfos,
|
||||
DateTime settleDate)
|
||||
{
|
||||
if (td == null || currentEodPositions == null || currentEodPositions.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 登记日收盘后即切换实时 BOD。EffectiveDate 只用于确认这条记录仍是未来生效的
|
||||
// 公司行为;无论登记日与生效日之间有一个还是多个非交易日,都不能漏掉这次切换。
|
||||
var pendingInfos = (registrationInfos ?? Array.Empty<ex_dividend_info>())
|
||||
.Where(x => x.EffectiveDate.HasValue && x.EffectiveDate.Value.Date > settleDate.Date)
|
||||
.ToList();
|
||||
var appliedInfos = effectiveInfos ?? Array.Empty<ex_dividend_info>();
|
||||
|
||||
foreach (var eod in currentEodPositions.Where(x => x != null
|
||||
&& x.PosiDirection > 0
|
||||
&& IsTrsCorporateActionInstrument(x.UnderlyingInstrumentType)
|
||||
&& !string.IsNullOrWhiteSpace(x.UnderlyingCode)))
|
||||
{
|
||||
var realtime = DbContext.swap_position.FirstOrDefault(x => x.SwapTradeId == td.id
|
||||
&& !x.Invalid
|
||||
&& !x.IsInitial
|
||||
&& x.PositionId == eod.PositionId);
|
||||
if (realtime == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var pending = pendingInfos.FirstOrDefault(x => string.Equals(
|
||||
x.UnderlyingCode, eod.UnderlyingCode, StringComparison.OrdinalIgnoreCase));
|
||||
if (pending != null)
|
||||
{
|
||||
// 必须从登记日 EOD 基线生成下一交易日 BOD,而不是在旧实时腿上继续套系数;
|
||||
// 这样 100000/100 只会变成一次 200000/50,并且不会把初始腿改掉。
|
||||
// 先将实时腿恢复为登记日 EOD 的旧基线,再只对实时腿应用一次公司行为。
|
||||
// EOD 仍保持除权前快照;因此 7/10 EOD=100000/100,而 7/13 BOD=200000/50。
|
||||
var baseline = eod.Clone();
|
||||
UpdateSwapPositionWithRealTime(baseline);
|
||||
var closePrice = GetFundCorporateActionClosePrice(pending, baseline.PosiGrossPrice);
|
||||
ApplyCorporateActionToPosition(realtime, pending, closePrice, 0m);
|
||||
continue;
|
||||
}
|
||||
|
||||
var applied = appliedInfos.FirstOrDefault(x => string.Equals(
|
||||
x.UnderlyingCode, eod.UnderlyingCode, StringComparison.OrdinalIgnoreCase));
|
||||
if (applied != null)
|
||||
{
|
||||
// 生效日 EOD 已经完成 Q/P 调整,实时腿直接同步最终快照,不再二次套系数。
|
||||
UpdateSwapPositionWithRealTime(eod.Clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入公司行为生命周期审计事件。
|
||||
/// 登记日:保存调整前快照并标记 Applied=false;
|
||||
@@ -1212,72 +1187,66 @@ namespace YLErp.Modules.SwapModule
|
||||
return SwapEventService.BuildCorporateActionEventReason(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧测试/扩展调用的直接现金分红辅助方法。
|
||||
/// GiveCashAmount 按每 10 份金额计算:1000 份、每 10 份派 10,结果为 1000。
|
||||
/// 当前生产 SwapPositionCompose 不再调用此方法:公司行为现金分红由同步任务
|
||||
/// 写入 bond_payment_info,EffectiveDate 收盘通过 CalcBondPayment 进入 EOD,
|
||||
/// 以避免登记日提前入账及与债券付息链路重复。保留方法是为了不破坏已有测试替身
|
||||
/// 或外部扩展类的编译契约;新增业务代码不得再直接传入 ex_dividend_info。
|
||||
/// </summary>
|
||||
protected void ApplyFundCashDividends(
|
||||
IReadOnlyCollection<eod_swap_position> currentEodPositions,
|
||||
IReadOnlyCollection<eod_swap_position> previousEodPositions,
|
||||
IReadOnlyDictionary<string, ex_dividend_info> exDividendByCode,
|
||||
DateTime settleDate)
|
||||
/// <summary>公司行为调整后的持仓 Q/P 结果,供 EOD、实时腿和盘中平仓共用。</summary>
|
||||
private readonly struct CorporateActionValues
|
||||
{
|
||||
if (currentEodPositions == null
|
||||
|| exDividendByCode == null
|
||||
|| exDividendByCode.Count == 0)
|
||||
public CorporateActionValues(decimal quantity, decimal grossPrice, decimal netPrice, decimal? netFeePrice, decimal? netNoFeePrice)
|
||||
{
|
||||
return;
|
||||
Quantity = quantity;
|
||||
GrossPrice = grossPrice;
|
||||
NetPrice = netPrice;
|
||||
NetFeePrice = netFeePrice;
|
||||
NetNoFeePrice = netNoFeePrice;
|
||||
}
|
||||
|
||||
var dividendTaxRate = GetDividendTaxRate();
|
||||
var previousList = previousEodPositions ?? Array.Empty<eod_swap_position>();
|
||||
foreach (var current in currentEodPositions)
|
||||
public decimal Quantity { get; }
|
||||
public decimal GrossPrice { get; }
|
||||
public decimal NetPrice { get; }
|
||||
public decimal? NetFeePrice { get; }
|
||||
public decimal? NetNoFeePrice { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 统一计算公司行为后的 Q/P。EOD、实时腿和盘中平仓只负责提供基线,
|
||||
/// 不再各自复制数量、毛价和净价的调整公式。
|
||||
/// </summary>
|
||||
private static CorporateActionValues CalculateCorporateActionValues(
|
||||
decimal quantity,
|
||||
decimal grossPrice,
|
||||
decimal netPrice,
|
||||
decimal? netFeePrice,
|
||||
decimal? netNoFeePrice,
|
||||
ex_dividend_info dividendInfo,
|
||||
decimal closePrice,
|
||||
decimal dividendTaxRate,
|
||||
int grossPriceRound)
|
||||
{
|
||||
var factors = DividendService.CalculateCorporateActionFactors(
|
||||
dividendInfo,
|
||||
closePrice,
|
||||
dividendTaxRate,
|
||||
adjustCashDividendPrice: false);
|
||||
if (factors.PriceRatio <= 0)
|
||||
{
|
||||
if (current == null
|
||||
|| current.PosiDirection == 0
|
||||
|| !IsTrsCorporateActionInstrument(current.UnderlyingInstrumentType)
|
||||
|| string.IsNullOrWhiteSpace(current.UnderlyingCode)
|
||||
|| !exDividendByCode.TryGetValue(current.UnderlyingCode, out var dividendInfo)
|
||||
|| !dividendInfo.ExDividendDate.HasValue
|
||||
|| dividendInfo.ExDividendDate.Value.Date != settleDate.Date)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var previous = previousList.FirstOrDefault(
|
||||
x => x != null && x.PositionId == current.PositionId);
|
||||
var entitlementQuantity = previous?.PosiQuantity ?? current.PosiQuantity;
|
||||
var directionRatio = DirectionRatio.ReceivePay(current.PosiDirection);
|
||||
var currentDividend = entitlementQuantity > 0m
|
||||
? entitlementQuantity / 10m
|
||||
* dividendInfo.GiveCashAmount
|
||||
* (1m - dividendTaxRate)
|
||||
* directionRatio
|
||||
: 0m;
|
||||
|
||||
// 当日浮动端分红。公司行为现金分红采用现金模式:不调期初价格,
|
||||
// 只增加待实现分红,支付日仍由既有 DealDividends/付息链路结算。
|
||||
current.TdPosiDividend = RoundMoney(currentDividend);
|
||||
var previousDividendSum = previous?.PosiDividendSum ?? 0m;
|
||||
// 浮动端平仓盈亏·分红未实现 = 前日待实现 + 当日公司行为分红
|
||||
// - 当日已实现分红;本次公司行为尚未支付,因此不能写入 RealizedDividend。
|
||||
current.PosiDividendSum = current.PosiQuantity > 0m
|
||||
? RoundMoney(previousDividendSum + current.TdPosiDividend - current.TdCloseDividend)
|
||||
: 0m;
|
||||
// 现金模式不从 PosiMtmPnL 剥离分红:价格没有被除权,分红只存在于待实现字段。
|
||||
current.PosiProfitSum = RoundMoney(MtmCalc.ReturnLegProfitSum(
|
||||
current.PosiMtmPnL,
|
||||
current.PosiDividendSum,
|
||||
current.PosiFeePending));
|
||||
SetFloatingRealizedPnl(current);
|
||||
current.SwapPositionValue = RoundMoney(PositionValueCalc.Calc(
|
||||
current.InterestProfitSum,
|
||||
current.PosiProfitSum));
|
||||
throw new InvalidOperationException(
|
||||
$"标的【{dividendInfo?.UnderlyingCode}】计算得到无效除权系数");
|
||||
}
|
||||
|
||||
var adjustedQuantity = Math.Round(quantity * factors.PriceRatio, 12, MidpointRounding.AwayFromZero);
|
||||
var adjustedGrossPrice = Math.Round(grossPrice / factors.PriceRatio, grossPriceRound, MidpointRounding.AwayFromZero);
|
||||
var adjustedNetPrice = Math.Round(netPrice / factors.PriceRatio, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
|
||||
var adjustedNetFeePrice = netFeePrice.HasValue
|
||||
? Math.Round(netFeePrice.Value / factors.PriceRatio, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero)
|
||||
: (decimal?)null;
|
||||
var adjustedNetNoFeePrice = netNoFeePrice.HasValue
|
||||
? Math.Round(netNoFeePrice.Value / factors.PriceRatio, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero)
|
||||
: (decimal?)null;
|
||||
return new CorporateActionValues(
|
||||
adjustedQuantity,
|
||||
adjustedGrossPrice,
|
||||
adjustedNetPrice,
|
||||
adjustedNetFeePrice,
|
||||
adjustedNetNoFeePrice);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1288,7 +1257,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// 现金模式调用公式时使用 adjustCashDividendPrice=false,现金权益只进入分红字段,
|
||||
/// 不改变 Stock/Fund 的期初价格。
|
||||
/// </summary>
|
||||
public static bool ApplyFundCorporateActionToPosition(
|
||||
public static bool ApplyCorporateActionToPosition(
|
||||
swap_position position,
|
||||
ex_dividend_info dividendInfo,
|
||||
decimal corporateActionClosePrice,
|
||||
@@ -1303,44 +1272,21 @@ namespace YLErp.Modules.SwapModule
|
||||
return false;
|
||||
}
|
||||
|
||||
var factors = DividendService.CalculateCorporateActionFactors(
|
||||
var adjusted = CalculateCorporateActionValues(
|
||||
position.PosiQuantity,
|
||||
position.PosiGrossPrice,
|
||||
position.PosiNetPrice,
|
||||
position.PosiNetFeePrice,
|
||||
position.PosiNetNoFeePrice,
|
||||
dividendInfo,
|
||||
corporateActionClosePrice,
|
||||
dividendTaxRate,
|
||||
adjustCashDividendPrice: false);
|
||||
if (factors.PriceRatio <= 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Fund 标的【{position.UnderlyingCode}】计算得到无效除权系数");
|
||||
}
|
||||
|
||||
var originalQuantity = position.PosiQuantity;
|
||||
position.PosiQuantity = Math.Round(
|
||||
originalQuantity * factors.PriceRatio,
|
||||
12,
|
||||
MidpointRounding.AwayFromZero);
|
||||
position.PosiGrossPrice = Math.Round(
|
||||
position.PosiGrossPrice / factors.PriceRatio,
|
||||
ConsGlobal.SwapDeliveryPriceRound,
|
||||
MidpointRounding.AwayFromZero);
|
||||
position.PosiNetPrice = Math.Round(
|
||||
position.PosiNetPrice / factors.PriceRatio,
|
||||
ConsGlobal.PriceRound,
|
||||
MidpointRounding.AwayFromZero);
|
||||
if (position.PosiNetFeePrice.HasValue)
|
||||
{
|
||||
position.PosiNetFeePrice = Math.Round(
|
||||
position.PosiNetFeePrice.Value / factors.PriceRatio,
|
||||
ConsGlobal.PriceRound,
|
||||
MidpointRounding.AwayFromZero);
|
||||
}
|
||||
if (position.PosiNetNoFeePrice.HasValue)
|
||||
{
|
||||
position.PosiNetNoFeePrice = Math.Round(
|
||||
position.PosiNetNoFeePrice.Value / factors.PriceRatio,
|
||||
ConsGlobal.PriceRound,
|
||||
MidpointRounding.AwayFromZero);
|
||||
}
|
||||
ConsGlobal.SwapDeliveryPriceRound);
|
||||
position.PosiQuantity = adjusted.Quantity;
|
||||
position.PosiGrossPrice = adjusted.GrossPrice;
|
||||
position.PosiNetPrice = adjusted.NetPrice;
|
||||
position.PosiNetFeePrice = adjusted.NetFeePrice;
|
||||
position.PosiNetNoFeePrice = adjusted.NetNoFeePrice;
|
||||
position.PosiNotionalValue = Math.Round(
|
||||
position.PosiGrossPrice * position.PosiQuantity * position.ContractSize,
|
||||
ConsGlobal.MoneyRound,
|
||||
|
||||
@@ -207,8 +207,8 @@ namespace YLErp.Modules.SwapModule
|
||||
return events;
|
||||
}
|
||||
/// <summary>
|
||||
/// 获取交易操作历史。返回前会过滤掉登记日创建且尚未应用(Applied=false)
|
||||
/// 的公司行为事件,避免交易详情在真实调整前展示一条已完成历史。
|
||||
/// 获取交易操作历史。登记日创建但尚未到 EffectiveDate 的公司行为事件也保留,
|
||||
/// 由 EventData.Applied=false 表示“待生效”,保证审计日志完整可追溯。
|
||||
/// </summary>
|
||||
/// <param name="tradeId">交易id</param>
|
||||
/// <returns></returns>
|
||||
@@ -218,40 +218,7 @@ namespace YLErp.Modules.SwapModule
|
||||
.Where(x => x.SwapTradeId == tradeId)
|
||||
.OrderByDescending(o => o.id)
|
||||
.ToList();
|
||||
return FilterOperationHistory(list);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 过滤登记日创建且 Applied=false 的待生效公司行为事件,避免交易详情在真正
|
||||
/// 调整前展示一条“已完成”历史。其他事件仍保留;无法解析的旧格式公司行为也
|
||||
/// 保持可见,审计查询不能因为新 JSON 结构而静默丢失历史记录。
|
||||
/// </summary>
|
||||
public static List<swap_event> FilterOperationHistory(IEnumerable<swap_event> events)
|
||||
{
|
||||
return (events ?? Enumerable.Empty<swap_event>())
|
||||
.Where(x => !IsPendingCorporateActionEvent(x))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断指定事件是否为待生效的公司行为事件:EventType 为公司行为(13),
|
||||
/// 且 EventData 反序列化后 Applied=false(登记日写入、尚未在真实除权日补齐调整后数据)。
|
||||
/// 非公司行为类型、无法解析的旧格式或已应用的事件均返回 false,保证历史审计记录不被误删。
|
||||
/// </summary>
|
||||
public static bool IsPendingCorporateActionEvent(swap_event swapEvent)
|
||||
{
|
||||
if (swapEvent == null || swapEvent.EventType != (int)SwapEventTypeEnum.公司行为)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryDeserializeCorporateActionEventData(swapEvent, out var data))
|
||||
{
|
||||
// 非快照格式的历史公司行为保持可见,避免误删审计记录。
|
||||
return false;
|
||||
}
|
||||
|
||||
return !data.Applied;
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -76,7 +76,12 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <param name="eodPayPosition"></param>
|
||||
public void UpdateSwapPositionWithRealTime(eod_swap_position eodPayPosition)
|
||||
{
|
||||
var position = DbContext.swap_position.FirstOrDefault(x => x.PositionId == eodPayPosition.PositionId);
|
||||
// 同一 PositionId 同时对应初始腿(id=PositionId)和实时腿(IsInitial=0)。
|
||||
// 日终/公司行为同步只能更新实时腿;若直接 FirstOrDefault,会随机命中初始腿,
|
||||
// 造成 EOD 已是 200000/50 而交易详情仍保持 100000/100,或反向污染交易初始腿。
|
||||
var position = DbContext.swap_position.FirstOrDefault(x => x.PositionId == eodPayPosition.PositionId
|
||||
&& !x.IsInitial
|
||||
&& !x.Invalid);
|
||||
if (position != null)
|
||||
{
|
||||
position.PosiQuantity = eodPayPosition.PosiQuantity;
|
||||
@@ -96,7 +101,13 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
else
|
||||
{
|
||||
// 兼容实时腿尚未生成的首日/自动合成场景:只从初始腿克隆创建实时腿,
|
||||
// 不能把初始腿当作可更新对象。
|
||||
position = DbContext.swap_position.FirstOrDefault(x => x.id == eodPayPosition.PositionId);
|
||||
if (position == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var posi = position.Clone();
|
||||
posi.id = 0;
|
||||
posi.PositionId = position.id;
|
||||
|
||||
@@ -1252,6 +1252,12 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// EffectiveDate 已存在时,当前有效 EOD 是唯一执行状态来源。
|
||||
// 回退会清理生效日及之后的 EOD,但不会删除 eodStatus 或不可篡改的
|
||||
// 公司行为审计事件;此处不能继续落入旧的登记日 eodStatus 判断,
|
||||
// 否则交易已回退仍会被错误判定为“已执行”而无法修改。
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user