Merge branch 'glms/feature/0812_zmr_divPower' into 'glms/feature/1.4.2'
refactor(EodModule): 移除债券支付服务中对公司行为同步镜像的依赖 See merge request otc-dev/zszq-trs!11
This commit is contained in:
@@ -112,5 +112,13 @@ namespace YLErp.DBModels
|
||||
[DisplayName("更新时间")]
|
||||
[Column("update_time")]
|
||||
public DateTime? update_time { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否由 ex_dividend_info 在内存中补充的 Stock/Fund 公司行为现金分红。
|
||||
/// bond_payment_info 原生记录的 paying_interest 按每 100 份存储;公司行为记录的
|
||||
/// GiveCashAmount 按每 10 份存储。该标记只在运行时区分两种金额单位,不落库。
|
||||
/// </summary>
|
||||
[NotMapped]
|
||||
public bool IsCorporateActionCashDividend { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,11 @@ namespace YLErp.Modules.EodModule
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalcPayment_StockOrFundDividend_DoesNotApplyBondScale()
|
||||
public void CalcPayment_CorporateActionDividend_UsesPerTenScale()
|
||||
{
|
||||
var payments = new List<BondPayment>
|
||||
{
|
||||
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<BondPayment>
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
// 最后的 /10 由 CalcPayment 非债券分支完成。
|
||||
// 去重:镜像任务完成后的正式记录带 jsid = -dividend.id;此处先按该负号标记,
|
||||
// 或用“同一实际付息日 + 同一派现金额”兜底去重,避免镜像完成后重复计息。
|
||||
// Stock/Fund 公司行为直接从 ex_dividend_info 读取,内部仍按“每 10 份派现金额”保存 GiveCashAmount 原值,
|
||||
// 最后的 /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
|
||||
@@ -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,10 +168,9 @@ 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
|
||||
update_time = dividend.OptDate,
|
||||
IsCorporateActionCashDividend = true
|
||||
});
|
||||
}
|
||||
return result;
|
||||
@@ -184,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);
|
||||
}
|
||||
/// <summary>
|
||||
/// 计算某标的期间现金流。债券与 Stock/Fund 公司行为共用 bond_payment_info,
|
||||
/// 但通过 useBondPriceScale 明确区分两种入库金额单位。
|
||||
/// 计算某标的期间现金流。债券期间付息与 Stock/Fund 公司行为现金分红可能同时命中,
|
||||
/// 必须按每条记录的来源单位分别计算,不能按标的类型对整个集合统一除以 10 或 100。
|
||||
/// </summary>
|
||||
/// <param name="payments">期间付息集合</param>
|
||||
/// <param name="qty">持仓数量</param>
|
||||
/// <param name="longRatio">多空方向</param>
|
||||
/// <param name="payDirection">收支方向</param>
|
||||
/// <param name="useBondPriceScale">
|
||||
/// 是否按债券报价的百分比口径换算。债券的 payment_interest 是每 100 元面值的票息,
|
||||
/// 需要继续通过 BondPriceConverter 转成入库金额;Fund/Stock 的公司行为现金分红
|
||||
/// 在 bond_payment_info 中按“每 10 份派现金额”存储,payment_interest * qty / 10 才是实际现金,
|
||||
/// 不能再套债券的 /100。默认 true 是为了保持所有历史债券调用方的原有口径。
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
public decimal CalcPayment(
|
||||
List<BondPayment> 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<BondPayment>()).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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -342,25 +342,19 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
/// <summary>
|
||||
/// 计算期间现金流(生产: BondPaymentService;测试: 返回固定值)。
|
||||
/// BondPaymentService 的默认仍是债券百分比价格口径;TRS Stock/Fund 的公司行为
|
||||
/// 分红行按每 10 份金额入库,因此必须显式关闭 BondPriceConverter 的 /100 换算。
|
||||
/// 标的资料缺失时沿用债券口径,避免把未知历史数据放大 100 倍。
|
||||
/// BondPaymentService 会按每条付款记录的数据来源换算:bond_payment_info 原生期间付息
|
||||
/// 按每 100 份,公司行为表补充的现金分红按每 10 份。不能只按 Fund/Stock 标的类型
|
||||
/// 选择一个统一除数,否则两类记录同时命中时会错算。
|
||||
/// </summary>
|
||||
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 分支)----
|
||||
@@ -628,10 +622,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,
|
||||
|
||||
Reference in New Issue
Block a user