feat(swap): 更新互换估值功能并优化数据模型

- 添加互换估值V1配置常量以支持新版本
- 将PeriodAmount和DividendAmount字段改为可空decimal类型
- 更新字段注释说明预付金利率和利息的计算方式
- 修改净额结算金额定义,排除期初和追加预付金本金
- 在报表服务中添加安全求和操作防止空值异常
- 增加多个汇总字段包括股息、保证金利息等统计
- 重构预付金处理逻辑,改用加权平均计算方式
- 新增加权保证金利率和利息计算辅助方法
- 调整前端表格列顺序,优化交易编号显示位置
- 更新数值格式化器以支持空值处理和精度控制
- 添加文档列顺序恢复功能确保表格一致性
This commit is contained in:
张名锐
2026-07-14 09:46:54 +08:00
parent a7c4344436
commit 4449bd5824
10 changed files with 324 additions and 179 deletions
+22 -7
View File
@@ -52,19 +52,21 @@ namespace YLErp.DBModels
[DataChange]
public string StructureType { get; set; }
/// <summary>
/// 合约名义本金
/// 合约名义本金。取交易原始等价名义本金,表示合约约定规模;
/// 不等于多头与空头日终腿的代数和。
/// </summary>
[DisplayName("合约名义本金")]
[DataChange]
public decimal NotionalValue { get; set; }
/// <summary>
/// 合约多头名义本金
/// 合约多头名义本金。框架合约展示口径中多头始终为正数。
/// </summary>
[DisplayName("合约多头名义本金")]
[DataChange]
public decimal NotionalValueLong { get; set; }
/// <summary>
/// 合约空头名义本金
/// 合约空头名义本金。框架合约展示口径中空头始终为负数,
/// 以便与多头直接相加得到净方向。
/// </summary>
[DisplayName("合约空头名义本金")]
[DataChange]
@@ -177,30 +179,43 @@ namespace YLErp.DBModels
public string UnderlyingType { get; set; }
/// <summary>
/// 合约期内已实现加待实现的付息/分红金额。
/// 该字段用于框架合约风险展示,不按每日估值报告的“期间付息/期间分红”列拆分。
/// </summary>
public decimal PeriodAmount { get; set; }
/// <summary>
/// 合约浮动端待实现收益,不包含期间付息/分红
/// 合约浮动端待实现收益,仅取浮动腿盯市收益,
/// 不包含期间付息/分红,避免与 <see cref="PeriodAmount"/> 重复。
/// </summary>
public decimal FloatingUnrealizedPnl { get; set; }
/// <summary>
/// 付息方式
/// 付息/分红支付方式:到期轧差时计入到期轧差估值,派息日支付时在期间支付口径展示。
/// </summary>
public string InterestPaymentMethod { get; set; }
/// <summary>
/// 合约估值(到期轧差口径)
/// 合约估值(到期轧差口径)= 浮动端待实现收益 + 利率端待实现收益 + 期间付息/分红。
/// 仅当支付方式为到期轧差时赋值。
/// </summary>
public decimal? MaturityNettingValuation { get; set; }
/// <summary>
/// 合约估值(期间支付派息口径)
/// 合约估值(派息日支付口径)= 浮动端待实现收益 + 利率端待实现收益。
/// 派息/分红在支付日独立结算,因此不计入该估值。
/// </summary>
public decimal? PeriodPaymentValuation { get; set; }
/// <summary>
/// 我方收取的保证金利息累计额,按保证金腿方向归集并取绝对值展示。
/// </summary>
public decimal MarginInterestGain { get; set; }
/// <summary>
/// 我方支付的保证金利息累计额,按保证金腿方向归集并取绝对值展示。
/// </summary>
public decimal MarginInterestLoss { get; set; }
}
}
@@ -20,8 +20,7 @@ namespace YLErp.DBModels
/// </summary>
public string StructureType { get; set; }
/// <summary>
/// <summary>
/// 交易对手方名称
/// 交易对手方名称。每日估值报告页面当前不展示该列,但发送报告与其他调用方仍可使用。
/// </summary>
public string ClientName { get; set; }
/// <summary>
@@ -33,57 +32,62 @@ namespace YLErp.DBModels
/// </summary>
public string TradeNumber { get; set; }
/// <summary>
/// 期间付息
/// 期间付息。仅现券标的赋值;ETF、指数及其他标的返回 <c>null</c>,由前端和 Excel 显示为空白。
/// </summary>
public decimal PeriodAmount { get; set; }
public decimal? PeriodAmount { get; set; }
/// <summary>
/// 到期结算日
/// 到期结算日,直接取日终浮动腿的到期日期,不叠加结算规则或节假日顺延。
/// </summary>
public DateTime? MaturitySettlementDate { get; set; }
/// <summary>
/// 期间分红
/// 期间分红。仅 ETF 标的赋值;现券、指数及其他标的返回 <c>null</c>,避免同一金额在不适用列展示。
/// </summary>
public decimal DividendAmount { get; set; }
public decimal? DividendAmount { get; set; }
/// <summary>
/// 期初标的成交收益率。仅现券标的直接取交易录入的 <c>trade.InitYtm</c>;其他标的返回 <c>null</c>。
/// </summary>
public decimal? InitYtm { get; set; }
/// <summary>
/// 期限
/// 实际期限,按估值日与起始日的自然日差加一计算,包含起始日。
/// </summary>
public int DayCount { get; set; }
/// <summary>
/// 期初预付金-不包含追加预付金 取轧差
/// 期初预付金本金,仅汇总初始预付金交易腿;收取为正、支付为负。
/// </summary>
public decimal OpenMarginAmount { get; set; }
/// <summary>
/// 期初预付金利率-不包含追加预付金 取轧差
/// 预付金利率,初始和追加预付金腿按本金规模加权平均
/// </summary>
public decimal OpenMarginRate { get; set; }
/// <summary>
/// 预付金利息 取轧差
/// 预付金利息,初始和追加预付金腿按本金规模加权平均
/// </summary>
public decimal MarginInterestAmount { get; set; }
/// <summary>
/// 追加预付金 取轧差
/// 追加预付金本金,仅汇总估值日前已生效的追加预付金交易腿;收取为正、支付为负。
/// </summary>
public decimal AdditionalMarginAmount { get; set; }
/// <summary>
/// 浮动利率(绝对)利率端待实现收益/(标的名义金额/期初标的交割价格全价)
/// 浮动利率(绝对)= 利率收益金额 / 标的名义金额。
/// 该字段是展示型比例,不参与净额结算金额计算。
/// </summary>
public decimal FloatRateAbs { get; set; }
/// <summary>
/// 利差
/// 利差,汇总非预付金利息腿的约定利率。
/// </summary>
public decimal InterestRate { get; set; }
/// <summary>
/// 利率收益金额 利率端待实现收益
/// 利率收益金额,汇总非预付金利息腿的 <c>InterestIncomeSum</c>,并转换为我方视角。
/// </summary>
public decimal InterestAmount { get; set; }
/// <summary>
/// 净额结算金额 互换持仓价值+待返还的预付金本金
/// 净额结算金额 = 利率收益金额 + 浮动收益金额 + 开平仓交易费用 + 预付金利息
/// + 到期轧差方式下应计入的期间付息/分红;不包含两类预付金本金。
/// </summary>
public decimal NetSettmentAmount { get; set; }
/// <summary>
/// TRS估值
/// TRS估值 = 净额结算金额 + 期初预付金 + 追加预付金。
/// </summary>
public decimal TrsValue { get; set; }
/// <summary>
+16 -2
View File
@@ -15,13 +15,27 @@ namespace YLErp.Model
{
/// <summary>
///
/// </summary>
/// 估值日。每日估值报告的互换估值查询当前按该日期精确筛选,
/// 交易日期同时不得晚于该日期。
/// </summary>
public DateTime? ValueDate { get; set; }
/// <summary>
/// 请求携带的估值日下界。互换持仓明细、交易流水等调用方可使用该字段;
/// 当前 <c>GetSearchEodPositionList</c> 未启用该下界,仍是单日估值查询。
/// </summary>
public DateTime? ValueDateFrom { get; set; }
/// <summary>
/// 对手方筛选条件。为空时不按对手方收窄结果。
/// </summary>
public int? ClientId { get; set; }
/// <summary>
/// 簿记账户筛选条件,对应 <c>trade.AssetId</c>;为空时包含该对手方下全部簿记账户。
/// </summary>
public int? BookId { get; set; }
/// <summary>
/// 调用方传入的结构类型。互换估值查询当前固定同时覆盖普通债券类收益互换和普通收益互换。
/// </summary>
public string StructureType { get; set; }
}
/// <summary>
+1
View File
@@ -156,6 +156,7 @@ namespace YLErp.Model
public const string = "tradeMarginTemplateList";
public const string = "SettmentEodSwapPositionList";
public const string V1 = "SettmentEodSwapPositionListV1";
public const string = "compare_heitai_data";
}
@@ -28,6 +28,13 @@
/// </summary>
public string MarginDetail { get; set; }
public int ClientId { get; set; }
/// <summary>
/// 每日估值报告页面选择的簿记账户。为空时按客户维度生成全量报告;
/// 有值时仅筛选互换估值页的交易所属账户。
/// </summary>
public int? BookId { get; set; }
public DateTime From { get; set; }
public DateTime To { get; set; }
public double PayableMargin { get; set; }
@@ -4,6 +4,7 @@ using OfficeOpenXml.Style;
using Org.BouncyCastle.Ocsp;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
@@ -14,6 +15,7 @@ using YLErp.BLL;
using YLErp.BLL.EodSettlement;
using YLErp.Configuration;
using YLErp.Core.Helpers;
using YLErp.DBModels;
using YLErp.Enums;
using YLErp.Helpers;
using YLErp.Model;
@@ -60,7 +62,16 @@ namespace YLErp.Modules.ReportModule.SettlementReportModule
#endregion
if (emailData.SendContent.Contains("互换估值"))
{
var eodReq = new ClientSwapPositionRequest { ClientId = emailData.ClientId, ValueDate = emailData.To, ValueDateFrom = emailData.From,page=1, rows=10000,StructureType= "普通债券类收益互换" };
var eodReq = new ClientSwapPositionRequest
{
ClientId = emailData.ClientId,
BookId = emailData.BookId,
ValueDate = emailData.To,
ValueDateFrom = emailData.From,
page = 1,
rows = 10000,
StructureType = "普通债券类收益互换"
};
report.EodSwapPositions = swapEodPositionService.SearchEodPositionList(eodReq).rows.ToList();
}
if (emailData.SendContent.Contains("互换持仓明细"))
@@ -208,7 +219,7 @@ namespace YLErp.Modules.ReportModule.SettlementReportModule
Directory.CreateDirectory(targetPath);
}
var clientName = report.client.Name;
var fileName = report.ReportFrom == DateTime.MinValue ? $"证券_估值表_{report.ReportEnd:yyyyMMdd}_{clientName}" : $"证券_估值表_{report.ReportFrom:yyyyMMdd}_{report.ReportEnd:yyyyMMdd}_{clientName}";
var fileName = $"{clientName}_每日估值报告_{report.ReportEnd:yyyyMMdd}";
var targetFileName = Path.Combine(targetPath, $"{fileName}.xlsx");
var excelDeclareModel = new ExcelDeclareModel()
@@ -238,14 +249,22 @@ namespace YLErp.Modules.ReportModule.SettlementReportModule
{
modelDict.Add("互换估值", new
{
// 明细列表供模板渲染;以下 *Sum 字段用于“互换估值”页签合计行。
EodSwapPositions = report.EodSwapPositions,
// 持仓规模、期间收益和利息/分红类金额合计。
PosiNotionalValueSum= report.EodSwapPositions.Sum(x => x.position.PosiNotionalValue),
PosiQuantitySum= report.EodSwapPositions.Sum(x => x.position.PosiQuantity),
PeriodAmountSum= report.EodSwapPositions.Sum(x => x.PeriodAmount),
PeriodAmountSum= report.EodSwapPositions.Sum(x => x.PeriodAmount) ?? 0m,
DividendAmountSum = report.EodSwapPositions.Sum(x => x.DividendAmount) ?? 0m,
InterestAmountSum= report.EodSwapPositions.Sum(x => x.InterestAmount),
PosiFeePendingSum = report.EodSwapPositions.Sum(x => x.position.PosiFeePending),
PosiProfitSum= report.EodSwapPositions.Sum(x => x.position.PosiProfitSum),
// 保证金相关收益和保证金占用金额合计。
MarginInterestAmountSum = report.EodSwapPositions.Sum(x => x.MarginInterestAmount),
OpenMarginAmountSum = report.EodSwapPositions.Sum(x => x.OpenMarginAmount),
AdditionalMarginAmountSum = report.EodSwapPositions.Sum(x => x.AdditionalMarginAmount),
// 净结算金额为日终估值口径;TRS价值在净结算金额基础上叠加期初/追加保证金。
NetSettmentAmountSum= report.EodSwapPositions.Sum(x => x.NetSettmentAmount),
TrsValueSum = report.EodSwapPositions.Sum(x => x.TrsValue),
});
}
if (report.SwapPositions != null)
@@ -287,7 +306,10 @@ namespace YLErp.Modules.ReportModule.SettlementReportModule
string sourceFileName = Path.Combine(sourcePath, $"结算报告模板.xlsx");
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
var pdffile = ExcelTemplate.GeneratePDFFromExeclTemplate(sourcePath, sourceFileName, modelDict, targetPath, targetFileName
, shouldDeleteSheet: true, needToPdf: false);
, shouldDeleteSheet: true, needToPdf: false, callback: sheets =>
{
FormatSwapValuationDisplayCells(sheets, report.EodSwapPositions);
});
if (needToPdf)
{
var targetPdfFileName = FileHelper.ReplaceExtension(targetFileName, ".pdf");
@@ -297,6 +319,53 @@ namespace YLErp.Modules.ReportModule.SettlementReportModule
return Path.Combine(targetPath, targetFileName);
}
/// <summary>
/// 部分 Office 版本会将可选小数格式(例如 <c>#,##0.##</c>)错误显示为 <c>20,000.</c>。
/// 互换估值中的这些列是展示字段,不参与 Excel 公式计算,因此在模板替换完成后写为已格式化文本,
/// 既保留去尾零口径,也避免留下孤立的小数点。
/// </summary>
private static void FormatSwapValuationDisplayCells(IEnumerable<ExcelWorksheet> sheets, IEnumerable<EodSwapPositionResponse> positions)
{
var worksheet = sheets.FirstOrDefault(x => x.Name == "互换估值");
var positionList = positions?.ToList() ?? new List<EodSwapPositionResponse>();
if (worksheet == null || !positionList.Any())
{
return;
}
const int dataStartRow = 2;
for (var index = 0; index < positionList.Count; index++)
{
var row = dataStartRow + index;
var item = positionList[index];
SetTrimmedExcelText(worksheet.Cells[row, 8], item.position.PosiNotionalValue, 2);
SetTrimmedExcelText(worksheet.Cells[row, 9], item.position.PosiQuantity, 9);
SetTrimmedExcelText(worksheet.Cells[row, 10], item.PeriodAmount, 2);
SetTrimmedExcelText(worksheet.Cells[row, 11], item.DividendAmount, 2);
SetTrimmedExcelText(worksheet.Cells[row, 13], item.InitYtm, 4, percent: true);
}
var totalRow = dataStartRow + positionList.Count;
SetTrimmedExcelText(worksheet.Cells[totalRow, 8], positionList.Sum(x => x.position.PosiNotionalValue), 2);
SetTrimmedExcelText(worksheet.Cells[totalRow, 10], positionList.Sum(x => x.PeriodAmount) ?? 0m, 2);
SetTrimmedExcelText(worksheet.Cells[totalRow, 11], positionList.Sum(x => x.DividendAmount) ?? 0m, 2);
}
private static void SetTrimmedExcelText(ExcelRange cell, decimal? value, int decimalPlaces, bool percent = false)
{
if (!value.HasValue)
{
cell.Value = null;
return;
}
var displayValue = percent ? value.Value * 100m : value.Value;
var format = "#,##0." + new string('#', decimalPlaces);
var text = displayValue.ToString(format, CultureInfo.InvariantCulture).TrimEnd('.');
cell.Value = percent ? text + "%" : text;
cell.Style.Numberformat.Format = "@";
}
/// <summary>
/// 财务状况
/// </summary>
@@ -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;
@@ -6,9 +6,8 @@
var pageObj = new
{
EndTime = ViewBag.EndTime?.ToString(),
StartTime = ViewBag.StartTime?.ToString(),
valueDate = valuedateBLL.ValueDate.ToString("yyyy-MM-dd"),
configcolumn = configcolumn_data.互换估值,
configcolumn = configcolumn_data.互换估值V1,
StructureType = ViewBag.StructureType
};
}
@@ -23,7 +22,7 @@
width: 250px !important;
}
#ValueDateFrom, #ValueDate {
#ValueDate {
width: 152px !important;
font-size: 1rem !important;
}
@@ -51,9 +50,8 @@
<div class="searchdiv">
@Html.MyAceDropdownInput("ClientId", "客户名称", ClientDataModel.GetAllClient(), false, true, null, false)
@Html.MyAceDropdownInput("BookId", "簿记账户", AssetunitController.GetClientassetunit(), false, true, null, false)
@Html.CheckBox("ParentFlag", false) @Html.Label(null,"包含子级", new { @style = "font-size:12px;" })
@Html.ShortInput("ValueDateFrom", "起始日期:")
@Html.MyAceDropdownInput("BookId", "簿记账户", AssetunitController.GetClientassetunit(), false, true, null, false)
@Html.ShortInput("ValueDate", "结束日期:")
@MyControls.SearchBtn()
@if (CurUser.结算管理_每日估值报告邮件发送)
@@ -2,30 +2,13 @@
$(function () {
var PostData = {};
//控件选择时的触发事件
main.setTradeDatePicker("", "#ValueDate", page.calcDate, function (selectedDate) {
if (selectedDate) {
$("#ValueDateFrom").datepicker("option", "maxDate", selectedDate);
}
});
main.setTradeDatePicker("", "#ValueDateFrom", page.calcDate, function (selectedDate) {
if (selectedDate) {
$("#ValueDate").datepicker("option", "minDate", selectedDate);
}
});
//手动修改时的触发事件
$("#ValueDate").change(function () {
$("#ValueDateFrom").datepicker("option", "maxDate", $("#ValueDate").val());
});
$("#ValueDateFrom").change(function () {
$("#ValueDate").datepicker("option", "minDate", $("#ValueDateFrom").val());
});
main.setTradeDatePicker("", "#ValueDate", page.calcDate);
//默认初始值赋值逻辑
// 互换估值页仅暴露一个估值日。当前后端按 ValueDate 单日查询,
// 因此 ValueDateFrom 传同日仅用于保持请求对象和报告参数的日期语义一致。
$("#ValueDate").val(page.EndTime || page.calcDate);
$("#ValueDateFrom").val(page.StartTime && page.EndTime ? page.StartTime : '');
PostData.StructureType = page.StructureType;
PostData.ValueDateFrom = $("#ValueDateFrom").val();
PostData.ValueDateFrom = $("#ValueDate").val();
PostData.ValueDate = $("#ValueDate").val();
PostData.ClientId = $("#ClientId").val();
PostData.BookId = $("#BookId").val();
@@ -110,11 +93,7 @@ function searchPositionDetials(isSearchclick) {
// main.message("结束日期不能大于当前系统日期!");
// return;
//}
if ($("#ValueDate").val() < $("#ValueDateFrom").val()) {
main.message("起始日期不能大于结束日期!");
return;
}
listGrid.appendPostData({ ValueDateFrom: $("#ValueDateFrom").val() });
listGrid.appendPostData({ ValueDateFrom: $("#ValueDate").val() });
listGrid.appendPostData({ ValueDate: $("#ValueDate").val() });
listGrid.appendPostData({ StructureType: page.StructureType });
if (typeof (isSearchclick) != "undefined" && isSearchclick) {
@@ -156,14 +135,6 @@ var colModelGrid = [
index: 'UnwindDate',
hidden: true,
optionHide: true
}, {
name: 'TradeNumber',
label: '交易编号',
index: 'TradeNumber',
sortIndex: i++,
width: 180,
align: 'center',
sortable: false
}, {
name: 'ConfrimNo',
label: '确认书编号',
@@ -173,11 +144,11 @@ var colModelGrid = [
align: 'center',
sortable: false
}, {
name: 'ClientName',
label: '交易对手',
index: 'ClientName',
name: 'TradeNumber',
label: '交易编号',
index: 'TradeNumber',
sortIndex: i++,
width: 120,
width: 180,
align: 'center',
sortable: false
}, {
@@ -222,21 +193,6 @@ var colModelGrid = [
align: 'center',
sortable: false,
formatter: SpreadRateFormat
}, {
name: 'position.FloatRateUnderlyingCode',
label: '基准利率',
index: 'position.FloatRateUnderlyingCode',
width: 120,
align: 'center',
sortable: false
}, {
name: 'position.FloatRate',
label: '当日适用基准利率',
index: 'position.FloatRate',
width: 120,
align: 'center',
sortable: false,
formatter: RateFormat
}, {
name: 'position.PosiNotionalValue',
label: '标的名义金额',
@@ -244,7 +200,7 @@ var colModelGrid = [
width: 150,
align: 'center',
sortable: false,
formatter: StockEqvNotionalFormat,
formatter: AmountFormat,
}, {
name: 'position.PosiQuantity',
label: '标的数量',
@@ -253,21 +209,21 @@ var colModelGrid = [
align: 'center',
sortable: false,
formatter: StockEqvNotionalFormat,
}, {
name: 'DividendAmount',
label: '期间分红',
index: 'DividendAmount',
width: 100,
align: 'center',
formatter: AmountFormat,
sortable: false
}, {
name: 'PeriodAmount',
label: '期间付息',
index: 'PeriodAmount',
width: 100,
align: 'center',
formatter: AmountFormat,
formatter: NullableAmountFormat,
sortable: false
}, {
name: 'DividendAmount',
label: '期间分红',
index: 'DividendAmount',
width: 100,
align: 'center',
formatter: NullableAmountFormat,
sortable: false
}, {
name: 'position.PosiGrossPrice',
@@ -284,12 +240,7 @@ var colModelGrid = [
width: 150,
align: 'center',
sortable: false,
formatter: function (cellValue, options, rowObject) {
if (cellValue == null) {
return "";
}
return otcformat.trading.premiumRateP(cellValue);
}
formatter: YieldRateFormat
},
{
name: 'position.UnderlyingPrice',
@@ -320,15 +271,7 @@ var colModelGrid = [
index: 'InterestAmount',
width: 120,
align: 'center',
formatter: StockEqvNotionalFormat,
sortable: false,
}, {
name: 'position.PosiFeePending',
label: '开平仓交易费用',
index: 'position.PosiFeePending',
width: 120,
align: 'center',
formatter: StockEqvNotionalFormat,
formatter: FixedAmountFormat,
sortable: false,
}, {
name: 'position.PosiProfitSum',
@@ -336,7 +279,15 @@ var colModelGrid = [
index: 'position.PosiProfitSum',
width: 120,
align: 'center',
formatter: StockEqvNotionalFormat,
formatter: FixedAmountFormat,
sortable: false,
}, {
name: 'position.PosiFeePending',
label: '开平仓交易费用',
index: 'position.PosiFeePending',
width: 120,
align: 'center',
formatter: FixedAmountFormat,
sortable: false,
}, {
name: 'OpenMarginRate',
@@ -344,7 +295,7 @@ var colModelGrid = [
index: 'OpenMarginRate',
width: 120,
align: 'center',
formatter: RateFormat,
formatter: TrimmedRateFormat,
sortable: false,
}, {
name: 'MarginInterestAmount',
@@ -352,7 +303,7 @@ var colModelGrid = [
index: 'MarginInterestAmount',
width: 120,
align: 'center',
formatter: AmountFormat,
formatter: FixedAmountFormat,
sortable: false,
}, {
name: 'OpenMarginAmount',
@@ -360,7 +311,7 @@ var colModelGrid = [
index: 'OpenMarginAmount',
width: 120,
align: 'center',
formatter: AmountFormat,
formatter: FixedAmountFormat,
sortable: false,
}, {
name: 'AdditionalMarginAmount',
@@ -368,7 +319,7 @@ var colModelGrid = [
index: 'AdditionalMarginAmount',
width: 120,
align: 'center',
formatter: AmountFormat,
formatter: FixedAmountFormat,
sortable: false,
}, {
name: 'NetSettmentAmount',
@@ -376,7 +327,7 @@ var colModelGrid = [
index: 'NetSettmentAmount',
width: 120,
align: 'center',
formatter: StockEqvNotionalFormat,
formatter: FixedAmountFormat,
sortable: false,
}, {
name: 'TrsValue',
@@ -384,13 +335,15 @@ var colModelGrid = [
index: 'TrsValue',
width: 120,
align: 'center',
formatter: StockEqvNotionalFormat,
formatter: FixedAmountFormat,
sortable: false,
}
];
// 页面字段顺序以《估值模块V1》第二部分为准;历史个人列配置只能控制显隐,不能打乱业务列顺序。
var defaultVisibleColumnNames = colModelGrid
.filter(function (column) { return column.hidden !== true; })
.map(function (column) { return column.name; });
var documentColumnOrder = colModelGrid.map(function (column) { return column.name; });
function formatter6(cellvalue, options, rowObject) {
return main.formatNumber(cellvalue, 6);
@@ -421,12 +374,27 @@ function gridComplete() {
}
//jgrid.sortGrid(g_sort.name, g_sort.order);
$.when(main.setcolumnChooser(jgrid, page.configcolumn)).always(function () {
restoreDocumentColumnOrder(jgrid);
ensureBusinessColumnsVisible(jgrid);
});
$(".selftooltip").tooltip({ html: true, show: 50000, trigger: "hover" });
$(window).off('resize.jqGrid');
}
function restoreDocumentColumnOrder(jgrid) {
var colModel = jgrid.jqGrid('getGridParam', 'colModel') || [];
var currentNames = colModel.map(function (column) { return column.name; });
var targetNames = currentNames
.filter(function (name) { return documentColumnOrder.indexOf(name) < 0; })
.concat(documentColumnOrder);
var permutation = targetNames.map(function (name) { return currentNames.indexOf(name); });
if (permutation.length === currentNames.length
&& permutation.every(function (index) { return index >= 0; })
&& permutation.some(function (index, targetIndex) { return index !== targetIndex; })) {
jgrid.jqGrid('remapColumns', permutation, true);
}
}
function ensureBusinessColumnsVisible(jgrid) {
var colModel = jgrid.jqGrid('getGridParam', 'colModel') || [];
var hasVisibleBusinessColumn = colModel.some(function (column) {
@@ -475,10 +443,6 @@ function SendReport() {
main.message("结束日期不能大于当前系统日期!");
return;
}
if ($("#ValueDate").val() < $("#ValueDateFrom").val()) {
main.message("起始日期不能大于结束日期!");
return;
}
main.open("向{0}发送报告".template(clientName), "/clientbalance/TradeMarketClientSend?clientid=" + param.ClientId + "&ParentFlag=" + param.ParentFlag);
}
function DownLoadReport() {
@@ -499,18 +463,16 @@ function DownLoadReport() {
main.message("结束日期不能大于当前系统日期!");
return;
}
if ($("#ValueDate").val() < $("#ValueDateFrom").val()) {
main.message("起始日期不能大于结束日期!");
return;
}
main.post("/clientbalance/ViewTradeMarketFile", screenData()).done(function (res) {
window.open(res.obj);
});
}
function screenData() {
var data = { From: $("#ValueDateFrom").val(), To: $("#ValueDate").val() };
// 已移除起始日期控件,预览报告按单个估值日生成,From/To 保持同日。
var data = { From: $("#ValueDate").val(), To: $("#ValueDate").val() };
data.ClientId = $("#ClientId").val();
// 报告下载和发送弹窗均从 screenData 取参数,必须保留当前簿记账户筛选。
data.BookId = $("#BookId").val();
if ($("#ParentFlag").prop("checked"))
data.ParentFlag = true;
else
@@ -522,7 +484,7 @@ function showChiCang() {
main.showcolumnChooser(jQuery('#listGrid'), page.configcolumn);
}
function PriceFormat(cellValue, options, rowObject) {
return otcformat.trading.umprice(cellValue);
return main.formatNumber(cellValue, 9, { grouping: true });
}
function StockEqvNotionalFormat(cellValue, options, rowObject) {
@@ -531,6 +493,26 @@ function StockEqvNotionalFormat(cellValue, options, rowObject) {
function AmountFormat(cellValue, options, rowObject) {
return main.formatNumber(cellValue, 2, { trimTailZeros: true });
}
function NullableAmountFormat(cellValue, options, rowObject) {
if (cellValue === null || cellValue === undefined || cellValue === '') {
return '';
}
return AmountFormat(cellValue, options, rowObject);
}
function FixedAmountFormat(cellValue, options, rowObject) {
return main.formatNumber(cellValue, 2);
}
function YieldRateFormat(cellValue, options, rowObject) {
if (cellValue === null || cellValue === undefined || cellValue === '') {
return '';
}
// percent 格式会在数字末尾添加 %,通用 trimTailZeros 无法识别其后的 0。
// 先将小数收益率转为百分比数值,再格式化并追加 %,确保最多保留四位小数且去尾零。
return main.formatNumber(cellValue * 100, 4, { trimTailZeros: true }) + '%';
}
function TrimmedRateFormat(cellValue, options, rowObject) {
return main.formatNumber(cellValue, 4, { percent: true, trimTailZeros: true });
}
function RateFormat(cellValue, options, rowObject) {
if (cellValue) {
var num = new Number(cellValue) * 100;
@@ -552,14 +534,10 @@ function locationChange(tab) {
main.message("结束日期不能大于当前系统日期!");
return;
}
if ($("#ValueDate").val() < $("#ValueDateFrom").val()) {
main.message("起始日期不能大于结束日期!");
return;
}
if ($("#ParentFlag").prop("checked"))
ParentFlag = true;
else
ParentFlag = false;
window.location.href = tab + "?clientId=" + $("#ClientId").val() + "&startTime=" + $("#ValueDateFrom").val() + "&endTime=" + $("#ValueDate").val() + "&ParentFlag=" + ParentFlag;
window.location.href = tab + "?clientId=" + $("#ClientId").val() + "&startTime=" + $("#ValueDate").val() + "&endTime=" + $("#ValueDate").val() + "&ParentFlag=" + ParentFlag;
return;
}