feat(eodswapposition): 新增框架合约新口径展示功能
- 在EodPnlCalculator中添加CalculateEodSwapRiskNewFields方法,实现新口径计算逻辑 - 创建EodSwapRiskNewFields和EodSwapRiskNewResponse模型类,支持新的展示字段 - 实现SearchEodSwapNewList查询方法,复用旧查询权限控制并计算新字段 - 添加index=3的新框架合约Tab页面,使用独立查询接口和列配置 - 前端JavaScript中实现colModelGridEodSwapNew列模型,替换旧字段并新增9个字段 - 完善导出功能支持新Tab的标准格式导出,保持与旧口径分离 - 更新测试用例验证新框架合约前端接线正确性
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using YLErp;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
using YLErp.Modules.SwapModule.ReturnLegs;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
@@ -179,5 +181,101 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
position.RealizedPnl = position.RealizedInterest + position.RealizedInterestFee;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算 EQD-7084 新“框架合约”Tab 的纯展示口径。
|
||||
/// 浮动腿盯市收益、开平仓费用和普通利息分别计算;保证金腿的利息
|
||||
/// 仅作为估值组成项保留一次,不混入新 Tab 的普通利息列。
|
||||
/// </summary>
|
||||
public static EodSwapRiskNewFields CalculateEodSwapRiskNewFields(
|
||||
IEnumerable<eod_swap_position> floatingLegs,
|
||||
IEnumerable<eod_swap_position> interestLegs,
|
||||
string structureType,
|
||||
decimal notionalValue,
|
||||
DateTime? startDate,
|
||||
DateTime? maturityDate,
|
||||
decimal periodAmount,
|
||||
int dividendPayDate)
|
||||
{
|
||||
// 日终明细以 UnderlyingCode 是否存在区分浮动腿和利息腿;调用方即使传入混合集合,
|
||||
// 这里也会重新过滤,避免保证金/利息数据被带入浮动端新口径。
|
||||
var floating = (floatingLegs ?? Enumerable.Empty<eod_swap_position>())
|
||||
.Where(x => x != null && !string.IsNullOrEmpty(x.UnderlyingCode))
|
||||
.ToList();
|
||||
var interests = (interestLegs ?? Enumerable.Empty<eod_swap_position>())
|
||||
.Where(x => x != null && string.IsNullOrEmpty(x.UnderlyingCode))
|
||||
.ToList();
|
||||
// MarginModes 覆盖初始/维持保证金相关腿。它们的利息不属于需求中的“利息端待实现收益”,
|
||||
// 但必须单独保留,以使两个合约估值与旧口径总额保持一致。
|
||||
var ordinaryInterests = interests.Where(x => !MarginModes.Contains(x.InterestMode)).ToList();
|
||||
var marginInterests = interests.Where(x => MarginModes.Contains(x.InterestMode)).ToList();
|
||||
var firstFloating = floating.FirstOrDefault();
|
||||
|
||||
// PosiGrossPrice 已是 EOD 归档口径的期初全价;债券价格不可在报表接口再次乘 100。
|
||||
var initialPrice = firstFloating?.PosiGrossPrice;
|
||||
// PosiFeePending 是日终归一后的我方损益方向:支付费用为负、收取费用为正。
|
||||
// 本列独立展示它,下面的 valuation 再加回一次,不能因展示拆列而改变合约估值。
|
||||
var openingClosingFee = floating.Sum(x => x.PosiFeePending);
|
||||
// PosiMtmPnL 已排除分红和费用,避免从 PosiProfitSum 重复拆分历史费用。
|
||||
var floatingUnrealizedPnl = floating.Sum(x => x.PosiMtmPnL);
|
||||
var ordinaryInterestPnl = ordinaryInterests.Sum(x =>
|
||||
x.InterestProfitSum * DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode));
|
||||
var marginInterestAmount = marginInterests.Sum(x =>
|
||||
x.InterestProfitSum * DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode));
|
||||
|
||||
// 新口径估值 = 去费用浮动收益 + 开平仓费用 + 普通利息 + 保证金利息。
|
||||
// “浮动端待实现收益”列不包含费用,而合约估值仍沿用旧总额,故费用只能在此加一次。
|
||||
var valuation = floatingUnrealizedPnl
|
||||
+ openingClosingFee
|
||||
+ ordinaryInterestPnl
|
||||
+ marginInterestAmount;
|
||||
var result = new EodSwapRiskNewFields
|
||||
{
|
||||
UnderlyingInstrumentType = firstFloating?.UnderlyingInstrumentType,
|
||||
UnderlyingDirection = string.Join(",", floating
|
||||
.Select(x => x.PositionType == (int)PositionTypeFlag.Long ? "多头"
|
||||
: x.PositionType == (int)PositionTypeFlag.Short ? "空头" : "")
|
||||
.Where(x => !string.IsNullOrEmpty(x))
|
||||
.Distinct()),
|
||||
UnderlyingCode = string.Join(",", floating
|
||||
.Select(x => x.UnderlyingCode)
|
||||
.Where(x => !string.IsNullOrEmpty(x))
|
||||
.Distinct()),
|
||||
InitialPrice = initialPrice,
|
||||
NotionalQuantity = notionalValue,
|
||||
ContractStartDate = startDate,
|
||||
ContractMaturityDate = maturityDate,
|
||||
// 只要普通利息腿存在 FR007,即按需求显示 FR007;保证金腿不影响该展示基准。
|
||||
InterestBenchmark = ordinaryInterests.Any(x =>
|
||||
!string.IsNullOrWhiteSpace(x.FloatRateUnderlyingCode)
|
||||
&& x.FloatRateUnderlyingCode.IndexOf("FR007", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
? "FR007" : "固定利率",
|
||||
// 使用日终当日实际适用的 TdInterestRate 合计,而非合同初始利率或利差字段。
|
||||
InterestRatePrice = ordinaryInterests.Sum(x => x.TdInterestRate),
|
||||
OpeningClosingFee = openingClosingFee,
|
||||
FloatingUnrealizedPnl = floatingUnrealizedPnl,
|
||||
OrdinaryInterestPnl = ordinaryInterestPnl,
|
||||
MarginInterestAmount = marginInterestAmount,
|
||||
MarginInterestGain = marginInterests
|
||||
.Where(x => x.InterestDirection == (int)SwapDirectionEnum.支付)
|
||||
.Sum(x => Math.Abs(x.InterestIncomeSum)),
|
||||
MarginInterestLoss = marginInterests
|
||||
.Where(x => x.InterestDirection == (int)SwapDirectionEnum.收取)
|
||||
.Sum(x => -Math.Abs(x.InterestIncomeSum))
|
||||
};
|
||||
|
||||
// DividendPayDate=0 表示到期才与本金轧差,期间付息/分红需要加进该口径;
|
||||
// 其余支付方式则由现金支付承担期间金额,估值字段不再包含 periodAmount。
|
||||
if (dividendPayDate == 0)
|
||||
{
|
||||
result.MaturityNettingValuation = valuation + periodAmount;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.PeriodPaymentValuation = valuation;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3370,6 +3370,91 @@ namespace YLErp.Modules.SwapModule
|
||||
return retListResult;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询 EQD-7084 新“框架合约”字段。
|
||||
/// 旧查询负责筛选、排序、分页及旧字段计算;新字段只基于当前页对应的日终腿补充计算,
|
||||
/// 避免改变旧接口的返回口径。
|
||||
/// </summary>
|
||||
public SearchListResult<EodSwapRiskNewResponse> SearchEodSwapNewList(EodSwapQueryRequest req)
|
||||
{
|
||||
// 新 Tab 与旧 Tab 共享同一套权限、筛选、排序和分页边界;先复用旧查询,
|
||||
// 再只替换需求明确调整的展示字段,避免新接口悄然改变旧口径或查询范围。
|
||||
var oldResult = SearchEodSwapList(req);
|
||||
var oldRows = oldResult.rows?.ToList() ?? new List<EodSwapResponse>();
|
||||
var tradeIds = oldRows.Select(x => x.position.SwapTradeId).Distinct().ToList();
|
||||
var valueDates = oldRows.Select(x => x.position.ValueDate).Distinct().ToList();
|
||||
|
||||
if (tradeIds.Count == 0)
|
||||
{
|
||||
return new SearchListResult<EodSwapRiskNewResponse>(oldResult,
|
||||
Enumerable.Empty<EodSwapRiskNewResponse>());
|
||||
}
|
||||
|
||||
// 当前页的交易、日终明细和扩展信息各批量读取一次,随后在内存按“交易 + 日终日”配对。
|
||||
// 不在 rows.Select 内查询数据库,避免分页结果产生 N+1 查询。
|
||||
var trades = DbContext.trade
|
||||
.Where(x => tradeIds.Contains(x.id))
|
||||
.Select(x => new { x.id, x.StartDate, x.ExerciseDate })
|
||||
.ToDictionary(x => x.id);
|
||||
var eodPositionDetails = DbContext.eod_swap_position
|
||||
.Where(x => tradeIds.Contains(x.SwapTradeId)
|
||||
&& valueDates.Contains(x.ValueDate)
|
||||
&& !x.Invalid)
|
||||
.ToList();
|
||||
var tradeExtends = DbContext.trade_extend
|
||||
.Where(x => tradeIds.Contains(x.TradeId))
|
||||
.ToList();
|
||||
|
||||
var rows = oldRows.Select(item =>
|
||||
{
|
||||
// 同一交易可出现在多个日终日;必须同时匹配 ValueDate,不能把其他日期的腿混入本行。
|
||||
var details = eodPositionDetails
|
||||
.Where(x => x.SwapTradeId == item.position.SwapTradeId
|
||||
&& x.ValueDate == item.position.ValueDate)
|
||||
.ToList();
|
||||
var floatingLegs = details.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList();
|
||||
var interestLegs = details.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)).ToList();
|
||||
var tradeExtend = tradeExtends.FirstOrDefault(x => x.TradeId == item.position.SwapTradeId);
|
||||
// 缺少扩展信息时按“期间支付”处理,和旧接口的默认值保持一致。
|
||||
var dividendPayDate = tradeExtend?.ExtendObj?.DividendPayDate ?? 1;
|
||||
trades.TryGetValue(item.position.SwapTradeId, out var tradeInfo);
|
||||
|
||||
return new EodSwapRiskNewResponse
|
||||
{
|
||||
position = item.position,
|
||||
TradeDate = item.TradeDate,
|
||||
SwapTradeNo = item.SwapTradeNo,
|
||||
ClientName = item.ClientName,
|
||||
StructureType = item.StructureType,
|
||||
AssetBookName = item.AssetBookName,
|
||||
ClientId = item.ClientId,
|
||||
SwapTradeTypeStr = item.SwapTradeTypeStr,
|
||||
UnderlyingType = item.UnderlyingType,
|
||||
PeriodAmount = item.PeriodAmount,
|
||||
FloatingUnrealizedPnl = item.FloatingUnrealizedPnl,
|
||||
InterestPaymentMethod = item.InterestPaymentMethod,
|
||||
MaturityNettingValuation = item.MaturityNettingValuation,
|
||||
PeriodPaymentValuation = item.PeriodPaymentValuation,
|
||||
MarginInterestGain = item.MarginInterestGain,
|
||||
MarginInterestLoss = item.MarginInterestLoss,
|
||||
// 所有 EQD-7084 差异集中在 NewFields;上方复制的旧字段用于保留原报表的
|
||||
// 基本信息、DV、期间金额及已实现收益,前端再将六个差异列绑定到 NewFields。
|
||||
NewFields = CalculateEodSwapRiskNewFields(
|
||||
floatingLegs,
|
||||
interestLegs,
|
||||
item.StructureType,
|
||||
item.position.NotionalValue,
|
||||
tradeInfo?.StartDate,
|
||||
tradeInfo?.ExerciseDate,
|
||||
item.PeriodAmount,
|
||||
dividendPayDate)
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
return new SearchListResult<EodSwapRiskNewResponse>(oldResult, rows);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取互换交易日终持仓数据
|
||||
/// </summary>
|
||||
@@ -3448,6 +3533,29 @@ namespace YLErp.Modules.SwapModule
|
||||
return retListResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算 EQD-7084 新“框架合约”Tab 的字段口径。
|
||||
/// 纯函数只依赖日终浮动腿、利息腿和交易级展示参数,供查询接口及无库单测共用。
|
||||
/// </summary>
|
||||
public static EodSwapRiskNewFields CalculateEodSwapRiskNewFields(
|
||||
IEnumerable<eod_swap_position> floatingLegs,
|
||||
IEnumerable<eod_swap_position> interestLegs,
|
||||
string structureType,
|
||||
decimal notionalValue,
|
||||
DateTime? startDate,
|
||||
DateTime? ExerciseDate,
|
||||
decimal periodAmount,
|
||||
int dividendPayDate)
|
||||
=> EodPnlCalculator.CalculateEodSwapRiskNewFields(
|
||||
floatingLegs,
|
||||
interestLegs,
|
||||
structureType,
|
||||
notionalValue,
|
||||
startDate,
|
||||
ExerciseDate,
|
||||
periodAmount,
|
||||
dividendPayDate);
|
||||
|
||||
/// <summary>
|
||||
/// 互换持仓明细查询
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user