From 02da9d78a37fa4b8de107a874197228768fcecf1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BC=A0=E5=90=8D=E9=94=90?= <1565842059@qq.com>
Date: Fri, 21 Aug 2026 13:50:31 +0800
Subject: [PATCH 1/2] =?UTF-8?q?refactor(EodModule):=20=E7=A7=BB=E9=99=A4?=
=?UTF-8?q?=E5=80=BA=E5=88=B8=E6=94=AF=E4=BB=98=E6=9C=8D=E5=8A=A1=E4=B8=AD?=
=?UTF-8?q?=E5=AF=B9=E5=85=AC=E5=8F=B8=E8=A1=8C=E4=B8=BA=E5=90=8C=E6=AD=A5?=
=?UTF-8?q?=E9=95=9C=E5=83=8F=E7=9A=84=E4=BE=9D=E8=B5=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 不再依赖 bond-sync 镜像,Stock/Fund 公司行为直接作为展示行返回
- 展示金额按"每 10 份派现金额"换算为 GiveCashAmount / 10
- 添加公司行为除权表数据查询逻辑,与现有查询结果合并
- 简化债券支付服务中公司行为处理逻辑,移除去重机制
- 更新现金分红处理逻辑,统一通过 CalcBondPayment 读取 ex_dividend_info
- 移除不再需要的 channel_source 和 jsid 字段设置
---
.../Modules/EodModule/BondPaymentService.cs | 55 +++++++++++++------
.../SwapModule/SwapEodPositionService.cs | 7 +--
2 files changed, 41 insertions(+), 21 deletions(-)
diff --git a/YLErpDAL/Modules/EodModule/BondPaymentService.cs b/YLErpDAL/Modules/EodModule/BondPaymentService.cs
index 9605de64..e71c2e6a 100644
--- a/YLErpDAL/Modules/EodModule/BondPaymentService.cs
+++ b/YLErpDAL/Modules/EodModule/BondPaymentService.cs
@@ -59,7 +59,43 @@ namespace YLErp.Modules.EodModule
create_time = source.create_time,
update_time = source.update_time
};
- var result = query.ToSearchList(req);
+ // 不再依赖 bond-sync 镜像:Stock/Fund 公司行为直接作为展示行返回。
+ // 展示金额按“每 10 份派现金额”换算为 GiveCashAmount / 10;EOD 计算仍使用
+ // GetBondPayments 的内部单位口径,不受此处展示换算影响。
+ var corporateQuery = from un in queryUn
+ join dividend in DbContext.ex_dividend_info.AsNoTracking()
+ on un.UnderlyingCode equals dividend.UnderlyingCode
+ where dividend.ValidStatus
+ && dividend.EffectiveDate.HasValue
+ && dividend.EffectiveDate.Value >= valueDtStart
+ && dividend.EffectiveDate.Value < valueDtEnd
+ && dividend.GiveCashAmount != 0
+ && (string.IsNullOrEmpty(req.UnderlyingCode)
+ || dividend.UnderlyingCode.Contains(req.UnderlyingCode))
+ && (un.UnderlyingInstrumentType == ConsGlobal.InstrumentType.Stock
+ || un.UnderlyingInstrumentType == ConsGlobal.InstrumentType.Fund)
+ && (string.IsNullOrEmpty(req.DataSource)
+ || "公司行为除权表".Contains(req.DataSource))
+ select new BondPaymentDto
+ {
+ id = -dividend.id,
+ channel_source = "公司行为除权表",
+ MarketName = un.MarketName,
+ security_id = un.UnderlyingCode,
+ symbol = un.UnderlyingName,
+ coupon_rate = null,
+ payment_date = dividend.EffectiveDate,
+ payment_interest = dividend.GiveCashAmount / 10m,
+ payment_parvalue = null,
+ paying_price = dividend.GiveCashAmount / 10m,
+ create_time = dividend.OptDate,
+ update_time = dividend.OptDate
+ };
+ // EF Core 无法翻译两个对 BondPaymentDto 继承属性赋值集合不完全一致的投影
+ // 直接 Concat;分别执行后在内存合并,不改变两组查询的筛选口径。
+ var rows = query.ToList();
+ rows.AddRange(corporateQuery.ToList());
+ var result = rows.AsQueryable().ToSearchList(req);
return result;
}
@@ -107,11 +143,8 @@ namespace YLErp.Modules.EodModule
string.Join(",", result.Select(r => r.reg_date?.ToString("yyyy-MM-dd"))));
// 让 Copy/Update EOD 始终只依赖 BondPaymentService,而不必在收盘链路直接累加 ex_dividend_info。
- // 口径约定:bond_payment_info.payment_interest 对 Stock/Fund 统一按“每 10 份派现金额”存储,
- // 即直接存 ex_dividend_info.GiveCashAmount 原值,不做 /10;
+ // Stock/Fund 公司行为直接从 ex_dividend_info 读取,内部仍按“每 10 份派现金额”保存 GiveCashAmount 原值,
// 最后的 /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
@@ -128,16 +161,6 @@ namespace YLErp.Modules.EodModule
{
// 按“每 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,
@@ -145,8 +168,6 @@ namespace YLErp.Modules.EodModule
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
});
diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs
index fc73a583..5b7944db 100644
--- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs
+++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs
@@ -628,10 +628,9 @@ namespace YLErp.Modules.SwapModule
preSettleDate,
flowEvents);
- // 现金分红不在登记日直接读取 ex_dividend_info 累加。
- // 同步任务会把 GiveCashAmount(每 10 份派现金额)写入 bond_payment_info,Copy/Update EOD 在
- // EffectiveDate 通过 CalcBondPayment 命中该行并生成 TdPosiDividend。
- // 这样登记日快照不提前变化,也不会与债券付息/平仓链路重复计算。
+ // 现金分红不在登记日直接累加;Copy/Update EOD 通过 CalcBondPayment
+ // 读取 EffectiveDate 命中的 ex_dividend_info,并生成 TdPosiDividend。
+ // 这样登记日快照不提前变化,且公司行为分红与债券付息共用同一待实现余额。
RecordCorporateActionEvents(
td,
curEodPosis,
From 392e36a8205db2ce452300b551d8bda49dddab97 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BC=A0=E5=90=8D=E9=94=90?= <1565842059@qq.com>
Date: Fri, 21 Aug 2026 16:51:55 +0800
Subject: [PATCH 2/2] =?UTF-8?q?feat(bond):=20=E6=B7=BB=E5=8A=A0=E5=85=AC?=
=?UTF-8?q?=E5=8F=B8=E8=A1=8C=E4=B8=BA=E7=8E=B0=E9=87=91=E5=88=86=E7=BA=A2?=
=?UTF-8?q?=E6=A0=87=E8=AF=86=E5=B9=B6=E4=BC=98=E5=8C=96=E4=BB=98=E6=81=AF?=
=?UTF-8?q?=E8=AE=A1=E7=AE=97=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 在 BondPayment 模型中添加 IsCorporateActionCashDividend 属性用于区分不同数据来源
- 修改 BondPaymentService 中的 CalcPayment 方法,按每条记录来源分别处理金额单位转换
- 更新单元测试以验证混合债券付息和公司行为分红的正确计算
- 移除不再需要的 useBondPriceScale 参数简化方法接口
- 修正注释说明以反映新的按记录类型分别计算的逻辑
---
Framework/YLErp.Core/DBModels/BondPayment.cs | 8 ++++
.../BondPaymentServiceCalculationTest.cs | 27 ++++++++++--
.../Modules/EodModule/BondPaymentService.cs | 41 ++++++++-----------
.../Modules/SwapModule/SwapDealService.cs | 11 ++---
.../SwapModule/SwapEodPositionService.cs | 14 ++-----
5 files changed, 55 insertions(+), 46 deletions(-)
diff --git a/Framework/YLErp.Core/DBModels/BondPayment.cs b/Framework/YLErp.Core/DBModels/BondPayment.cs
index 3de640b4..58838d6e 100644
--- a/Framework/YLErp.Core/DBModels/BondPayment.cs
+++ b/Framework/YLErp.Core/DBModels/BondPayment.cs
@@ -112,5 +112,13 @@ namespace YLErp.DBModels
[DisplayName("更新时间")]
[Column("update_time")]
public DateTime? update_time { get; set; }
+
+ ///
+ /// 是否由 ex_dividend_info 在内存中补充的 Stock/Fund 公司行为现金分红。
+ /// bond_payment_info 原生记录的 paying_interest 按每 100 份存储;公司行为记录的
+ /// GiveCashAmount 按每 10 份存储。该标记只在运行时区分两种金额单位,不落库。
+ ///
+ [NotMapped]
+ public bool IsCorporateActionCashDividend { get; set; }
}
}
diff --git a/UnitTestProject/Modules/EodModule/BondPaymentServiceCalculationTest.cs b/UnitTestProject/Modules/EodModule/BondPaymentServiceCalculationTest.cs
index 822de107..237cf1b0 100644
--- a/UnitTestProject/Modules/EodModule/BondPaymentServiceCalculationTest.cs
+++ b/UnitTestProject/Modules/EodModule/BondPaymentServiceCalculationTest.cs
@@ -29,11 +29,11 @@ namespace YLErp.Modules.EodModule
}
[TestMethod]
- public void CalcPayment_StockOrFundDividend_DoesNotApplyBondScale()
+ public void CalcPayment_CorporateActionDividend_UsesPerTenScale()
{
var payments = new List
{
- new BondPayment { payment_interest = 10m }
+ new BondPayment { payment_interest = 10m, IsCorporateActionCashDividend = true }
};
// GiveCashAmount=10(每 10 份派 10)时,payment_interest 直接存 10;
@@ -42,10 +42,29 @@ namespace YLErp.Modules.EodModule
payments,
qty: 1000m,
longRatio: 1m,
- payDirection: 1m,
- useBondPriceScale: false);
+ payDirection: 1m);
Assert.AreEqual(1000m, actual);
}
+
+ [TestMethod]
+ public void CalcPayment_FundMixedBondPaymentAndCorporateAction_UsesEachRecordScale()
+ {
+ var payments = new List
+ {
+ // bond_payment_info:每 100 份付 2,200000 份应为 4000。
+ new BondPayment { payment_interest = 2m },
+ // ex_dividend_info:每 10 份派 10,200000 份应为 200000。
+ new BondPayment { payment_interest = 10m, IsCorporateActionCashDividend = true }
+ };
+
+ var actual = CreateService().CalcPayment(
+ payments,
+ qty: 200000m,
+ longRatio: 1m,
+ payDirection: 1m);
+
+ Assert.AreEqual(204000m, actual);
+ }
}
}
diff --git a/YLErpDAL/Modules/EodModule/BondPaymentService.cs b/YLErpDAL/Modules/EodModule/BondPaymentService.cs
index e71c2e6a..19366db5 100644
--- a/YLErpDAL/Modules/EodModule/BondPaymentService.cs
+++ b/YLErpDAL/Modules/EodModule/BondPaymentService.cs
@@ -144,7 +144,7 @@ namespace YLErp.Modules.EodModule
// 让 Copy/Update EOD 始终只依赖 BondPaymentService,而不必在收盘链路直接累加 ex_dividend_info。
// Stock/Fund 公司行为直接从 ex_dividend_info 读取,内部仍按“每 10 份派现金额”保存 GiveCashAmount 原值,
- // 最后的 /10 由 CalcPayment 非债券分支完成。
+ // 最后的 /10 只应用于这类内存补充记录;bond_payment_info 原生记录仍按每 100 份处理。
var corporatePayments = (from dividend in DbContext.ex_dividend_info.AsNoTracking()
join underlying in DbContext.underlying_manager.AsNoTracking()
on dividend.UnderlyingCode equals underlying.UnderlyingCode
@@ -169,7 +169,8 @@ namespace YLErp.Modules.EodModule
payment_interest = paymentInterest,
paying_price = paymentInterest,
create_time = dividend.OptDate,
- update_time = dividend.OptDate
+ update_time = dividend.OptDate,
+ IsCorporateActionCashDividend = true
});
}
return result;
@@ -205,43 +206,35 @@ namespace YLErp.Modules.EodModule
DateTime endDate,
decimal qty,
decimal longRatio,
- decimal payDirection,
- bool useBondPriceScale = true)
+ decimal payDirection)
{
var payments = GetBondPayments(underlyingCode, startDate, endDate);
- return CalcPayment(payments, qty, longRatio, payDirection, useBondPriceScale);
+ return CalcPayment(payments, qty, longRatio, payDirection);
}
///
- /// 计算某标的期间现金流。债券与 Stock/Fund 公司行为共用 bond_payment_info,
- /// 但通过 useBondPriceScale 明确区分两种入库金额单位。
+ /// 计算某标的期间现金流。债券期间付息与 Stock/Fund 公司行为现金分红可能同时命中,
+ /// 必须按每条记录的来源单位分别计算,不能按标的类型对整个集合统一除以 10 或 100。
///
/// 期间付息集合
/// 持仓数量
/// 多空方向
/// 收支方向
- ///
- /// 是否按债券报价的百分比口径换算。债券的 payment_interest 是每 100 元面值的票息,
- /// 需要继续通过 BondPriceConverter 转成入库金额;Fund/Stock 的公司行为现金分红
- /// 在 bond_payment_info 中按“每 10 份派现金额”存储,payment_interest * qty / 10 才是实际现金,
- /// 不能再套债券的 /100。默认 true 是为了保持所有历史债券调用方的原有口径。
- ///
///
public decimal CalcPayment(
List payments,
decimal qty,
decimal longRatio,
- decimal payDirection,
- bool useBondPriceScale = true)
+ decimal payDirection)
{
- var interest = payments.Sum(s => s.payment_interest ?? 0);
- var paymentAmount = interest * qty;
- // 债券:interest 为每 100 元面值的票息,×qty 后需 ÷100 转为实际金额。
- // Fund/Stock 公司行为:payment_interest 存的是“每 10 份派现金额”(GiveCashAmount 原值),
- // interest × qty 得到“每 10 份派现额 × 持仓份数”,需再 ÷10 才是实际现金;
- // 既不能套债券的 /100,也不能直接返回 paymentAmount(那样会放大 10 倍)。
- var actualAmount = useBondPriceScale
- ? BondPriceConverter.ToStorage(paymentAmount)
- : paymentAmount / 10;
+ var actualAmount = (payments ?? new List()).Sum(payment =>
+ {
+ var paymentAmount = (payment.payment_interest ?? 0m) * qty;
+ // bond_payment_info 原生期间付息按每 100 份存储;由 ex_dividend_info 补充的
+ // 公司行为现金分红按每 10 份存储。Fund 标的可能同时命中两类记录,故必须逐条分流。
+ return payment.IsCorporateActionCashDividend
+ ? paymentAmount / 10m
+ : BondPriceConverter.ToStorage(paymentAmount);
+ });
return actualAmount * longRatio * payDirection;
}
}
diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs
index e42d0602..d8e08c7d 100644
--- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs
+++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs
@@ -2156,18 +2156,13 @@ namespace YLErp.Modules.SwapModule
int shortRatio = DirectionRatio.LongShort(flowEvent.PositionType);
int directionRatio = DirectionRatio.ReceivePay(flowEvent.PayDirection);
var um = DataCacheProvider.GetUnderlyingDataSource().GetData(flowEvent.UnderlyingCode);
- // 债券付息按每百元票息存储,继续走 BondPriceConverter;Stock/Fund 的公司行为
- // 现金分红按每 10 份金额存储,实际现金 = payment_interest * qty / 10,不能 /100。
- // 标的资料缺失时保持旧债券口径,避免未知标的的历史平仓金额被放大。
- var useBondPriceScale = um == null
- || !SwapEodPositionService.IsCorporateActionInstrument(um.UnderlyingInstrumentType);
- // + 付息日>上日日终且小于等于平仓日期的分红数据
+ // 期间付息和公司行为现金分红可能同时命中;BondPaymentService 逐条按来源单位换算,
+ // 原生 bond_payment_info 记录按每 100 份,公司行为表补充记录按每 10 份。
var dividendIn = servie.CalcPayment(
payments,
unwindQty,
shortRatio,
- directionRatio,
- useBondPriceScale);
+ directionRatio);
decimal tax = um?.ValueAddedTax ?? 0;
dividendIn = DividendCalc.AfterTaxRaw(dividendIn, tax);
diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs
index 5b7944db..9650d66d 100644
--- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs
+++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs
@@ -342,25 +342,19 @@ namespace YLErp.Modules.SwapModule
///
/// 计算期间现金流(生产: BondPaymentService;测试: 返回固定值)。
- /// BondPaymentService 的默认仍是债券百分比价格口径;TRS Stock/Fund 的公司行为
- /// 分红行按每 10 份金额入库,因此必须显式关闭 BondPriceConverter 的 /100 换算。
- /// 标的资料缺失时沿用债券口径,避免把未知历史数据放大 100 倍。
+ /// BondPaymentService 会按每条付款记录的数据来源换算:bond_payment_info 原生期间付息
+ /// 按每 100 份,公司行为表补充的现金分红按每 10 份。不能只按 Fund/Stock 标的类型
+ /// 选择一个统一除数,否则两类记录同时命中时会错算。
///
protected virtual decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate, decimal qty, int shortRatio, int directionRatio)
{
- var underlying = GetUnderlyingData(underlyingCode);
- // 本期现金分红只覆盖 TRS Stock/Fund。其他非债券(期货、期权等)虽然也不属于
- // 债券,但尚未接入本现金分红表,继续使用默认债券换算,避免扩大改造范围。
- var useBondPriceScale = underlying == null
- || !IsCorporateActionInstrument(underlying.UnderlyingInstrumentType);
return new BondPaymentService(UserInfo).CalcPayment(
underlyingCode,
fromDate,
toDate,
qty,
shortRatio,
- directionRatio,
- useBondPriceScale);
+ directionRatio);
}
// ---- SwapPositionCompose 路径专用 seam(借鉴 testable 分支)----