Merge branch 'glms/feature/0828_EQD-7084' into 'glms/feature/1.4.2'
feat(eodswapposition): 新增框架合约新口径展示功能 See merge request otc-dev/zszq-trs!14
This commit is contained in:
@@ -220,4 +220,86 @@ namespace YLErp.DBModels
|
||||
/// </summary>
|
||||
public decimal MarginInterestLoss { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EQD-7084 新“框架合约”Tab 的新增字段及拆分后的估值口径。
|
||||
/// 该模型不映射数据库,仅由新查询接口计算返回。
|
||||
/// </summary>
|
||||
public class EodSwapRiskNewFields
|
||||
{
|
||||
/// <summary>
|
||||
/// 浮动收益端标的类型,仅供前端按债券/非债券选择期初价格精度使用,
|
||||
/// 不参与任何收益或估值计算。
|
||||
/// </summary>
|
||||
public string UnderlyingInstrumentType { get; set; }
|
||||
|
||||
/// <summary>浮动收益端多空方向。</summary>
|
||||
public string UnderlyingDirection { get; set; }
|
||||
|
||||
/// <summary>浮动收益端标的代码。</summary>
|
||||
public string UnderlyingCode { get; set; }
|
||||
|
||||
/// <summary>期初标的价格;债券按百分价格展示。</summary>
|
||||
public decimal? InitialPrice { get; set; }
|
||||
|
||||
/// <summary>名义数量,取合约名义本金。</summary>
|
||||
public decimal NotionalQuantity { get; set; }
|
||||
|
||||
/// <summary>合约起始日。</summary>
|
||||
public DateTime? ContractStartDate { get; set; }
|
||||
|
||||
/// <summary>合约到期日。</summary>
|
||||
public DateTime? ContractMaturityDate { get; set; }
|
||||
|
||||
/// <summary>利息端基准:FR007 或固定利率。</summary>
|
||||
public string InterestBenchmark { get; set; }
|
||||
|
||||
/// <summary>普通利息腿当前交易日适用利率合计。</summary>
|
||||
public decimal InterestRatePrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开平仓费用。日终腿已按我方收益方向归一:我方支付为负、我方收取为正;
|
||||
/// 新 Tab 单独展示该金额,但估值中仍须计入一次。
|
||||
/// </summary>
|
||||
public decimal OpeningClosingFee { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 不含开平仓费用的浮动端待实现收益,来源为日终浮动腿的 PosiMtmPnL;
|
||||
/// 不可再由旧口径的 PosiProfitSum 反推,避免把费用重新混入本列。
|
||||
/// </summary>
|
||||
public decimal FloatingUnrealizedPnl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排除初始/维持保证金腿后的普通利息端待实现收益。保证金利息保留在其独立两列,
|
||||
/// 且只通过 MarginInterestAmount 参与估值,以满足“利息端仅展示利息端盈亏”的新口径。
|
||||
/// </summary>
|
||||
public decimal OrdinaryInterestPnl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 保证金利息净额,仅供两种合约估值维持旧总额;前端不直接绑定该字段,
|
||||
/// 以防它再次落入“合约利息端待实现收益”。
|
||||
/// </summary>
|
||||
public decimal MarginInterestAmount { get; set; }
|
||||
|
||||
/// <summary>收取对手方保证金利息。</summary>
|
||||
public decimal MarginInterestGain { get; set; }
|
||||
|
||||
/// <summary>支付对手方保证金利息。</summary>
|
||||
public decimal MarginInterestLoss { get; set; }
|
||||
|
||||
/// <summary>到期轧差口径估值;仅 DividendPayDate=0 时有值,且包含期间付息/分红。</summary>
|
||||
public decimal? MaturityNettingValuation { get; set; }
|
||||
|
||||
/// <summary>期间支付派息口径估值;仅 DividendPayDate 非 0 时有值,不重复计入期间付息/分红。</summary>
|
||||
public decimal? PeriodPaymentValuation { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EQD-7084 新“框架合约”Tab 响应。继承旧响应以保持原有列字段完全一致,
|
||||
/// 新接口只额外序列化新增字段。
|
||||
/// </summary>
|
||||
public class EodSwapRiskNewResponse : EodSwapResponse
|
||||
{
|
||||
public EodSwapRiskNewFields NewFields { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
|
||||
namespace YLErp.Modules.SwapModule;
|
||||
|
||||
/// <summary>
|
||||
/// EQD-7084 新“框架合约”Tab 的口径测试。
|
||||
/// 纯计算测试不依赖数据库,直接锁定 EodPnlCalculator 的新口径。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SwapEodPositionRiskNewTabTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void 新口径_普通利息排除保证金_浮动收益剔除费用并保留估值总额()
|
||||
{
|
||||
var floating = new[]
|
||||
{
|
||||
// EOD 归一后,费用已经按我方收益视角落库;支付费用为负数。
|
||||
FloatingLeg("600000", 1, 100m, 0m, -12m, "普通收益互换")
|
||||
};
|
||||
var interests = new[]
|
||||
{
|
||||
InterestLeg(1, (int)InterestModeEnum.固定值, 80m, 0.02m, 0.02m),
|
||||
InterestLeg(1, (int)InterestModeEnum.初始预付金, 20m, 0.01m, 0.01m)
|
||||
};
|
||||
|
||||
var fields = InvokeCalculation(
|
||||
floating,
|
||||
interests,
|
||||
structureType: "普通收益互换",
|
||||
notionalValue: 1_000m,
|
||||
startDate: new DateTime(2026, 1, 1),
|
||||
maturityDate: new DateTime(2026, 12, 31),
|
||||
periodAmount: 5m,
|
||||
dividendPayDate: 0);
|
||||
|
||||
Assert.AreEqual(100m, GetDecimal(fields, "FloatingUnrealizedPnl"), 0.0001m,
|
||||
"新浮动端待实现收益应排除 PosiFeePending:PosiProfitSum(88) - PosiFeePending(-12) = 100");
|
||||
Assert.AreEqual(-12m, GetDecimal(fields, "OpeningClosingFee"), 0.0001m,
|
||||
"开平仓费用直接使用 EOD 已归一的 PosiFeePending");
|
||||
Assert.AreEqual(80m, GetDecimal(fields, "OrdinaryInterestPnl"), 0.0001m,
|
||||
"利息端待实现收益应排除初始/维持保证金腿");
|
||||
Assert.AreEqual(-20m, GetDecimal(fields, "MarginInterestAmount"), 0.0001m,
|
||||
"保证金利息仍应按保证金腿方向计入估值");
|
||||
Assert.AreEqual(153m, GetDecimal(fields, "MaturityNettingValuation"), 0.0001m,
|
||||
"估值应保持旧口径:100 - 12 + 80 - 20 + 5 = 153;费用只计一次");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 新口径_当前利率合计使用普通利息腿TdInterestRate_并识别FR007()
|
||||
{
|
||||
var fr007Leg = InterestLeg(2, (int)InterestModeEnum.合约名义本金规模, 20m, 0.03m, 0.018m);
|
||||
fr007Leg.FloatRateUnderlyingCode = "FR007";
|
||||
fr007Leg.FloatRate = 0.015m;
|
||||
var fields = InvokeCalculation(
|
||||
new[] { FloatingLeg("600001", 2, 100m, 0m, 0m, "普通收益互换") },
|
||||
new[]
|
||||
{
|
||||
InterestLeg(1, (int)InterestModeEnum.固定值, 10m, 0.02m, 0.0125m),
|
||||
fr007Leg
|
||||
},
|
||||
structureType: "普通收益互换",
|
||||
notionalValue: 100m,
|
||||
startDate: new DateTime(2026, 2, 1),
|
||||
maturityDate: new DateTime(2026, 8, 1),
|
||||
periodAmount: 0m,
|
||||
dividendPayDate: 1);
|
||||
|
||||
Assert.AreEqual(0.0305m, GetDecimal(fields, "InterestRatePrice"), 0.0000001m,
|
||||
"利率端价格应为普通利息腿当前 TdInterestRate 合计,而非默认利差合计");
|
||||
Assert.AreEqual("FR007", GetString(fields, "InterestBenchmark"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 新口径_普通利息腿无FR007时基准为固定利率()
|
||||
{
|
||||
var fields = InvokeCalculation(
|
||||
new[] { FloatingLeg("600002", 1, 100m, 0m, 0m, "普通收益互换") },
|
||||
new[] { InterestLeg(1, (int)InterestModeEnum.固定值, 10m, 0.02m, 0.0125m) },
|
||||
structureType: "普通收益互换",
|
||||
notionalValue: 100m,
|
||||
startDate: new DateTime(2026, 2, 1),
|
||||
maturityDate: new DateTime(2026, 8, 1),
|
||||
periodAmount: 0m,
|
||||
dividendPayDate: 1);
|
||||
|
||||
Assert.AreEqual("固定利率", GetString(fields, "InterestBenchmark"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 新口径_债券期初价格按风险页约定放大百分价格_并保留合同字段()
|
||||
{
|
||||
var fields = InvokeCalculation(
|
||||
new[] { FloatingLeg("110000", 1, 99.12m, 0m, 0m, "普通债券类收益互换", "Bond") },
|
||||
new[] { InterestLeg(1, (int)InterestModeEnum.固定值, 1m, 0.01m, 0.01m) },
|
||||
structureType: "普通债券类收益互换",
|
||||
notionalValue: 9_900m,
|
||||
startDate: new DateTime(2026, 3, 1),
|
||||
maturityDate: new DateTime(2027, 3, 1),
|
||||
periodAmount: 0m,
|
||||
dividendPayDate: 1);
|
||||
|
||||
Assert.AreEqual(99.12m, GetDecimal(fields, "InitialPrice"), 0.0001m,
|
||||
"债券日终 PosiGrossPrice 已由 SetPosiPrice 按风险页口径缩放,新接口不能再次乘 100");
|
||||
Assert.AreEqual(9_900m, GetDecimal(fields, "NotionalQuantity"), 0.0001m);
|
||||
Assert.AreEqual("多头", GetString(fields, "UnderlyingDirection"));
|
||||
Assert.AreEqual("110000", GetString(fields, "UnderlyingCode"));
|
||||
Assert.AreEqual("Bond", GetString(fields, "UnderlyingInstrumentType"));
|
||||
Assert.AreEqual(new DateTime(2026, 3, 1), GetDate(fields, "ContractStartDate"));
|
||||
Assert.AreEqual(new DateTime(2027, 3, 1), GetDate(fields, "ContractMaturityDate"));
|
||||
}
|
||||
|
||||
private static object InvokeCalculation(
|
||||
IEnumerable<eod_swap_position> floating,
|
||||
IEnumerable<eod_swap_position> interests,
|
||||
string structureType,
|
||||
decimal notionalValue,
|
||||
DateTime startDate,
|
||||
DateTime maturityDate,
|
||||
decimal periodAmount,
|
||||
int dividendPayDate)
|
||||
{
|
||||
return EodPnlCalculator.CalculateEodSwapRiskNewFields(
|
||||
floating,
|
||||
interests,
|
||||
structureType,
|
||||
notionalValue,
|
||||
startDate,
|
||||
maturityDate,
|
||||
periodAmount,
|
||||
dividendPayDate);
|
||||
}
|
||||
|
||||
private static decimal GetDecimal(object fields, string name)
|
||||
=> Convert.ToDecimal(fields.GetType().GetProperty(name)!.GetValue(fields));
|
||||
|
||||
private static string GetString(object fields, string name)
|
||||
=> (string)fields.GetType().GetProperty(name)!.GetValue(fields)!;
|
||||
|
||||
private static DateTime GetDate(object fields, string name)
|
||||
=> (DateTime)fields.GetType().GetProperty(name)!.GetValue(fields)!;
|
||||
|
||||
private static eod_swap_position FloatingLeg(
|
||||
string code,
|
||||
int positionType,
|
||||
decimal mtm,
|
||||
decimal dividend,
|
||||
decimal fee,
|
||||
string structureType,
|
||||
string instrumentType = null)
|
||||
=> new()
|
||||
{
|
||||
UnderlyingCode = code,
|
||||
UnderlyingInstrumentType = instrumentType ?? structureType,
|
||||
PositionType = positionType,
|
||||
PosiGrossPrice = mtm,
|
||||
PosiMtmPnL = mtm,
|
||||
PosiDividendSum = dividend,
|
||||
PosiFeePending = fee,
|
||||
PosiProfitSum = mtm + dividend + fee,
|
||||
PosiNotionalValue = 100m
|
||||
};
|
||||
|
||||
private static eod_swap_position InterestLeg(
|
||||
int direction,
|
||||
int mode,
|
||||
decimal profit,
|
||||
decimal defaultRate,
|
||||
decimal currentRate)
|
||||
=> new()
|
||||
{
|
||||
InterestDirection = direction,
|
||||
InterestMode = mode,
|
||||
InterestProfitSum = profit,
|
||||
InterestRateDefault = defaultRate,
|
||||
TdInterestRate = currentRate
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
@@ -180,5 +182,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3391,6 +3391,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>
|
||||
@@ -3469,6 +3554,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>
|
||||
|
||||
@@ -965,6 +965,23 @@ namespace YLErp.Web.Controllers
|
||||
var retListResult = service.SearchEodSwapList(req);
|
||||
return Json(retListResult);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 日终持仓-互换新框架合约查询。
|
||||
/// 先与旧框架合约接口执行相同的账簿、资产单元和客户权限收敛,
|
||||
/// 再返回 EQD-7084 拆分后的展示字段;不能直接绕过这些条件调用服务层。
|
||||
/// </summary>
|
||||
/// <param name="req"></param>
|
||||
/// <returns></returns>
|
||||
public JsonResult EodSwapRiskNewQuery(EodSwapQueryRequest req)
|
||||
{
|
||||
req.BookIds = AssetUnitModel.IntersectAssetUnits(req.AssetIdGroupList, req.BookIds).ToList();
|
||||
req.UserAssets = CurUser.GetAssetUnitIds();
|
||||
req.UserClients = CurUser.GetClientIdsByCurUser();
|
||||
var service = new SwapEodPositionService(CurUser);
|
||||
var retListResult = service.SearchEodSwapNewList(req);
|
||||
return Json(retListResult);
|
||||
}
|
||||
#endregion
|
||||
#region 结算报告
|
||||
/// <summary>
|
||||
|
||||
@@ -46,7 +46,12 @@
|
||||
<a href="/swaptrade2/EodPositionRisks?index=1">日终持仓</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/swaptrade2/EodPositionRisks?index=2">框架合约</a>
|
||||
@* index=2 固定保留历史报表与导出配置,供新旧口径并行核对。 *@
|
||||
<a href="/swaptrade2/EodPositionRisks?index=2">框架合约(旧口径)</a>
|
||||
</li>
|
||||
<li>
|
||||
@* index=3 才使用 EQD-7084 新查询与拆分字段,不能复用旧 Tab 的列配置。 *@
|
||||
<a href="/swaptrade2/EodPositionRisks?index=3">框架合约</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,8 @@ const vm = require('vm');
|
||||
|
||||
function loadEodPositionRiskHelpers() {
|
||||
const filePath = path.join(__dirname, '../wwwroot/Scripts/app/swaptrade/EodPositionRisks.js');
|
||||
const code = fs.readFileSync(filePath, 'utf8') + '\nmodule.exports = { TradeDirectionFormat };';
|
||||
const source = fs.readFileSync(filePath, 'utf8');
|
||||
const code = source + '\nmodule.exports = { TradeDirectionFormat, colModelGridEodSwap, colModelGridEodSwapNew, eodSwapGroupConfig, eodSwapRiskNewGroupConfig };';
|
||||
const sandbox = {
|
||||
module: { exports: {} },
|
||||
exports: {},
|
||||
@@ -13,14 +14,25 @@ function loadEodPositionRiskHelpers() {
|
||||
numberFormat() {
|
||||
return function () { };
|
||||
}
|
||||
},
|
||||
otcformat: {
|
||||
trading: {
|
||||
notional() { return ''; },
|
||||
StockEqvNotional() { return ''; },
|
||||
tradePrice() { return ''; }
|
||||
}
|
||||
},
|
||||
swapPricePrecision: {
|
||||
format() { return ''; }
|
||||
}
|
||||
};
|
||||
|
||||
vm.runInNewContext(code, sandbox, { filename: filePath });
|
||||
return sandbox.module.exports;
|
||||
return { helpers: sandbox.module.exports, source };
|
||||
}
|
||||
|
||||
const { TradeDirectionFormat } = loadEodPositionRiskHelpers();
|
||||
const loaded = loadEodPositionRiskHelpers();
|
||||
const { TradeDirectionFormat, colModelGridEodSwap, colModelGridEodSwapNew, eodSwapGroupConfig, eodSwapRiskNewGroupConfig } = loaded.helpers;
|
||||
|
||||
describe('互换日终持仓交易方向', () => {
|
||||
test.each([
|
||||
@@ -37,3 +49,54 @@ describe('互换日终持仓交易方向', () => {
|
||||
expect(TradeDirectionFormat(1, {}, { eodPosition: { PosiDirection: 0, PositionType: 1 } })).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('EQD-7084 新框架合约前端接线', () => {
|
||||
test('新列模型保留旧列并追加九个字段,六个展示列绑定 NewFields', () => {
|
||||
const oldColumns = colModelGridEodSwap();
|
||||
const newColumns = colModelGridEodSwapNew();
|
||||
const oldNames = oldColumns.map(column => column.name);
|
||||
const newNames = newColumns.map(column => column.name);
|
||||
const replacements = {
|
||||
FloatingUnrealizedPnl: 'NewFields.FloatingUnrealizedPnl',
|
||||
'position.InterestPnL': 'NewFields.OrdinaryInterestPnl',
|
||||
MarginInterestGain: 'NewFields.MarginInterestGain',
|
||||
MarginInterestLoss: 'NewFields.MarginInterestLoss',
|
||||
MaturityNettingValuation: 'NewFields.MaturityNettingValuation',
|
||||
PeriodPaymentValuation: 'NewFields.PeriodPaymentValuation'
|
||||
};
|
||||
const newFields = [
|
||||
'NewFields.UnderlyingDirection',
|
||||
'NewFields.UnderlyingCode',
|
||||
'NewFields.InitialPrice',
|
||||
'NewFields.NotionalQuantity',
|
||||
'NewFields.ContractStartDate',
|
||||
'NewFields.ContractMaturityDate',
|
||||
'NewFields.InterestBenchmark',
|
||||
'NewFields.InterestRatePrice',
|
||||
'NewFields.OpeningClosingFee'
|
||||
];
|
||||
|
||||
expect(newColumns).toHaveLength(oldColumns.length + 9);
|
||||
Object.entries(replacements).forEach(([oldName, newName]) => {
|
||||
expect(newNames).toContain(newName);
|
||||
expect(newNames).not.toContain(oldName);
|
||||
expect(newColumns.find(column => column.name === newName).label)
|
||||
.toBe(oldColumns.find(column => column.name === oldName).label);
|
||||
});
|
||||
oldNames
|
||||
.filter(oldName => !Object.prototype.hasOwnProperty.call(replacements, oldName))
|
||||
.forEach(oldName => expect(newNames).toContain(oldName));
|
||||
newFields.forEach(field => expect(newNames).toContain(field));
|
||||
});
|
||||
|
||||
test('index=2 保留旧 endpoint/config,index=3 使用独立 endpoint/config 且界面不启用分组', () => {
|
||||
expect(loaded.source).toContain("queryurl = '/swaptrade2/EodSwapRiskQuery';");
|
||||
expect(loaded.source).toContain("cloumnTargetName = \"eodSwapList\";");
|
||||
expect(loaded.source).toContain("queryurl = '/swaptrade2/EodSwapRiskNewQuery';");
|
||||
expect(loaded.source).toContain("cloumnTargetName = \"eodSwapRiskNewList\";");
|
||||
expect(loaded.source).toContain('eodSwapRiskNewExportColumnNames');
|
||||
expect(loaded.source).not.toMatch(/main\.initCollapsibleGroupHeaders\s*\(/);
|
||||
expect(eodSwapGroupConfig).not.toBe(eodSwapRiskNewGroupConfig);
|
||||
expect(eodSwapRiskNewGroupConfig.some(group => group.columns.includes('NewFields.InitialPrice'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
var queryurl = '/swaptrade2/EodPositionRiskQuery';
|
||||
var cloumnTargetName = "eodSwapPositionList";
|
||||
var eodSwapExportColumnNames = [];
|
||||
var eodSwapRiskNewExportColumnNames = [];
|
||||
$(function () {
|
||||
var PostData = {};
|
||||
$("#DateValueDate").datepicker({
|
||||
@@ -24,6 +25,18 @@ $(function () {
|
||||
}).map(function (col) {
|
||||
return col.name;
|
||||
});
|
||||
} else if (page.tabIndex == 3) {
|
||||
// 新旧口径并行:独立 endpoint、列设置 key 与标准导出列,避免用户在新 Tab 调列后影响旧报表。
|
||||
queryurl = '/swaptrade2/EodSwapRiskNewQuery';
|
||||
$("#myTab li:first").removeClass("active");
|
||||
$("#myTab li:eq(2)").addClass("active");
|
||||
cloumnTargetName = "eodSwapRiskNewList";
|
||||
colModelGrid = colModelGridEodSwapNew();
|
||||
eodSwapRiskNewExportColumnNames = colModelGrid.filter(function (col) {
|
||||
return !col.optionHide;
|
||||
}).map(function (col) {
|
||||
return col.name;
|
||||
});
|
||||
}
|
||||
PostData.ValueDate = $("#DateValueDate").val();
|
||||
var grid = jQuery('#listGrid').jqGrid({
|
||||
@@ -44,7 +57,8 @@ $(function () {
|
||||
pagerpos: 'left',
|
||||
rowNum: 25,
|
||||
rowList: [25, 50, 100, 200, 10000],
|
||||
footerrow: page.tabIndex == 2,
|
||||
// 两个框架合约 Tab 都需要承载后端返回的 DV 汇总;普通日终持仓维持原行为。
|
||||
footerrow: page.tabIndex == 2 || page.tabIndex == 3,
|
||||
loadComplete: gridComplete,
|
||||
onPaging: onJqgridPaging,
|
||||
grouping: true
|
||||
@@ -687,6 +701,58 @@ function colModelGridEodSwap() {
|
||||
return colModelGrid;
|
||||
}
|
||||
|
||||
// EQD-7084 新框架合约:复用旧列定义,只替换新口径字段并追加新增列。
|
||||
function colModelGridEodSwapNew() {
|
||||
var colModelGrid = colModelGridEodSwap().map(function (col) {
|
||||
return Object.assign({}, col);
|
||||
});
|
||||
|
||||
// 替换后仍保留旧字段 index:后端沿用旧查询处理排序,NewFields 只是显示用的计算字段。
|
||||
function replaceColumn(oldName, newName) {
|
||||
var column = colModelGrid.find(function (col) { return col.name === oldName; });
|
||||
if (column) {
|
||||
column.name = newName;
|
||||
// 新字段在服务端计算,沿用旧列的数据库排序字段,保持分页/排序请求有效。
|
||||
column.index = oldName;
|
||||
}
|
||||
}
|
||||
|
||||
function newColumn(name, label, formatter, index) {
|
||||
return {
|
||||
name: name,
|
||||
label: label,
|
||||
index: index || name,
|
||||
width: 150,
|
||||
align: 'center',
|
||||
formatter: formatter,
|
||||
sortable: false
|
||||
};
|
||||
}
|
||||
|
||||
// 插入点必须在原“名义本金”前,使新需求字段与旧字段的业务阅读顺序、标准导出顺序一致。
|
||||
var contractInfoIndex = colModelGrid.findIndex(function (col) {
|
||||
return col.name === 'position.NotionalValue';
|
||||
});
|
||||
colModelGrid.splice(contractInfoIndex, 0,
|
||||
newColumn('NewFields.UnderlyingDirection', '标的多空(浮动端)'),
|
||||
newColumn('NewFields.UnderlyingCode', '标的代码'),
|
||||
newColumn('NewFields.InitialPrice', '期初价格', InitialPriceFormat),
|
||||
newColumn('NewFields.NotionalQuantity', '名义数量', otcformat.trading.notional),
|
||||
newColumn('NewFields.ContractStartDate', '合约起始日', 'date'),
|
||||
newColumn('NewFields.ContractMaturityDate', '合约到期日', 'date'),
|
||||
newColumn('NewFields.InterestBenchmark', '利息端基准'),
|
||||
newColumn('NewFields.InterestRatePrice', '利率端价格', PercentFormat),
|
||||
newColumn('NewFields.OpeningClosingFee', '开平仓费用', StockEqvNotionalFormat));
|
||||
|
||||
replaceColumn('FloatingUnrealizedPnl', 'NewFields.FloatingUnrealizedPnl');
|
||||
replaceColumn('position.InterestPnL', 'NewFields.OrdinaryInterestPnl');
|
||||
replaceColumn('MarginInterestGain', 'NewFields.MarginInterestGain');
|
||||
replaceColumn('MarginInterestLoss', 'NewFields.MarginInterestLoss');
|
||||
replaceColumn('MaturityNettingValuation', 'NewFields.MaturityNettingValuation');
|
||||
replaceColumn('PeriodPaymentValuation', 'NewFields.PeriodPaymentValuation');
|
||||
return colModelGrid;
|
||||
}
|
||||
|
||||
//框架合约分组配置(对应需求《估值模块V1》2.2 字段定义)
|
||||
//columns 使用 colModel.name;组内列在 colModel 中必须连续
|
||||
var eodSwapGroupConfig = [
|
||||
@@ -699,14 +765,27 @@ var eodSwapGroupConfig = [
|
||||
{ title: '估值与实现收益', columns: ['position.dv01', 'InterestPaymentMethod', 'MaturityNettingValuation', 'PeriodPaymentValuation', 'position.RealizedPnL'] }
|
||||
];
|
||||
|
||||
// 新 Tab 页面不渲染可折叠分组表头(产品已要求取消界面分组);
|
||||
// 此配置只服务“导出标准格式”,因此必须与旧 Tab 分开维护而不能删除。
|
||||
var eodSwapRiskNewGroupConfig = [
|
||||
{ title: '基本信息', columns: ['position.ValueDate', 'AssetBookName', 'ClientName', 'SwapTradeNo', 'StructureType', 'SwapTradeTypeStr', 'UnderlyingType'] },
|
||||
{ title: '新增字段', columns: ['NewFields.UnderlyingDirection', 'NewFields.UnderlyingCode', 'NewFields.InitialPrice', 'NewFields.NotionalQuantity', 'NewFields.ContractStartDate', 'NewFields.ContractMaturityDate', 'NewFields.InterestBenchmark', 'NewFields.InterestRatePrice', 'NewFields.OpeningClosingFee'] },
|
||||
{ title: '名义本金', columns: ['position.NotionalValue', 'position.NotionalValueLong', 'position.NotionalValueShort'] },
|
||||
{ title: '标的市值', columns: ['position.MarketValueLong', 'position.MarketValueShort'] },
|
||||
{ title: '浮动端', columns: ['NewFields.FloatingUnrealizedPnl', 'PeriodAmount'] },
|
||||
{ title: '利息端', columns: ['NewFields.OrdinaryInterestPnl'] },
|
||||
{ title: '保证金', columns: ['position.InitMarginGain', 'position.PostionMarginGain', 'position.InitMarginLoss', 'position.PostionMarginLoss', 'NewFields.MarginInterestGain', 'NewFields.MarginInterestLoss'] },
|
||||
{ title: '估值与实现收益', columns: ['position.dv01', 'InterestPaymentMethod', 'NewFields.MaturityNettingValuation', 'NewFields.PeriodPaymentValuation', 'position.RealizedPnL'] }
|
||||
];
|
||||
|
||||
|
||||
function gridComplete() {
|
||||
var jgrid = $(this);
|
||||
if (arguments[0].Sum) {
|
||||
jgrid.footerData("set", { 'position.dv01': arguments[0].Sum["DV"] });
|
||||
}
|
||||
//框架合约Tab:列设置应用完成后补充期间付息提示。
|
||||
if (page.tabIndex == 2) {
|
||||
// 两个框架合约 Tab 均保留 DV footer 与列设置;界面使用普通单层表头。
|
||||
if (page.tabIndex == 2 || page.tabIndex == 3) {
|
||||
var defer = main.setcolumnChooser(jgrid, cloumnTargetName);
|
||||
$.when(defer).done(function () {
|
||||
jgrid.jqGrid('setLabel', 'PeriodAmount', null, null, {
|
||||
@@ -734,13 +813,21 @@ function starttradeView(id) {
|
||||
function exportVisibleColumns() {
|
||||
var jgrid = jQuery('#listGrid');
|
||||
var dateStr = $("#DateValueDate").val() || '';
|
||||
var tabName = page.tabIndex == 2 ? '框架合约' : '日终持仓';
|
||||
var tabName = page.tabIndex == 2
|
||||
? '框架合约(旧口径)'
|
||||
: page.tabIndex == 3 ? '框架合约' : '日终持仓';
|
||||
var fileName = '日终持仓风险_互换_' + tabName + (dateStr ? '_' + dateStr : '');
|
||||
if (page.tabIndex != 2) {
|
||||
if (page.tabIndex != 2 && page.tabIndex != 3) {
|
||||
main.exportVisibleColumnsToExcel(jgrid, fileName, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// 虽然新 Tab 不展示分组表头,标准格式导出仍按需求输出分组标题和固定列顺序。
|
||||
var groupConfig = page.tabIndex == 3 ? eodSwapRiskNewGroupConfig : eodSwapGroupConfig;
|
||||
var standardColumnNames = page.tabIndex == 3
|
||||
? eodSwapRiskNewExportColumnNames
|
||||
: eodSwapExportColumnNames;
|
||||
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: '选择导出方式',
|
||||
@@ -753,7 +840,7 @@ function exportVisibleColumns() {
|
||||
'</div>',
|
||||
success: function (layero, index) {
|
||||
layero.find('.js-export-eod-swap-standard').on('click', function () {
|
||||
exportEodSwapRows(jgrid, fileName, eodSwapGroupConfig, eodSwapExportColumnNames);
|
||||
exportEodSwapRows(jgrid, fileName, groupConfig, standardColumnNames);
|
||||
layer.close(index);
|
||||
});
|
||||
layero.find('.js-export-eod-swap-visible').on('click', function () {
|
||||
@@ -810,6 +897,13 @@ function RealizedPnlFormat(cellValue, options, rowObject) {
|
||||
function StockEqvNotionalFormat(cellValue, options, rowObject) {
|
||||
return otcformat.trading.StockEqvNotional(cellValue);
|
||||
}
|
||||
function InitialPriceFormat(cellValue, options, rowObject) {
|
||||
// 类型来自 NewFields(不再是旧 eodPosition 嵌套对象),以便债券按全价精度、非债券按普通价格精度展示。
|
||||
var instrumentType = rowObject
|
||||
&& rowObject.NewFields
|
||||
&& rowObject.NewFields.UnderlyingInstrumentType;
|
||||
return swapPricePrecision.format(cellValue, instrumentType, 'grossPrice');
|
||||
}
|
||||
function NullableStockEqvNotionalFormat(cellValue, options, rowObject) {
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '') {
|
||||
return '';
|
||||
|
||||
Reference in New Issue
Block a user