feat(swap): 更新互换估值功能并优化数据模型
- 添加互换估值V1配置常量以支持新版本 - 将PeriodAmount和DividendAmount字段改为可空decimal类型 - 更新字段注释说明预付金利率和利息的计算方式 - 修改净额结算金额定义,排除期初和追加预付金本金 - 在报表服务中添加安全求和操作防止空值异常 - 增加多个汇总字段包括股息、保证金利息等统计 - 重构预付金处理逻辑,改用加权平均计算方式 - 新增加权保证金利率和利息计算辅助方法 - 调整前端表格列顺序,优化交易编号显示位置 - 更新数值格式化器以支持空值处理和精度控制 - 添加文档列顺序恢复功能确保表格一致性
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using BaseOUDAL;
|
||||
using Newtonsoft.Json;
|
||||
using NPOI.POIFS.Properties;
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Consts;
|
||||
@@ -1158,7 +1159,8 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
ratio = -ratio;
|
||||
}
|
||||
var lastInterestIncomeSum = eodPayPosition.InterestIncomeSum;
|
||||
// 首次日终结算可能包含当日收盘,因此尚无先前的日终利息持仓。
|
||||
var lastInterestIncomeSum = eodPayPosition?.InterestIncomeSum ?? 0m;
|
||||
eodPayPosition = new eod_swap_position();
|
||||
eodPayPosition.ClientId = td.ClientId;
|
||||
eodPayPosition.SwapTradeId = td.id;
|
||||
@@ -1898,9 +1900,13 @@ namespace YLErp.Modules.SwapModule
|
||||
eod_Swap = new eod_swap();
|
||||
}
|
||||
var tradeSpan = DbContext.trade_span.FirstOrDefault(x => x.TradeId == td.id && x.ValueDate == settleDate);
|
||||
// eod_swap 是交易级汇总;eod_swap_position 是浮动腿、利息腿和保证金腿的明细。
|
||||
// 以下先按日终明细拆腿,再按框架合约展示口径汇总。
|
||||
var eodSwapPositions = DbContext.eod_swap_position.Where(x => x.SwapTradeId == td.id && x.ValueDate == settleDate && !x.Invalid).ToList();
|
||||
var interestPositions = eodSwapPositions.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//利息腿
|
||||
var positions = eodSwapPositions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//持仓腿
|
||||
// 框架合约的方向约定:多头为正、空头为负;总名义本金取交易原始规模,
|
||||
// 不能直接用多空腿相加,否则会把对冲方向误当成合约规模变化。
|
||||
eod_Swap.NotionalValueLong = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue);
|
||||
eod_Swap.NotionalValueShort = -Math.Abs(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue));
|
||||
eod_Swap.NotionalValue = Convert.ToDecimal(td.OriginalStockEqvNotional ?? td.StockEqvNotional);
|
||||
@@ -1915,6 +1921,8 @@ namespace YLErp.Modules.SwapModule
|
||||
eod_Swap.FloatingPnL = positions.Sum(s => s.PosiProfitSum);
|
||||
eod_Swap.dv01 = positions.Sum(s => s.dv01 ?? 0);
|
||||
decimal interestPnL = 0;
|
||||
// 利息腿按我方视角归集。保证金腿的利息现金流方向与普通利息腿相反,
|
||||
// 因此保证金腿需要额外反转符号,确保 InterestPnL 表示我方的合约利率端收益。
|
||||
interestPositions.ForEach(x =>
|
||||
{
|
||||
decimal ratio = x.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;//收取为正,支付为负
|
||||
@@ -1969,6 +1977,7 @@ namespace YLErp.Modules.SwapModule
|
||||
eod_Swap.ValueDate = settleDate;
|
||||
DbContext.eod_swap.Add(eod_Swap);
|
||||
}
|
||||
// 单标的调整与首次归档使用同一套框架合约汇总口径,避免重算后多空和名义本金展示不一致。
|
||||
var eodSwapPositions = DbContext.eod_swap_position.Where(x => x.SwapTradeId == td.id && x.ValueDate == settleDate && !x.Invalid).ToList();
|
||||
var interestPositions = eodSwapPositions.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//利息腿
|
||||
var positions = eodSwapPositions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//持仓腿
|
||||
@@ -2352,6 +2361,7 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
private SearchListResult<EodSwapPositionResponse> GetSearchEodPositionList(ClientSwapPositionRequest req)
|
||||
{
|
||||
// 每日估值报告以有数量的浮动腿为主记录;利息腿和保证金腿仅作为同交易、同估值日的辅助数据参与汇总。
|
||||
var predicate = PredicateBuilder.Create<eod_swap_position>(n => !n.Invalid && n.PosiQuantity > 0);
|
||||
var interestPredicate = PredicateBuilder.Create<eod_swap_position>(n => !n.Invalid && n.InterestDirection > 0);
|
||||
var tradePredicate = PredicateBuilder.Create<trade>(n => n.ValidState != "InValid");
|
||||
@@ -2366,10 +2376,11 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
tradePredicate = tradePredicate.And(x => x.AssetId == req.BookId.Value);
|
||||
}
|
||||
if (req.ValueDateFrom != null)
|
||||
{
|
||||
predicate = predicate.And(x => x.ValueDate >= req.ValueDateFrom);
|
||||
}
|
||||
// ValueDateFrom 保留在请求模型中,但当前互换估值查询按 ValueDate 单日取数。
|
||||
// if (req.ValueDateFrom != null)
|
||||
// {
|
||||
// predicate = predicate.And(x => x.ValueDate >= req.ValueDateFrom);
|
||||
// }
|
||||
if (req.ValueDate != null)
|
||||
{
|
||||
predicate = predicate.And(x => x.ValueDate == req.ValueDate);
|
||||
@@ -2406,77 +2417,125 @@ namespace YLErp.Modules.SwapModule
|
||||
interestPredicate = interestPredicate.And(x => tradeIds.Contains(x.SwapTradeId));
|
||||
var valueDates = retListResult.rows.Select(s => s.position.ValueDate).Distinct().ToList();
|
||||
interestPredicate = interestPredicate.And(x => valueDates.Contains(x.ValueDate));
|
||||
// 主查询分页后再取同交易、同估值日的全部辅助腿,避免利息/保证金归集跨估值日串数据。
|
||||
var eodPositions = DbContext.eod_swap_position.Where(interestPredicate).ToList();
|
||||
var positions = DbContext.swap_position.Where(x => tradeIds.Contains(x.SwapTradeId) && x.InterestMode == (int)InterestModeEnum.初始预付金 && x.IsInitial && !x.Invalid).ToList();
|
||||
var marginPositions = DbContext.swap_position
|
||||
.Where(x => tradeIds.Contains(x.SwapTradeId) && marginTypes.Contains(x.InterestMode) && x.IsInitial && !x.Invalid)
|
||||
.ToList();
|
||||
var tradeExtends = DbContext.trade_extend.Where(x => tradeIds.Contains(x.TradeId)).ToList();
|
||||
Dictionary<string, bool> tradeDic = new Dictionary<string, bool>();
|
||||
foreach (var item in retListResult.rows)
|
||||
{
|
||||
var tradeExtend = tradeExtends.FirstOrDefault(x => x.TradeId == item.position.SwapTradeId);
|
||||
var eventDate = item.position.ValueDate;
|
||||
if (tradeExtend != null)
|
||||
{
|
||||
eventDate = QdpCalendarHelper.GetNonHoliday(eventDate.AddDays(tradeExtend.ExtendObj.SettlementRules));
|
||||
if (item.position.PosiMatuirityDate.HasValue)
|
||||
{
|
||||
item.MaturitySettlementDate = QdpCalendarHelper.GetNonHoliday(item.position.PosiMatuirityDate.Value.AddDays(tradeExtend.ExtendObj.SettlementRules));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
item.MaturitySettlementDate = item.position.PosiMatuirityDate;
|
||||
}
|
||||
item.DayCount = Math.Max(0, (eventDate - item.position.PosiStartDate).Days + 1);
|
||||
// 到期结算日按合同到期日展示;实际期限按自然日且包含起始日,二者均不使用结算规则偏移。
|
||||
item.MaturitySettlementDate = item.position.PosiMatuirityDate;
|
||||
item.DayCount = Math.Max(0, (item.position.ValueDate - item.position.PosiStartDate).Days + 1);
|
||||
//item.position.PosiProfitSum += item.position.VTradingFee-item.position.PosiFeePending;
|
||||
SetClientEodPosition(item.position);
|
||||
//item.position.PosiProfitSum += item.TradingFee;
|
||||
var posiProfitSum = item.position.PosiProfitSum;
|
||||
//item.position.PosiProfitSum 不需要加交易费用
|
||||
// PosiProfitSum 原值包含交易费用和期间付息/分红。先拆出这两部分,
|
||||
// 使“浮动收益金额”仅反映标的盯市收益,后续净额公式再按支付方式决定是否加回期间金额。
|
||||
var pendingDividend = item.position.PosiDividendSum;
|
||||
item.position.PosiProfitSum = item.position.PosiProfitSum - item.position.PosiFeePending - pendingDividend;
|
||||
if (ConsGlobal.InstrumentType.IsBond(item.position.UnderlyingInstrumentType))
|
||||
// 现券仅展示期间付息和期初成交收益率;ETF(标的主数据类型 Fund)仅展示期间分红。
|
||||
// 其余标的的三列均不适用,返回 null 使页面和 Excel 模板保持空白,而不是展示 0。
|
||||
var isCashBond = ConsGlobal.InstrumentType.IsBond(item.position.UnderlyingInstrumentType);
|
||||
var isEtf = ConsGlobal.InstrumentType.Fund.Equals(
|
||||
item.position.UnderlyingInstrumentType,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
if (isCashBond)
|
||||
{
|
||||
item.PeriodAmount = pendingDividend;
|
||||
item.DividendAmount = 0;
|
||||
item.DividendAmount = null;
|
||||
// 期初标的成交收益率是债券现券成交口径,非现券不展示该交易录入值。
|
||||
}
|
||||
else if (isEtf)
|
||||
{
|
||||
item.PeriodAmount = null;
|
||||
item.DividendAmount = pendingDividend;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.PeriodAmount = 0;
|
||||
item.DividendAmount = pendingDividend;
|
||||
item.PeriodAmount = null;
|
||||
item.DividendAmount = null;
|
||||
}
|
||||
item.NetSettmentAmount = item.position.PosiProfitSum + item.PeriodAmount + item.DividendAmount + item.position.PosiFeePending;
|
||||
var margins = positions.Where(x => x.SwapTradeId == item.position.SwapTradeId);
|
||||
if (!isCashBond)
|
||||
{
|
||||
item.InitYtm = null;
|
||||
}
|
||||
// 预付金本金和利率来自交易腿,并以发生日判断在估值日是否已生效;
|
||||
// 预付金利息则来自当日日终腿,以获得截至估值日的 InterestIncomeSum。
|
||||
var tradeMargins = marginPositions
|
||||
.Where(x => x.SwapTradeId == item.position.SwapTradeId
|
||||
&& (!x.HappenDate.HasValue || x.HappenDate.Value <= item.position.ValueDate))
|
||||
.ToList();
|
||||
var interests = eodPositions.Where(x => x.SwapTradeId == item.position.SwapTradeId && x.ValueDate == item.position.ValueDate);
|
||||
var eodMargins = interests.Where(x => marginTypes.Contains(x.InterestMode));
|
||||
var eodInterests = interests.Where(x => !marginTypes.Contains(x.InterestMode));
|
||||
var initialMargins = eodMargins.Where(x => x.InterestMode == (int)InterestModeEnum.初始预付金);
|
||||
var additionalMargins = eodMargins.Where(x => x.InterestMode == (int)InterestModeEnum.追加预付金);
|
||||
var eodMargins = interests.Where(x => marginTypes.Contains(x.InterestMode)).ToList();
|
||||
var eodInterests = interests.Where(x => !marginTypes.Contains(x.InterestMode)).ToList();
|
||||
var initialMargins = tradeMargins.Where(x => x.InterestMode == (int)InterestModeEnum.初始预付金).ToList();
|
||||
var additionalMargins = tradeMargins.Where(x => x.InterestMode == (int)InterestModeEnum.追加预付金).ToList();
|
||||
var floatRateInterest = eodInterests.Where(x => !string.IsNullOrEmpty(x.FloatRateUnderlyingCode)).FirstOrDefault();
|
||||
item.position.FloatRateUnderlyingCode = floatRateInterest?.FloatRateUnderlyingCode;
|
||||
item.position.FloatRate = floatRateInterest?.FloatRate ?? 0;
|
||||
item.OpenMarginAmount = initialMargins.Any()
|
||||
? initialMargins.Sum(s => s.InterestPrincipalFix * (s.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1))
|
||||
: margins.Sum(s => s.InterestPrincipalFix * (s.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1));
|
||||
item.OpenMarginRate = initialMargins.Any()
|
||||
? initialMargins.Sum(s => s.InterestRateDefault * (s.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1))
|
||||
: margins.Sum(s => s.InterestRateDefault * (s.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1));
|
||||
item.OpenMarginAmount = initialMargins.Sum(s => s.InterestPrincipalFix * (s.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1));
|
||||
item.OpenMarginRate = CalculateWeightedMarginRate(tradeMargins);
|
||||
item.AdditionalMarginAmount = additionalMargins.Sum(s => s.InterestPrincipalFix * (s.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1));
|
||||
item.MarginInterestAmount = eodMargins.Sum(s => s.InterestIncomeSum * (s.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1));
|
||||
item.MarginInterestAmount = CalculateWeightedMarginInterest(eodMargins);
|
||||
item.InterestAmount = eodInterests.Sum(s => s.InterestIncomeSum * (s.InterestDirection == (int)SwapDirectionEnum.收取 ? -1 : 1));
|
||||
item.InterestRate = eodInterests.Sum(s => s.InterestRateDefault);
|
||||
item.NetSettmentAmount += item.InterestAmount + item.MarginInterestAmount;
|
||||
// 到期轧差才把期间付息/分红并入净额结算;派息日支付已在现金流层独立结算,不能重复计入估值。
|
||||
var nettingDividend = (tradeExtend?.ExtendObj?.DividendPayDate ?? 1) == 0 ? pendingDividend : 0m;
|
||||
item.NetSettmentAmount = item.InterestAmount
|
||||
+ item.position.PosiProfitSum
|
||||
+ item.position.PosiFeePending
|
||||
+ item.MarginInterestAmount
|
||||
+ nettingDividend;
|
||||
item.NetSettmentAmount = Math.Round(item.NetSettmentAmount, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
item.TrsValue = Math.Round(item.NetSettmentAmount + item.OpenMarginAmount + item.AdditionalMarginAmount, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
if (item.position.PosiNotionalValue != 0 && item.position.PosiNetPrice != 0)
|
||||
{
|
||||
item.FloatRateAbs = item.position.PosiNotionalValue == 0 ? 0 : item.InterestAmount / item.position.PosiNotionalValue;
|
||||
}
|
||||
SetPosiPrice(item.position);
|
||||
// 交易录入的债券类收益互换价格以小数保存,展示时转为百分比价格;
|
||||
// 普通收益互换录入的是数量/原始数值,不做乘 100 转换。
|
||||
SetPosiPrice(item.position, item.StructureType == "普通债券类收益互换");
|
||||
}
|
||||
return retListResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置客户视角
|
||||
/// 计算预付金利率。多条初始/追加预付金腿按本金绝对值加权,
|
||||
/// 不按收付方向轧差,避免相反方向本金抵消后放大利率。
|
||||
/// </summary>
|
||||
private static decimal CalculateWeightedMarginRate(IEnumerable<swap_position> margins)
|
||||
{
|
||||
var marginList = margins.ToList();
|
||||
var totalWeight = marginList.Sum(x => Math.Abs(x.InterestPrincipalFix));
|
||||
return totalWeight == 0
|
||||
? 0
|
||||
: marginList.Sum(x => x.InterestRateDefault * Math.Abs(x.InterestPrincipalFix)) / totalWeight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算预付金利息。先按收取为正、支付为负转换为我方视角,
|
||||
/// 再按日终本金绝对值加权平均;本金合计为零时返回零。
|
||||
/// </summary>
|
||||
private static decimal CalculateWeightedMarginInterest(IEnumerable<eod_swap_position> margins)
|
||||
{
|
||||
var marginList = margins.ToList();
|
||||
var totalWeight = marginList.Sum(x => Math.Abs(x.InterestPrincipalFix));
|
||||
return totalWeight == 0
|
||||
? 0
|
||||
: marginList.Sum(x => x.InterestIncomeSum
|
||||
* (x.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1)
|
||||
* Math.Abs(x.InterestPrincipalFix)) / totalWeight;
|
||||
}
|
||||
/// <summary>
|
||||
/// 将数据库中以公司/交易簿记方向保存的日终字段转换为客户视角。
|
||||
/// 该转换必须在拆分浮动收益、费用和期间付息/分红之前完成,
|
||||
/// 否则页面、Excel 和净额结算金额会出现相反符号。
|
||||
/// </summary>
|
||||
/// <param name="position"></param>
|
||||
private void SetClientEodPosition(eod_swap_position position)
|
||||
@@ -2499,10 +2558,10 @@ namespace YLErp.Modules.SwapModule
|
||||
position.SwapPositionValue = -position.SwapPositionValue;
|
||||
position.PosiDividendSum = -position.PosiDividendSum;
|
||||
}
|
||||
private void SetPosiPrice(eod_swap_position position)
|
||||
private void SetPosiPrice(eod_swap_position position, bool? useBondPriceScale = null)
|
||||
{
|
||||
var um = DataCacheProvider.GetUnderlyingDataSource().GetData(position.UnderlyingCode);
|
||||
if (um != null && um.IsBond())
|
||||
if (useBondPriceScale ?? (um != null && um.IsBond()))
|
||||
{
|
||||
position.PosiNetPrice *= 100;
|
||||
position.UnderlyingPrice *= 100;
|
||||
|
||||
Reference in New Issue
Block a user