207 lines
9.6 KiB
C#
207 lines
9.6 KiB
C#
using YLErp.DBModels;
|
|
using YLErp.DBModels.Consts;
|
|
using YLErp.DBModels.Enums;
|
|
using YLErp.Models;
|
|
using YLErp.Modules.SwapModule;
|
|
using YLErp.Modules.SwapModule.ReturnLegs;
|
|
|
|
namespace YLErp.Modules.TradeModule.DocGenerateModule;
|
|
|
|
/// <summary>
|
|
/// 构造普通收益互换结算单单行数据所需的业务输入。
|
|
/// 数据来自平仓事件、关联交易及结算日日终持仓。
|
|
/// </summary>
|
|
public sealed class SwapSettlementBillRowInput
|
|
{
|
|
/// <summary>有效交易确认书编号。</summary>
|
|
public string ConfirmNo { get; set; }
|
|
|
|
/// <summary>结算单所属交易对手名称。</summary>
|
|
public string ClientName { get; set; }
|
|
|
|
/// <summary>平仓事件关联的收益互换交易。</summary>
|
|
public trade Trade { get; set; }
|
|
|
|
/// <summary>当前生成行对应的平仓流水。</summary>
|
|
public swap_flow_event CloseFlow { get; set; }
|
|
|
|
/// <summary>与当前平仓事件同 EventId 的利息、预付金等流水。</summary>
|
|
public IReadOnlyList<swap_flow_event> EventFlows { get; set; }
|
|
|
|
/// <summary>交易的初始持仓腿,用于识别融资腿、浮动腿和预付金。</summary>
|
|
public IList<swap_position> Positions { get; set; }
|
|
|
|
/// <summary>标的品种,用于区分现券付息、ETF 分红和收益率字段。</summary>
|
|
public string UnderlyingInstrumentType { get; set; }
|
|
|
|
/// <summary>本次平仓标的名义本金。</summary>
|
|
public decimal CloseNotionalValue { get; set; }
|
|
|
|
/// <summary>平仓事件浮动腿记录的期末结算收益率(展示态数值)。</summary>
|
|
public decimal? ExitYtm { get; set; }
|
|
|
|
/// <summary>结算日日终持仓中的当日浮动端分红,保留原始收付方向。</summary>
|
|
public decimal CurrentDayFloatingDividend { get; set; }
|
|
|
|
/// <summary>期间付息或分红是否计入本次净额结算。</summary>
|
|
public bool IncludePeriodPaymentInNetting { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// 将平仓事件数据按国联普通结算单口径计算并格式化为模板行。
|
|
/// </summary>
|
|
public static class SwapSettlementBillRowBuilder
|
|
{
|
|
/// <summary>
|
|
/// 计算一条结算记录。金额方向统一转换为客户视角,返回值已按模板精度格式化。
|
|
/// </summary>
|
|
public static ExcelReportModel Build(SwapSettlementBillRowInput input)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(input);
|
|
ArgumentNullException.ThrowIfNull(input.Trade);
|
|
ArgumentNullException.ThrowIfNull(input.CloseFlow);
|
|
|
|
var positions = input.Positions ?? Array.Empty<swap_position>();
|
|
var eventFlows = input.EventFlows ?? Array.Empty<swap_flow_event>();
|
|
|
|
// 三个业务日期是结算单和文件命名的必填项,缺失时直接阻止生成不完整附件。
|
|
var startDate = input.Trade.StartDate
|
|
?? throw new ServiceException("结算单缺少交易起始日");
|
|
var eventDate = input.CloseFlow.UnwindDate
|
|
?? throw new ServiceException("结算单缺少结算日");
|
|
var payDate = input.CloseFlow.PayDate
|
|
?? throw new ServiceException("结算单缺少支付日");
|
|
|
|
// 将同一事件的普通利息与预付金利息分开;预付金本金仅统计结算日前已生效的腿。
|
|
var settlementPosition = positions.FirstOrDefault(x => x.id == input.CloseFlow.PositionId);
|
|
var interestEvents = eventFlows
|
|
.Where(x => ConsTrade.InterestModels.Contains(x.InterestMode))
|
|
.ToList();
|
|
var marginEvents = eventFlows
|
|
.Where(x => ConsTrade.InterestMarginModels.Contains(x.InterestMode))
|
|
.ToList();
|
|
var effectiveMargins = positions
|
|
.Where(x => ConsTrade.InterestMarginModels.Contains(x.InterestMode)
|
|
&& (!x.HappenDate.HasValue || x.HappenDate.Value <= eventDate))
|
|
.ToList();
|
|
|
|
// 数据库存储的是系统/券商视角金额,结算单统一取反为客户视角;预付金本金按收付方向换算。
|
|
var interestAmount = -interestEvents.Sum(x => x.InterestClosePnL);
|
|
var floatingAmount = -input.CloseFlow.MarkClosePnl;
|
|
var fee = -(input.CloseFlow.TradingFee + input.CloseFlow.TradingFeePending);
|
|
var marginInterest = -marginEvents.Sum(x => x.InterestClosePnL);
|
|
var initialMargin = SumMargin(effectiveMargins, InterestModeEnum.初始预付金);
|
|
var additionalMargin = SumMargin(effectiveMargins, InterestModeEnum.追加预付金);
|
|
|
|
// 净额结算仅包含全部事件利息和浮动盈亏,不包含预付金返还本金。
|
|
var netSettlementAmount = -eventFlows.Sum(x => x.InterestClosePnL)
|
|
- input.CloseFlow.FloatPnlSum;
|
|
var maturitySettlementAmount = netSettlementAmount + initialMargin + additionalMargin;
|
|
var floatRateAbs = input.CloseNotionalValue == 0m
|
|
? 0m
|
|
: interestAmount / input.CloseNotionalValue;
|
|
|
|
// 日终持仓的当日浮动端分红按标的类型拆分:债券展示期间付息,非债券展示期间分红。
|
|
var isCashBond = ConsGlobal.InstrumentType.IsBond(input.UnderlyingInstrumentType);
|
|
|
|
// 此处集中完成模板字段映射和展示精度处理,生成器只负责组装原始业务数据。
|
|
return new ExcelReportModel
|
|
{
|
|
TradeNumber = input.ConfirmNo,
|
|
ClientName = input.ClientName,
|
|
StartDate = startDate.ToString("yyyy-MM-dd"),
|
|
EventDate = eventDate.ToString("yyyy-MM-dd"),
|
|
PayDate = payDate.ToString("yyyy-MM-dd"),
|
|
UnderlyingCode = input.CloseFlow.UnderlyingCode,
|
|
BaseRate = ResolveBaseRate(positions),
|
|
InterestRate = interestEvents.Sum(x => x.InterestRate).ToString("0.00%"),
|
|
PosiNotionalValue = input.CloseNotionalValue.ToString("0.00"),
|
|
Quantity = input.CloseFlow.Quantity.ToString("0.00"),
|
|
DividendIn = isCashBond
|
|
? input.CurrentDayFloatingDividend.ToString("0.00")
|
|
: string.Empty,
|
|
PeriodDividend = isCashBond
|
|
? string.Empty
|
|
: input.CurrentDayFloatingDividend.ToString("0.00"),
|
|
PosiNetPrice = ((settlementPosition?.PosiGrossPrice ?? 0m) * 100m).ToString("0.00000000"),
|
|
InitYtm = isCashBond && input.Trade.InitYtm.HasValue
|
|
? input.Trade.InitYtm.Value.ToString("0.####%")
|
|
: string.Empty,
|
|
ClosePrice = (input.CloseFlow.TradingAmountAvg * 100m).ToString("0.00000000"),
|
|
ExitYtm = input.ExitYtm.HasValue
|
|
? input.ExitYtm.Value.ToString("0.0000")
|
|
: string.Empty,
|
|
RateDays = Math.Max(0, (eventDate - startDate).Days).ToString(),
|
|
FloatRateAbs = floatRateAbs.ToString("0.0000%"),
|
|
FloatRate = floatRateAbs.ToString("0.0000%"),
|
|
InterestAmount = interestAmount.ToString("0.00"),
|
|
PosiPnl = floatingAmount.ToString("0.00"),
|
|
Fee = fee.ToString("0.00"),
|
|
MarginRebateRate = EodPnlCalculator.CalculateWeightedMarginRate(effectiveMargins)
|
|
.ToString("0.0000%"),
|
|
MarginInterestAmount = marginInterest.ToString("0.00"),
|
|
InitialMargin = initialMargin.ToString("0.00"),
|
|
AdditionalMargin = additionalMargin.ToString("0.00"),
|
|
MarginAmout = Math.Abs(initialMargin).ToString("0.00"),
|
|
MarkClosePnl = (-input.CloseFlow.FloatPnlSum).ToString("0.00"),
|
|
NetSettleAmout = netSettlementAmount.ToString("0.00"),
|
|
MaturitySettlementAmount = maturitySettlementAmount.ToString("0.00")
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// 校验同一附件中的支付日唯一,并返回用于文件名的支付日。
|
|
/// </summary>
|
|
public static DateTime GetSinglePayDate(IEnumerable<DateTime?> payDates)
|
|
{
|
|
var dates = payDates?.ToList()
|
|
?? throw new ServiceException("结算单缺少支付日");
|
|
if (dates.Count == 0 || dates.Any(x => !x.HasValue))
|
|
{
|
|
throw new ServiceException("结算单缺少支付日");
|
|
}
|
|
|
|
var distinctDates = dates.Select(x => x.Value.Date).Distinct().OrderBy(x => x).ToList();
|
|
if (distinctDates.Count != 1)
|
|
{
|
|
throw new ServiceException(
|
|
$"同一结算单存在多个支付日:{string.Join("、", distinctDates.Select(x => x.ToString("yyyy-MM-dd")))}");
|
|
}
|
|
|
|
return distinctDates[0];
|
|
}
|
|
|
|
/// <summary>
|
|
/// 按交易对手和支付日生成普通结算单文件名;重复生成时追加顺序号。
|
|
/// </summary>
|
|
public static string BuildOutputFileName(string clientName, DateTime payDate, int count)
|
|
{
|
|
var suffix = count == 0 ? string.Empty : $"-{count + 1}";
|
|
return $"结算单-{clientName}-{payDate:yyyy-MM-dd}{suffix}.xlsx";
|
|
}
|
|
|
|
/// <summary>
|
|
/// 按预付金类型汇总本金,并根据收付方向转换为结算方向金额。
|
|
/// </summary>
|
|
private static decimal SumMargin(
|
|
IEnumerable<swap_position> margins,
|
|
InterestModeEnum interestMode)
|
|
{
|
|
return margins
|
|
.Where(x => x.InterestMode == (int)interestMode)
|
|
.Sum(x => x.InterestPrincipalFix * DirectionRatio.ReceivePay(x.InterestDirection));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 从非预付金融资腿读取挂钩基准;未配置浮动基准时按固定利率展示。
|
|
/// </summary>
|
|
private static string ResolveBaseRate(IEnumerable<swap_position> positions)
|
|
{
|
|
return positions
|
|
.Where(x => !ConsTrade.InterestMarginModels.Contains(x.InterestMode))
|
|
.Select(x => x.FloatRateUnderlyingCode)
|
|
.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x))
|
|
?? "固定利率";
|
|
}
|
|
}
|