Merge branch 'glms/feature/1.4.2' into glms/feature/0812_zmr_divPower
# Conflicts: # YLErpDAL/Modules/EodModule/BondPaymentService.cs # YLErpDAL/Modules/SwapModule/SwapDealService.cs
This commit is contained in:
@@ -52,11 +52,7 @@ SwapModule/
|
||||
│
|
||||
├── Margin/ 保证金(mode 5/6)
|
||||
│ ├── MarginModes mode 判断(含 ForLinq for EF Core)
|
||||
│ ├── MarginBalance 保证金余额(值对象)
|
||||
│ ├── MarginAccount 余额管理 + AccrueInterest 计息入口
|
||||
│ ├── MarginCalc 纯函数(PreviousBalance/FlipDirection/AccumulateSettlement)
|
||||
│ ├── IMarginResolver 保证金形态接口
|
||||
│ └── Cash/Credit/Guarantee 三种形态实现
|
||||
│ └── MarginCalc 纯函数(PreviousBalance/FlipDirection/AccumulateSettlement)
|
||||
│
|
||||
├── ReturnLegs/ 标的端
|
||||
│ ├── ReturnLegSummary 标的端汇总值
|
||||
@@ -66,6 +62,14 @@ SwapModule/
|
||||
│ ├── DirectionRatio 方向因子(LongShort + ReceivePay)
|
||||
│ └── PositionValueCalc 持仓价值汇总(利息端 + 浮动端)
|
||||
│
|
||||
├── Accrual/ 计息(生产实现,自洽域)
|
||||
│ ├── InterestMath 共用数学:Round/AccrualDays/FundingLegPrecision + AccrualBoundary/InterestResult
|
||||
│ ├── SimpleInterestAccrual 单利纯函数(AccrueEod 单日 + AccruePeriod 多日)
|
||||
│ ├── CompoundInterestAccrual 复利纯函数(EodBasis/AccrueEod/AccruePeriod)
|
||||
│ ├── AccrualPolicy 计息政策(算头算尾/单复利/重置周期/年化)
|
||||
│ ├── AccrualTrace 计息 trace 收集器(SwapCalcTrace.Write 常驻落盘)
|
||||
│ └── FundingLegRate all-in 利率值对象
|
||||
│
|
||||
├── SwapDealService.cs 盘中平仓/互换主逻辑
|
||||
├── SwapEodPositionService.cs EOD 日终归档主逻辑
|
||||
├── SwapDealIndexFixer.cs SwapDealService 专用取价器(委托 TryGetFloatRate)
|
||||
@@ -76,12 +80,16 @@ SwapModule/
|
||||
|
||||
```
|
||||
Interest/
|
||||
├── SwapInterest.cs 纯函数库(AccrueSimple/AccrueCompound/ApplyUnwind)
|
||||
├── IIndexFixer.cs 取价接口
|
||||
├── IndexFixerBase.cs 取价日计算工具
|
||||
└── Fr007IndexFixer.cs FR007 取价生产实现(调 EodPriceQueryService)
|
||||
└── IndexFixerBase.cs 取价日计算工具
|
||||
```
|
||||
|
||||
> 注:① `Fr007IndexFixer.cs`(FR007 取价生产实现)在 SwapModule 下,不在本目录。
|
||||
> ② 2026-08 计息类型(InterestMath/AccrualBoundary/InterestResult/AccrualTrace)已整体迁至 SwapModule/Accrual/,
|
||||
> Core 不再持有计息实现。原 Core 层 SwapInterest 的算法方法(AccrueSimple/AccrueCompoundInArrears/ApplyUnwind/
|
||||
> AccrueUnrealized/ToInterestRate)与 AccrualContext/InterestRate 从未接线(生产走 Accrual/ 目录),作为孤儿死代码删除——
|
||||
> 其舍入/rollover 口径与生产实现已分叉,若将来重建须先补对账测试,勿凭记忆复原。
|
||||
|
||||
## InterestModeEnum(显式赋值,DB 契约)
|
||||
|
||||
```
|
||||
@@ -132,7 +140,7 @@ Unknown = 0
|
||||
|---|---|---|
|
||||
| 公司行为(送股/拆股) | QtyRollforward.corpActionDeltaQty | ✅ |
|
||||
| 公司行为(登记日快照) | DividendCalc + BondPayment | 见 corp-action-refactor-proposal.md |
|
||||
| 保证金配置/规则/占用 | MarginAccount + MarginCalc | ✅ |
|
||||
| 保证金配置/规则/占用 | MarginCalc | ✅ |
|
||||
| RecordMarginCashFlow 迁入 Margin | AddClientCash 加 virtual | 待做 |
|
||||
| EOD 编排拆分 | SwapPositionCompose | 待业务需求驱动 |
|
||||
```
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using YLErp.Derivatives.Interest;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
/// <summary>
|
||||
@@ -11,7 +9,7 @@ namespace YLErp.Modules.SwapModule.Accrual;
|
||||
/// </summary>
|
||||
public sealed class AccrualPolicy
|
||||
{
|
||||
/// <summary>算头算尾约定(复用 SwapInterest 已有的 AccrualBoundary,物理上杜绝 calcFirst/calcLast 传反)。</summary>
|
||||
/// <summary>算头算尾约定(AccrualBoundary,物理上杜绝 calcFirst/calcLast 传反)。</summary>
|
||||
public AccrualBoundary Convention { get; }
|
||||
|
||||
/// <summary>是否复利(利滚利)。来自 DB 的 InterestTypeEnum;单利=false,复利=true。</summary>
|
||||
@@ -29,4 +27,8 @@ public sealed class AccrualPolicy
|
||||
|
||||
public AccrualPolicy(AccrualBoundary convention, bool isCompound, int resetPeriodDays, int annualDays, bool isAnnualized = false)
|
||||
=> (Convention, IsCompound, ResetPeriodDays, AnnualDays, IsAnnualized) = (convention, isCompound, resetPeriodDays, annualDays, isAnnualized);
|
||||
|
||||
/// <summary>从 swap_position 构造 EOD 计息政策(算头算尾,重置周期取 interest_rest_days)。</summary>
|
||||
public static AccrualPolicy BuildEod(DBModels.swap_position position, int annualDays, bool isCompound)
|
||||
=> new(AccrualBoundary.Both, isCompound, position.interest_rest_days ?? 1, annualDays, position.IsAnnualized);
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
/// <summary>
|
||||
/// 融资腿逐日计息的跨日状态(不可变值对象)。
|
||||
/// 这是"待实现利息"在日间滚动的快照,区别于已落库的 <c>swap_flow_event</c>。
|
||||
///
|
||||
/// 旧字段 → 领域命名映射(DB 列不可改,仅在边界处适配;本类内部一律用下列自描述名):
|
||||
/// <list type="table">
|
||||
/// <item><term>TdInterestPrincipal</term><description>逐日滚动的计息本金 → <see cref="AccrualPrincipal"/></description></item>
|
||||
/// <item><term>InterestIncomeSum</term><description>累计待实现利息 → <see cref="UnrealizedInterest"/></description></item>
|
||||
/// <item><term>consumedInterest</term><description>历史已实现利息(legacy) → <see cref="RealizedInterest"/></description></item>
|
||||
/// <item><term>ValueDate</term><description>快照截至日 → <see cref="ValueDate"/>(EOD 续接起算日,Bug C / 5-11 跳过需据此判断从哪天接续)。</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public readonly struct AccrualState
|
||||
{
|
||||
/// <summary>用于计算当日利息的计息本金。单利=名义本金基数;复利=本金+累计利息。</summary>
|
||||
public decimal AccrualPrincipal { get; }
|
||||
|
||||
/// <summary>累计待实现(未平仓)利息。</summary>
|
||||
public decimal UnrealizedInterest { get; }
|
||||
|
||||
/// <summary>历史各次平仓已确认的已实现利息,从剩余待实现中扣除。</summary>
|
||||
public decimal RealizedInterest { get; }
|
||||
|
||||
/// <summary>快照截至日(来自 eod_swap_position.ValueDate)。编排层据此判断计息区间起点,避免 5-11 等"跳过日"误重算。</summary>
|
||||
public DateTime ValueDate { get; }
|
||||
|
||||
public AccrualState(decimal accrualPrincipal, decimal unrealizedInterest, decimal realizedInterest, DateTime valueDate)
|
||||
=> (AccrualPrincipal, UnrealizedInterest, RealizedInterest, ValueDate) = (accrualPrincipal, unrealizedInterest, realizedInterest, valueDate);
|
||||
|
||||
/// <summary>向后兼容:未携带快照日期时(如纯内存构造)用默认日。</summary>
|
||||
public AccrualState(decimal accrualPrincipal, decimal unrealizedInterest, decimal realizedInterest)
|
||||
: this(accrualPrincipal, unrealizedInterest, realizedInterest, default) { }
|
||||
|
||||
/// <summary>空状态(新开仓首个计息日之前)。</summary>
|
||||
public static readonly AccrualState Zero = new(0m, 0m, 0m);
|
||||
|
||||
/// <summary>
|
||||
/// 从上一日日终归档 <see cref="eod_swap_position"/> 适配(边界适配:DB 列名 → 领域名)。
|
||||
/// 仅映射计息状态;名义本金基数 / 平仓比例 / 已实现利息等由调用方另行传入。
|
||||
/// </summary>
|
||||
public static AccrualState FromPreviousEod(eod_swap_position previousEod)
|
||||
=> previousEod == null || previousEod.id == 0
|
||||
? Zero
|
||||
: new AccrualState(previousEod.TdInterestPrincipal, previousEod.InterestIncomeSum, 0m, previousEod.ValueDate);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
namespace YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
/// <summary>
|
||||
/// 计息过程追踪收集器(值对象,非日志)。2026-08 自 Core 层(YLErp.Core.Interest)迁入 DAL,
|
||||
/// 与 Simple/CompoundInterestAccrual、AccrualBoundary 同处一域,Core 不再持有计息类型。
|
||||
///
|
||||
/// <para><b>为什么是收集器而不是日志调用</b>:计息数学(Simple/CompoundInterestAccrual)必须保持纯函数、
|
||||
/// 可单测、不依赖 NLog;但按工程铁律,关键路径日志须<b>无条件常驻落盘</b>(出问题时事后翻日志定位,不能依赖开关)。
|
||||
/// 折中:纯函数把"发生了什么"记录为结构化条目写入本收集器,由<b>适配器(IO 边界)</b>统一经
|
||||
/// <c>SwapCalcTrace.Write</c> 常驻落盘。落盘职责归一处,计息代码零日志依赖、保持干净。</para>
|
||||
///
|
||||
/// <para><b>可 diff</b>:<see cref="ToString"/> 产出稳定、有序、与 SwapCalcTrace.Day 对齐的逐行文本,
|
||||
/// 新旧引擎对同一笔交易跑出的 trace 可直接 diff,定位"是计算变了还是重构引入了漂移"。</para>
|
||||
///
|
||||
/// <para>所有记录方法均为语义化命名(Day / ResetBefore / Rollover …),调用点一眼即懂,不污染数学可读性。</para>
|
||||
/// </summary>
|
||||
public sealed class AccrualTrace
|
||||
{
|
||||
private readonly List<AccrualTraceEntry> _entries = new();
|
||||
|
||||
/// <summary>已记录的追踪条目(只读)。</summary>
|
||||
public IReadOnlyList<AccrualTraceEntry> Entries => _entries;
|
||||
|
||||
/// <summary>计息区间起点:标记本次计算的整体边界与年化口径。</summary>
|
||||
public void MarkStart(DateTime start, DateTime end, AccrualBoundary boundary, int annualDays, bool annualized)
|
||||
=> Add(AccrualTraceEvent.Start, start,
|
||||
$"START 区间[{start:yyyy-MM-dd},{end:yyyy-MM-dd}] {boundary} annualDays={annualDays} annualized={annualized}");
|
||||
|
||||
/// <summary>逐日明细:当日生效利率、计息基数、当日利息、累计利息。这是"为何 accrued N 天而非 M 天"的直接证据。</summary>
|
||||
public void Day(int idx, DateTime date, decimal rate, decimal basePrincipal, decimal dayInterest, decimal accumulated)
|
||||
=> Add(AccrualTraceEvent.DayAccrual, date,
|
||||
$" [{idx}] {date:yyyy-MM-dd} rate={rate:P6} base={basePrincipal:F4} day={dayInterest:F6} acc={accumulated:F6}");
|
||||
|
||||
/// <summary>分段明细(多日计息,按重置日分段):段区间[segStart,segEnd)、天数、生效利率、计息基数、段利息、累计利息。
|
||||
/// 这是分段模型与旧逐日循环对账的核心证据——每段的"前后日期/基数/利率"一目了然。</summary>
|
||||
public void Segment(int idx, DateTime segStart, DateTime segEnd, int days, decimal rate, decimal basis, decimal segInterest, decimal accumulated)
|
||||
=> Add(AccrualTraceEvent.DayAccrual, segStart,
|
||||
$" [seg{idx}] [{segStart:yyyy-MM-dd},{segEnd:yyyy-MM-dd}) days={days} rate={rate:P6} basis={basis:F4} segInterest={segInterest:F6} acc={accumulated:F6}");
|
||||
|
||||
/// <summary>EOD 上下文:计息日、重置日标志、平仓比例、基数各分量。供日终单日计息定位"重置日分支选对了吗"。</summary>
|
||||
public void EodContext(DateTime eodDate, bool isResetDay, decimal unwindFraction, decimal priorAccrued, decimal priorNotional, decimal notional, decimal remainingFraction)
|
||||
=> Add(AccrualTraceEvent.Start, eodDate,
|
||||
$" CTX {eodDate:yyyy-MM-dd} reset={isResetDay} unwind={unwindFraction:P2} priorAccrued={priorAccrued:F4} priorNotional={priorNotional:F4} notional={notional:F4} remainingFrac={remainingFraction:P4}");
|
||||
|
||||
/// <summary>重置日<b>前</b>:生效利率(旧)与计息本金(滚动前)。利率/本金切换的"因"。</summary>
|
||||
public void ResetBefore(DateTime resetDate, decimal rateOld, decimal principalBefore)
|
||||
=> Add(AccrualTraceEvent.ResetBefore, resetDate,
|
||||
$" RESET↓ {resetDate:yyyy-MM-dd} rate(old)={rateOld:P6} principal(before)={principalBefore:F4}");
|
||||
|
||||
/// <summary>重置日<b>后</b>:生效利率(新)与计息本金(滚动后,已并本金)。利率/本金切换的"果"。</summary>
|
||||
public void ResetAfter(DateTime resetDate, decimal rateNew, decimal principalAfter)
|
||||
=> Add(AccrualTraceEvent.ResetAfter, resetDate,
|
||||
$" RESET↑ {resetDate:yyyy-MM-dd} rate(new)={rateNew:P6} principal(after)={principalAfter:F4}");
|
||||
|
||||
/// <summary>本金增加(利息滚入计息基数):复利段末并本金的瞬间,记录滚入额与并本金后的新基数。</summary>
|
||||
public void Rollover(DateTime resetDate, decimal accruedRolled, decimal newBasis)
|
||||
=> Add(AccrualTraceEvent.Rollover, resetDate,
|
||||
$" ROLLOVER {resetDate:yyyy-MM-dd} accrued(rolled)={accruedRolled:F6} newBasis={newBasis:F4}");
|
||||
|
||||
/// <summary>平仓缩放:平仓比例、累计已实现、剩余未实现。</summary>
|
||||
public void Unwind(DateTime date, decimal unwindPercent, decimal realized, decimal remainingUnrealized)
|
||||
=> Add(AccrualTraceEvent.Unwind, date,
|
||||
$" UNWIND {date:yyyy-MM-dd} pct={unwindPercent:P2} realized={realized:F6} remaining={remainingUnrealized:F6}");
|
||||
|
||||
/// <summary>收尾:最终累计利息与当日利息。</summary>
|
||||
public void MarkEnd(decimal totalAccrued, decimal totalToday)
|
||||
=> Add(AccrualTraceEvent.End, default,
|
||||
$"END accrued={totalAccrued:F6} today={totalToday:F6}");
|
||||
|
||||
/// <summary>自由文本注解(如罚息接缝的诊断行),不绑定特定计息语义。</summary>
|
||||
public void Note(string message)
|
||||
=> Add(AccrualTraceEvent.Note, default, message);
|
||||
|
||||
private void Add(AccrualTraceEvent step, DateTime date, string line)
|
||||
=> _entries.Add(new AccrualTraceEntry(step, date, line));
|
||||
|
||||
/// <summary>稳定可 diff 的逐行文本(与 SwapCalcTrace.Day 格式对齐)。</summary>
|
||||
public override string ToString()
|
||||
=> _entries.Count == 0 ? "<empty trace>" : string.Join(Environment.NewLine, _entries.Select(e => e.Line));
|
||||
}
|
||||
|
||||
/// <summary>追踪条目的语义类别(对应 QuantLib/Strata 的"事件"概念),便于程序化筛选(如"只看重置日")。</summary>
|
||||
public enum AccrualTraceEvent
|
||||
{
|
||||
Start, DayAccrual, ResetBefore, ResetAfter, Rollover, Unwind, End, Note
|
||||
}
|
||||
|
||||
/// <summary>单条追踪记录:类别 + 日期 + 已渲染文本。</summary>
|
||||
public readonly record struct AccrualTraceEntry(AccrualTraceEvent Step, DateTime Date, string Line);
|
||||
@@ -1,6 +1,3 @@
|
||||
using YLErp.Core.Interest;
|
||||
using YLErp.Derivatives.Interest;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
/// <summary>
|
||||
@@ -9,7 +6,7 @@ namespace YLErp.Modules.SwapModule.Accrual;
|
||||
/// </summary>
|
||||
public static class CompoundInterestAccrual
|
||||
{
|
||||
private const int Precision = SwapInterest.FundingLegPrecision;
|
||||
private const int Precision = InterestMath.FundingLegPrecision;
|
||||
|
||||
/// <summary>复利日终计息基数(单一真相源,纯函数与调用方共用):
|
||||
/// 重置日 = notional + 累计利息×剩余比例(利息并入本金);非重置日 = priorNotional(昨日滚动基数)。
|
||||
@@ -52,8 +49,8 @@ public static class CompoundInterestAccrual
|
||||
|
||||
var totalAccrued = priorAccrued * unwindFraction + dayInterest;
|
||||
var result = new InterestResult(
|
||||
SwapInterest.Round(totalAccrued, Precision),
|
||||
SwapInterest.Round(tdInterest, Precision));
|
||||
InterestMath.Round(totalAccrued, Precision),
|
||||
InterestMath.Round(tdInterest, Precision));
|
||||
|
||||
trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued);
|
||||
trace?.MarkEnd(result.Accrued, result.AccruedToday);
|
||||
@@ -64,6 +61,11 @@ public static class CompoundInterestAccrual
|
||||
/// 复利多日计息(替换 CalcDailyCompoundInterest 的纯数学部分)。
|
||||
/// 从 startDate 到 endDate 全程重放,每个重置日把累计利息并入本金。
|
||||
/// </summary>
|
||||
/// <param name="carryInInterest">
|
||||
/// 窗口前已计未结转利息(EQD-6977 罚息承接):在【首个重置日】并入计息基数——
|
||||
/// 与"持有至到期"全期轨迹严格对齐(恒等式:全期复利 = 平仓日已结利息 + 罚息窗口利息)。
|
||||
/// 默认 0 时与旧行为逐位一致。与 resetCarryInterest 互斥使用(后者是全平重放的末段存量替代)。
|
||||
/// </param>
|
||||
public static InterestResult AccruePeriod(
|
||||
decimal notional,
|
||||
IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
|
||||
@@ -76,10 +78,14 @@ public static class CompoundInterestAccrual
|
||||
decimal realizedInterest,
|
||||
decimal unwindFraction,
|
||||
out decimal finalBasis,
|
||||
AccrualTrace? trace = null)
|
||||
AccrualTrace? trace = null,
|
||||
decimal carryInInterest = 0m)
|
||||
{
|
||||
decimal accrualBasis = notional;
|
||||
decimal accrued = 0m;
|
||||
// carryInInterest 是"窗口前已计未结转利息":先并入 accrued,随首个重置日的
|
||||
// basis = notional + accrued 一并资本化,并在其后每个重置日持续留在基数里
|
||||
//(与全窗口重放时 accrued 含全部历史利息的轨迹严格一致);最终报告时扣除。
|
||||
decimal accrued = carryInInterest;
|
||||
|
||||
trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized);
|
||||
|
||||
@@ -107,7 +113,7 @@ public static class CompoundInterestAccrual
|
||||
|
||||
var segIncludeStart = (si == 0) ? boundary.IncludeStart : true;
|
||||
var segIncludeEnd = isLastSegment ? boundary.IncludeEnd : false;
|
||||
var days = SwapInterest.AccrualDays(segmentRates[si].StartDate, segEnd,
|
||||
var days = InterestMath.AccrualDays(segmentRates[si].StartDate, segEnd,
|
||||
AccrualBoundary.Of(segIncludeStart, segIncludeEnd));
|
||||
if (days <= 0) continue;
|
||||
|
||||
@@ -119,13 +125,16 @@ public static class CompoundInterestAccrual
|
||||
|
||||
finalBasis = accrualBasis;
|
||||
|
||||
// 报告口径只含窗口内增量(carryIn 是窗口前已结利息,由正常平仓流单独结算)
|
||||
accrued -= carryInInterest;
|
||||
|
||||
if (realizedInterest != 0m)
|
||||
trace?.Unwind(endDate, unwindFraction, realizedInterest * unwindFraction, accrued - realizedInterest * unwindFraction);
|
||||
accrued -= realizedInterest * unwindFraction;
|
||||
|
||||
var result = new InterestResult(
|
||||
SwapInterest.Round(accrued, Precision),
|
||||
SwapInterest.Round(accrued, Precision));
|
||||
InterestMath.Round(accrued, Precision),
|
||||
InterestMath.Round(accrued, Precision));
|
||||
trace?.MarkEnd(result.Accrued, result.AccruedToday);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
/// <summary>
|
||||
@@ -26,4 +28,10 @@ public readonly struct FundingLegRate
|
||||
/// <summary>构造浮动腿利率(all-in = 加点利差 + 指数定盘)。</summary>
|
||||
public static FundingLegRate Floating(decimal spread, decimal indexFixing)
|
||||
=> new(spread + indexFixing);
|
||||
|
||||
/// <summary>从 swap_position 构造:固定腿→Fixed(spread),浮动腿→Floating(spread+fixing)。</summary>
|
||||
public static FundingLegRate Build(swap_position position, decimal spread, decimal effectiveFloat)
|
||||
=> string.IsNullOrEmpty(position.FloatRateUnderlyingCode)
|
||||
? Fixed(spread)
|
||||
: Floating(spread, effectiveFloat);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
namespace YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 词汇表(本文件只允许出现下列用词,同一概念不得出现第二种叫法)
|
||||
//
|
||||
// 概念 唯一用词 与既有代码的对应
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
// 区间起点/终点 Start / End startDate / endDate
|
||||
// 计息 Accrue CalcDailySimpleInterest / CalcDailyCompoundInterest
|
||||
// 平仓 Unwind unwindPercent(既有字段 closePercent)
|
||||
// 已实现利息 Realized realizedInterest(legacy 字段 consumedInterest)
|
||||
// 待实现收益 Unrealized 预付金模式下的待实现收益余额
|
||||
// 计息基数 principal principal / dynomicPrincipal
|
||||
// 年化天数 annualDays tradeExtend.ExtendObj.AnnualDays
|
||||
//
|
||||
// 入参一律沿用既有代码的字段名,调用点两边读起来同名,不产生心智翻译成本。
|
||||
// 出参改用自描述名(Accrued / AccruedToday),因为 "Td" 对新读者是黑话。
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 计息区间边界(算头 / 算尾)。
|
||||
/// 用具名值取代两个相邻 bool,物理上杜绝 calcFirst / calcLast 传反这一类历史缺陷。
|
||||
/// </summary>
|
||||
public readonly struct AccrualBoundary
|
||||
{
|
||||
/// <summary>算头:含 startDate。</summary>
|
||||
public bool IncludeStart { get; }
|
||||
|
||||
/// <summary>算尾:含 endDate。</summary>
|
||||
public bool IncludeEnd { get; }
|
||||
|
||||
private AccrualBoundary(bool includeStart, bool includeEnd)
|
||||
=> (IncludeStart, IncludeEnd) = (includeStart, includeEnd);
|
||||
|
||||
/// <summary>算头算尾 [start, end]。</summary>
|
||||
public static readonly AccrualBoundary Both = new(true, true);
|
||||
|
||||
/// <summary>算头不算尾 [start, end)。</summary>
|
||||
public static readonly AccrualBoundary StartOnly = new(true, false);
|
||||
|
||||
/// <summary>不算头算尾 (start, end]。</summary>
|
||||
public static readonly AccrualBoundary EndOnly = new(false, true);
|
||||
|
||||
/// <summary>不算头不算尾 (start, end)。</summary>
|
||||
public static readonly AccrualBoundary None = new(false, false);
|
||||
|
||||
/// <summary>由既有 calcFirst / calcLast 布尔对构造,供旧调用方渐进迁移。</summary>
|
||||
public static AccrualBoundary Of(bool includeStart, bool includeEnd) => new(includeStart, includeEnd);
|
||||
|
||||
public override string ToString()
|
||||
=> $"{(IncludeStart ? "算头" : "不算头")}{(IncludeEnd ? "算尾" : "不算尾")}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计息结果。Accrued → 记账字段 InterestAmount / InterestProfitSum;AccruedToday → TdInterestAmount。
|
||||
/// </summary>
|
||||
public readonly struct InterestResult
|
||||
{
|
||||
/// <summary>区间累计应计利息。</summary>
|
||||
public decimal Accrued { get; }
|
||||
|
||||
/// <summary>末日(当日)应计利息。</summary>
|
||||
public decimal AccruedToday { get; }
|
||||
|
||||
public InterestResult(decimal accrued, decimal accruedToday)
|
||||
=> (Accrued, AccruedToday) = (accrued, accruedToday);
|
||||
|
||||
public static readonly InterestResult Zero = new(0m, 0m);
|
||||
|
||||
public override string ToString() => $"Accrued={Accrued}, AccruedToday={AccruedToday}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 利息腿共用数学工具:舍入、应计天数、精度常量。
|
||||
///
|
||||
/// <para><b>沿革</b>:2026-08 自 Core 层 SwapInterest 迁入 DAL(生产消费面整体搬家)。
|
||||
/// 原 SwapInterest 的算法方法(AccrueSimple/AccrueCompoundInArrears/ApplyUnwind/AccrueUnrealized)
|
||||
/// 与 AccrualContext/InterestRate 始终未接线(生产计息走本目录 Simple/CompoundInterestAccrual,
|
||||
/// 两者舍入与 rollover 口径已分叉),作为孤儿死代码删除——接线前须先补对账,勿凭记忆重建。</para>
|
||||
///
|
||||
/// <para>为何不复用 Qdp 的 IDayCount:
|
||||
/// a. 语义——Qdp 的 DaysInPeriod = end − start 是写死的半开区间,只能表达四种算头算尾中的一种;
|
||||
/// b. 精度——Qdp 返回 double 年化系数,本系统 decimal 对账;
|
||||
/// c. 依赖方向——Qdp 用自有 Date 类型,引入会让本模块反向依赖定价库。</para>
|
||||
/// </summary>
|
||||
public static class InterestMath
|
||||
{
|
||||
/// <summary>资金腿与保证金腿的生产计息精度(落库/对账均以 12 位为准)。
|
||||
/// 提升至公共常量,消除 SwapDealService 与 SimpleInterestAccrual 的重复定义。</summary>
|
||||
public const int FundingLegPrecision = 12;
|
||||
|
||||
/// <summary>应计天数。边界规则由日期区间表达,计息函数内不再出现 flag 分支。</summary>
|
||||
public static int AccrualDays(DateTime startDate, DateTime endDate, AccrualBoundary boundary)
|
||||
{
|
||||
var s = boundary.IncludeStart ? startDate : startDate.AddDays(1);
|
||||
var e = boundary.IncludeEnd ? endDate : endDate.AddDays(-1);
|
||||
var days = (int)(e - s).TotalDays + 1; // 含两端
|
||||
return days < 0 ? 0 : days;
|
||||
}
|
||||
|
||||
/// <summary>统一舍入:MidpointRounding.AwayFromZero。所有计息路径收口到此处,避免散落的 Math.Round 不一致。</summary>
|
||||
public static decimal Round(decimal value, int precision)
|
||||
=> Math.Round(value, precision, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
@@ -1,6 +1,3 @@
|
||||
using YLErp.Core.Interest;
|
||||
using YLErp.Derivatives.Interest;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
/// <summary>
|
||||
@@ -9,7 +6,7 @@ namespace YLErp.Modules.SwapModule.Accrual;
|
||||
/// </summary>
|
||||
public static class SimpleInterestAccrual
|
||||
{
|
||||
private const int Precision = SwapInterest.FundingLegPrecision;
|
||||
private const int Precision = InterestMath.FundingLegPrecision;
|
||||
|
||||
/// <summary>
|
||||
/// 单利日终计息(替换 CalcDailySimpleInterestByEod 的纯数学部分)。
|
||||
@@ -38,8 +35,8 @@ public static class SimpleInterestAccrual
|
||||
|
||||
var totalAccrued = priorAccrued + dayInterest;
|
||||
var result = new InterestResult(
|
||||
SwapInterest.Round(totalAccrued, Precision),
|
||||
SwapInterest.Round(tdInterest, Precision));
|
||||
InterestMath.Round(totalAccrued, Precision),
|
||||
InterestMath.Round(tdInterest, Precision));
|
||||
|
||||
trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued);
|
||||
trace?.MarkEnd(result.Accrued, result.AccruedToday);
|
||||
@@ -85,7 +82,7 @@ public static class SimpleInterestAccrual
|
||||
var includeStart = effectiveStart == startDate ? boundary.IncludeStart : true;
|
||||
var isLastSegment = si == segmentRates.Count - 1;
|
||||
var segBoundary = AccrualBoundary.Of(includeStart, isLastSegment && boundary.IncludeEnd);
|
||||
var days = SwapInterest.AccrualDays(effectiveStart, segEnd, segBoundary);
|
||||
var days = InterestMath.AccrualDays(effectiveStart, segEnd, segBoundary);
|
||||
if (days <= 0) { segStart = segEnd; continue; }
|
||||
|
||||
var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate;
|
||||
@@ -98,8 +95,8 @@ public static class SimpleInterestAccrual
|
||||
}
|
||||
|
||||
var result = new InterestResult(
|
||||
SwapInterest.Round(accrued, Precision),
|
||||
SwapInterest.Round(accruedUnscaled, Precision));
|
||||
InterestMath.Round(accrued, Precision),
|
||||
InterestMath.Round(accruedUnscaled, Precision));
|
||||
trace?.MarkEnd(result.Accrued, result.AccruedToday);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.SwapModule;
|
||||
|
||||
/// <summary>
|
||||
/// 平仓比例(ClosePercent) 数学——占期初(A) / 占剩余(B) 两种口径的转换。
|
||||
/// 从 SwapDealService 提取为共享模块,两个 service 均可引用。
|
||||
/// </summary>
|
||||
public static class ClosePercentMath
|
||||
{
|
||||
/// <summary>
|
||||
/// 取上一日终的浮动端名义本金(orginPv 的来源)。
|
||||
/// 优先取浮动腿 PosiNotionalValue 之和,取不到用 eod_swap 多空绝对值之和,都没有用 currentNotional 兜底。
|
||||
/// </summary>
|
||||
public static decimal ResolveUnwindPreviousNotional(
|
||||
eod_swap lastEod,
|
||||
IEnumerable<eod_swap_position> lastEodPositions,
|
||||
decimal currentNotional)
|
||||
{
|
||||
var floatingPositions = lastEodPositions?.Where(x => x.PosiDirection > 0).ToList();
|
||||
decimal previousNotional;
|
||||
if (floatingPositions?.Count > 0)
|
||||
{
|
||||
previousNotional = floatingPositions.Sum(x => x.PosiNotionalValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
previousNotional = lastEod == null
|
||||
? currentNotional
|
||||
: Math.Abs(lastEod.NotionalValueLong) + Math.Abs(lastEod.NotionalValueShort);
|
||||
}
|
||||
|
||||
return previousNotional == 0m && currentNotional != 0m
|
||||
? currentNotional
|
||||
: previousNotional;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A(占期初) → B(占剩余),用于把前端传入的占期初比例换算成后端计算用的占剩余比例。
|
||||
/// </summary>
|
||||
public static decimal ToRemainingClosePercent(decimal originalClosePercent, decimal notionalValue, decimal posiNotionalValue)
|
||||
{
|
||||
if (posiNotionalValue <= 0) return originalClosePercent;
|
||||
var remaining = originalClosePercent * notionalValue / posiNotionalValue;
|
||||
return remaining > 1 ? 1 : remaining;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// B(占剩余) → A(占期初),用于落库 / 事件列表展示还原。见 ToRemainingClosePercent。
|
||||
/// </summary>
|
||||
public static decimal ToOriginalClosePercent(decimal remainingClosePercent, decimal notionalValue, decimal posiNotionalValue)
|
||||
{
|
||||
if (notionalValue <= 0) return remainingClosePercent;
|
||||
return remainingClosePercent * posiNotionalValue / notionalValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算 InitUnwind 默认占期初(A)平仓比例 = PosiNotionalValue / NotionalValue。
|
||||
/// 未平仓时 =1(平100%);部分平仓后自动变为剩余比例。
|
||||
/// </summary>
|
||||
public static decimal CalcDefaultInitClosePercent(decimal notionalValue, decimal posiNotionalValue)
|
||||
{
|
||||
return notionalValue > 0 ? posiNotionalValue / notionalValue : 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using YLErp;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
using YLErp.Modules.SwapModule.ReturnLegs;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 互换日终盈亏/精度计算纯函数集合。
|
||||
/// 自 SwapEodPositionService 抽出,支持无库单测;同类内部调用无需前缀。
|
||||
/// </summary>
|
||||
public static class EodPnlCalculator
|
||||
{
|
||||
// 日终利息待实现需跨日累计,按表设计保留 12 位;已实现结算仍按金额两位处理。
|
||||
private const int EodInterestStoragePrecision = 12;
|
||||
|
||||
internal static decimal RoundMoney(decimal value)
|
||||
{
|
||||
return Math.Round(value, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
internal static decimal RoundEodInterest(decimal value)
|
||||
{
|
||||
return Math.Round(value, EodInterestStoragePrecision, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仅在写入 eod_swap_position 前统一快照精度。
|
||||
/// 浮动腿收益最终以金额两位展示和存储;利息腿的待实现、计息基数及利率保留 12 位,
|
||||
/// 使部分结算后的尾差可继续参与后续计息。
|
||||
/// </summary>
|
||||
internal static void NormalizeEodPositionForStorage(eod_swap_position position)
|
||||
{
|
||||
if (string.IsNullOrEmpty(position.UnderlyingCode))
|
||||
{
|
||||
// 利息腿没有标的代码:待实现字段保留高精度,已实现结算字段收敛到金额两位。
|
||||
position.InterestPrincipalFix = RoundEodInterest(position.InterestPrincipalFix);
|
||||
position.InterestRateDefault = RoundEodInterest(position.InterestRateDefault);
|
||||
position.InterestFeePending = RoundEodInterest(position.InterestFeePending);
|
||||
position.TdInterestPrincipal = RoundEodInterest(position.TdInterestPrincipal);
|
||||
position.TdInterestRate = RoundEodInterest(position.TdInterestRate);
|
||||
position.TdInterestIncome = RoundEodInterest(position.TdInterestIncome);
|
||||
position.TdInterestFee = RoundEodInterest(position.TdInterestFee);
|
||||
position.InterestIncomeSum = RoundEodInterest(position.InterestIncomeSum);
|
||||
position.InterestFeeSum = RoundEodInterest(position.InterestFeeSum);
|
||||
position.InterestProfitSum = RoundEodInterest(position.InterestProfitSum);
|
||||
position.FloatRate = RoundEodInterest(position.FloatRate);
|
||||
position.SwapPositionValue = RoundEodInterest(position.SwapPositionValue);
|
||||
position.TdCloseInterest = RoundMoney(position.TdCloseInterest);
|
||||
position.TdCloseInterestFee = RoundMoney(position.TdCloseInterestFee);
|
||||
position.RealizedInterest = RoundMoney(position.RealizedInterest);
|
||||
position.RealizedInterestFee = RoundMoney(position.RealizedInterestFee);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 浮动腿有标的代码:其损益作为金额结果落库,统一按两位四舍五入。
|
||||
position.TdPosiDividend = RoundMoney(position.TdPosiDividend);
|
||||
position.PosiMtmPnL = RoundMoney(position.PosiMtmPnL);
|
||||
position.PosiDividendSum = RoundMoney(position.PosiDividendSum);
|
||||
position.PosiFeePending = RoundMoney(position.PosiFeePending);
|
||||
position.PosiProfitSum = RoundMoney(position.PosiProfitSum);
|
||||
position.TdCloseMtmPnl = RoundMoney(position.TdCloseMtmPnl);
|
||||
position.TdCloseDividend = RoundMoney(position.TdCloseDividend);
|
||||
position.TdCloseFee = RoundMoney(position.TdCloseFee);
|
||||
position.RealizedMtmPnL = RoundMoney(position.RealizedMtmPnL);
|
||||
position.RealizedDividend = RoundMoney(position.RealizedDividend);
|
||||
position.RealizedFee = RoundMoney(position.RealizedFee);
|
||||
position.SwapPositionValue = RoundMoney(position.SwapPositionValue);
|
||||
}
|
||||
position.RealizedPnl = RoundMoney(position.RealizedPnl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 浮动腿累计已实现盈亏由盯市、分红和费用三个已实现组成项汇总。
|
||||
/// 各组成项已经按本方视角落库,此处不再额外转换方向。
|
||||
/// </summary>
|
||||
internal static void SetFloatingRealizedPnl(eod_swap_position position)
|
||||
{
|
||||
position.RealizedPnl = position.RealizedMtmPnL
|
||||
+ position.RealizedDividend
|
||||
+ position.RealizedFee;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 汇总单条日终腿的我方已实现收益。
|
||||
/// 浮动腿及普通利息腿维持数据库记录的方向;初始/追加预付金腿的利息
|
||||
/// 则与保证金本金方向相反。这样“收取对手方保证金”产生的利息会作为
|
||||
/// 我方支付给对手方的成本计入,而不会错误增加框架合约已实现收益。
|
||||
/// 抽为静态纯函数以支持无库单测(marginTypes 等价于 ConsTrade.InterestMarginModels)。
|
||||
/// </summary>
|
||||
public static decimal CalculateSwapRealizedPnl(eod_swap_position position)
|
||||
{
|
||||
var interestRatio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
|
||||
|
||||
return position.RealizedMtmPnL
|
||||
+ position.RealizedDividend
|
||||
+ position.RealizedFee
|
||||
+ position.RealizedInterest * interestRatio
|
||||
+ position.RealizedInterestFee;
|
||||
}
|
||||
|
||||
/// <summary>填充框架合约的持仓腿汇总字段(多空名义本金/市值/浮动盈亏/dv01/平仓量)。
|
||||
/// SaveEodSwap 与 UpdateEodSwap 共用,消除 ~10 行重复。</summary>
|
||||
internal static void FillPositionLegSummary(eod_swap eod_Swap, List<eod_swap_position> positions)
|
||||
{
|
||||
eod_Swap.NotionalValueLong = Math.Round(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
eod_Swap.NotionalValueShort = Math.Round(-Math.Abs(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue)), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
eod_Swap.MarketValueLong = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.UnderlyingMarketValue);
|
||||
eod_Swap.MarketValueShort = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.UnderlyingMarketValue);
|
||||
eod_Swap.FloatingPnL = positions.Sum(s => s.PosiProfitSum);
|
||||
eod_Swap.dv01 = positions.Sum(s => s.dv01 ?? 0);
|
||||
eod_Swap.TdCloseQty = positions.Sum(s => s.TdCloseQty);
|
||||
}
|
||||
|
||||
/// <summary>利息腿 PnL 汇总(按方向比例 + 保证金翻转)。原 SaveEodSwap/UpdateEodSwap 各一段 ForEach。</summary>
|
||||
internal static decimal SumInterestPnL(List<eod_swap_position> interestPositions)
|
||||
{
|
||||
decimal interestPnL = 0;
|
||||
foreach (var x in interestPositions)
|
||||
interestPnL += x.InterestProfitSum * DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode);
|
||||
return interestPnL;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 风险报表符号归一化:把历史两种符号口径的 TdCloseInterest/RealizedInterest
|
||||
/// 统一按"绝对金额 × 业务方向"重写。普通利息腿收取为正、支付为负;
|
||||
/// 预付金腿利息方向与保证金本金方向相反。随后重算 RealizedPnl。
|
||||
/// 抽为 public static 纯函数以支持无库单测(见 SwapReportInterestSignNormalizeTest)。
|
||||
/// 仅当 InterestDirection > 0 时执行(与原内联逻辑等价)。
|
||||
/// </summary>
|
||||
public static void NormalizeInterestSignForReport(eod_swap_position position)
|
||||
{
|
||||
if (position.InterestDirection <= 0) return;
|
||||
|
||||
if (position.InterestMode == (int)InterestModeEnum.标的期初全价)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var interestRatio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
|
||||
position.TdCloseInterest = Math.Abs(position.TdCloseInterest) * interestRatio;
|
||||
position.RealizedInterest = Math.Abs(position.RealizedInterest) * interestRatio;
|
||||
// 兼容修复前已落库的利息腿:当时只累计了明细字段,未同步写入 RealizedPnl。
|
||||
position.RealizedPnl = position.RealizedInterest + position.RealizedInterestFee;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算预付金利率。多条初始/追加预付金腿按本金绝对值加权,
|
||||
/// 不按收付方向轧差,避免相反方向本金抵消后放大利率。
|
||||
/// </summary>
|
||||
internal 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>
|
||||
/// 计算预付金利息金额。InterestIncomeSum 已是各腿利息金额,
|
||||
/// 按收取为正、支付为负直接轧差求和,不做本金加权。
|
||||
/// 抽为 public static 纯函数以支持无库单测(见 SwapWeightedMarginInterestTest)。
|
||||
/// </summary>
|
||||
public static decimal CalculateWeightedMarginInterest(IEnumerable<eod_swap_position> margins)
|
||||
{
|
||||
return margins.Sum(x =>
|
||||
x.InterestIncomeSum * DirectionRatio.ReceivePay(x.InterestDirection));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 固定利息腿的累计已实现盈亏 = 累计已实现利息 + 累计已实现利息费用。
|
||||
/// 4 处 SaveAutoEodInterestPosition/SaveEodInterestPosition 路径口径一致,
|
||||
/// 抽为 public static 纯函数以支持无库单测(见 SwapFixedLegRealizedPnlTest),
|
||||
/// 并消除复制粘贴带来的笔误风险(如 L1296 历史双分号)。
|
||||
/// </summary>
|
||||
public static void SetFixedLegRealizedPnl(eod_swap_position position)
|
||||
{
|
||||
position.RealizedPnl = position.RealizedInterest + position.RealizedInterestFee;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// eod_swap_position 查询收口(Query Object)。
|
||||
/// 规则"某交易某日日终的有效持仓 = SwapTradeId 匹配 + ValueDate 匹配 + 未作废(!Invalid)"集中于此,
|
||||
/// 避免多处复制同一谓词导致语义漂移(漏写 !Invalid 即静默出 bug)。
|
||||
/// 仅返回 IQueryable,不调用 SaveChanges,不破坏跟踪/Include/事务边界。
|
||||
/// </summary>
|
||||
public static class EodSwapPositionQueries
|
||||
{
|
||||
public static IQueryable<eod_swap_position> ActiveByTradeAndDate(
|
||||
this IQueryable<eod_swap_position> query, int tradeId, DateTime valueDate)
|
||||
=> query.Where(x => x.SwapTradeId == tradeId && x.ValueDate == valueDate && !x.Invalid);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,6 @@ public sealed class ContractNotionalLeg : IFundingLegStrategy
|
||||
{
|
||||
public InterestModeEnum Mode => InterestModeEnum.合约名义本金规模;
|
||||
|
||||
public NotionalResult CalcNotional(decimal fix, decimal posiNotional, decimal posiLong, decimal posiShort, decimal closePercent)
|
||||
public NotionalResult CalcNotional(decimal fix, decimal posiNotional, decimal closePercent)
|
||||
=> new(posiNotional * closePercent, posiNotional, closePercent);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@ public sealed class FixedAmountLeg : IFundingLegStrategy
|
||||
{
|
||||
public InterestModeEnum Mode => InterestModeEnum.固定值;
|
||||
|
||||
public NotionalResult CalcNotional(decimal fix, decimal posiNotional, decimal posiLong, decimal posiShort, decimal closePercent)
|
||||
public NotionalResult CalcNotional(decimal fix, decimal posiNotional, decimal closePercent)
|
||||
=> new(fix, fix, 1m);
|
||||
}
|
||||
|
||||
@@ -22,10 +22,8 @@ public interface IFundingLegStrategy
|
||||
/// </summary>
|
||||
/// <param name="fix">合约固定本金(固定值/预付金腿用;其余腿忽略)。</param>
|
||||
/// <param name="posiNotional">当前剩余名义本金(数量 × 全价)。</param>
|
||||
/// <param name="posiLong">多头剩余名义本金(多空存续腿用,当前界面已禁用)。</param>
|
||||
/// <param name="posiShort">空头剩余名义本金。</param>
|
||||
/// <param name="closePercent">平仓比例(占剩余,0~1)。</param>
|
||||
NotionalResult CalcNotional(decimal fix, decimal posiNotional, decimal posiLong, decimal posiShort, decimal closePercent);
|
||||
NotionalResult CalcNotional(decimal fix, decimal posiNotional, decimal closePercent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.FundingLegs;
|
||||
|
||||
@@ -7,13 +7,13 @@ namespace YLErp.Modules.SwapModule.FundingLegs;
|
||||
/// 计息基数 = 标的期初含费全价(PosiGrossPrice/EntryDirtyPrice) × 数量。
|
||||
/// "期初(Entry)"是关键——建仓时点的全价,非当前估值全价。
|
||||
/// 主路径 CalcNotionalByMode 公式与合约名义本金规模(2)相同;
|
||||
/// 差异在衡泰路径会乘 grossPrice 折算(SwapDealService.GetUnwindInterestsByHT),
|
||||
/// 衡泰回执折算路径(原 SwapDealService.GetUnwindInterestsByHT 乘 grossPrice 折算)已随死链清理移除;
|
||||
/// 以及 EOD 复利部分平仓后直接返回剩余本金(禁止反推,SwapEodPositionService:1458-1465)。
|
||||
/// </summary>
|
||||
public sealed class UnderlyingEntryFullPriceLeg : IFundingLegStrategy
|
||||
{
|
||||
public InterestModeEnum Mode => InterestModeEnum.标的期初全价;
|
||||
|
||||
public NotionalResult CalcNotional(decimal fix, decimal posiNotional, decimal posiLong, decimal posiShort, decimal closePercent)
|
||||
public NotionalResult CalcNotional(decimal fix, decimal posiNotional, decimal closePercent)
|
||||
=> new(posiNotional * closePercent, posiNotional, closePercent);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
namespace YLErp.Modules.SwapModule;
|
||||
|
||||
/// <summary>
|
||||
/// GetInterests 参数对象(2026-08 参数显式化)。
|
||||
///
|
||||
/// 动机:原 GetInterests 20 个位置参数中,名义本金簇(posiNotionalValue/closePosiNotionalValue/closePercent)
|
||||
/// 在【盘中平仓】与【EOD 平仓后收盘】两类场景下语义相反(详见 GetInterests "根因位置"注释与
|
||||
/// GetInterestsEntrySemanticsTest 的口径留档),位置参数无法表达该约束。
|
||||
///
|
||||
/// 用法:只能经两个场景工厂构造——工厂形参名即该场景语义(平仓前剩余 / 平仓后剩余 / 实际平掉额),
|
||||
/// 物理上防止两套语义混传。needPrice/grossPrice(原方法死参数)与 posiLong/posiShortNotionalValue
|
||||
/// (多空组合子系统删除后计息链零消费的管道死参数)均不承载。
|
||||
/// </summary>
|
||||
public sealed class InterestCalcRequest
|
||||
{
|
||||
public trade Td { get; }
|
||||
public trade_extend TradeExtend { get; }
|
||||
public DateTime ValueDate { get; }
|
||||
public DateTime UnwindDate { get; }
|
||||
public List<eod_swap_position> EodPositions { get; }
|
||||
public List<swap_position> Positions { get; }
|
||||
|
||||
/// <summary>当日适用名义本金。语义随场景:盘中=平仓【前】剩余;EOD平仓后收盘=平仓【后】剩余;EOD增量=当前剩余。</summary>
|
||||
public decimal PosiNotionalValue { get; }
|
||||
|
||||
/// <summary>本次实际平掉本金(两场景恒同义)。mode2 无条件覆盖 / mode9 全平兜底的输入。</summary>
|
||||
public decimal ClosePosiNotionalValue { get; }
|
||||
|
||||
/// <summary>平仓比例。语义随场景:盘中=实际比例(B 占剩余);EOD平仓后收盘=恒1(全额结息)。</summary>
|
||||
public decimal ClosePercent { get; }
|
||||
|
||||
public int EventType { get; }
|
||||
public bool TdClose { get; }
|
||||
public decimal OrginPv { get; }
|
||||
public bool Add { get; }
|
||||
public bool NewCalcLast { get; }
|
||||
public List<swap_flow_event> CloseList { get; }
|
||||
|
||||
/// <summary>是否计罚息(EQD-6977):利息端按持有至到期计息。由平仓页下拉经 UnwindData 透传;默认 false。</summary>
|
||||
public bool IsPenaltyInterest { get; }
|
||||
|
||||
private InterestCalcRequest(
|
||||
trade td, trade_extend tradeExtend, DateTime valueDate, DateTime unwindDate,
|
||||
List<eod_swap_position> eodPositions, List<swap_position> positions,
|
||||
decimal posiNotionalValue,
|
||||
decimal closePosiNotionalValue, decimal closePercent,
|
||||
int eventType, bool tdClose, decimal orginPv,
|
||||
bool add, bool newCalcLast, List<swap_flow_event> closeList,
|
||||
bool isPenaltyInterest = false)
|
||||
{
|
||||
Td = td; TradeExtend = tradeExtend; ValueDate = valueDate; UnwindDate = unwindDate;
|
||||
EodPositions = eodPositions; Positions = positions;
|
||||
PosiNotionalValue = posiNotionalValue; ClosePosiNotionalValue = closePosiNotionalValue;
|
||||
ClosePercent = closePercent; EventType = eventType; TdClose = tdClose; OrginPv = orginPv;
|
||||
Add = add; NewCalcLast = newCalcLast; CloseList = closeList;
|
||||
IsPenaltyInterest = isPenaltyInterest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 【盘中平仓/互换结息】场景(→ GetIntradayUnwindInterests,settment:false 盘中重放)。
|
||||
/// </summary>
|
||||
/// <param name="preCloseNotional">平仓【前】实时剩余本金(原 GetUnwindInterests.stockEqvNotional)。</param>
|
||||
/// <param name="closedNotional">本次实际平掉本金(= preCloseNotional × closePercentRemaining)。</param>
|
||||
/// <param name="closePercentRemaining">平仓比例,B 语义【占剩余】(前端传 A 占期初须先经 ToRemainingClosePercent 转换)。</param>
|
||||
public static InterestCalcRequest IntradayUnwind(
|
||||
trade td, trade_extend tradeExtend, DateTime valueDate, DateTime unwindDate,
|
||||
List<eod_swap_position> eodPositions, List<swap_position> positions,
|
||||
decimal preCloseNotional, decimal closedNotional, decimal closePercentRemaining,
|
||||
int eventType, bool tdClose, decimal orginPv,
|
||||
bool add, bool newCalcLast, List<swap_flow_event> closeList,
|
||||
bool isPenaltyInterest = false)
|
||||
=> new(td, tradeExtend, valueDate, unwindDate, eodPositions, positions,
|
||||
preCloseNotional, closedNotional, closePercentRemaining,
|
||||
eventType, tdClose, orginPv, add, newCalcLast, closeList, isPenaltyInterest);
|
||||
|
||||
/// <summary>
|
||||
/// 【EOD 当日有平仓后的收盘结息】场景(→ CalcEodPostCloseSettleInterests,settment:false 全额结息)。
|
||||
/// 该场景触发 GetInterests 内 mode2 无条件覆盖 / mode9 全平兜底(见其"根因位置"注释,勿删)。
|
||||
/// </summary>
|
||||
/// <param name="remainingNotionalAfterClose">平仓【后】剩余本金(GetInterests.posiNotionalValue 形参位)。</param>
|
||||
/// <param name="closedNotional">本次实际平掉本金。</param>
|
||||
public static InterestCalcRequest EodPostCloseSettle(
|
||||
trade td, trade_extend tradeExtend, DateTime valueDate, DateTime unwindDate,
|
||||
List<eod_swap_position> eodPositions, List<swap_position> positions,
|
||||
decimal remainingNotionalAfterClose, decimal closedNotional,
|
||||
int eventType, bool tdClose, decimal orginPv,
|
||||
bool add, bool newCalcLast)
|
||||
=> new(td, tradeExtend, valueDate, unwindDate, eodPositions, positions,
|
||||
remainingNotionalAfterClose, closedNotional, 1m, // 恒1:本次事件全额结息(非 closeNational / 期初比例)
|
||||
eventType, tdClose, orginPv, add, newCalcLast, closeList: null);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using YLErp.Models;
|
||||
|
||||
namespace YLErp.Modules.SwapModule;
|
||||
|
||||
/// <summary>
|
||||
/// 利息腿 EOD 归档场景分派规格(纯函数集;表驱动单测见 InterestEodScenarioDispatchTest)。
|
||||
/// 优先级链(承重业务语义,不能乱序):
|
||||
/// ① hasSwap(手工互换) → 按事件流水重新生成,压制观察日自动结息
|
||||
/// ② observationInterval(观察日) 存在 → 自动结息;同日有平仓 → autoSwap=true
|
||||
/// ③ hasClose(纯平仓, 非观察日) → autoSwap=false
|
||||
/// ④ 普通日 → 复制上一日终并计提当日新增
|
||||
/// 注意:hasSwap/hasClose 是交易级标志(整笔合约当天有无事件),
|
||||
/// observationInterval 是腿级(本条利息腿当天是否观察日)——粒度不同,分派依赖此区分。
|
||||
/// 生产分派点:SwapEodPositionService.DealInterests(分派结构接线前为影子规格,见其分派处注释)。
|
||||
/// </summary>
|
||||
public enum InterestEodScenario
|
||||
{
|
||||
ManualSwap, // ① 手工互换:压制观察日自动结息
|
||||
AutoSettleWithClose, // ② 观察日 + 当日平仓 (autoSwap=true)
|
||||
AutoSettle, // ② 观察日 + 当日无平仓
|
||||
CloseOnly, // ③ 非观察日 + 当日平仓 (autoSwap=false)
|
||||
RollForward, // ④ 普通日滚动
|
||||
}
|
||||
|
||||
public static class InterestEodScenarioDispatch
|
||||
{
|
||||
/// <summary>三分量布尔 → 场景(8 组合表驱动见 InterestEodScenarioDispatchTest)。</summary>
|
||||
public static InterestEodScenario ResolveInterestScenario(bool hasInterval, bool hasSwap, bool hasClose)
|
||||
{
|
||||
if (hasSwap)
|
||||
return InterestEodScenario.ManualSwap;
|
||||
if (hasInterval)
|
||||
return hasClose ? InterestEodScenario.AutoSettleWithClose : InterestEodScenario.AutoSettle;
|
||||
if (hasClose)
|
||||
return InterestEodScenario.CloseOnly;
|
||||
return InterestEodScenario.RollForward;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找利息腿在指定结算日的观察日信息(SwapIntervalList 中 Date==settleDate 且 Settlement==1 的记录)。
|
||||
/// 观察日即自动结息触发日;返回 null 表示当日非观察日。分派见 ResolveInterestScenario。
|
||||
/// </summary>
|
||||
public static IntervalModel FindObservationInterval(swap_position interest, DateTime settleDate)
|
||||
{
|
||||
return interest.SwapIntervalList.FirstOrDefault(x => x.Date == settleDate && x.Settlement == 1);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
/// <summary>现金保证金:余额 = 现金余额。</summary>
|
||||
public sealed class CashMargin : IMarginResolver
|
||||
{
|
||||
public MarginForm Form => MarginForm.Cash;
|
||||
|
||||
public MarginBalance Resolve(decimal postedAmount)
|
||||
=> new(postedAmount);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
/// <summary>授信保证:余额 = 已用授信额度。</summary>
|
||||
public sealed class CreditMargin : IMarginResolver
|
||||
{
|
||||
public MarginForm Form => MarginForm.Credit;
|
||||
|
||||
public MarginBalance Resolve(decimal postedAmount)
|
||||
=> new(postedAmount);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
/// <summary>担保品:余额 = 担保品市值。</summary>
|
||||
public sealed class GuaranteeMargin : IMarginResolver
|
||||
{
|
||||
public MarginForm Form => MarginForm.Guarantee;
|
||||
|
||||
public MarginBalance Resolve(decimal postedAmount)
|
||||
=> new(postedAmount);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
namespace YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
/// <summary>保证金形态:现金 / 授信 / 担保。预留扩展。</summary>
|
||||
public enum MarginForm
|
||||
{
|
||||
/// <summary>现金保证金:余额 = 现金余额。</summary>
|
||||
Cash,
|
||||
/// <summary>授信保证:余额 = 已用授信额度。</summary>
|
||||
Credit,
|
||||
/// <summary>担保品:余额 = 担保品市值。</summary>
|
||||
Guarantee,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按保证金形态解析余额。三种形态可互换地产出一个 MarginBalance(满足 LSP),
|
||||
/// 这是保证金领域唯一合理的多态点(差异仅在"余额如何取得")。
|
||||
/// 具体余额来源(资金流水 / 授信占用 / 担保估值)后续按形态填充。
|
||||
/// </summary>
|
||||
public interface IMarginResolver
|
||||
{
|
||||
MarginForm Form { get; }
|
||||
|
||||
MarginBalance Resolve(decimal postedAmount);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
using YLErp.Core.Interest;
|
||||
using YLErp.Derivatives.Interest;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
/// <summary>
|
||||
/// 保证金账户。管理保证金余额的变动(追加/释放/返还),并提供计息入口。
|
||||
///
|
||||
/// 保证金是独立的资金管理概念(初始保证金/维持保证金/保证金余额/追保),
|
||||
/// 与融资腿(funding leg)完全无关。现有代码把保证金塞进 InterestMode==5/6
|
||||
/// 当计息腿处理是错误的,本类是正确建模的起点。
|
||||
///
|
||||
/// 利息计算委托 SwapInterest 纯函数(余额×利率×天数/年化),
|
||||
/// 保证金账户只提供余额和计息入口,不自己实现计息算法。
|
||||
/// </summary>
|
||||
public sealed class MarginAccount
|
||||
{
|
||||
/// <summary>当前保证金余额。</summary>
|
||||
public MarginBalance Balance { get; private set; }
|
||||
|
||||
public MarginAccount(MarginBalance openingBalance)
|
||||
=> Balance = openingBalance;
|
||||
|
||||
/// <summary>追加保证金(余额增加)。</summary>
|
||||
public void Deposit(decimal amount)
|
||||
=> Balance = new MarginBalance(Balance.Balance + amount);
|
||||
|
||||
/// <summary>释放/返还保证金(余额减少,不低于 0)。</summary>
|
||||
public void Withdraw(decimal amount)
|
||||
=> Balance = new MarginBalance(Math.Max(0m, Balance.Balance - amount));
|
||||
|
||||
/// <summary>
|
||||
/// 按当前余额计算保证金利息。委托 SwapInterest.AccrueSimple。
|
||||
/// 保证金利息是券商对客户保证金存款付息(方向与融资腿相反)。
|
||||
/// </summary>
|
||||
/// <param name="rate">保证金利率(年化,如 0.03 = 3%)。</param>
|
||||
/// <param name="startDate">计息开始日。</param>
|
||||
/// <param name="endDate">计息结束日。</param>
|
||||
/// <param name="boundary">算头算尾规则。</param>
|
||||
/// <param name="annualDays">年化天数(365 或 360)。</param>
|
||||
public InterestResult AccrueInterest(decimal rate, System.DateTime startDate, System.DateTime endDate, AccrualBoundary boundary, int annualDays)
|
||||
=> SwapInterest.AccrueSimple(new AccrualContext(annualDays), Balance.Balance, rate, startDate, endDate, boundary);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
namespace YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
/// <summary>
|
||||
/// 保证金余额。现金、授信、担保等多种保证金形态的统一表达。
|
||||
///
|
||||
/// 保证金就是保证金——有余额、有利率、有利息,不存在"计息基数/Notional"概念。
|
||||
/// 余额随追加/释放/盈亏变动,利息由 SwapInterest 纯函数按 余额×利率×天数/年化 计算。
|
||||
/// </summary>
|
||||
public readonly struct MarginBalance
|
||||
{
|
||||
/// <summary>保证金余额:现金余额 / 授信占用 / 担保品市值。</summary>
|
||||
public decimal Balance { get; }
|
||||
|
||||
public MarginBalance(decimal balance)
|
||||
=> Balance = balance;
|
||||
}
|
||||
@@ -7,14 +7,18 @@ namespace YLErp.Modules.SwapModule.Margin;
|
||||
/// <summary>
|
||||
/// 保证金计息模式(mode 5 初始预付金 / mode 6 追加预付金)的统一判断口径。
|
||||
///
|
||||
/// 现状(待收敛):同一集合 {初始预付金, 追加预付金} 在代码里复制了至少 6 次——
|
||||
/// ConsTrade.InterestMarginModels(框架级)
|
||||
/// SwapEodPositionService.marginTypes(实例字段)
|
||||
/// SwapEodPositionService.premiumModes(局部变量)
|
||||
/// SwapEventEmailService.marginTypes
|
||||
/// EodClientBalanceCalc.marginTypes
|
||||
/// ClientBalanceUtility.marginTypes
|
||||
/// 任何一处漏改(如新增保证金形态)都会导致口径分裂。本类收敛到单一来源。
|
||||
/// 依赖方向:本类位于 YLErpDAL 层,单一真源是框架层常量
|
||||
/// <see cref="ConsTrade.InterestMarginModels"/>(YLErp.DBModels)。Core 不能反向依赖 DAL,
|
||||
/// 故本类的集合直接由该框架常量派生(new HashSet/List),而非独立重写——
|
||||
/// 任何一处要新增保证金形态,只需改 ConsTrade.InterestMarginModels 即全局生效。
|
||||
///
|
||||
/// 收敛历史:早期同一集合 {初始预付金, 追加预付金} 在代码里被复制多次
|
||||
/// (ConsTrade.InterestMarginModels / SwapEodPositionService.marginTypes /
|
||||
/// SwapEodPositionService.premiumModes / SwapEventEmailService.marginTypes /
|
||||
/// EodClientBalanceCalc.marginTypes / ClientBalanceUtility.marginTypes)。
|
||||
/// 现余额/邮件/利息等入口已改用本类;SwapEodPositionService.premiumModes 局部变量
|
||||
/// 也已替换为 MarginModes.ForLinq。ConsTrade.InterestMarginModels 作为框架级常量保留
|
||||
/// (它是唯一真源,并非冗余)。
|
||||
///
|
||||
/// 注意:这里的"保证金 mode"是现有系统把保证金错误建模为计息腿的历史遗留。
|
||||
/// 按 Margin 限界上下文的设计方向,未来保证金不应用 InterestMode 标识,
|
||||
@@ -22,34 +26,24 @@ namespace YLErp.Modules.SwapModule.Margin;
|
||||
/// </summary>
|
||||
public static class MarginModes
|
||||
{
|
||||
/// <summary>所有属于保证金的 InterestMode(初始预付金 / 追加预付金)。</summary>
|
||||
public static readonly IReadOnlyCollection<int> All = new HashSet<int>
|
||||
{
|
||||
(int)InterestModeEnum.初始预付金,
|
||||
(int)InterestModeEnum.追加预付金,
|
||||
};
|
||||
/// <summary>所有属于保证金的 InterestMode(派生自 ConsTrade.InterestMarginModels)。</summary>
|
||||
public static readonly IReadOnlyCollection<int> All = new HashSet<int>(ConsTrade.InterestMarginModels);
|
||||
|
||||
/// <summary>
|
||||
/// List 形态,供 EF Core LINQ 表达式用(HashSet.Contains 无法翻译成 SQL)。
|
||||
/// 替代 ConsTrade.InterestMarginModels。
|
||||
/// 内容派生自框架常量 ConsTrade.InterestMarginModels(单一真源),本类仅做形态适配。
|
||||
/// </summary>
|
||||
public static readonly List<int> ForLinq = new()
|
||||
{
|
||||
(int)InterestModeEnum.初始预付金,
|
||||
(int)InterestModeEnum.追加预付金,
|
||||
};
|
||||
public static readonly List<int> ForLinq = new List<int>(ConsTrade.InterestMarginModels);
|
||||
|
||||
/// <summary>判断 mode 是否属于保证金(非 LINQ 场景用)。</summary>
|
||||
public static bool Contains(int interestMode) => All.Contains(interestMode);
|
||||
|
||||
/// <summary>固定值 + 保证金 mode 集合(固定值/初始预付金/追加预付金)。
|
||||
/// 用于 EOD 场景判断"计息基数取 InterestPrincipalFix 而非持仓名义本金"的腿。
|
||||
/// 替代 SwapEodPositionService 中 3 处内联 new List{固定值, 初始预付金, 追加预付金}。</summary>
|
||||
public static readonly IReadOnlyCollection<int> FixedAmountAndMargin = new HashSet<int>
|
||||
/// 保证金部分派生自 ConsTrade.InterestMarginModels,固定值额外并入。</summary>
|
||||
public static readonly IReadOnlyCollection<int> FixedAmountAndMargin = new HashSet<int>(ConsTrade.InterestMarginModels)
|
||||
{
|
||||
(int)InterestModeEnum.固定值,
|
||||
(int)InterestModeEnum.初始预付金,
|
||||
(int)InterestModeEnum.追加预付金,
|
||||
};
|
||||
|
||||
/// <summary>判断 mode 是否为固定值或保证金。</summary>
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
using YLErp.Modules.SwapModule.Accrual;
|
||||
using YLErp.Modules.SwapModule.FundingLegs;
|
||||
using YLErp.Modules.SwapModule.ReturnLegs;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Penalty;
|
||||
|
||||
/// <summary>
|
||||
/// EQD-6977 罚息接缝(纯函数,无 DB 依赖):把罚息金额**并入既有平仓利息流的 InterestFee(其他费用含罚息)**。
|
||||
/// 不产生独立罚息事件——前端其他费用列/盈亏公式(含 InterestFee)与日终 TdCloseInterestFee 链路天然承接。
|
||||
///
|
||||
/// 设计:上帝类(SwapDealService.GetIntradayUnwindInterests)仅注入三个外部依赖委托——
|
||||
/// getSpread(加点利差)/ getPreEod(上一日终快照行)/ tryGetFixing(定盘取价),本类零 DB 耦合、可 headless 单测。
|
||||
///
|
||||
/// 复利承接量(精确续接口径的关键)**必须取实际计息状态**,严禁冻结利率重放推导:
|
||||
/// 承接① capitalized = max(0, preEod.TdInterestPrincipal×份额 − closePrincipal) —— 实际滚动复利基数中已并入部分;
|
||||
/// 承接② carryIn = 正常平仓流实结 InterestAmount − ① —— 最近重置日后实际已计利息;
|
||||
/// 无 preEod(首日平仓):①=0、②=实结金额。
|
||||
/// 逐腿全程 trace 落盘(SwapCalcTrace),供计算过程分析与错误定位。
|
||||
/// </summary>
|
||||
public static class PenaltyInterestFeeMerger
|
||||
{
|
||||
/// <summary>
|
||||
/// 对每条融资腿:解析冻结利率 → 以实际计息状态推导承接量 → 计算罚息 → 并入该腿正常平仓利息事件的 InterestFee。
|
||||
/// 取不到冻结利率(浮动腿缺价且无 preEod)时跳过该腿(不阻断正常平仓),留 trace。
|
||||
/// </summary>
|
||||
public static void Merge(
|
||||
trade td,
|
||||
List<swap_position> fundingPositions,
|
||||
List<swap_flow_event> interests,
|
||||
DateTime unwindDate,
|
||||
int annualDays,
|
||||
bool unwindDaySettled,
|
||||
bool maturityCalcLast,
|
||||
decimal posiNotionalValue,
|
||||
decimal closePosiNotionalValue,
|
||||
decimal closePercent,
|
||||
Func<swap_position, decimal> getSpread,
|
||||
Func<swap_position, eod_swap_position?> getPreEod,
|
||||
Func<DateTime, string, decimal?> tryGetFixing,
|
||||
AccrualTrace? trace = null)
|
||||
{
|
||||
if (td.ExerciseDate == null)
|
||||
{
|
||||
trace?.Note("PENALTY|跳过 交易无到期日(ExerciseDate=null)");
|
||||
return;
|
||||
}
|
||||
var maturityDate = td.ExerciseDate.Value;
|
||||
|
||||
foreach (var position in fundingPositions)
|
||||
{
|
||||
// 正常平仓利息流(GetInterests 刚产出)——承接②的事实源与罚息并入目标
|
||||
var normalEvent = interests.FirstOrDefault(x => x.PositionId == position.id);
|
||||
if (normalEvent == null)
|
||||
{
|
||||
trace?.Note($"PENALTY|p{position.id} 跳过 无正常平仓利息流(意外:融资腿应有对应事件)");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 复用 GetInterests 的本金口径(mode2 无条件覆盖 / mode9 全平兜底,见其根因位置注释)
|
||||
var mode = (InterestModeEnum)position.InterestMode;
|
||||
var r = FundingLegStrategyFactory.Get(mode)
|
||||
.CalcNotional(position.InterestPrincipalFix, posiNotionalValue, closePercent);
|
||||
decimal closePrincipal = r.ClosePrincipal;
|
||||
if (mode == InterestModeEnum.合约名义本金规模
|
||||
|| (mode == InterestModeEnum.标的期初全价 && posiNotionalValue == 0m))
|
||||
{
|
||||
closePrincipal = closePosiNotionalValue;
|
||||
}
|
||||
var share = r.PosiPrincipal > 0m ? Math.Min(1m, closePrincipal / r.PosiPrincipal) : 1m;
|
||||
var isCompound = position.InterestType == (int)InterestTypeEnum.复利;
|
||||
|
||||
// 冻结利率:前一晚收盘在役利率优先(preEod.FloatRate),无快照再按取价日=unwindDate-1 所在区间取定盘
|
||||
FundingLegRate frozenRate;
|
||||
string rateSource;
|
||||
var preEod = getPreEod(position);
|
||||
try
|
||||
{
|
||||
frozenRate = PenaltyLegRateResolver.ResolveFrozenRate(
|
||||
position, getSpread(position), preEod?.FloatRate, unwindDate,
|
||||
d => tryGetFixing(d, position.FloatRateUnderlyingCode));
|
||||
rateSource = preEod != null
|
||||
? $"preEod.FloatRate@{preEod.ValueDate:yyyy-MM-dd}"
|
||||
: "定盘取价(unwindDate-1区间)";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
trace?.Note($"PENALTY|p{position.id} 跳过 冻结利率解析失败:{ex.Message}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 复利承接:实际滚动基数中已并入部分(①)+ 段内实际已计利息(②)。单利无并本金语义恒 0。
|
||||
// ① 的取值依赖平仓日是否为重置日、有无日终快照(数据契约):
|
||||
// 段中平仓 + 有快照:TdInterestPrincipal 即当前段滚动基数(=本金+①),直接作差;
|
||||
// 段中平仓 + 无快照:兜底取 normalEvent.InterestPrincipal——复利重放(CalcDailyCompoundInterest)
|
||||
// 会把它写为末次并本金后的基数(=被平份额本金+①),同样是实际值而非推导值;
|
||||
// 重置日当天平仓:快照基数仍是【上一段】的(今日并入尚未发生),须改取
|
||||
// preEod.InterestIncomeSum(昨日全部待实现利息 = 今日并入新段基数的那部分)。
|
||||
decimal capitalized = 0m, carryIn = 0m;
|
||||
if (isCompound)
|
||||
{
|
||||
var periodDays = position.interest_rest_days ?? 1;
|
||||
var unwindOnResetDay = SwapDealService.IsResetDay(unwindDate, position.PosiStartDate, periodDays);
|
||||
if (unwindOnResetDay)
|
||||
{
|
||||
capitalized = (preEod?.InterestIncomeSum ?? 0m) * share;
|
||||
if (preEod == null && (unwindDate - position.PosiStartDate).Days >= periodDays)
|
||||
trace?.Note($"PENALTY|p{position.id} 注意 无preEod且平仓日=重置日:①退化0(此前重置并入额缺失,请核对日终归档完整性)");
|
||||
}
|
||||
else if (preEod != null)
|
||||
{
|
||||
capitalized = Math.Max(0m, preEod.TdInterestPrincipal * share - closePrincipal);
|
||||
}
|
||||
else
|
||||
{
|
||||
capitalized = Math.Max(0m, normalEvent.InterestPrincipal - closePrincipal);
|
||||
}
|
||||
// ① 不得超过实结金额(数据异常时钳制并留痕,避免负②进入计息)
|
||||
if (capitalized > Math.Max(0m, normalEvent.InterestAmount))
|
||||
{
|
||||
trace?.Note($"PENALTY|p{position.id} 注意 承接①钳制:推导 {capitalized:F4} > 实结 {normalEvent.InterestAmount:F4}(快照/事件数据异常,请核对 preEod.TdInterestPrincipal/InterestIncomeSum)");
|
||||
capitalized = Math.Max(0m, normalEvent.InterestAmount);
|
||||
}
|
||||
carryIn = normalEvent.InterestAmount - capitalized;
|
||||
}
|
||||
|
||||
var policy = AccrualPolicy.BuildEod(position, annualDays, isCompound);
|
||||
// 锚点 = PosiStartDate:与正常计息重放(CalcDailyCompoundInterest 的分段网格)一致,延期腿勿用 td.StartDate
|
||||
var penalty = SwapPenaltyInterestCalculator.CalcPenaltyAmount(
|
||||
position, closePrincipal, unwindDate, maturityDate,
|
||||
unwindDaySettled, maturityCalcLast,
|
||||
capitalized, carryIn,
|
||||
frozenRate, policy, position.PosiStartDate, trace);
|
||||
penalty = InterestMath.Round(penalty, InterestMath.FundingLegPrecision);
|
||||
|
||||
var feeBefore = normalEvent.InterestFee;
|
||||
normalEvent.InterestFee += penalty;
|
||||
normalEvent.InterestClosePnL += penalty * DirectionRatio.ReceivePay(position.InterestDirection);
|
||||
|
||||
trace?.Note(
|
||||
$"PENALTY|p{position.id} 完成 mode={mode} {(isCompound ? "复利" : "单利")} " +
|
||||
$"窗口=[{unwindDate:yyyy-MM-dd}→{maturityDate:yyyy-MM-dd}] 平仓日已结={unwindDaySettled} 到期算尾={maturityCalcLast} | " +
|
||||
$"本金 close={closePrincipal:F2} posi={r.PosiPrincipal:F2} share={share:P4} | " +
|
||||
$"冻结利率={frozenRate.AllInRate:P6} 来源={rateSource} | " +
|
||||
$"承接①={capitalized:F4} ②={carryIn:F4} 实结={normalEvent.InterestAmount:F4} | " +
|
||||
$"罚息={penalty:F2} → InterestFee {feeBefore:F2}→{normalEvent.InterestFee:F2} PnL含罚息={normalEvent.InterestClosePnL:F2}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using YLErp.Derivatives.Interest;
|
||||
using YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Penalty;
|
||||
|
||||
/// <summary>
|
||||
/// EQD-6977 罚息冻结利率解析(纯函数)。
|
||||
///
|
||||
/// 规则(需求 2.2.2):剩余期限利率冻结为「最后一个重置区间」的 FR007 定盘值——
|
||||
/// 终止日恰为重置日且下午已出新价时,仍取上一重置区间(边缘场景显式落地)。
|
||||
///
|
||||
/// 冻结来源优先级:
|
||||
/// 1. preEod.FloatRate——上一日终快照即昨日「实际在役」利率(GetFloatRate 非重置日正是沿用它),
|
||||
/// 天然覆盖重置日下午边缘;且避开 td.StartDate / PosiStartDate 双锚点推导(见 GetFloatDate 锚点注记);
|
||||
/// 2. 无 preEod(首日平仓等):取价日 = GetFixingDate(unwindDate-1)(-1 所在重置区间的定盘,
|
||||
/// interest_rule 0=当前营业日/-1=前一营业日由 IndexFixerBase 统一处理)。
|
||||
/// 固定腿利率本即冻结,直接 Fixed;剩余期限的加点利差由调用方按 SwapIntervalList 取 as-of 平仓日值传入。
|
||||
/// </summary>
|
||||
public static class PenaltyLegRateResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// 解析罚息窗口的冻结 all-in 利率。
|
||||
/// </summary>
|
||||
/// <param name="position">利息腿(融资腿,非保证金)</param>
|
||||
/// <param name="spread">加点利差(调用方按 SwapIntervalList 取 as-of unwindDate 值,同 GetFixedRate 口径)</param>
|
||||
/// <param name="preEodFloatRate">上一日终快照 FloatRate;无 preEod 传 null</param>
|
||||
/// <param name="unwindDate">提前终止日</param>
|
||||
/// <param name="tryGetFixing">定盘取价委托(测试可注入);入参=取价日,无价返回 null</param>
|
||||
public static FundingLegRate ResolveFrozenRate(
|
||||
swap_position position,
|
||||
decimal spread,
|
||||
decimal? preEodFloatRate,
|
||||
DateTime unwindDate,
|
||||
Func<DateTime, decimal?> tryGetFixing)
|
||||
{
|
||||
if (string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
|
||||
return FundingLegRate.Fixed(spread);
|
||||
|
||||
if (preEodFloatRate.HasValue)
|
||||
return FundingLegRate.Floating(spread, preEodFloatRate.Value);
|
||||
|
||||
var fixingDate = IndexFixerBase.GetFixingDate(unwindDate.AddDays(-1), position.interest_rule);
|
||||
var fixing = tryGetFixing(fixingDate);
|
||||
if (!fixing.HasValue)
|
||||
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
|
||||
return FundingLegRate.Floating(spread, fixing.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
using YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Penalty;
|
||||
|
||||
/// <summary>
|
||||
/// EQD-6977 平仓罚息计算器(纯函数)——返回罚息金额。
|
||||
///
|
||||
/// 口径(2026-08-20 裁定,评审 11.5):**精确续接**。唯一近似 = 未来 FR007 不可得——
|
||||
/// 剩余窗口一律用「前一晚收盘在役利率」(preEod.FloatRate,由 PenaltyLegRateResolver 解析);
|
||||
/// 其余与正常到期计息**丝毫不能差**:每 7 天重置节奏照旧、并本金照旧、单/复利走同一套
|
||||
/// Accrual 纯函数。金标准恒等式(验收基准):
|
||||
///
|
||||
/// 全期利息 = 平仓日已结利息(正常平仓流) + 罚息金额(本方法)
|
||||
///
|
||||
/// 边界规格(经金标准恒等式测试钉死):
|
||||
/// - IncludeStart = !unwindDaySettled:正常结算已计平仓日(算尾)→ 罚息自次日起;不算尾 → 含平仓日;
|
||||
/// - IncludeEnd = maturityCalcLast:到期日沿用交易自身算尾约定(非本次平仓的 newCalcLast)。
|
||||
///
|
||||
/// 复利承接(平仓日落在重置段中间时与全期轨迹逐日对齐的两个量,**必须来自实际计息状态**,
|
||||
/// 由调用方 PenaltyInterestFeeMerger 从 preEod 快照与正常平仓流实结金额推导——严禁冻结利率重放推导,
|
||||
/// FR007 有真实利率历史时重放值必偏):
|
||||
/// - capitalizedInterest = 已并入最近重置日的累计利息 → 窗口首日起即加入计息基数;
|
||||
/// - carryInInterest = 最近重置日之后已计至平仓日的利息 → 首个窗口重置日并入并持续留在基数。
|
||||
/// 二者之和 = 被平部分的平仓日实结利息(正常平仓流 InterestAmount)。
|
||||
///
|
||||
/// 产物为金额,由接缝并入既有利息事件的 InterestFee(其他费用含罚息);不产生独立罚息事件。
|
||||
/// 仅融资腿;保证金腿(MarginModes)与浮动端 P&L 不进入本模块。
|
||||
/// </summary>
|
||||
public static class SwapPenaltyInterestCalculator
|
||||
{
|
||||
/// <summary>
|
||||
/// 计算罚息窗口 [unwindDate, maturityDate] 的罚息金额。
|
||||
/// </summary>
|
||||
/// <param name="position">被平的融资腿</param>
|
||||
/// <param name="closePrincipal">被平部分计息本金(部分平仓仅算被平份额)</param>
|
||||
/// <param name="unwindDate">提前终止日(窗口起点)</param>
|
||||
/// <param name="maturityDate">合约原始到期日(窗口终点,= td.ExerciseDate)</param>
|
||||
/// <param name="unwindDaySettled">正常平仓利息是否已计平仓日(effectiveCalcLast = calcLast || newCalcLast)</param>
|
||||
/// <param name="maturityCalcLast">交易到期日算尾约定(tradeExtend.CalcLast)</param>
|
||||
/// <param name="capitalizedInterest">复利承接①:已并入最近重置日的累计利息(被平份额),窗口首日起即入基数;单利传 0</param>
|
||||
/// <param name="carryInInterest">复利承接②:最近重置日后已计至平仓日的利息(被平份额),首个窗口重置日并入;单利传 0</param>
|
||||
/// <param name="frozenRate">冻结利率(PenaltyLegRateResolver.ResolveFrozenRate 产物 = 前一晚收盘在役利率)</param>
|
||||
/// <param name="policy">计息政策(单复利/重置周期/年化天数;Convention 由本方法覆盖)</param>
|
||||
/// <param name="resetAnchor">重置日锚点 = position.PosiStartDate(与正常计息重放网格一致,勿用 td.StartDate)</param>
|
||||
/// <param name="trace">计息轨迹(可选,SwapCalcTrace 落盘)</param>
|
||||
public static decimal CalcPenaltyAmount(
|
||||
swap_position position,
|
||||
decimal closePrincipal,
|
||||
DateTime unwindDate,
|
||||
DateTime maturityDate,
|
||||
bool unwindDaySettled,
|
||||
bool maturityCalcLast,
|
||||
decimal capitalizedInterest,
|
||||
decimal carryInInterest,
|
||||
FundingLegRate frozenRate,
|
||||
AccrualPolicy policy,
|
||||
DateTime resetAnchor,
|
||||
AccrualTrace? trace = null)
|
||||
{
|
||||
var boundary = AccrualBoundary.Of(includeStart: !unwindDaySettled, includeEnd: maturityCalcLast);
|
||||
var allInRate = frozenRate.AllInRate;
|
||||
|
||||
return policy.IsCompound
|
||||
? AccrueCompound(closePrincipal, capitalizedInterest, carryInInterest, unwindDate, maturityDate, boundary, policy, resetAnchor, allInRate, trace)
|
||||
: AccrueSimple(closePrincipal, unwindDate, maturityDate, boundary, policy, allInRate, trace);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 复利罚息计息:即使冻结利率为单值,也必须按重置日分段(并本金发生在分段边界),每段同一冻结利率。
|
||||
/// notional = 本金 + 已并入最近重置日的利息(capitalizedInterest):全期轨迹中当前重置段的滚动基数,
|
||||
/// 段内每一天都在其上计息——平仓日落在段中间时与全期逐日对齐的关键。carryInInterest 在首个窗口重置日并入。
|
||||
/// </summary>
|
||||
private static decimal AccrueCompound(
|
||||
decimal closePrincipal, decimal capitalizedInterest, decimal carryInInterest,
|
||||
DateTime unwindDate, DateTime maturityDate, AccrualBoundary boundary, AccrualPolicy policy,
|
||||
DateTime resetAnchor, decimal allInRate, AccrualTrace? trace)
|
||||
{
|
||||
var segments = BuildFrozenSegments(unwindDate, maturityDate, policy.ResetPeriodDays, resetAnchor, allInRate);
|
||||
var r = CompoundInterestAccrual.AccruePeriod(
|
||||
notional: closePrincipal + capitalizedInterest,
|
||||
segmentRates: segments,
|
||||
startDate: unwindDate,
|
||||
endDate: maturityDate,
|
||||
boundary: boundary,
|
||||
annualDays: policy.AnnualDays,
|
||||
isAnnualized: policy.IsAnnualized,
|
||||
resetCarryInterest: 0m,
|
||||
realizedInterest: 0m,
|
||||
unwindFraction: 1m,
|
||||
finalBasis: out _,
|
||||
trace: trace,
|
||||
carryInInterest: carryInInterest);
|
||||
return r.Accrued;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单利罚息计息:无并本金语义,冻结利率即单段全程(需求 2.2.1 公式:利率 × 名义本金 × 剩余天数 / 计息基准)。
|
||||
/// </summary>
|
||||
private static decimal AccrueSimple(
|
||||
decimal closePrincipal, DateTime unwindDate, DateTime maturityDate,
|
||||
AccrualBoundary boundary, AccrualPolicy policy, decimal allInRate, AccrualTrace? trace)
|
||||
{
|
||||
var r = SimpleInterestAccrual.AccruePeriod(
|
||||
priorAccrued: 0m,
|
||||
notional: closePrincipal,
|
||||
unwindFraction: 1m,
|
||||
segmentRates: new List<(DateTime StartDate, decimal Rate)> { (unwindDate, allInRate) },
|
||||
startDate: unwindDate,
|
||||
endDate: maturityDate,
|
||||
priorValueDate: unwindDate.AddDays(-1),
|
||||
boundary: boundary,
|
||||
annualDays: policy.AnnualDays,
|
||||
isAnnualized: policy.IsAnnualized,
|
||||
trace: trace);
|
||||
return r.Accrued;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 复利冻结分段:段边界 = 窗口内重置日((d - anchor) % period == 0,对齐 IsResetDay 公式),
|
||||
/// 每段填同一冻结利率。首段必为 (unwindDate, rate);[start,end] 含端点的重置日也生成段
|
||||
/// (末段起点==到期日时由 AccruePeriod 的边界决定是否计息)。
|
||||
/// </summary>
|
||||
private static List<(DateTime StartDate, decimal Rate)> BuildFrozenSegments(
|
||||
DateTime start, DateTime end, int periodDays, DateTime anchor, decimal rate)
|
||||
{
|
||||
if (periodDays <= 1)
|
||||
return new List<(DateTime, decimal)> { (start, rate) };
|
||||
|
||||
var segments = new List<(DateTime StartDate, decimal Rate)> { (start, rate) };
|
||||
for (var d = start.AddDays(1); d <= end; d = d.AddDays(1))
|
||||
{
|
||||
if (SwapDealService.IsResetDay(d, anchor, periodDays))
|
||||
segments.Add((d, rate));
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using YLErp.Core.Interest;
|
||||
using YLErp.Helpers;
|
||||
using YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -161,72 +161,13 @@ namespace YLErp.Modules.SwapModule
|
||||
: ConsGlobal.SwapDeliveryPriceRound;
|
||||
}
|
||||
|
||||
// 日终利息待实现需跨日累计,按表设计保留 12 位;已实现结算仍按金额两位处理。
|
||||
private const int EodInterestStoragePrecision = 12;
|
||||
|
||||
private static decimal RoundMoney(decimal value)
|
||||
{
|
||||
return Math.Round(value, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
private static decimal RoundEodInterest(decimal value)
|
||||
{
|
||||
return Math.Round(value, EodInterestStoragePrecision, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仅在写入 eod_swap_position 前统一快照精度。
|
||||
/// 浮动腿收益最终以金额两位展示和存储;利息腿的待实现、计息基数及利率保留 12 位,
|
||||
/// 使部分结算后的尾差可继续参与后续计息。
|
||||
/// </summary>
|
||||
private static void NormalizeEodPositionForStorage(eod_swap_position position)
|
||||
{
|
||||
if (string.IsNullOrEmpty(position.UnderlyingCode))
|
||||
{
|
||||
// 利息腿没有标的代码:待实现字段保留高精度,已实现结算字段收敛到金额两位。
|
||||
position.InterestPrincipalFix = RoundEodInterest(position.InterestPrincipalFix);
|
||||
position.InterestRateDefault = RoundEodInterest(position.InterestRateDefault);
|
||||
position.InterestFeePending = RoundEodInterest(position.InterestFeePending);
|
||||
position.TdInterestPrincipal = RoundEodInterest(position.TdInterestPrincipal);
|
||||
position.TdInterestRate = RoundEodInterest(position.TdInterestRate);
|
||||
position.TdInterestIncome = RoundEodInterest(position.TdInterestIncome);
|
||||
position.TdInterestFee = RoundEodInterest(position.TdInterestFee);
|
||||
position.InterestIncomeSum = RoundEodInterest(position.InterestIncomeSum);
|
||||
position.InterestFeeSum = RoundEodInterest(position.InterestFeeSum);
|
||||
position.InterestProfitSum = RoundEodInterest(position.InterestProfitSum);
|
||||
position.FloatRate = RoundEodInterest(position.FloatRate);
|
||||
position.SwapPositionValue = RoundEodInterest(position.SwapPositionValue);
|
||||
position.TdCloseInterest = RoundMoney(position.TdCloseInterest);
|
||||
position.TdCloseInterestFee = RoundMoney(position.TdCloseInterestFee);
|
||||
position.RealizedInterest = RoundMoney(position.RealizedInterest);
|
||||
position.RealizedInterestFee = RoundMoney(position.RealizedInterestFee);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 浮动腿有标的代码:其损益作为金额结果落库,统一按两位四舍五入。
|
||||
position.TdPosiDividend = RoundMoney(position.TdPosiDividend);
|
||||
position.PosiMtmPnL = RoundMoney(position.PosiMtmPnL);
|
||||
position.PosiDividendSum = RoundMoney(position.PosiDividendSum);
|
||||
position.PosiFeePending = RoundMoney(position.PosiFeePending);
|
||||
position.PosiProfitSum = RoundMoney(position.PosiProfitSum);
|
||||
position.TdCloseMtmPnl = RoundMoney(position.TdCloseMtmPnl);
|
||||
position.TdCloseDividend = RoundMoney(position.TdCloseDividend);
|
||||
position.TdCloseFee = RoundMoney(position.TdCloseFee);
|
||||
position.RealizedMtmPnL = RoundMoney(position.RealizedMtmPnL);
|
||||
position.RealizedDividend = RoundMoney(position.RealizedDividend);
|
||||
position.RealizedFee = RoundMoney(position.RealizedFee);
|
||||
position.SwapPositionValue = RoundMoney(position.SwapPositionValue);
|
||||
}
|
||||
position.RealizedPnl = RoundMoney(position.RealizedPnl);
|
||||
}
|
||||
|
||||
#region 可测试化接缝(Seams)——override 这些虚方法可在测试中替换 DB/外部调用,生产代码行为不变
|
||||
|
||||
/// <summary>持久化 eod 持仓记录(生产: DbContext.Add;测试: 收集到列表)</summary>
|
||||
protected virtual void PersistEodSwapPosition(eod_swap_position position)
|
||||
{
|
||||
// 所有新增或更新的日终持仓都经过此入口,避免不同日终分支出现精度差异。
|
||||
NormalizeEodPositionForStorage(position);
|
||||
EodPnlCalculator.NormalizeEodPositionForStorage(position);
|
||||
var storagePriceRound = GetStorageDeliveryPriceRound(position.UnderlyingInstrumentType, position.UnderlyingCode);
|
||||
position.PosiGrossPrice = Math.Round(position.PosiGrossPrice, storagePriceRound, MidpointRounding.AwayFromZero);
|
||||
position.UnderlyingPrice = Math.Round(position.UnderlyingPrice, storagePriceRound, MidpointRounding.AwayFromZero);
|
||||
@@ -254,26 +195,49 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算利息腿利息明细(生产: new SwapDealService(this).GetInterests;测试: 用StubSwapDealService内存算)
|
||||
/// 计算利息腿利息明细(生产: new SwapDealService(this).GetInterests;测试: 用StubSwapDealService内存算)。
|
||||
/// 参数与 SwapDealService.GetInterests 完全一致,保证行为不变。
|
||||
/// (needPrice/grossPrice 死参数已随 2026-08 收口删除,两侧同步。)
|
||||
/// </summary>
|
||||
protected virtual List<swap_flow_event> CalcSwapInterests(
|
||||
trade td, trade_extend tradeExtend,
|
||||
DateTime valueDate, DateTime unwindDate,
|
||||
List<eod_swap_position> eodPositions, List<swap_position> positions,
|
||||
decimal posiNotionalValue, decimal posiLongNotionalValue, decimal posiShortNotionalValue,
|
||||
decimal posiNotionalValue,
|
||||
decimal closePosiNotionalValue, decimal closePrecent,
|
||||
int eventType, bool tdClose, bool needPrice,
|
||||
decimal grossPrice, decimal orginPv,
|
||||
int eventType, bool tdClose,
|
||||
decimal orginPv,
|
||||
bool add = false, bool settment = true, bool newCalcLast = false,
|
||||
List<swap_flow_event> closeList = null)
|
||||
{
|
||||
return new SwapDealService(this).GetInterests(td, tradeExtend, valueDate, unwindDate,
|
||||
eodPositions, positions, posiNotionalValue, posiLongNotionalValue, posiShortNotionalValue,
|
||||
closePosiNotionalValue, closePrecent, eventType, tdClose, needPrice,
|
||||
grossPrice, orginPv, add, settment, newCalcLast, closeList);
|
||||
eodPositions, positions, posiNotionalValue,
|
||||
closePosiNotionalValue, closePrecent, eventType, tdClose,
|
||||
orginPv, add, settment, newCalcLast, closeList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 【EOD 当日有平仓后的收盘结息】显式入口——原 SaveAutoEodWithCloseInterestPosition 直调
|
||||
/// CalcSwapInterests(settment:false) 的具名封装(2026-08 显式化重构)。
|
||||
|
||||
/// 语义契约见 InterestCalcRequest.EodPostCloseSettle 工厂注释(平仓后剩余 + 实际平掉额 + 恒1全额结息,
|
||||
/// 触发 GetInterests 内 mode2/mode9 本金修正)。计息走 CalcUnwindInterest 全区间重放。
|
||||
/// 默认实现仍经 CalcSwapInterests 转发,保持既有测试替身对该虚接缝的拦截不变。
|
||||
/// </summary>
|
||||
protected virtual List<swap_flow_event> CalcEodPostCloseSettleInterests(InterestCalcRequest req)
|
||||
=> CalcSwapInterests(req.Td, req.TradeExtend, req.ValueDate, req.UnwindDate, req.EodPositions, req.Positions,
|
||||
req.PosiNotionalValue,
|
||||
req.ClosePosiNotionalValue, req.ClosePercent, req.EventType, req.TdClose,
|
||||
req.OrginPv, req.Add, settment: false, req.NewCalcLast, req.CloseList);
|
||||
|
||||
/// <summary>
|
||||
/// 持仓延续腿重置日再定盘(EQD-6968 口径自洽化接缝)。
|
||||
/// 生产:Fr007IndexFixer.GetFixingOrThrow——缺价抛异常,与 ByEod 重置日再定盘/EodCheckSettlePrice
|
||||
/// 同口径(EOD 时点当日 FR007 已由收盘前检查把关);测试:override 注入受控定盘。
|
||||
/// </summary>
|
||||
protected virtual decimal ResolveOngoingResetFixing(swap_position position, DateTime valueDate)
|
||||
=> Fr007IndexFixer.Instance.GetFixingOrThrow(valueDate, position.interest_rule, position.FloatRateUnderlyingCode);
|
||||
|
||||
// FindTrade 已上提到基类 SwapTradeBaseService(三子类实现一致,消除重复)
|
||||
|
||||
/// <summary>查找交易扩展(生产: DbContext.trade_extend;测试: 内存字典)</summary>
|
||||
@@ -291,7 +255,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <summary>查找交易持仓(生产: DbContext.swap_position.Where;测试: 内存列表)</summary>
|
||||
protected virtual List<swap_position> FindSwapPositions(int swapTradeId)
|
||||
{
|
||||
return DbContext.swap_position.Where(x => x.SwapTradeId == swapTradeId && !x.Invalid).ToList();
|
||||
return DbContext.swap_position.ActiveByTrade(swapTradeId).ToList();
|
||||
}
|
||||
|
||||
/// <summary>查找框架合约日终汇总(生产: DbContext.eod_swap.FirstOrDefault;测试: 内存字典)</summary>
|
||||
@@ -306,6 +270,12 @@ namespace YLErp.Modules.SwapModule
|
||||
return new SwapEventService(this).AddSwapEventDate(tradeDate, swapTradeId, eventType, data, clientCashId, save, reason);
|
||||
}
|
||||
|
||||
/// <summary>持久化互换流水事件(生产: DbContext.swap_flow_event.Add;测试: 收集到列表)</summary>
|
||||
protected virtual void PersistFlowEvent(swap_flow_event flowEvent)
|
||||
{
|
||||
DbContext.swap_flow_event.Add(flowEvent);
|
||||
}
|
||||
|
||||
/// <summary>在事务中执行(生产: BeginTransaction/Commit/Rollback;测试: 直接执行不包事务)</summary>
|
||||
protected virtual void ExecuteInTransaction(Action action)
|
||||
{
|
||||
@@ -677,17 +647,17 @@ namespace YLErp.Modules.SwapModule
|
||||
var closePosiNotional = curEodPosis.Where(s => s.TdCloseQty > 0).Sum(s => s.TdCloseQty * s.ContractSize * s.PosiGrossPrice);
|
||||
var grossPrice = curEodPosis.Where(x => x.PosiDirection > 0).FirstOrDefault()?.PosiGrossPrice ?? 0;
|
||||
//处理利息腿
|
||||
DealInterests(interestList, eodPositions, todyEodPositions, settleDate, td, flowEvents, autoInterests, lastEodSwap, posiLongNotional, posiShortNotional, closePosiNotional, grossPrice, orginPv);
|
||||
//获取自动互换的 interval 信息,用于确定结算日期
|
||||
IntervalModel autoInterval = null;
|
||||
DealInterests(interestList, eodPositions, todyEodPositions, settleDate, td, flowEvents, autoInterests, lastEodSwap, posiLongNotional + posiShortNotional, closePosiNotional, grossPrice, orginPv);
|
||||
//获取自动互换的观察日信息,用于确定结算日期
|
||||
IntervalModel observationInterval = null;
|
||||
foreach (var interest in interestList)
|
||||
{
|
||||
autoInterval = interest.SwapIntervalList.FirstOrDefault(x => x.Date == settleDate && x.Settlement == 1);
|
||||
if (autoInterval != null)
|
||||
observationInterval = InterestEodScenarioDispatch.FindObservationInterval(interest, settleDate);
|
||||
if (observationInterval != null)
|
||||
break;
|
||||
}
|
||||
// 自动互换(仅利息/预付金,不含分红)
|
||||
DealAutoInterests(autoInterests, td, settleDate, preDealDate, posiLongNotional + posiShortNotional, autoInterval);
|
||||
DealAutoInterests(autoInterests, td, settleDate, preDealDate, posiLongNotional + posiShortNotional, observationInterval);
|
||||
// 分红独立处理:只要当天有债券需要分红,则生成分红自动互换,与利息互换无关
|
||||
DealDividends(curEodPosis, td, settleDate, tradeExtend);
|
||||
//多空组合判断是否已到到期日且无持仓信息
|
||||
@@ -1342,8 +1312,7 @@ namespace YLErp.Modules.SwapModule
|
||||
List<swap_flow_event> flowEvents,
|
||||
List<swap_flow_event> autoInterests,
|
||||
eod_swap lastEodSwap,
|
||||
decimal posiLongNational,
|
||||
decimal posiShortNational,
|
||||
decimal posiTotalNotional,
|
||||
decimal closeNational,
|
||||
decimal grossPrice,
|
||||
decimal orginPv)
|
||||
@@ -1354,7 +1323,7 @@ namespace YLErp.Modules.SwapModule
|
||||
Log.Info($"[DealInterests] 参数验证 - settleDate: {settleDate:yyyy-MM-dd}, td.id: {td?.id}, td.TradeNumber: {td?.TradeNumber}");
|
||||
Log.Info($"[DealInterests] 参数验证 - interestList.Count: {interestList?.Count ?? 0}, eodPositions.Count: {eodPositions?.Count ?? 0}, todyEodPositions.Count: {todyEodPositions?.Count ?? 0}");
|
||||
Log.Info($"[DealInterests] 参数验证 - flowEvents.Count: {flowEvents?.Count ?? 0}, autoInterests.Count: {autoInterests?.Count ?? 0}");
|
||||
Log.Info($"[DealInterests] 参数验证 - posiLongNational: {posiLongNational}, posiShortNational: {posiShortNational}, closeNational: {closeNational}, grossPrice: {grossPrice}, orginPv: {orginPv}");
|
||||
Log.Info($"[DealInterests] 参数验证 - posiTotalNotional: {posiTotalNotional}, closeNational: {closeNational}, grossPrice: {grossPrice}, orginPv: {orginPv}");
|
||||
|
||||
// 验证关键参数
|
||||
if (td == null)
|
||||
@@ -1391,18 +1360,20 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
var eodPosition = eodPositions.FirstOrDefault(x => x.PositionId == interest.id);//上一日日终利息信息 可能不存在
|
||||
var tdEodPosition = todyEodPositions.FirstOrDefault(x => x.PositionId == interest.id);//当前结算日日终利息信息
|
||||
var insterval = interest.SwapIntervalList.FirstOrDefault(x => x.Date == settleDate && x.Settlement == 1);//自动互换观察日信息
|
||||
var observationInterval = InterestEodScenarioDispatch.FindObservationInterval(interest, settleDate);//自动互换观察日信息
|
||||
List<swap_flow_event> dealInterests = new List<swap_flow_event>();
|
||||
dealInterests.AddRange(flowEvents);
|
||||
var dealInterest = dealInterests.FirstOrDefault(n => n.PositionId == interest.id);//当日是否做过互换或平仓
|
||||
var swapEvents = flowEvents.Where(x => (x.EventType == (int)SwapEventTypeEnum.互换 || x.EventType == (int)SwapEventTypeEnum.平仓) && x.PositionId == interest.id).ToList();
|
||||
//如果当日有互换/当日有平仓 不再重新生成或更新
|
||||
Log.Info($"insterval is {insterval},hasSwap is {hasSwap},hasClose is {hasClose}");
|
||||
if (insterval != null && !hasSwap)
|
||||
Log.Info($"observationInterval is {observationInterval},hasSwap is {hasSwap},hasClose is {hasClose}");
|
||||
// 分派优先级与粒度说明见 ResolveInterestScenario;8 组合表驱动覆盖见 InterestEodScenarioDispatchTest。
|
||||
// 仅观察日两个分支把返回值收进 autoInterests(→资金记录)——分派错序=静默少结。
|
||||
if (observationInterval != null && !hasSwap)
|
||||
{
|
||||
if (!hasClose)//当日无平仓
|
||||
{
|
||||
var _autoInterests = SaveAutoEodInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, insterval, lastEodSwap, posiLongNational, posiShortNational, grossPrice, orginPv);
|
||||
var _autoInterests = SaveAutoEodInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, observationInterval, lastEodSwap, posiTotalNotional, grossPrice, orginPv);
|
||||
if (_autoInterests.Count > 0)
|
||||
{
|
||||
autoInterests.AddRange(_autoInterests);
|
||||
@@ -1410,7 +1381,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
else
|
||||
{
|
||||
var _autoInterests = SaveAutoEodWithCloseInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, insterval, posiLongNational, posiShortNational, swapEvents, closeNational, true, grossPrice, orginPv);
|
||||
var _autoInterests = SaveAutoEodWithCloseInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, observationInterval, posiTotalNotional, swapEvents, closeNational, autoSwap: true, grossPrice, orginPv);
|
||||
if (_autoInterests.Count > 0)
|
||||
{
|
||||
autoInterests.AddRange(_autoInterests);
|
||||
@@ -1423,14 +1394,15 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
else if (hasClose)
|
||||
{
|
||||
SaveAutoEodWithCloseInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, insterval, posiLongNational, posiShortNational, swapEvents, closeNational, false, grossPrice, orginPv);
|
||||
SaveAutoEodWithCloseInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, observationInterval, posiTotalNotional, swapEvents, closeNational, autoSwap: false, grossPrice, orginPv);
|
||||
}
|
||||
else//无自动互换、互换/平仓,复制上一日终信息,并计算当日新增利息
|
||||
{
|
||||
SaveEodInterestPositionCopy(eodPosition, tdEodPosition, settleDate, td, interest, lastEodSwap, true, posiLongNational, posiShortNational, grossPrice, orginPv);
|
||||
SaveEodInterestPositionCopy(eodPosition, tdEodPosition, settleDate, td, interest, lastEodSwap, true, posiTotalNotional, grossPrice, orginPv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理浮动腿归档
|
||||
/// </summary>
|
||||
@@ -1517,7 +1489,7 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
autoInterests.ForEach(x => x.PayDate = settleDate);
|
||||
|
||||
var premiumModes = new List<int>() { (int)InterestModeEnum.初始预付金, (int)InterestModeEnum.追加预付金 };
|
||||
var premiumModes = MarginModes.ForLinq;
|
||||
var premiumInterests = autoInterests.Where(x => premiumModes.Contains(x.InterestMode)).ToList();
|
||||
var interestLegs = autoInterests.Where(x => !premiumModes.Contains(x.InterestMode)).ToList();
|
||||
|
||||
@@ -1664,24 +1636,26 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
unwindData.ClientCashIds = clientCashIds;
|
||||
string data = JsonConvert.SerializeObject(unwindData);
|
||||
var swapEvent = new SwapEventService(this).AddSwapEventDate(unwindData.ValueDate, unwindData.SwapTradeId, (int)SwapEventTypeEnum.自动互换, data, clientCashId, true, "系统操作-自动互换");//将互换总额存入事件
|
||||
// 走虚方法 AddSwapEvent(与 ComposePage:800 一致),让测试可 override 捕获事件;
|
||||
// 默认实现仍是 new SwapEventService(this).AddSwapEventDate,生产行为不变。
|
||||
var swapEvent = AddSwapEvent(unwindData.ValueDate, unwindData.SwapTradeId, (int)SwapEventTypeEnum.自动互换, data, clientCashId, true, "系统操作-自动互换");//将互换总额存入事件
|
||||
if (flowEvents!=null)
|
||||
{
|
||||
flowEvents.ForEach(x =>
|
||||
{
|
||||
x.EventId = swapEvent.id;
|
||||
DbContext.swap_flow_event.Add(x);
|
||||
PersistFlowEvent(x);
|
||||
});
|
||||
UpdateInitalPostion(flowEvents, td.id);
|
||||
}
|
||||
|
||||
|
||||
// 保存分红事件
|
||||
if (dividendEvents != null)
|
||||
{
|
||||
dividendEvents.ForEach(x =>
|
||||
{
|
||||
x.EventId = swapEvent.id;
|
||||
DbContext.swap_flow_event.Add(x);
|
||||
PersistFlowEvent(x);
|
||||
});
|
||||
UpdateInitalPostion(dividendEvents, td.id);
|
||||
}
|
||||
@@ -1881,6 +1855,46 @@ namespace YLErp.Modules.SwapModule
|
||||
trans?.Dispose();
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 利息腿字段拷贝(SaveEodInterestPosition / SaveAutoEodInterestPosition / SaveAutoEodWithCloseInterestPosition 共用)。
|
||||
/// FloatRate 来源随场景不同(手工互换=当日流水;自动互换/平仓=计息结果),由调用方算好传入,勿在本方法内统一。
|
||||
/// 场景差异字段(PosiStatus / InterestFeePending / TdInterestPrincipal / TdInterestRate)留在各调用点。
|
||||
/// </summary>
|
||||
private static void CopyInterestLegFields(eod_swap_position newEodPayPosition, swap_position position, decimal floatRate)
|
||||
{
|
||||
newEodPayPosition.InterestDirection = position.InterestDirection;
|
||||
newEodPayPosition.InterestMode = position.InterestMode;
|
||||
newEodPayPosition.InterestPrincipalFix = position.InterestPrincipalFix;
|
||||
newEodPayPosition.InterestRateDefault = position.InterestRateDefault;
|
||||
newEodPayPosition.InterestSwapInterval = position.InterestSwapInterval;
|
||||
newEodPayPosition.IsAnnualized = position.IsAnnualized;
|
||||
newEodPayPosition.HappenDate = position.HappenDate;
|
||||
newEodPayPosition.Currency = position.Currency;
|
||||
newEodPayPosition.InterestType = position.InterestType;
|
||||
newEodPayPosition.interest_rest_days = position.interest_rest_days;
|
||||
newEodPayPosition.interest_rule = position.interest_rule;
|
||||
newEodPayPosition.FloatRate = floatRate;
|
||||
newEodPayPosition.FloatRateUnderlyingCode = position.FloatRateUnderlyingCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 利息腿日终滚存收尾(四个 Save* 共用):RollRealized 滚累计已实现 → SetFixedLegRealizedPnl → 汇率 → TdCurrency。
|
||||
/// RealizedInterest 只增不回滚:上日累计已实现 + 当日结息按方向后的金额。
|
||||
/// interestDirection 是 RateType 的方向来源——三个方法取 position.InterestDirection,
|
||||
/// SaveEodInterestPositionCopy 取 eodPayPosition.InterestDirection(现状差异,勿统一)。
|
||||
/// PersistEodSwapPosition 与各自日志留在调用点(持久化边界 + 日志顺序各不相同)。
|
||||
/// </summary>
|
||||
private void FinalizeInterestEodRoll(eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition, int ratio, trade td, DateTime valueDate, int interestDirection)
|
||||
{
|
||||
var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio);
|
||||
newEodPayPosition.RealizedInterest = rolled.Interest;
|
||||
newEodPayPosition.RealizedInterestFee = rolled.Fee;
|
||||
SetFixedLegRealizedPnl(newEodPayPosition);
|
||||
var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true,
|
||||
DirectionRatio.RateType(interestDirection));
|
||||
newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 产生互换用
|
||||
/// </summary>
|
||||
@@ -1924,20 +1938,8 @@ namespace YLErp.Modules.SwapModule
|
||||
UpdateDbOption(newEodPayPosition);
|
||||
newEodPayPosition.PosiStatus = 0;
|
||||
newEodPayPosition.Invalid = false;
|
||||
//持仓内容-利息腿
|
||||
newEodPayPosition.InterestDirection = position.InterestDirection;
|
||||
newEodPayPosition.InterestMode = position.InterestMode;
|
||||
newEodPayPosition.InterestPrincipalFix = position.InterestPrincipalFix;
|
||||
newEodPayPosition.InterestRateDefault = position.InterestRateDefault;
|
||||
newEodPayPosition.InterestSwapInterval = position.InterestSwapInterval;
|
||||
newEodPayPosition.IsAnnualized = position.IsAnnualized;
|
||||
newEodPayPosition.HappenDate = position.HappenDate;
|
||||
newEodPayPosition.Currency = position.Currency;
|
||||
newEodPayPosition.InterestType = position.InterestType;
|
||||
newEodPayPosition.interest_rest_days = position.interest_rest_days;
|
||||
newEodPayPosition.interest_rule = position.interest_rule;
|
||||
newEodPayPosition.FloatRate = flowEvents.FirstOrDefault()?.FloatRate ?? 0;
|
||||
newEodPayPosition.FloatRateUnderlyingCode = position.FloatRateUnderlyingCode;
|
||||
//持仓内容-利息腿(FloatRate 取当日互换/平仓流水)
|
||||
CopyInterestLegFields(newEodPayPosition, position, flowEvents.FirstOrDefault()?.FloatRate ?? 0);
|
||||
newEodPayPosition.InterestFeePending = 0;
|
||||
//利息端估值用信息
|
||||
newEodPayPosition.TdInterestPrincipal = flowEvents.FirstOrDefault()?.InterestPrincipal ?? 0;
|
||||
@@ -1956,8 +1958,8 @@ namespace YLErp.Modules.SwapModule
|
||||
var interestFeeBeforeSettlement = eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee;
|
||||
var isMaturityFinalSettlement = valueDate.Date >= td.ExerciseDate.Value.Date
|
||||
&& flowEvents.Any()
|
||||
&& RoundMoney(interestIncomeBeforeSettlement) == RoundMoney(newEodPayPosition.TdCloseInterest)
|
||||
&& RoundMoney(interestFeeBeforeSettlement) == RoundMoney(newEodPayPosition.TdCloseInterestFee);
|
||||
&& EodPnlCalculator.RoundMoney(interestIncomeBeforeSettlement) == EodPnlCalculator.RoundMoney(newEodPayPosition.TdCloseInterest)
|
||||
&& EodPnlCalculator.RoundMoney(interestFeeBeforeSettlement) == EodPnlCalculator.RoundMoney(newEodPayPosition.TdCloseInterestFee);
|
||||
|
||||
if (isMaturityFinalSettlement)
|
||||
{
|
||||
@@ -1968,21 +1970,15 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
else
|
||||
{
|
||||
newEodPayPosition.InterestIncomeSum = RoundEodInterest(interestIncomeBeforeSettlement - newEodPayPosition.TdCloseInterest);
|
||||
newEodPayPosition.InterestFeeSum = RoundEodInterest(interestFeeBeforeSettlement - newEodPayPosition.TdCloseInterestFee);
|
||||
newEodPayPosition.InterestIncomeSum = EodPnlCalculator.RoundEodInterest(interestIncomeBeforeSettlement - newEodPayPosition.TdCloseInterest);
|
||||
newEodPayPosition.InterestFeeSum = EodPnlCalculator.RoundEodInterest(interestFeeBeforeSettlement - newEodPayPosition.TdCloseInterestFee);
|
||||
}
|
||||
newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum;
|
||||
//持仓价值
|
||||
newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio);
|
||||
|
||||
//累计已实现
|
||||
var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio);
|
||||
newEodPayPosition.RealizedInterest = rolled.Interest;
|
||||
newEodPayPosition.RealizedInterestFee = rolled.Fee;
|
||||
SetFixedLegRealizedPnl(newEodPayPosition);
|
||||
var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true,
|
||||
DirectionRatio.RateType(position.InterestDirection));
|
||||
newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate);
|
||||
//累计已实现(滚存收尾见 FinalizeInterestEodRoll)
|
||||
FinalizeInterestEodRoll(eodPayPosition, newEodPayPosition, ratio, td, valueDate, position.InterestDirection);
|
||||
PersistEodSwapPosition(newEodPayPosition);
|
||||
}
|
||||
/// <summary>
|
||||
@@ -1997,7 +1993,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <param name="preDealDate">上一平仓/互换日期</param>
|
||||
/// <param name="closeAmount">当日平仓金额</param>
|
||||
/// <param name="lastEodSwap">上一日终框架合约估值</param>
|
||||
protected List<swap_flow_event> SaveAutoEodInterestPosition(eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition, swap_position position, trade td, DateTime valueDate, IntervalModel interval, eod_swap lastEodSwap, decimal posiLongNotional, decimal posiShortNational, decimal grossPrice, decimal orginPv)
|
||||
protected List<swap_flow_event> SaveAutoEodInterestPosition(eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition, swap_position position, trade td, DateTime valueDate, IntervalModel interval, eod_swap lastEodSwap, decimal posiTotalNotional, decimal grossPrice, decimal orginPv)
|
||||
{
|
||||
Log.Info($"[SaveAutoEodInterestPosition] 开始执行 - valueDate: {valueDate:yyyy-MM-dd}, td.id: {td?.id}, position.id: {position?.id}");
|
||||
|
||||
@@ -2036,7 +2032,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
var tradeExtend = td.trade_extend.ExtendObj;
|
||||
decimal posiNotionalValue = posiLongNotional + posiShortNational;
|
||||
decimal posiNotionalValue = posiTotalNotional;
|
||||
decimal closePercent = 1;
|
||||
var ratio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
|
||||
if (eodPayPosition == null)
|
||||
@@ -2056,12 +2052,14 @@ namespace YLErp.Modules.SwapModule
|
||||
positions.Add(position);
|
||||
List<eod_swap_position> preEodPositions = new List<eod_swap_position>();
|
||||
preEodPositions.Add(eodPayPosition);
|
||||
// orginPv 在此仅对固定值腿(mode 1)生效;保证金腿(5/6)的 orginPv 虽在此赋值,
|
||||
// 但 GetInterests 保证金分支已走 CalcMarginInterest(内部自算 orginPv=PreviousBalance),忽略此处传入值。
|
||||
var interestModes = MarginModes.FixedAmountAndMargin;
|
||||
if (interestModes.Contains(position.InterestMode))
|
||||
{
|
||||
orginPv = eodPayPosition.InterestPrincipalFix;
|
||||
}
|
||||
var interests = CalcSwapInterests(td, td.trade_extend, valueDate, valueDate, preEodPositions, positions, posiNotionalValue, posiLongNotional, posiShortNational, posiNotionalValue, closePercent, (int)SwapEventTypeEnum.自动互换, false, true, grossPrice, orginPv, true);
|
||||
var interests = CalcSwapInterests(td, td.trade_extend, valueDate, valueDate, preEodPositions, positions, posiNotionalValue, posiNotionalValue, closePercent, (int)SwapEventTypeEnum.自动互换, false, orginPv, true);
|
||||
decimal interestAmountBeforeSettlement = interests.Sum(x => x.InterestAmount);
|
||||
decimal tdInterestAmount = interests.Sum(x => x.TdInterestAmount);
|
||||
|
||||
@@ -2069,8 +2067,8 @@ namespace YLErp.Modules.SwapModule
|
||||
// 日终快照仍使用上面的高精度应结金额计算待实现尾差,避免把舍入差提前丢掉。
|
||||
interests.ForEach(x =>
|
||||
{
|
||||
x.InterestAmount = RoundMoney(x.InterestAmount);
|
||||
x.InterestClosePnL = RoundMoney(x.InterestClosePnL);
|
||||
x.InterestAmount = EodPnlCalculator.RoundMoney(x.InterestAmount);
|
||||
x.InterestClosePnL = EodPnlCalculator.RoundMoney(x.InterestClosePnL);
|
||||
});
|
||||
decimal settledInterestAmount = interests.Sum(x => x.InterestAmount);
|
||||
|
||||
@@ -2078,20 +2076,8 @@ namespace YLErp.Modules.SwapModule
|
||||
newEodPayPosition.PositionId = position.id;
|
||||
UpdateDbOption(newEodPayPosition);
|
||||
newEodPayPosition.Invalid = false;
|
||||
//持仓内容-利息腿
|
||||
newEodPayPosition.InterestDirection = position.InterestDirection;
|
||||
newEodPayPosition.InterestMode = position.InterestMode;
|
||||
newEodPayPosition.InterestPrincipalFix = position.InterestPrincipalFix;
|
||||
newEodPayPosition.InterestRateDefault = position.InterestRateDefault;
|
||||
newEodPayPosition.InterestSwapInterval = position.InterestSwapInterval;
|
||||
newEodPayPosition.IsAnnualized = position.IsAnnualized;
|
||||
newEodPayPosition.HappenDate = position.HappenDate;
|
||||
newEodPayPosition.Currency = position.Currency;
|
||||
newEodPayPosition.InterestType = position.InterestType;
|
||||
newEodPayPosition.interest_rest_days = position.interest_rest_days;
|
||||
newEodPayPosition.interest_rule = position.interest_rule;
|
||||
newEodPayPosition.FloatRate = interests.Count > 0 ? interests.First().FloatRate ?? 0 : 0;
|
||||
newEodPayPosition.FloatRateUnderlyingCode = position.FloatRateUnderlyingCode;
|
||||
//持仓内容-利息腿(FloatRate 取计息结果)
|
||||
CopyInterestLegFields(newEodPayPosition, position, interests.Count > 0 ? interests.First().FloatRate ?? 0 : 0);
|
||||
newEodPayPosition.InterestFeePending = 0;
|
||||
//利息端估值用信息
|
||||
newEodPayPosition.TdInterestPrincipal = interestModes.Contains(position.InterestMode) ? eodPayPosition.InterestPrincipalFix : posiNotionalValue;
|
||||
@@ -2107,22 +2093,16 @@ namespace YLErp.Modules.SwapModule
|
||||
// 到期自动互换是最后一次自动结算:两位实际金额已落流水/资金,待实现不再滚入下一日。
|
||||
newEodPayPosition.InterestIncomeSum = isMaturityFinalAutoSettlement
|
||||
? 0
|
||||
: RoundEodInterest(interestAmountBeforeSettlement - settledInterestAmount);
|
||||
: EodPnlCalculator.RoundEodInterest(interestAmountBeforeSettlement - settledInterestAmount);
|
||||
newEodPayPosition.InterestFeeSum = isMaturityFinalAutoSettlement
|
||||
? 0
|
||||
: RoundEodInterest(eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee - newEodPayPosition.TdCloseInterestFee);
|
||||
: EodPnlCalculator.RoundEodInterest(eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee - newEodPayPosition.TdCloseInterestFee);
|
||||
newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum;
|
||||
//持仓价值
|
||||
newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio);
|
||||
|
||||
//累计已实现
|
||||
var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio);
|
||||
newEodPayPosition.RealizedInterest = rolled.Interest;
|
||||
newEodPayPosition.RealizedInterestFee = rolled.Fee;
|
||||
SetFixedLegRealizedPnl(newEodPayPosition);
|
||||
var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true,
|
||||
DirectionRatio.RateType(position.InterestDirection));
|
||||
newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate);
|
||||
//累计已实现(滚存收尾见 FinalizeInterestEodRoll)
|
||||
FinalizeInterestEodRoll(eodPayPosition, newEodPayPosition, ratio, td, valueDate, position.InterestDirection);
|
||||
PersistEodSwapPosition(newEodPayPosition);
|
||||
Log.Info($"the last newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}");
|
||||
return interests;
|
||||
@@ -2146,7 +2126,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <param name="closeAmount">当日平仓金额</param>
|
||||
/// <param name="lastEodSwap">上一日终框架合约估值</param>
|
||||
/// <param name="unwintotal">平仓主信息</param>
|
||||
protected List<swap_flow_event> SaveAutoEodWithCloseInterestPosition(eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition, swap_position position, trade td, DateTime valueDate, IntervalModel interval, decimal posiLongNotional, decimal posiShortNational, List<swap_flow_event> flowEvents, decimal closeNational, bool autoSwap, decimal grossPrice, decimal orginPv)
|
||||
protected List<swap_flow_event> SaveAutoEodWithCloseInterestPosition(eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition, swap_position position, trade td, DateTime valueDate, IntervalModel interval, decimal posiTotalNotional, List<swap_flow_event> flowEvents, decimal closeNational, bool autoSwap, decimal grossPrice, decimal orginPv)
|
||||
{
|
||||
Log.Info($"eodPayPosition is {JsonHelper.Serialize(eodPayPosition, false)},newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}");
|
||||
var tradeExtend = td.trade_extend.ExtendObj;
|
||||
@@ -2157,8 +2137,8 @@ namespace YLErp.Modules.SwapModule
|
||||
// 调用共享计息器。因此策略的 "posiNotional × closePercent" 在本例会得到 212197382.46,
|
||||
// 而本次实际应结的平仓本金是 closeNational=90941735.34。该语义错位由
|
||||
// SwapDealService.GetInterests 的模式2无条件修正、模式9全平零值兜底分流处理,不能删除。
|
||||
decimal oriPosiNotionalValue = posiLongNotional + posiShortNational + closeNational;
|
||||
decimal posiNotionalValue = posiLongNotional + posiShortNational;
|
||||
decimal oriPosiNotionalValue = posiTotalNotional + closeNational;
|
||||
decimal posiNotionalValue = posiTotalNotional;
|
||||
// ratio 只负责把腿内原始金额转换为本方盈亏方向,不参与计息金额本身的计算。
|
||||
var ratio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
|
||||
// 首次日终结算可能包含当日收盘,因此尚无先前的日终利息持仓。
|
||||
@@ -2194,6 +2174,8 @@ namespace YLErp.Modules.SwapModule
|
||||
newEodPayPosition = eodPayPosition.Clone();
|
||||
newEodPayPosition.id = 0;
|
||||
}
|
||||
// orginPv 在此仅对固定值腿(mode 1)生效;保证金腿(5/6)的 orginPv 虽在此赋值,
|
||||
// 但 GetInterests 保证金分支已走 CalcMarginInterest(内部自算 orginPv=PreviousBalance),忽略此处传入值。
|
||||
var interestModes = MarginModes.FixedAmountAndMargin;
|
||||
if (interestModes.Contains(position.InterestMode))
|
||||
{
|
||||
@@ -2219,10 +2201,19 @@ namespace YLErp.Modules.SwapModule
|
||||
positions.Add(position);
|
||||
List<eod_swap_position> preEodPositions = new List<eod_swap_position>();
|
||||
preEodPositions.Add(eodPayPosition);
|
||||
var calcLast = tradeExtend?.InterestCalcMode?.EndsWith("1") ?? true;
|
||||
// 此处 closePercent=1 表示 EOD 计算本次事件时走全额结息;它不是 closeNational / oriPosiNotionalValue。
|
||||
// 与上方“收盘后剩余本金”同时传入会触发共享计息器的模式2/9本金修正,见 GetInterests。
|
||||
var interests = CalcSwapInterests(td, td.trade_extend, valueDate, valueDate, preEodPositions, positions, posiNotionalValue, posiLongNotional, posiShortNational, closeNational, 1, eventType, false, true, grossPrice, orginPv, true, settment: false, newCalcLast: autoSwap || calcLast);
|
||||
var calcLast = tradeExtend?.CalcLast ?? true;
|
||||
// 显式入口:平仓后剩余本金 + 实际平掉额 + 恒1全额结息(语义见 InterestCalcRequest.EodPostCloseSettle)。
|
||||
// 该组合触发 GetInterests 内共享计息器的模式2/9本金修正(见其"根因位置"注释,勿删)。
|
||||
// 恒1 重算的 InterestAmount 是结算现金流的直接输入(非无害中间值):系统端到端结算结果由 DI_EXCEL_SCENARIO4 家族对账确认书公式保障(最终全平=剩余额×∏利率,2026-08-18 手算复核)。改动本口径前必读该测试家族——任何破坏 ∏ 恒等式的调整都会被其拦截。
|
||||
// 口径选择常驻记录(快速定位第一入口):出问题先看这行确认当日本次事件的金额输入,再顺着
|
||||
// SwapCalcTrace 分段过程日志追计算;autoSwap=观察日结现路径。
|
||||
Log.Info($"[EOD平仓后收盘结息] tradeId={td.id} valueDate={valueDate:yyyy-MM-dd} autoSwap={autoSwap} " +
|
||||
$"口径=全额结息(恒1惯例) " +
|
||||
$"oriPosi(平仓前)={oriPosiNotionalValue} posi(剩余)={posiNotionalValue} close(平掉)={closeNational}");
|
||||
var interests = CalcEodPostCloseSettleInterests(InterestCalcRequest.EodPostCloseSettle(
|
||||
td, td.trade_extend, valueDate, valueDate, preEodPositions, positions,
|
||||
posiNotionalValue, closeNational,
|
||||
eventType, tdClose: false, orginPv, add: true, newCalcLast: autoSwap || calcLast));
|
||||
// TdInterestAmount:计息器返回的全腿当日/累计参考值,用于拆出 EOD 的当日新增。
|
||||
// interestAmountBeforeSettlement:本次事件发生前理论应结的高精度利息。
|
||||
// manualSettledInterestAmount:swap_flow_event 实际落库的手工结息,金额已按分处理。
|
||||
@@ -2232,7 +2223,7 @@ namespace YLErp.Modules.SwapModule
|
||||
decimal autoSettledInterestAmount = 0m;
|
||||
if (autoSwap && interests.Count > 0)
|
||||
{
|
||||
autoSettledInterestAmount = RoundMoney(interestAmountBeforeSettlement - manualSettledInterestAmount);
|
||||
autoSettledInterestAmount = EodPnlCalculator.RoundMoney(interestAmountBeforeSettlement - manualSettledInterestAmount);
|
||||
var autoInterest = interests[0];
|
||||
autoInterest.InterestAmount = autoSettledInterestAmount;
|
||||
autoInterest.InterestClosePnL = autoSettledInterestAmount
|
||||
@@ -2242,22 +2233,23 @@ namespace YLErp.Modules.SwapModule
|
||||
newEodPayPosition.PositionId = position.id;
|
||||
UpdateDbOption(newEodPayPosition);
|
||||
newEodPayPosition.Invalid = false;
|
||||
//持仓内容-利息腿
|
||||
newEodPayPosition.InterestDirection = position.InterestDirection;
|
||||
newEodPayPosition.InterestMode = position.InterestMode;
|
||||
// ResolveInterestLegPositions 已提供平仓后的实时剩余本金,日终不再重复扣减。
|
||||
newEodPayPosition.InterestPrincipalFix = position.InterestPrincipalFix;
|
||||
// newEodPayPosition.InterestPrincipalFix *= (1 - closePercent);
|
||||
newEodPayPosition.InterestRateDefault = position.InterestRateDefault;
|
||||
newEodPayPosition.InterestSwapInterval = position.InterestSwapInterval;
|
||||
newEodPayPosition.IsAnnualized = position.IsAnnualized;
|
||||
newEodPayPosition.HappenDate = position.HappenDate;
|
||||
newEodPayPosition.Currency = position.Currency;
|
||||
newEodPayPosition.InterestType = position.InterestType;
|
||||
newEodPayPosition.FloatRate = interests.Count > 0 ? interests.First().FloatRate ?? 0 : 0;
|
||||
newEodPayPosition.FloatRateUnderlyingCode = position.FloatRateUnderlyingCode;
|
||||
newEodPayPosition.interest_rest_days = position.interest_rest_days;
|
||||
newEodPayPosition.interest_rule = position.interest_rule;
|
||||
//持仓内容-利息腿(FloatRate 取计息结果)。InterestPrincipalFix 保持腿现值:
|
||||
// ResolveInterestLegPositions 已提供平仓后的实时剩余本金,日终不再重复扣减(勿恢复 *(1-closePercent))。
|
||||
CopyInterestLegFields(newEodPayPosition, position, interests.Count > 0 ? interests.First().FloatRate ?? 0 : 0);
|
||||
// ── 持仓延续腿重置日再定盘(EQD-6968 自洽化)──
|
||||
// 排除日取价已收口为"纯跳过":事件利率=末段已消费利率。但剩余持仓自当日起进入新计息周期,
|
||||
// 快照 FloatRate 是后续非重置日(ByEod 沿用 preEod.FloatRate)与当日应计(intersetAcmount)的
|
||||
// 利率载体——平仓日恰为重置日时必须显式取当日新定盘(与 ByEod 日增路径的重置日行为同构)。
|
||||
// 全平(剩余=0)/算尾(事件利率已是新定盘)/观察日(autoSwap 恒1已含当日)无需再定盘。
|
||||
if (!autoSwap && !calcLast && posiNotionalValue > 0m
|
||||
&& !string.IsNullOrEmpty(position.FloatRateUnderlyingCode)
|
||||
&& SwapDealService.IsResetDay(valueDate, td.StartDate.Value, position.interest_rest_days ?? 1))
|
||||
{
|
||||
var ongoingFixing = ResolveOngoingResetFixing(position, valueDate);
|
||||
SwapCalcTrace.Critical(
|
||||
$"FIX EodCloseRefix p{position.id} {valueDate:yyyy-MM-dd} 平仓日=重置日→剩余持仓快照再定盘 {newEodPayPosition.FloatRate:P6}→{ongoingFixing:P6}");
|
||||
newEodPayPosition.FloatRate = ongoingFixing;
|
||||
}
|
||||
//利息端估值用信息
|
||||
// TdInterestPrincipal 是“下一日继续计息的收盘后本金”,不是原始合同规模,也不是本次平仓本金。
|
||||
// 模式9单利直接取剩余名义本金;复利还要保留重置时已经并入本金的待实现利息。
|
||||
@@ -2362,13 +2354,13 @@ namespace YLErp.Modules.SwapModule
|
||||
// InterestIncomeSum 是收盘后仍未结算的尾差/剩余利息。
|
||||
// 部分平仓:扣款前待实现 - TdCloseInterest;最终全平且两位金额已覆盖时直接清零。
|
||||
newEodPayPosition.InterestIncomeSum = closePercent == 1
|
||||
&& RoundMoney(pendingInterestBeforeSettlement) == RoundMoney(newEodPayPosition.TdCloseInterest)
|
||||
&& EodPnlCalculator.RoundMoney(pendingInterestBeforeSettlement) == EodPnlCalculator.RoundMoney(newEodPayPosition.TdCloseInterest)
|
||||
? 0m
|
||||
: RoundEodInterest(pendingInterestBeforeSettlement - newEodPayPosition.TdCloseInterest);
|
||||
: EodPnlCalculator.RoundEodInterest(pendingInterestBeforeSettlement - newEodPayPosition.TdCloseInterest);
|
||||
newEodPayPosition.InterestFeeSum = closePercent == 1
|
||||
&& RoundMoney(pendingInterestFeeBeforeSettlement) == RoundMoney(newEodPayPosition.TdCloseInterestFee)
|
||||
&& EodPnlCalculator.RoundMoney(pendingInterestFeeBeforeSettlement) == EodPnlCalculator.RoundMoney(newEodPayPosition.TdCloseInterestFee)
|
||||
? 0m
|
||||
: RoundEodInterest(pendingInterestFeeBeforeSettlement - newEodPayPosition.TdCloseInterestFee);
|
||||
: EodPnlCalculator.RoundEodInterest(pendingInterestFeeBeforeSettlement - newEodPayPosition.TdCloseInterestFee);
|
||||
//持仓内容-利息腿-损益统计(本方视角)
|
||||
// InterestProfitSum 是利息腿待实现总额,包含利息和费用;无费用时等于 InterestIncomeSum。
|
||||
newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum;
|
||||
@@ -2379,16 +2371,8 @@ namespace YLErp.Modules.SwapModule
|
||||
$",TdCloseInterest is {newEodPayPosition.TdCloseInterest}");
|
||||
Log.Info($"InterestFeeSum is {eodPayPosition.InterestFeeSum},TdInterestFee is {newEodPayPosition.TdInterestFee}" +
|
||||
$",TdCloseInterestFee is {newEodPayPosition.TdCloseInterestFee}");
|
||||
//累计已实现
|
||||
// RealizedInterest 只增不回滚:上日累计已实现 + 当日结息按方向后的金额。
|
||||
// 收取腿的 -37119.14 会把累计已实现更新为 -37119.14;后续普通 EOD 保持该值。
|
||||
var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio);
|
||||
newEodPayPosition.RealizedInterest = rolled.Interest;
|
||||
newEodPayPosition.RealizedInterestFee = rolled.Fee;
|
||||
SetFixedLegRealizedPnl(newEodPayPosition);
|
||||
var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true,
|
||||
DirectionRatio.RateType(position.InterestDirection));
|
||||
newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate);
|
||||
//累计已实现(滚存语义见 FinalizeInterestEodRoll:只增不回滚)
|
||||
FinalizeInterestEodRoll(eodPayPosition, newEodPayPosition, ratio, td, valueDate, position.InterestDirection);
|
||||
Log.Info($"即将插入数据库的 newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}");
|
||||
PersistEodSwapPosition(newEodPayPosition);
|
||||
return interests;
|
||||
@@ -2402,11 +2386,13 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <param name="preSettleDate">上一交易日</param>
|
||||
/// <param name="valueDate">当前结算日</param>
|
||||
/// <param name="td">互换交易主干</param>
|
||||
protected void SaveEodInterestPositionCopy(eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition, DateTime valueDate, trade td, swap_position position, eod_swap lastEodSwap, bool needPrice, decimal posiLongNational, decimal posiShortNational, decimal grossPrice, decimal orginPv)
|
||||
protected void SaveEodInterestPositionCopy(eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition, DateTime valueDate, trade td, swap_position position, eod_swap lastEodSwap, bool needPrice, decimal posiTotalNotional, decimal grossPrice, decimal orginPv)
|
||||
{
|
||||
Log.Info($"eodPayPosition is {JsonHelper.Serialize(eodPayPosition, false)},newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}");
|
||||
List<IntervalModel> intervals = position.SwapIntervalList;
|
||||
var tradeExtend = td.trade_extend.ExtendObj;
|
||||
// orginPv 在此仅对固定值腿(mode 1)生效;保证金腿(5/6)的 orginPv 虽在此赋值,
|
||||
// 但 GetInterests 保证金分支已走 CalcMarginInterest(内部自算 orginPv=PreviousBalance),忽略此处传入值。
|
||||
var interestModes = MarginModes.FixedAmountAndMargin;
|
||||
if (eodPayPosition == null)
|
||||
{
|
||||
@@ -2424,7 +2410,7 @@ namespace YLErp.Modules.SwapModule
|
||||
eodPayPosition.InterestPrincipalFix = position.InterestPrincipalFix;
|
||||
eodPayPosition.InterestRateDefault = position.InterestRateDefault;
|
||||
eodPayPosition.InterestSwapInterval = position.InterestSwapInterval;
|
||||
eodPayPosition.TdInterestPrincipal = interestModes.Contains(position.InterestMode) ? eodPayPosition.InterestPrincipalFix : posiLongNational + posiShortNational;
|
||||
eodPayPosition.TdInterestPrincipal = interestModes.Contains(position.InterestMode) ? eodPayPosition.InterestPrincipalFix : posiTotalNotional;
|
||||
eodPayPosition.PosiStartDate = td.StartDate.Value;
|
||||
eodPayPosition.PosiMatuirityDate = td.ExerciseDate.Value;
|
||||
eodPayPosition.IsAnnualized = position.IsAnnualized;
|
||||
@@ -2447,7 +2433,7 @@ namespace YLErp.Modules.SwapModule
|
||||
orginPv = eodPayPosition.InterestPrincipalFix;
|
||||
}
|
||||
bool longShort = td.StructureType == ClientMarginTypeEnum.多空组合.ToString();
|
||||
decimal oriPosiNotionalValue = posiLongNational + posiShortNational;
|
||||
decimal oriPosiNotionalValue = posiTotalNotional;
|
||||
decimal posiNotionalValue = oriPosiNotionalValue;
|
||||
if (lastEodSwap == null)
|
||||
{
|
||||
@@ -2472,7 +2458,7 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
preEodPositions.Add(eodPayPosition);
|
||||
}
|
||||
var interests = CalcSwapInterests(td, td.trade_extend, valueDate, valueDate, preEodPositions, positions, posiNotionalValue, posiLongNational, posiShortNational, posiNotionalValue, closePercent, 0, false, needPrice, grossPrice, orginPv);
|
||||
var interests = CalcSwapInterests(td, td.trade_extend, valueDate, valueDate, preEodPositions, positions, posiNotionalValue, posiNotionalValue, closePercent, 0, false, orginPv);
|
||||
UpdateDbOption(newEodPayPosition);
|
||||
|
||||
newEodPayPosition.PosiStatus = 0;
|
||||
@@ -2500,14 +2486,8 @@ namespace YLErp.Modules.SwapModule
|
||||
//持仓价值
|
||||
newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio);
|
||||
|
||||
//累计已实现
|
||||
var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio);
|
||||
newEodPayPosition.RealizedInterest = rolled.Interest;
|
||||
newEodPayPosition.RealizedInterestFee = rolled.Fee;
|
||||
SetFixedLegRealizedPnl(newEodPayPosition);
|
||||
var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true,
|
||||
DirectionRatio.RateType(eodPayPosition.InterestDirection));
|
||||
newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate);
|
||||
//累计已实现(滚存收尾见 FinalizeInterestEodRoll;方向源=eodPayPosition,与其他三方法不同,勿统一)
|
||||
FinalizeInterestEodRoll(eodPayPosition, newEodPayPosition, ratio, td, valueDate, eodPayPosition.InterestDirection);
|
||||
Log.Info($"the last newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}");
|
||||
PersistEodSwapPosition(newEodPayPosition);
|
||||
|
||||
@@ -2603,7 +2583,7 @@ namespace YLErp.Modules.SwapModule
|
||||
newEodPayPosition.RealizedFee = closeFee;
|
||||
newEodPayPosition.RealizedMtmPnL = newEodPayPosition.TdCloseMtmPnl;
|
||||
newEodPayPosition.RealizedDividend = newEodPayPosition.TdCloseDividend;
|
||||
SetFloatingRealizedPnl(newEodPayPosition);
|
||||
EodPnlCalculator.SetFloatingRealizedPnl(newEodPayPosition);
|
||||
|
||||
newEodPayPosition.PosiStatus = payQty == 0 ? 1 : 0;
|
||||
UpdateDbOption(newEodPayPosition);
|
||||
@@ -2658,6 +2638,11 @@ namespace YLErp.Modules.SwapModule
|
||||
curretEod.TdPosiDividend = DividendCalc.AfterTax(payment, tax);
|
||||
}
|
||||
curretEod.PosiDividendSum = eod.PosiQuantity > 0 ? Math.Round(eod.PosiDividendSum + curretEod.TdPosiDividend, 2) : 0;
|
||||
// 分红递推过程常驻记录(快速定位):窗口/数量/税率/当日新计/累计前后值——
|
||||
// 配合 BondPaymentService 的[分红-登记日口径]窗口命中日志,构成"命中哪些登记日→算出多少→账滚到多少"全链
|
||||
Log.Info($"[分红-EOD计提Copy] tradeId={td.id} posiId={eod.PositionId} valueDate={valueDate:yyyy-MM-dd} " +
|
||||
$"window=({eod.ValueDate:yyyy-MM-dd},{valueDate:yyyy-MM-dd}] qty={curretEod.PosiQuantity} tax={tax} " +
|
||||
$"TdPosiDividend={curretEod.TdPosiDividend} PosiDividendSum {eod.PosiDividendSum}->{curretEod.PosiDividendSum}");
|
||||
curretEod.PosiQuantity = eod.PosiQuantity;
|
||||
if (curretEod.PosiStatus == 1)
|
||||
{
|
||||
@@ -2677,7 +2662,7 @@ namespace YLErp.Modules.SwapModule
|
||||
curretEod.RealizedMtmPnL = eod.RealizedMtmPnL + curretEod.TdCloseMtmPnl;
|
||||
curretEod.RealizedDividend = eod.RealizedDividend + curretEod.TdCloseDividend;
|
||||
curretEod.RealizedFee = eod.RealizedFee + curretEod.TdCloseFee;
|
||||
SetFloatingRealizedPnl(curretEod);
|
||||
EodPnlCalculator.SetFloatingRealizedPnl(curretEod);
|
||||
var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, td.StartDate.Value
|
||||
, seekPreday: true, currencyRateType: DirectionRatio.RateType(curretEod.PosiDirection));
|
||||
curretEod.TdCurrency = Convert.ToDecimal(currencyRate);
|
||||
@@ -2692,17 +2677,6 @@ namespace YLErp.Modules.SwapModule
|
||||
return curretEod;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 浮动腿累计已实现盈亏由盯市、分红和费用三个已实现组成项汇总。
|
||||
/// 各组成项已经按本方视角落库,此处不再额外转换方向。
|
||||
/// </summary>
|
||||
private static void SetFloatingRealizedPnl(eod_swap_position position)
|
||||
{
|
||||
position.RealizedPnl = position.RealizedMtmPnL
|
||||
+ position.RealizedDividend
|
||||
+ position.RealizedFee;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新虚拟交易费用
|
||||
/// </summary>
|
||||
@@ -2741,11 +2715,11 @@ namespace YLErp.Modules.SwapModule
|
||||
int shortRatio = DirectionRatio.LongShort(eod.PositionType);
|
||||
int directionRatio = DirectionRatio.ReceivePay(eod.PosiDirection);
|
||||
var price = GetSwapValuationPrice(eod.UnderlyingCode, dealDate, out decimal vobp);
|
||||
var todayConsumedDividend = CalcConsumedDividend(curretEod, unwindEvents);
|
||||
var originNotional = (decimal)td.OriginalStockEqvNotional / swapPosition.PosiNetPrice;
|
||||
decimal totalPayment = CalcBondPayment(curretEod.UnderlyingCode, td.StartDate.Value, valueDate, (decimal)originNotional, shortRatio, directionRatio);
|
||||
// 历史遗留死代码已删(2026-08-16,论证+边界测试见 DividendEodNoDoubleCountTest.脏数据边界_*):
|
||||
// todayConsumedDividend / originNotional / totalPayment / totalInterest 自 0910969e(2026-07-02
|
||||
// 改递推式) 起计算结果从未被消费,仅残留一次全历史 CalcBondPayment 只读查询+日志副作用,
|
||||
// 且构成脏数据(OriginalStockEqvNotional=null/PosiNetPrice=0)下的 EOD 崩溃点。回退=git revert 本提交。
|
||||
decimal tax = um.ValueAddedTax ?? 0;
|
||||
decimal totalInterest = DividendCalc.AfterTaxRaw(totalPayment, tax);
|
||||
SetPriceInfoByFlowEvent(eod, curretEod, unwindEvents, swapPosition);
|
||||
curretEod.dv01 = Dv01Helper.CalcDv01(eod.UnderlyingCode, curretEod.PosiQuantity, eod.PosiDirection, eod.PositionType, vobp);
|
||||
curretEod.UnderlyingPrice = price;
|
||||
@@ -2778,7 +2752,12 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
curretEod.PosiDividendSum = 0;
|
||||
}
|
||||
SetFloatingRealizedPnl(curretEod);
|
||||
// 分红递推过程常驻记录(快速定位):当日事件路径含实现扣减(前日+新计-当日实现)
|
||||
Log.Info($"[分红-EOD计提Update] tradeId={td.id} posiId={eod.PositionId} valueDate={valueDate:yyyy-MM-dd} " +
|
||||
$"window=({eod.ValueDate:yyyy-MM-dd},{valueDate:yyyy-MM-dd}] qty={curretEod.PosiQuantity} tax={tax} " +
|
||||
$"TdPosiDividend={curretEod.TdPosiDividend} TdCloseDividend={curretEod.TdCloseDividend} " +
|
||||
$"PosiDividendSum {eod.PosiDividendSum}->{curretEod.PosiDividendSum}");
|
||||
EodPnlCalculator.SetFloatingRealizedPnl(curretEod);
|
||||
curretEod.SwapPositionValue -= curretEod.TdCloseDividend;
|
||||
|
||||
curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending);
|
||||
@@ -2800,20 +2779,6 @@ namespace YLErp.Modules.SwapModule
|
||||
return curretEod;
|
||||
}
|
||||
|
||||
private decimal CalcConsumedDividend(eod_swap_position curretEod, List<swap_flow_event> events)
|
||||
{
|
||||
decimal consumedDividend = 0;
|
||||
|
||||
List<int> swapEventTypes = new List<int>() { (int)SwapEventTypeEnum.互换, (int)SwapEventTypeEnum.自动互换 };
|
||||
//这里要剔除掉平仓产生的分红
|
||||
consumedDividend = events
|
||||
.Where(x => x.SwapTradeId == curretEod.SwapTradeId
|
||||
&& swapEventTypes.Contains(x.EventType)
|
||||
&& x.DataState == (int)SwapFlowDateStateEnum.完成)
|
||||
.Sum(s => s.DividendIn);
|
||||
return consumedDividend;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据开平仓事件算价格及后付费用
|
||||
/// </summary>
|
||||
@@ -2958,7 +2923,7 @@ namespace YLErp.Modules.SwapModule
|
||||
curretEod.RealizedMtmPnL = curretEod.TdCloseMtmPnl;
|
||||
curretEod.RealizedDividend = curretEod.TdCloseDividend;
|
||||
curretEod.RealizedFee = curretEod.TdCloseFee;
|
||||
SetFloatingRealizedPnl(curretEod);
|
||||
EodPnlCalculator.SetFloatingRealizedPnl(curretEod);
|
||||
curretEod.PosiStatus = curretEod.PosiQuantity == 0 ? 1 : 0;
|
||||
if (curretEod.PosiStatus == 1)
|
||||
{
|
||||
@@ -3037,7 +3002,7 @@ namespace YLErp.Modules.SwapModule
|
||||
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 eodSwapPositions = DbContext.eod_swap_position.ActiveByTradeAndDate(td.id, settleDate).ToList();
|
||||
var interestPositions = eodSwapPositions.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//利息腿
|
||||
var positions = eodSwapPositions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//持仓腿
|
||||
// 框架合约的方向约定:多头为正、空头为负;总名义本金取交易原始规模,
|
||||
@@ -3049,8 +3014,8 @@ namespace YLErp.Modules.SwapModule
|
||||
eod_Swap.BookId = td.AssetId;
|
||||
eod_Swap.ValueDate = settleDate;
|
||||
eod_Swap.StructureType = td.StructureType;
|
||||
FillPositionLegSummary(eod_Swap, positions);
|
||||
eod_Swap.InterestPnL = SumInterestPnL(interestPositions);
|
||||
EodPnlCalculator.FillPositionLegSummary(eod_Swap, positions);
|
||||
eod_Swap.InterestPnL = EodPnlCalculator.SumInterestPnL(interestPositions);
|
||||
eod_Swap.PostionValue = eodSwapPositions.Sum(s => s.SwapPositionValue);
|
||||
// 保证金腿的利息现金流方向与保证金本金方向相反。
|
||||
// 不能直接汇总 RealizedPnl,否则“收取客户保证金”的腿会把应支付给客户的
|
||||
@@ -3099,12 +3064,12 @@ namespace YLErp.Modules.SwapModule
|
||||
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 eodSwapPositions = DbContext.eod_swap_position.ActiveByTradeAndDate(td.id, settleDate).ToList();
|
||||
var interestPositions = eodSwapPositions.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//利息腿
|
||||
var positions = eodSwapPositions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//持仓腿
|
||||
eod_Swap.NotionalValue = Math.Round(Convert.ToDecimal(td.OriginalStockEqvNotional ?? td.StockEqvNotional), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
FillPositionLegSummary(eod_Swap, positions);
|
||||
eod_Swap.InterestPnL += SumInterestPnL(interestPositions);
|
||||
EodPnlCalculator.FillPositionLegSummary(eod_Swap, positions);
|
||||
eod_Swap.InterestPnL += EodPnlCalculator.SumInterestPnL(interestPositions);
|
||||
eodSwapPositions.ForEach(x =>
|
||||
{
|
||||
var ratio = DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode);
|
||||
@@ -3130,38 +3095,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// 我方支付给对手方的成本计入,而不会错误增加框架合约已实现收益。
|
||||
/// 抽为静态纯函数以支持无库单测(marginTypes 等价于 ConsTrade.InterestMarginModels)。
|
||||
/// </summary>
|
||||
public static decimal CalculateSwapRealizedPnl(eod_swap_position position)
|
||||
{
|
||||
var interestRatio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
|
||||
|
||||
return position.RealizedMtmPnL
|
||||
+ position.RealizedDividend
|
||||
+ position.RealizedFee
|
||||
+ position.RealizedInterest * interestRatio
|
||||
+ position.RealizedInterestFee;
|
||||
}
|
||||
|
||||
/// <summary>填充框架合约的持仓腿汇总字段(多空名义本金/市值/浮动盈亏/dv01/平仓量)。
|
||||
/// SaveEodSwap 与 UpdateEodSwap 共用,消除 ~10 行重复。</summary>
|
||||
private static void FillPositionLegSummary(eod_swap eod_Swap, List<eod_swap_position> positions)
|
||||
{
|
||||
eod_Swap.NotionalValueLong = Math.Round(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
eod_Swap.NotionalValueShort = Math.Round(-Math.Abs(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue)), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
eod_Swap.MarketValueLong = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.UnderlyingMarketValue);
|
||||
eod_Swap.MarketValueShort = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.UnderlyingMarketValue);
|
||||
eod_Swap.FloatingPnL = positions.Sum(s => s.PosiProfitSum);
|
||||
eod_Swap.dv01 = positions.Sum(s => s.dv01 ?? 0);
|
||||
eod_Swap.TdCloseQty = positions.Sum(s => s.TdCloseQty);
|
||||
}
|
||||
|
||||
/// <summary>利息腿 PnL 汇总(按方向比例 + 保证金翻转)。原 SaveEodSwap/UpdateEodSwap 各一段 ForEach。</summary>
|
||||
private static decimal SumInterestPnL(List<eod_swap_position> interestPositions)
|
||||
{
|
||||
decimal interestPnL = 0;
|
||||
foreach (var x in interestPositions)
|
||||
interestPnL += x.InterestProfitSum * DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode);
|
||||
return interestPnL;
|
||||
}
|
||||
public static decimal CalculateSwapRealizedPnl(eod_swap_position position) => EodPnlCalculator.CalculateSwapRealizedPnl(position);
|
||||
|
||||
/// <summary>
|
||||
/// 风险报表符号归一化:把历史两种符号口径的 TdCloseInterest/RealizedInterest
|
||||
@@ -3170,20 +3104,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// 抽为 public static 纯函数以支持无库单测(见 SwapReportInterestSignNormalizeTest)。
|
||||
/// 仅当 InterestDirection > 0 时执行(与原内联逻辑等价)。
|
||||
/// </summary>
|
||||
public static void NormalizeInterestSignForReport(eod_swap_position position)
|
||||
{
|
||||
if (position.InterestDirection <= 0) return;
|
||||
|
||||
if (position.InterestMode == (int)InterestModeEnum.标的期初全价)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var interestRatio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
|
||||
position.TdCloseInterest = Math.Abs(position.TdCloseInterest) * interestRatio;
|
||||
position.RealizedInterest = Math.Abs(position.RealizedInterest) * interestRatio;
|
||||
// 兼容修复前已落库的利息腿:当时只累计了明细字段,未同步写入 RealizedPnl。
|
||||
position.RealizedPnl = position.RealizedInterest + position.RealizedInterestFee;
|
||||
}
|
||||
public static void NormalizeInterestSignForReport(eod_swap_position position) => EodPnlCalculator.NormalizeInterestSignForReport(position);
|
||||
|
||||
/// <summary>
|
||||
/// 获取多空组合 平仓详细
|
||||
@@ -3194,7 +3115,7 @@ namespace YLErp.Modules.SwapModule
|
||||
public SwapLongShortCloseModel GetCloseDetails(int tradeId, DateTime valueDate)
|
||||
{
|
||||
SwapLongShortCloseModel closeModel = new SwapLongShortCloseModel();
|
||||
var eodPositions = DbContext.eod_swap_position.Where(x => x.SwapTradeId == tradeId && x.ValueDate == valueDate && !x.Invalid).ToList();
|
||||
var eodPositions = DbContext.eod_swap_position.ActiveByTradeAndDate(tradeId, valueDate).ToList();
|
||||
var flowEvents = DbContext.swap_flow_event.Where(x => x.SwapTradeId == tradeId && x.EventDate == valueDate && x.DataState == (int)SwapFlowDateStateEnum.完成 && x.EventType == (int)SwapEventTypeEnum.平仓 && string.IsNullOrEmpty(x.UnderlyingCode)).ToList();
|
||||
closeModel.DealPositions = eodPositions.Where(x => x.TdCloseQty != 0).ToList();
|
||||
closeModel.DealInterests = flowEvents;
|
||||
@@ -3435,7 +3356,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <returns></returns>
|
||||
public List<eod_swap_position> GetPreEodPositions(int tradeId, DateTime valueDate)
|
||||
{
|
||||
return DbContext.eod_swap_position.Where(x => x.SwapTradeId == tradeId && x.ValueDate == valueDate && !x.Invalid).ToList();
|
||||
return DbContext.eod_swap_position.ActiveByTradeAndDate(tradeId, valueDate).ToList();
|
||||
}
|
||||
/// <summary>
|
||||
/// 获取互换交易日终持仓数据集合
|
||||
@@ -3673,7 +3594,7 @@ namespace YLErp.Modules.SwapModule
|
||||
item.position.FloatRateUnderlyingCode = floatRateInterest?.FloatRateUnderlyingCode;
|
||||
item.position.FloatRate = floatRateInterest?.FloatRate ?? 0;
|
||||
item.OpenMarginAmount = initialMargins.Sum(s => s.InterestPrincipalFix * DirectionRatio.ReceivePay(s.InterestDirection));
|
||||
item.OpenMarginRate = CalculateWeightedMarginRate(tradeMargins);
|
||||
item.OpenMarginRate = EodPnlCalculator.CalculateWeightedMarginRate(tradeMargins);
|
||||
item.AdditionalMarginAmount = additionalMargins.Sum(s => s.InterestPrincipalFix * DirectionRatio.ReceivePay(s.InterestDirection));
|
||||
item.MarginInterestAmount = CalculateWeightedMarginInterest(eodMargins);
|
||||
item.InterestAmount = eodInterests.Sum(s => s.InterestIncomeSum * (-DirectionRatio.ReceivePay(s.InterestDirection)));
|
||||
@@ -3698,29 +3619,12 @@ namespace YLErp.Modules.SwapModule
|
||||
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>
|
||||
/// 计算预付金利息金额。InterestIncomeSum 已是各腿利息金额,
|
||||
/// 按收取为正、支付为负直接轧差求和,不做本金加权。
|
||||
/// 抽为 public static 纯函数以支持无库单测(见 SwapWeightedMarginInterestTest)。
|
||||
/// </summary>
|
||||
public static decimal CalculateWeightedMarginInterest(IEnumerable<eod_swap_position> margins)
|
||||
{
|
||||
return margins.Sum(x =>
|
||||
x.InterestIncomeSum * DirectionRatio.ReceivePay(x.InterestDirection));
|
||||
}
|
||||
public static decimal CalculateWeightedMarginInterest(IEnumerable<eod_swap_position> margins) => EodPnlCalculator.CalculateWeightedMarginInterest(margins);
|
||||
|
||||
/// <summary>
|
||||
/// 固定利息腿的累计已实现盈亏 = 累计已实现利息 + 累计已实现利息费用。
|
||||
@@ -3728,10 +3632,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// 抽为 public static 纯函数以支持无库单测(见 SwapFixedLegRealizedPnlTest),
|
||||
/// 并消除复制粘贴带来的笔误风险(如 L1296 历史双分号)。
|
||||
/// </summary>
|
||||
public static void SetFixedLegRealizedPnl(eod_swap_position position)
|
||||
{
|
||||
position.RealizedPnl = position.RealizedInterest + position.RealizedInterestFee;
|
||||
}
|
||||
public static void SetFixedLegRealizedPnl(eod_swap_position position) => EodPnlCalculator.SetFixedLegRealizedPnl(position);
|
||||
/// <summary>
|
||||
/// 将数据库中以公司/交易簿记方向保存的日终字段转换为客户视角。
|
||||
/// 该转换必须在拆分浮动收益、费用和期间付息/分红之前完成,
|
||||
|
||||
@@ -10,6 +10,7 @@ using YLErp.Helpers;
|
||||
using YLErp.Model;
|
||||
using YLErp.Model.Enum;
|
||||
using YLErp.Modules.EodModule;
|
||||
using YLErp.Modules.DataProviderModule;
|
||||
using YLErp.Modules.SwapModule.Dto;
|
||||
using YLErp.Office;
|
||||
using YLErp.QdpModule;
|
||||
@@ -70,6 +71,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
DbContext.eod_commodity_future_price.Remove(frdata);
|
||||
DbContext.SaveChanges();
|
||||
Fr007FixingCache.Invalidate(); // 写侧失效:删除当日定盘后缓存立即重载,防旧值沿用
|
||||
return true;
|
||||
}
|
||||
/// <summary>
|
||||
@@ -108,16 +110,25 @@ namespace YLErp.Modules.SwapModule
|
||||
DbContext.Add(frdata);
|
||||
}
|
||||
frdata.UnderlyingId = EodPriceService.ResolveUnderlyingIdForCode(frdata.UnderlyingCode, frdata.UnderlyingId ?? 0, frUnderlying.id);
|
||||
frdata.ClosePrice = Math.Round(price, 4);
|
||||
frdata.SettlePrice = Math.Round(price, 4);
|
||||
frdata.ReferencePrice = Math.Round(price, 4);
|
||||
frdata.ClosePrice = RoundFr007Price(price);
|
||||
frdata.SettlePrice = RoundFr007Price(price);
|
||||
frdata.ReferencePrice = RoundFr007Price(price);
|
||||
frdata.OptId = UserInfo.UserId;
|
||||
frdata.OptName = UserInfo.UserName;
|
||||
frdata.OptDate = DateTime.Now;
|
||||
DbContext.SaveChanges();
|
||||
Fr007FixingCache.Invalidate(); // 写侧失效:新增/修改定盘后缓存立即重载
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// FR007 界面手工录入定盘的落库精度。FR007 官方发布为百分数下 4 位(如 1.4150%),
|
||||
/// 小数口径需 6 位(0.014150)——原 Math.Round(,4) 只保留百分数下 2 位,1.4150% 被截成
|
||||
/// 1.4200%(丢 0.5bp)。取 6 位与前端 toNumber(value/100, 6) 对齐;DB 列 double(18,10) 容纳无虞;
|
||||
/// bond-sync 自动同步链(BigDecimal 全精度透传)不经此函数。
|
||||
/// </summary>
|
||||
internal static double RoundFr007Price(double price) => Math.Round(price, 6);
|
||||
|
||||
/// <summary>
|
||||
/// 查询互换流水导入
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Linq;
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// swap_position 查询收口(Query Object)。
|
||||
/// 规则"有效持仓 = SwapTradeId 匹配且未作废(!Invalid)"集中于此,
|
||||
/// 避免多处复制同一谓词导致语义漂移(漏写 !Invalid 即静默出 bug)。
|
||||
/// 仅返回 IQueryable,不调用 SaveChanges,不破坏跟踪/Include/事务边界。
|
||||
/// </summary>
|
||||
public static class SwapPositionQueries
|
||||
{
|
||||
public static IQueryable<swap_position> ActiveByTrade(
|
||||
this IQueryable<swap_position> query, int tradeId)
|
||||
=> query.Where(x => x.SwapTradeId == tradeId && !x.Invalid);
|
||||
}
|
||||
}
|
||||
@@ -376,19 +376,18 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <param name="interestMode">计息方式</param>
|
||||
/// <param name="interestStart">计息开始日期</param>
|
||||
/// <param name="interestEnd">计息结束日期</param>
|
||||
/// <returns>true=计息窗口为空(interestStart>interestEnd,本次不计利息,调用方将利率与金额归零);
|
||||
/// 典型触发=①不算头首日(valueDate==StartDate → StartDate+1>StartDate) ②不算尾到期日回拨后窗口翻转
|
||||
/// (interestEnd=到期日−1 < interestStart)。判定只看日期窗口,与事件类型无关。
|
||||
/// 注:当日已结息(preSettleDate==valueDate)日期相等时本函数返回 false——利息归零由
|
||||
/// GetInterests 的 closeList 净额层处理,不在本判定。</returns>
|
||||
public bool InitInterestDate(DateTime valueDate, DateTime? preSettleDate, trade td, bool tdClose, out DateTime interestStart, out DateTime interestEnd)
|
||||
{
|
||||
interestStart = td.StartDate.Value;
|
||||
var exerciseDate = td.ExerciseDate.Value;
|
||||
interestEnd = valueDate > exerciseDate ? exerciseDate : valueDate;
|
||||
bool calcFirst = true;
|
||||
bool calcLast = true;
|
||||
|
||||
if (td.trade_extend != null)
|
||||
{
|
||||
calcFirst = td.trade_extend.ExtendObj.InterestCalcMode.StartsWith("1");//算头
|
||||
calcLast = td.trade_extend.ExtendObj.InterestCalcMode.EndsWith("1");//算尾
|
||||
}
|
||||
bool calcFirst = td.trade_extend?.ExtendObj.CalcFirst ?? true;
|
||||
bool calcLast = td.trade_extend?.ExtendObj.CalcLast ?? true;
|
||||
interestStart = calcFirst ? interestStart : interestStart.AddDays(1);
|
||||
if (preSettleDate.HasValue && preSettleDate >= interestStart)
|
||||
{
|
||||
@@ -398,7 +397,9 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
interestEnd = interestEnd.AddDays(-1);
|
||||
}
|
||||
if (interestStart > interestEnd || td.StartDate > interestStart)
|
||||
// 原第二 OR 子句 td.StartDate > interestStart 恒 false(interestStart 经上面调整恒 ≥ td.StartDate:
|
||||
// =开始日 / 不算头+1天 / preSettleDate 且仅当 ≥interestStart 才覆盖),死代码已删(2026-08-19)。
|
||||
if (interestStart > interestEnd)
|
||||
{
|
||||
interestStart = interestEnd;
|
||||
return true;//不记利息
|
||||
|
||||
@@ -1188,7 +1188,7 @@ namespace YLErp.Modules.SwapModule
|
||||
tradeObj.trade_Initial_Margin = new trade_initial_margin();
|
||||
}
|
||||
tradeObj.trade_extend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == intid);
|
||||
tradeObj.swap_positions = DbContext.swap_position.Where(x => x.SwapTradeId == intid && !x.Invalid).ToList();
|
||||
tradeObj.swap_positions = DbContext.swap_position.ActiveByTrade(intid).ToList();
|
||||
tradeObj.swap_positions = tradeObj.swap_positions.Where(x => x.PosiQuantity > 0 || x.InterestDirection > 0).ToList();
|
||||
var intervalPositions = tradeObj.swap_positions.Where(x => string.IsNullOrEmpty(x.UnderlyingCode) && x.IsInitial).ToList();
|
||||
var intervalPositionIds = intervalPositions.Select(s => s.id).ToList();
|
||||
@@ -1517,7 +1517,7 @@ namespace YLErp.Modules.SwapModule
|
||||
throw new ServiceException("交易不存在");
|
||||
}
|
||||
bool backToBegin = td.TradeDate == valueDate;
|
||||
var swapPositions = DbContext.swap_position.Where(x => x.SwapTradeId == tradeId && !x.Invalid).ToList();
|
||||
var swapPositions = DbContext.swap_position.ActiveByTrade(tradeId).ToList();
|
||||
td.trade_extend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == td.id);
|
||||
|
||||
//展期
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
# 融资腿计息测试矩阵
|
||||
|
||||
> 配套 [ARCHITECTURE.md](ARCHITECTURE.md)。目的:把"覆盖"从用例计数变成格子坐标运算——
|
||||
> 每个用例/fix 显式登记命中坐标,空洞一眼可见。2026-08 建立,依据近 6 周 fix 热力图回溯登记。
|
||||
|
||||
## 0. 范围声明
|
||||
|
||||
- 本矩阵只覆盖**融资腿 FundingLeg(mode 1 固定值 / 2 合约名义本金规模 / 9 标的期初全价)**。
|
||||
- **mode 5/6(保证金/预付金)不属于本矩阵**(历史遗留:被错误建模为计息腿,概念上与融资腿无关,
|
||||
见 `Margin/MarginModes.cs` 注释)。保证金有独立的余额模型与专属黄金回放(96 库 60 条,0 差异)作为护栏。
|
||||
禁止向本矩阵添加 5/6 格子。
|
||||
- 主力生产组合(确认书规定)**必须全格覆盖**,见 §1。
|
||||
|
||||
## 1. 主力族(第一优先级,必须全盖)
|
||||
|
||||
```
|
||||
InterestMode = 9 标的期初全价 × FR007 浮动(±点差) × InterestType = 复利 × InterestCalcMode = "10"(算头不算尾)
|
||||
```
|
||||
|
||||
代码锚点:`SwapDealService.GetInterests`(calcFirst=true / calcLast=false,SwapDealService.cs:646)。
|
||||
近 6 周 ≥9 个 fix 落在本族内——生产用得最多 = 人工测试打得最狠,fix 清单就是炸点热力图。
|
||||
|
||||
## 2. 维度定义
|
||||
|
||||
| 维度 | 取值 | 代码/数据锚点 |
|
||||
|---|---|---|
|
||||
| A 生命周期终点 | 持有至到期结算 / 盘中全平 / 盘中部分后持有 / 部分N次后全平 / EOD自动平仓(部分·全) / 互换(续作) | `SwapEventTypeEnum`;到期:`SwapEodPositionService` 到期结算路径 |
|
||||
| B 重置几何 | 第1重置期内平仓 / 跨≥1完整重置期 / 第3重置期内 / **平仓日=重置日** / 重置日±1天 / **末段非整周期**(di<7) | 重置频率=7天(已定格,§8);契约重置期定义见 §8a |
|
||||
| C 比例与次数 | 单次部分(30%) / 同日两次 / 跨重置期多次 / 全平(剩余=0) | `closePrecent`;双语义转换 `ClosePercentMath` |
|
||||
| D 交收 | T+0 / T+1 | `valueDate` vs `unwindDate` |
|
||||
| E FR007 形态 | 每重置日有价 / 加点(+0.25%) / 减点(-2.10%) / **取价日=重置日上一营业日**(契约规定) / 缺价分支 | `TryGetFloatRate` / `ResolveFloatRate`;66a97e03 对应此维 |
|
||||
| F 入口 | 见 §3 | |
|
||||
| G 断言投影 | ①最终利息金额 ②`TdInterestPrincipal` 逐日携带链 ③`InterestIncomeSum`+flow_event 全字段 ④方向/符号(报表口径) | 每格必须断言全部 4 个投影 |
|
||||
|
||||
## 3. 入口枚举(F 维)
|
||||
|
||||
| 入口 | 代码路径 |
|
||||
|---|---|
|
||||
| 盘中平仓/互换结息试算 | `SwapDealService.GetInterestsForUnwind`(SwapDealService.cs:617,settment:false → `CalcUnwindInterest`) |
|
||||
| EOD 正常收盘 | `GetInterests(settment:true)` → `CalcEodInterest` |
|
||||
| EOD 平仓后收盘 | `SwapEodPositionService.SaveAutoEodWithCloseInterestPosition`(:1246)→ `CalcSwapInterests`(:1579) |
|
||||
| EOD 自动互换 | `CalcSwapInterests`(:1161,EventType=自动互换) |
|
||||
|
||||
已知风险:`GetInterests` 参数语义随入口漂移(EOD 平仓后收盘传"剩余本金+percent=1",
|
||||
盘中传"平仓前本金+实际比例"),`GetInterestsEntrySemanticsTest` 曾实测双入口复利口径分歧(b01b485e)。
|
||||
|
||||
## 4. fix 热力图(本族,近 6 周)
|
||||
|
||||
| fix | 落点 | 格子坐标 | 自带测试 |
|
||||
|---|---|---|---|
|
||||
| 66a97e03 重置日=平仓日 calcLast 不跳过 FR007 取价 | SwapDealService:1249/1295 | B=重置日=平仓日 × E=取价边界 | GLMS20260805FR007UnderlyingIdDiagnoseTest(581行) |
|
||||
| 48e84479 重置日部分平仓本金 | SwapEodPositionService:1422 | B=重置日=平仓日 × C=部分 | SwapCloseConversationCasesRegressionTest(358行) |
|
||||
| d3afa6d2 T+1 部分平仓复利本金(算头不算尾快速路径) | SwapDealService:1243 | D=T+1 × C=部分 × A=部分后持有 | DealInterestsScenarioTest +36行 |
|
||||
| aa5a5ed8 算头不算尾期初复利部分平仓 | SwapEodPositionService:1418-1573 | **本族正中心** | DealInterestsScenarioTest |
|
||||
| a0be0eb0 复利平仓已结利息扣除 | SwapDealService:1294 | 已结利息差分(CalcDailyCompoundInterest 回放) | ConsumedInterestScenarioTest |
|
||||
| 5539bd9c 复利部分平仓后 EOD 本金 | SwapEodPositionService:782/1381 | G=携带链投影 | DealInterestsScenarioTest +34行 |
|
||||
| feffc196 Bug A/B/C 浮动部分/全平尾差 | SwapDealService:1214/1292 | C=部分/全平 × E=浮动 | **SwapInterestScenario3And4FloatingTest(24用例,Excel oracle)** |
|
||||
| 2035e1df EOD 平仓后收盘结息本金语义 | SwapDealService:1293 / EodService:860,1360 | **F=EOD平仓后收盘 × C** | **无测试** |
|
||||
| b01b485e 双入口口径分歧(实测发现) | — | F=入口 × 全族 | GetInterestsEntrySemanticsTest(字符化,非 oracle) |
|
||||
|
||||
**规律:fix 全部落在 `CalcUnwindInterest`(SwapDealService 1240-1300)和
|
||||
`SaveAutoEodWithCloseInterestPosition` 族(SwapEodPositionService 1380-1580)两个带。**
|
||||
|
||||
## 5. 现有用例登记
|
||||
|
||||
| 测试文件 | 覆盖格子 | oracle 类型 |
|
||||
|---|---|---|
|
||||
| SwapInterestScenario3And4FloatingTest(24) | 本族 A=全平/部分30%→全平 × B=第3重置期内 × D=T+0/T+1 × E=加减点 × F=EOD平仓后收盘 × G=仅金额投影 | Excel 手算(业务源) |
|
||||
| SwapInterestScenario1And2Test(32) | A=收盘平仓 × B=第1重置期内 × E=固定/浮动 | Excel 手算 |
|
||||
| DealInterestsScenarioTest(24 方法,工单逐个追加) | 部分平仓×复利族各点,含"10"×3 行 | 工单期望值 |
|
||||
| ConsumedInterestScenarioTest | 已结利息差分族 | 工单期望值 |
|
||||
| SwapUnwindSameDayDoublePartialTest | C=同日两次 | **字符化(非独立 oracle)** |
|
||||
| GetInterestsEntrySemanticsTest | F=双入口一致性 | **字符化** |
|
||||
| **ContractReferenceOracleTest(Accrual/,7)** | mode9/mode2 × 复利 × "10" × T+0 × 部分30% × B=跨12整期+末段(90/89天) × E=恒定利率(取价日免疫) | **契约公式参考实现(§7.4 第一级)**——引擎盘中重放已逐分对齐 oracle |
|
||||
| GetInterestsUnitTest_T0/T1(89) | mode 1 固定值 T+0/T+1 族(非本族) | 单点断言 |
|
||||
| GLMS20260805FR007UnderlyingIdDiagnoseTest | B=重置日=平仓日 × E | 诊断+断言 |
|
||||
|
||||
## 6. 空洞清单(热力图 ∩ 未覆盖,按优先级)
|
||||
|
||||
1. **F=EOD平仓后收盘 × C=部分平仓 × 本族** —— 2035e1df 无测试落地即合入,该入口×比例格子全裸。
|
||||
2. **G=携带链投影(全族)** —— 现有断言几乎全是最终金额;`TdInterestPrincipal` 逐日携带链无一处断言
|
||||
(7528670e 在单利上炸过同款,复利同投影裸奔)。
|
||||
3. **B=重置日±1天 / 跨重置期多次部分平仓** —— 热力图边缘未扫。
|
||||
4. **A=到期结算 × 本族** —— db46e48e 修过到期结算(28 断言),但非本族参数。
|
||||
5. **A=互换(续作) × 本族** —— 7411b9d2/421662a0 炸过续作初始化,本族续作无 oracle。
|
||||
6. **C=同日两次** —— 只有字符化测试,无独立 oracle(字符化=锁定现状,不证正确)。
|
||||
7. **E=缺价/取价日边界** —— 66a97e03 只修了取价跳过,缺价分支行为未钉。
|
||||
|
||||
## 7. 补盖执行顺序
|
||||
|
||||
1. 先铺**守恒不变量**(免 oracle,全格便宜):部分平仓后"期初=平掉+剩余"逐日守恒;全平后持仓=0;
|
||||
复利重置日动态本金=前段本金+利息;多次平仓 closePercent 连乘=累计比例。
|
||||
2. 空洞 1/2 优先:按 §2-G 四投影补 EOD平仓后收盘 × 部分 用例,oracle 用确认书公式 Excel 模板。
|
||||
3. 空洞 6 补独立 oracle(确认书公式),替换字符化地位(保留字符化作回归钉)。
|
||||
4. 每格期望值来源分级(已升级,见 §8a):**契约公式独立参考实现** > 生产已对账数字 > 业务签认 Excel > 新旧影子对比;**禁止当前代码输出充当 oracle**。
|
||||
5. 契约参考实现(§8a 公式)**已落地**(`UnitTestProject/Modules/SwapModule/Accrual/ContractReferenceCalc.cs`,
|
||||
独立于生产引擎,禁止引用计息类防同源),引擎对照首批 3 例全绿(mode9/mode2 × "10" × 部分30%,
|
||||
`ContractReferenceOracleTest`)。后续补格直接复用:期望值 = `ClosedInterest(平掉额, ReferenceRateAbsolute(...))`。
|
||||
待办:变利率引擎侧对照(取价日 E 维)、确认书生成器参数同源断言(`swap_position`)。
|
||||
|
||||
## 8. 生产参数(已确认,2026-08)
|
||||
|
||||
- **重置频率 = 7 天**(确认书:"重置频率每【周】";完整重置期 di=7 天)
|
||||
- **年化基数 = 365**(确认书:"计息基准 A/365",固定利率公式同除 365)
|
||||
- 生产只有这一种组合,无 360/其他重置频率。现有测试参数 `ResetPeriod=7 / AnnualDays=365` **即为生产主力参数,格子按此定格**。
|
||||
|
||||
## 8a. 契约 oracle(确认书公式原文)
|
||||
|
||||
模板:`Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/*.docx`(看多/看空 × 现券/债券ETF 共 4 份,计息条款一致);
|
||||
变量替换:`Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeConfirmationGenerator.cs`
|
||||
(`重置频率=interest_rest_days天`、`利差=InterestRateDefault×10000bp`,与计息引擎同源 `swap_position` 字段)。
|
||||
|
||||
**浮动利率复利公式(本族契约正文)**:
|
||||
|
||||
```
|
||||
参考利率(绝对) = ∏[i=1..k] ( 1 + (FR007i + 利差) × di / 365 ) − 1
|
||||
```
|
||||
|
||||
- k = 计息期包含的重置期个数;di = 第 i 个基准利率适用的日历日数,**完整重置期 di=7,末段不足 7 按实际日历日**(测试必须盖非整周期:如持有 17 天 = 2×7+3)
|
||||
- **利率确定日 = 每个重置期首日(重置日)的上一个营业日**,取该日 FR007;营业日准则=上一营业日(→ E 维度取值锚点,与 66a97e03 修复直接对应)
|
||||
- FR007 取中国货币网每日公布值
|
||||
|
||||
**计息期定义(= 算头不算尾的契约原文)**:自起始日(含)至到期日(不含)的自然日天数。
|
||||
⚠ 债券ETF 模板变体:计息期自**期初观察日**(含)至**期末观察日**(不含)——观察日→代码日期字段的映射需单独核实,是一个潜在口径分叉点。
|
||||
|
||||
**重置期定义**:每个重置期自上一重置日(含)至下一重置日(不含);首个重置期始于计息期首日;最后一个重置期的最后一日为计息期最后一日(末段收口)。重置日从计息期首日按重置频率依次推算。
|
||||
|
||||
**固定利率公式**:参考利率(绝对) = 固定利率 × 计息期 / 365。
|
||||
**期初预付金利息**:支付日(含)至到期日(**含**)×利率/计息基准——注意预付金契约上是"含尾"的,与利率腿"不含尾"相反。
|
||||
|
||||
**oracle 使用方式(升级 §7)**:最强形式是**按契约公式写独立参考实现**(约 20 行:重置日推算 + 分段取价 + ∏ 公式 + 末段收口),作为测试 oracle 与生产引擎对照,容差 0.01。它比逐格 Excel 手算更便宜且零同源风险;Excel 模板退化为抽样校验参考实现本身。
|
||||
|
||||
## 9. 合入规则(硬约束)
|
||||
|
||||
1. 计息类 fix:**先失败测试,后修代码**;测试须登记本矩阵坐标。
|
||||
2. 修一格必须**扫同矩阵行兄弟格子**(同 fix 家族的邻格)。
|
||||
3. 任何触碰 `GetInterests`/`CalcUnwindInterest`/`SaveAutoEodWithCloseInterestPosition` 的 PR:
|
||||
跑 `DealInterestsGoldenReplayTest` 全量 + 保证金黄金回放(防共享管线殃及)。
|
||||
4. 登记 fix 时发现同格已有用例而 bug 仍发生 → 先修断言投影,再修代码。
|
||||
5. **oracle 用例与裁决材料一律取 §8 生产参数**(7 天重置 / A365 / 真实点差 ±0.25%·−2.10% /
|
||||
千万级名义本金,如 5000 万)。玩具参数(千元级/重置 3 天/点差 1%)仅限字符化钉子测试——
|
||||
其用途是锁行为防漂移,不承担"证明数字正确"职责;用玩具数字做裁决依据会掩盖金额量级
|
||||
(0.03 vs 0.06 看着"不大",同参数放大到生产即 7.6 万 vs 25 万/笔)。
|
||||
@@ -0,0 +1,43 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Consts;
|
||||
|
||||
namespace YLErp.Modules.SwapModule;
|
||||
|
||||
/// <summary>
|
||||
/// 平仓手续费计算——纯 static,无 this 依赖。
|
||||
/// 从 SwapDealService 提取,零行为变更。
|
||||
/// </summary>
|
||||
public static class TradingFeeCalc
|
||||
{
|
||||
public static decimal CalcInitTradingFee(swap_position oriPosition, UnwindData unwindData)
|
||||
{
|
||||
if (oriPosition == null || unwindData == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (oriPosition.PosiFeeType == 1)
|
||||
{
|
||||
return Math.Round(oriPosition.PosiTradingFeeUnit * unwindData.CloseQty, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
return Math.Round(oriPosition.PosiTradingFeeUnit / 100m * unwindData.CloseNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
public static decimal CalcInitTradingFeePending(swap_position oriPosition, swap_position position, UnwindData unwindData)
|
||||
{
|
||||
if (oriPosition == null || unwindData == null || oriPosition.PosiTradingFeeUnit == 0)
|
||||
{
|
||||
return position?.PosiTradingFeePending ?? 0;
|
||||
}
|
||||
|
||||
var closeBase = oriPosition.PosiFeeType == 1 ? unwindData.CloseQty : unwindData.CloseNotionalValue;
|
||||
var originalBase = oriPosition.PosiFeeType == 1 ? unwindData.NotionalQty : unwindData.NotionalValue;
|
||||
if (originalBase <= 0)
|
||||
{
|
||||
return position?.PosiTradingFeePending ?? 0;
|
||||
}
|
||||
|
||||
return Math.Round(oriPosition.PosiTradingFeePending * closeBase / originalBase, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using YLErp.Helpers;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
namespace YLErp.Modules.SwapModule;
|
||||
|
||||
/// <summary>
|
||||
/// 平仓数据(UnwindData)规范化——纯 static,无 this 依赖。
|
||||
/// 从 SwapDealService 提取,零行为变更。
|
||||
/// </summary>
|
||||
internal static class UnwindNormalizer
|
||||
{
|
||||
internal static void NormalizeNotionalValues(UnwindData unwindData)
|
||||
{
|
||||
unwindData.NotionalValue = Math.Round(unwindData.NotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
unwindData.PosiNotionalValue = Math.Round(unwindData.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
unwindData.CloseNotionalValue = Math.Round(unwindData.CloseNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
internal static bool NormalizeFullCloseRequest(UnwindData unwindData)
|
||||
{
|
||||
if (unwindData.CloseMethod != (int)CloseMethodEnum.全部平仓
|
||||
&& unwindData.ClosePercent < 1
|
||||
&& !(unwindData.PositionQty > 0 && unwindData.CloseQty >= unwindData.PositionQty)
|
||||
&& !(unwindData.PosiNotionalValue > 0 && unwindData.CloseNotionalValue >= unwindData.PosiNotionalValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var closeQty = unwindData.CloseQty;
|
||||
var closeNotionalValue = unwindData.CloseNotionalValue;
|
||||
unwindData.ClosePercent = 1;
|
||||
if (unwindData.PositionQty > 0) unwindData.CloseQty = unwindData.PositionQty;
|
||||
if (unwindData.PosiNotionalValue > 0) unwindData.CloseNotionalValue = unwindData.PosiNotionalValue;
|
||||
return closeQty != unwindData.CloseQty || closeNotionalValue != unwindData.CloseNotionalValue;
|
||||
}
|
||||
|
||||
internal static void RecalculateNormalizedUnwindAmounts(UnwindData unwindData)
|
||||
{
|
||||
var floatLeg = unwindData.FlowEvents.FirstOrDefault(x => !string.IsNullOrEmpty(x.UnderlyingCode));
|
||||
if (floatLeg == null || floatLeg.PosiGrossPrice == 0) return;
|
||||
|
||||
var input = new UnwindInput
|
||||
{
|
||||
Multiplier = ConsGlobal.InstrumentType.IsBond(floatLeg.UnderlyingInstrumentType) ? 100 : 1,
|
||||
PosiGrossPrice = floatLeg.PosiGrossPrice,
|
||||
TradingAmountAvg = floatLeg.TradingAmountAvg,
|
||||
CloseQty = unwindData.CloseQty,
|
||||
PositionQty = unwindData.PositionQty,
|
||||
ContractSize = floatLeg.ContractSize,
|
||||
CloseNotionalValue = unwindData.CloseNotionalValue,
|
||||
PayDirection = floatLeg.PayDirection,
|
||||
PositionType = floatLeg.PositionType,
|
||||
TradingFee = floatLeg.TradingFee.ToString(),
|
||||
TradingFeePending = floatLeg.TradingFeePending.ToString(),
|
||||
DividendIn = floatLeg.DividendIn.ToString()
|
||||
};
|
||||
foreach (var leg in unwindData.FlowEvents.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)))
|
||||
{
|
||||
var target = MarginModes.Contains(leg.InterestMode)
|
||||
? input.MarginLegs
|
||||
: input.InterestLegs;
|
||||
target.Add(new LegInput { InterestClosePnL = leg.InterestClosePnL });
|
||||
}
|
||||
|
||||
var result = FrontendCalcReference.CalcUnwind(input);
|
||||
floatLeg.MarkClosePnl = result.MarkClosePnl;
|
||||
unwindData.SwapCloseAmount = result.SwapCloseAmount;
|
||||
unwindData.SwapRealizedPnL = result.SwapRealizedPnL;
|
||||
unwindData.SwapMarginRebatePnl = result.SwapMarginRebatePnl;
|
||||
}
|
||||
|
||||
internal static bool IsFullCloseAfterDeduction(UnwindData unwindData, double remainingNotional, double remainingQuantity)
|
||||
{
|
||||
return unwindData.ClosePercent == 1 || (remainingNotional == 0 && remainingQuantity == 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手工平仓、手工互换及收益结算的利息事件按金额两位落库。
|
||||
/// 自动平仓保留原有计算与落库口径,不适用本阶段的手工结算规则。
|
||||
/// </summary>
|
||||
internal static bool NormalizeSettledInterestAmounts(IEnumerable<swap_flow_event> flowEvents, int eventType, string eventReason)
|
||||
{
|
||||
if ((eventType != (int)SwapEventTypeEnum.平仓 && eventType != (int)SwapEventTypeEnum.互换)
|
||||
|| eventReason == "系统操作_自动平仓")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var flowEvent in flowEvents.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)))
|
||||
{
|
||||
flowEvent.InterestPrincipal = Math.Round(flowEvent.InterestPrincipal, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
flowEvent.InterestAmount = Math.Round(flowEvent.InterestAmount, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
flowEvent.TdInterestAmount = Math.Round(flowEvent.TdInterestAmount, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
flowEvent.InterestClosePnL = Math.Round(flowEvent.InterestClosePnL, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
flowEvent.InterestFee = Math.Round(flowEvent.InterestFee, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static void NormalizeEventUnwindDate(UnwindData unwindData)
|
||||
{
|
||||
unwindData.UnwindDate = unwindData.ValueDate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
# 任务 4:盘中复利从 EOD 续接(而非 PosiStartDate 全程重放)
|
||||
|
||||
## 状态:待立项(高风险,需专项验证)
|
||||
|
||||
## 现状
|
||||
|
||||
`CalcDailyCompoundInterest` 从 `position.PosiStartDate` 全程重放到 `endDate`,每个重置日把累计利息并入本金(复利),最后扣 `consumedInterest * closePercent`。
|
||||
|
||||
**调用链**:`InitSwapDealInterest` → `CalcDailyCompoundInterest(endDate, PosiStartDate→endDate 全程重放)`
|
||||
|
||||
**问题**:交易存续期长(数月~数年)时,每次盘中平仓都从起息日重放,计算量随天数线性增长。
|
||||
|
||||
## 提议
|
||||
|
||||
改为从上一日终快照(`preEodPosition`)续接:
|
||||
- 起点 = `preEodPosition.ValueDate + 1`
|
||||
- 初始本金 = `preEodPosition.TdInterestPrincipal`(已含历史滚入利息)
|
||||
- 只算 `ValueDate+1` 到 `endDate` 的增量利息
|
||||
|
||||
## 风险分析(为什么不能直接改)
|
||||
|
||||
### 风险 1:并本金起点不同导致终值不等
|
||||
|
||||
| | 全程重放(当前) | EOD 续接(提议) |
|
||||
|---|---|---|
|
||||
| 起点 | `principal`(原始平仓名义本金) | `preEod.TdInterestPrincipal`(已滚利息) |
|
||||
| 滚法 | 每段 `basis = principal + accrued` | 每段 `basis = preEod.TdInterestPrincipal + segmentAccrued` |
|
||||
|
||||
两段路径在**中间重置日的四舍五入路径不同**(精度 12 的 Round 作用在不同的中间值上),终值**不一定逐分相等**。
|
||||
|
||||
### 风险 2:consumedInterest 语义翻转
|
||||
|
||||
- 全程重放:总利息 - consumedInterest × closePercent = 增量
|
||||
- EOD 续接:直接算增量,**不需要**扣 consumedInterest
|
||||
|
||||
如果 EOD 快照的 `InterestIncomeSum` 与 consumedInterest 口径不完全一致,直接去掉扣减会引入误差。
|
||||
|
||||
### 风险 3:resetCarryInterest 耦合
|
||||
|
||||
当前逻辑:`resetCarryInterest`(上一日终待实现 × remainingPercent)只在 `endDate` 恰好是重置日时并入本金。EOD 续接模式下,重置日的判定、remainingPercent 的计算、carry 的注入时机都不同。
|
||||
|
||||
### 风险 4:全平重放逻辑(lines 1293-1304)
|
||||
|
||||
`InitSwapDealInterest` 在 `closePrecent == 1m` 时做 **两次** `CalcDailyCompoundInterest` 重放(截至平仓日 + 截至上一日终),取差值。EOD 续接模式下这段逻辑需要完全重新设计。
|
||||
|
||||
## 验证方案(立项前提)
|
||||
|
||||
1. 构造测试用例:同一笔复利交易,跨越 ≥2 个重置周期,有 preEod 快照
|
||||
2. 用**旧全程重放**算出 `(InterestAmount, TdInterestAmount, finalBasis)`
|
||||
3. 用**新 EOD 续接**算出同样三个值
|
||||
4. 断言差额 < 0.01(到分)
|
||||
5. 覆盖场景:
|
||||
- 部分平仓(closePercent < 1)
|
||||
- 全平(closePercent == 1)
|
||||
- 平仓日 = 重置日
|
||||
- 平仓日 ≠ 重置日
|
||||
- 有/无 consumedInterest
|
||||
- 有/无 resetCarryInterest
|
||||
|
||||
## 建议排期
|
||||
|
||||
单独 sprint 处理,不混入日常重构。改动范围:
|
||||
- `CompoundInterestAccrual.AccruePeriod` 新增 `startBasis` 参数(或新方法 `AccrueFromEod`)
|
||||
- `CalcDailyCompoundInterest` wrapper 改为传 `preEod.TdInterestPrincipal` 作为起点
|
||||
- `InitSwapDealInterest` 全平重放逻辑简化(不再需要两次重放取差值)
|
||||
- `consumedInterest` 扣减逻辑移除或调整
|
||||
Reference in New Issue
Block a user