Merge remote-tracking branch 'origin/glms/feature/1.4.2' into glms/feature/1.4.2-margin
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>
|
||||
|
||||
@@ -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,85 @@
|
||||
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}");
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/// <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);
|
||||
@@ -107,7 +104,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;
|
||||
|
||||
@@ -124,8 +121,8 @@ public static class CompoundInterestAccrual
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.FundingLegs;
|
||||
|
||||
@@ -7,7 +7,7 @@ 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
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
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; }
|
||||
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
=> new(td, tradeExtend, valueDate, unwindDate, eodPositions, positions,
|
||||
preCloseNotional, closedNotional, closePercentRemaining,
|
||||
eventType, tdClose, orginPv, add, newCalcLast, closeList);
|
||||
|
||||
/// <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);
|
||||
}
|
||||
@@ -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,42 +0,0 @@
|
||||
using YLErp.Core.Interest;
|
||||
using YLErp.Derivatives.Interest;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
/// <summary>
|
||||
/// 保证金账户。管理保证金余额的变动(追加/释放/返还),并提供计息入口(预留抽象,尚未接线)。
|
||||
///
|
||||
/// 保证金是独立的资金管理概念(初始保证金/维持保证金/保证金余额/追保),与融资腿(funding leg)无关。
|
||||
/// 生产保证金计息入口为 SwapDealService.CalcMarginInterest(仍以 InterestMode 5/6 标识):
|
||||
/// EOD 用昨日终本金 preEod.TdInterestPrincipal(无差分);盘中用 accrualBasis 差分(orginPv 经 PreviousBalance)。
|
||||
/// 本类尚未被生产代码实例化——其扁平"余额×利率×天数"模型无法表达盘中差分与多行分段,留作未来简化抽象。
|
||||
/// </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。
|
||||
/// 注意:当前未被生产代码调用——生产保证金计息入口为 SwapDealService.CalcMarginInterest
|
||||
/// (处理 EOD 昨日终本金与盘中差分;本方法的扁平余额模型不覆盖盘中差分口径)。
|
||||
/// </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"概念。
|
||||
/// 余额随追加/释放/盈亏变动,利息由计息层(SwapDealService.CalcMarginInterest)按 EOD 昨日终本金 / 盘中差分口径计算。
|
||||
/// </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>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
using MoreLinq.Extensions;
|
||||
using MoreLinq.Extensions;
|
||||
using Newtonsoft.Json;
|
||||
using YLErp.BLL;
|
||||
using YLErp.BLL.Eod;
|
||||
using YLErp.DBModels.Enums;
|
||||
using YLErp.Core.Interest;
|
||||
using YLErp.Derivatives.Interest;
|
||||
using YLErp.Helpers;
|
||||
using YLErp.Modules.DataProviderModule;
|
||||
@@ -50,8 +49,8 @@ namespace YLErp.Modules.SwapModule
|
||||
return SaveSwapDealInternal(unwindData, eventType, clientCashId, eventResason, approve);
|
||||
}
|
||||
|
||||
// 待实现利息会进入 decimal(30,12) 日终快照;精度常量统一引用 SwapInterest.FundingLegPrecision,消除重复定义。
|
||||
private const int InterestCalculationPrecision = SwapInterest.FundingLegPrecision;
|
||||
// 待实现利息会进入 decimal(30,12) 日终快照;精度常量统一引用 InterestMath.FundingLegPrecision,消除重复定义。
|
||||
private const int InterestCalculationPrecision = InterestMath.FundingLegPrecision;
|
||||
|
||||
// 客户现金在 SaveSwapDeal 之前创建,手工结算必须先收敛流水并重算汇总金额。
|
||||
private void NormalizeManualSettlementAmounts(UnwindData unwindData, int eventType, string eventReason)
|
||||
@@ -211,7 +210,7 @@ namespace YLErp.Modules.SwapModule
|
||||
public UnwindData InitUnwind(int tradeId)
|
||||
{
|
||||
var td = DbContext.trade.Find(tradeId);
|
||||
var positions = DbContext.swap_position.Where(x => x.SwapTradeId == tradeId && !x.Invalid);
|
||||
var positions = DbContext.swap_position.ActiveByTrade(tradeId);
|
||||
var um = DataCacheProvider.GetUnderlyingDataSource().GetData(td.UnderlyingCode);
|
||||
bool commodity = ConsGlobal.InstrumentType.CalcTypeIsFutures(um.UnderlyingInstrumentType);
|
||||
List<int> eventTyps = new List<int>() { (int)SwapEventTypeEnum.自动互换, (int)SwapEventTypeEnum.互换 };
|
||||
@@ -277,6 +276,7 @@ namespace YLErp.Modules.SwapModule
|
||||
// DividendPending = "待结算分红收益"(仍挂在账上、未来才结的存量 = PosiDividendSum 全量口径,
|
||||
// 见 GetPreEodDividendSum 注释的口径论证;切勿改回硬0或分摊,会落库回归)
|
||||
decimal preEodDividendSum = GetPreEodDividendSum(tradeId, position.PositionId, dealDate);
|
||||
Logger.Info($"[分红-平仓预览] 方案C DividendIn=DividendPending=PosiDividendSum全量 tradeId={tradeId} positionId={position.PositionId} dealDate={dealDate:yyyy-MM-dd} 值={preEodDividendSum}");
|
||||
floatEvent.DividendIn = preEodDividendSum;
|
||||
floatEvent.DividendPending = preEodDividendSum;
|
||||
floatEvent.UnderlyingCode = position.UnderlyingCode;
|
||||
@@ -355,7 +355,7 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
var checkEventTypes = new List<int>() { (int)SwapEventTypeEnum.互换, (int)SwapEventTypeEnum.自动互换 };
|
||||
var td = DbContext.trade.Find(tradeId);
|
||||
var positions = DbContext.swap_position.Where(x => x.SwapTradeId == tradeId && !x.Invalid);
|
||||
var positions = DbContext.swap_position.ActiveByTrade(tradeId);
|
||||
var um = DataCacheProvider.GetUnderlyingDataSource().GetData(td.UnderlyingCode);
|
||||
List<int> eventTypes = new List<int>() { (int)SwapFlowEventTypeEnum.互换, (int)SwapFlowEventTypeEnum.自动互换 };
|
||||
var maxIncomeValueDate = GetMaxIncomeValueDate(td);
|
||||
@@ -410,7 +410,9 @@ namespace YLErp.Modules.SwapModule
|
||||
floatEvent.PositionId = position.PositionId;
|
||||
// 方案C:分红收益改由上一收盘日 EOD PosiDividendSum 提供(单一可信源),
|
||||
// 前端 getDivindIn 不再覆盖;消除"期初持仓×totalInterest"对已平仓部分的重复计入。
|
||||
floatEvent.DividendIn = GetPreEodDividendSum(tradeId, position.PositionId, dealDate);
|
||||
decimal preEodDividendSum = GetPreEodDividendSum(tradeId, position.PositionId, dealDate);
|
||||
Logger.Info($"[分红-收益结算] DividendIn=PosiDividendSum全量 tradeId={tradeId} positionId={position.PositionId} dealDate={dealDate:yyyy-MM-dd} 值={preEodDividendSum}");
|
||||
floatEvent.DividendIn = preEodDividendSum;
|
||||
floatEvent.UnderlyingCode = position.UnderlyingCode;
|
||||
floatEvent.UnderlyingInstrumentType = position.UnderlyingInstrumentType;
|
||||
floatEvent.CloseFee = 0;
|
||||
@@ -458,23 +460,18 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
throw new ServiceException("未找到交易信息");
|
||||
}
|
||||
var allpositions = DbContext.swap_position.Where(x => x.SwapTradeId == tradeId && !x.Invalid).ToList();
|
||||
var allpositions = DbContext.swap_position.ActiveByTrade(tradeId).ToList();
|
||||
var origPositions = allpositions.Where(x => x.IsInitial).ToList();
|
||||
var realPostitions = allpositions.Where(x => !x.IsInitial).ToList();
|
||||
// 根因修复(多次部分平仓预付金返还错误):见 ResolveInterestLegPositions 注释。
|
||||
// 迭代源仍用 origPositions(保留 orig.id → eod_swap_position.PositionId 的日终匹配),
|
||||
// 仅对预付金腿以实时腿的剩余本金克隆覆盖,故此处不改任何日终匹配行为。
|
||||
var positions = ResolveInterestLegPositions(origPositions, realPostitions);
|
||||
var fpositions = origPositions.Where(x => x.PosiDirection > 0).ToList();
|
||||
var longPositions = fpositions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).ToList();
|
||||
var shortPositions = fpositions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).ToList();
|
||||
var tradeExtend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == tradeId);
|
||||
List<int> eventTypes = new List<int>() { (int)SwapEventTypeEnum.平仓, (int)SwapEventTypeEnum.互换, (int)SwapEventTypeEnum.自动互换 };
|
||||
var lastEod = DbContext.eod_swap.Where(x => x.ValueDate < unwindDate && x.SwapTradeId == tradeId).OrderByDescending(o => o.ValueDate).FirstOrDefault();
|
||||
var _preSetteDate = lastEod == null ? unwindDate.AddDays(-1) : lastEod.ValueDate;
|
||||
List<eod_swap_position> lastEodPositions = new SwapEodPositionService(this).GetPreEodPositions(tradeId, _preSetteDate);//上一交易数据
|
||||
var posiLongNotionalValue = longPositions.Sum(s => s.PosiNotionalValue);// 剩余名义本金
|
||||
var posiShortNotionalValue = shortPositions.Sum(s => s.PosiNotionalValue);// 剩余名义本金
|
||||
var stockEqvNotional = realPostitions.Where(x => x.PosiDirection > 0).Sum(s => s.PosiNotionalValue); // 当前平仓前的实时剩余本金
|
||||
var posiNotionalValue = stockEqvNotional * closePercent;// 本次平仓名义本金
|
||||
var orginPv = ResolveUnwindPreviousNotional(lastEod, lastEodPositions, stockEqvNotional); // 上一日终的浮动端本金
|
||||
@@ -484,7 +481,11 @@ namespace YLErp.Modules.SwapModule
|
||||
&& eventTypes.Contains(x.EventType)
|
||||
&& x.DataState == (int)SwapFlowDateStateEnum.完成).ToList();
|
||||
bool tdClose = closeList.Count > 0;
|
||||
interests = GetInterests(td, tradeExtend, valueDate, unwindDate, lastEodPositions, positions, stockEqvNotional, posiLongNotionalValue, posiShortNotionalValue, posiNotionalValue, closePercent, eventType, tdClose, false, grossPrice ?? 0, orginPv, true, false,false, closeList);
|
||||
// 显式入口:平仓前剩余本金 + 实际平掉额 + B语义比例,盘中重放(语义见 InterestCalcRequest.IntradayUnwind)
|
||||
interests = GetIntradayUnwindInterests(InterestCalcRequest.IntradayUnwind(
|
||||
td, tradeExtend, valueDate, unwindDate, lastEodPositions, positions,
|
||||
stockEqvNotional, posiNotionalValue,
|
||||
closePercent, eventType, tdClose, orginPv, add: true, newCalcLast: false, closeList));
|
||||
return interests;
|
||||
}
|
||||
|
||||
@@ -606,14 +607,22 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <param name="eodPositions">上一日终持仓</param>
|
||||
/// <param name="positions">期初利率端</param>
|
||||
/// <param name="posiNotionalValue">持仓名义本金</param>
|
||||
/// <param name="posiLongNotionalValue">多头持仓名义本金</param>
|
||||
/// <param name="posiShortNotionalValue">空头持仓名义本金</param>
|
||||
/// <param name="closePosiNotionalValue">平仓名义本金</param>
|
||||
/// <param name="closePrecent"></param>
|
||||
/// <param name="eventType"></param>
|
||||
/// <param name="tdClose"></param>
|
||||
/// <param name="add"></param>
|
||||
/// <returns></returns>
|
||||
/// <summary>
|
||||
/// 【盘中平仓/互换结息】显式入口——GetInterests(settment:false) 盘中语义的具名封装(2026-08 显式化重构)。
|
||||
/// 语义契约见 InterestCalcRequest.IntradayUnwind 工厂注释;计息走 CalcUnwindInterest 全区间重放。
|
||||
/// </summary>
|
||||
public List<swap_flow_event> GetIntradayUnwindInterests(InterestCalcRequest req)
|
||||
=> GetInterests(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);
|
||||
|
||||
public List<swap_flow_event> GetInterests(
|
||||
trade td,
|
||||
trade_extend tradeExtend,
|
||||
@@ -622,14 +631,10 @@ namespace YLErp.Modules.SwapModule
|
||||
List<eod_swap_position> eodPositions,
|
||||
List<swap_position> positions,
|
||||
decimal posiNotionalValue,
|
||||
decimal posiLongNotionalValue,
|
||||
decimal posiShortNotionalValue,
|
||||
decimal closePosiNotionalValue,
|
||||
decimal closePrecent,
|
||||
int eventType,
|
||||
bool tdClose,
|
||||
bool needPrice,
|
||||
decimal grossPrice,
|
||||
decimal orginPv,
|
||||
bool add = false,
|
||||
bool settment = true,
|
||||
@@ -652,27 +657,30 @@ namespace YLErp.Modules.SwapModule
|
||||
// true 跳过 不计利息; false 正常利息
|
||||
bool swap = InitInterestDate(unwindDate, preDealDate, td, tdClose, out DateTime startDate, out DateTime endDate);
|
||||
|
||||
// 计算名义本金
|
||||
decimal closePrincipal;
|
||||
decimal posiPrincipal;
|
||||
decimal newClosePercent = closePrecent;
|
||||
var mode = (InterestModeEnum)position.InterestMode;
|
||||
// 获取利率(保证金/融资腿共用:SwapIntervalList 取当日适用固定利率 + 精度收口)
|
||||
decimal rate = Math.Round(GetFixedRate(position, unwindDate), InterestCalculationPrecision, MidpointRounding.AwayFromZero); // 做精度调整 原数据有精度误差
|
||||
|
||||
// ── 边界隔离:保证金腿(5/6)在循环最外层路由,后续融资腿分支树不感知保证金概念 ──
|
||||
// 有意跳过 GetFloatRate:CalcMarginInterest 纯固定利率(FundingLegRate.Fixed)且 FloatRate 恒 0,
|
||||
// 浮动取价/回写对保证金无意义;即使脏数据填了 FloatRateUnderlyingCode 且缺价,也不应阻断保证金结算。
|
||||
if (MarginModes.Contains(position.InterestMode))
|
||||
{
|
||||
// 保证金腿: 计息基数 = InterestPrincipalFix(保证金余额)
|
||||
closePrincipal = position.InterestPrincipalFix * closePrecent;
|
||||
posiPrincipal = position.InterestPrincipalFix;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 融资腿(1/2/9): 走策略工厂
|
||||
var r = FundingLegStrategyFactory.Get(mode)
|
||||
.CalcNotional(position.InterestPrincipalFix, posiNotionalValue, closePrecent);
|
||||
closePrincipal = r.ClosePrincipal;
|
||||
posiPrincipal = r.PosiPrincipal;
|
||||
newClosePercent = r.ClosePercent;
|
||||
positionClone.InterestDirection = MarginCalc.FlipDirection(position.InterestDirection);
|
||||
// 保证金腿: 计息基数 = InterestPrincipalFix(保证金余额),无融资腿差分公式与 orginPv 维度 hack
|
||||
interests.Add(CalcMarginInterest(td, valueDate, endDate, positionClone, rate,
|
||||
position.InterestPrincipalFix * closePrecent, position.InterestPrincipalFix,
|
||||
closePrecent, annualDays, calcFirst, calcLast || newCalcLast, preEodPosition, eventType, add, settment, swap));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 计算名义本金(以下仅融资腿 1/2/9:走策略工厂)
|
||||
var mode = (InterestModeEnum)position.InterestMode;
|
||||
var r = FundingLegStrategyFactory.Get(mode)
|
||||
.CalcNotional(position.InterestPrincipalFix, posiNotionalValue, closePrecent);
|
||||
decimal closePrincipal = r.ClosePrincipal;
|
||||
decimal posiPrincipal = r.PosiPrincipal;
|
||||
decimal newClosePercent = r.ClosePercent;
|
||||
|
||||
// 根因位置:SwapEodPositionService.SaveAutoEodWithCloseInterestPosition 在平仓后收盘时传入
|
||||
// “收盘后剩余本金 + closePercent=1”,与盘中“平仓前本金 + 实际关闭比例”不是同一语义。
|
||||
// GetInterests 同时被盘中试算和 EOD 平仓后收盘调用:后者传入的
|
||||
@@ -681,29 +689,17 @@ namespace YLErp.Modules.SwapModule
|
||||
// 模式2(合约名义本金规模)的本次结息本金必须始终是实际平仓额,因此无条件覆盖,
|
||||
// 否则会错误地用剩余 70 结算本次平掉的 30。模式9(标的期初全价)的部分平仓
|
||||
// 仍保留既有的剩余/复利动态本金承接逻辑;仅最终全平时 posi=0,才覆盖以避免结息本金为 0。
|
||||
if ((InterestModeEnum)position.InterestMode == InterestModeEnum.合约名义本金规模
|
||||
|| ((InterestModeEnum)position.InterestMode == InterestModeEnum.标的期初全价
|
||||
if (mode == InterestModeEnum.合约名义本金规模
|
||||
|| (mode == InterestModeEnum.标的期初全价
|
||||
&& posiNotionalValue == 0m))
|
||||
{
|
||||
closePrincipal = closePosiNotionalValue;
|
||||
}
|
||||
if (MarginModes.Contains(position.InterestMode))
|
||||
{
|
||||
positionClone.InterestDirection = MarginCalc.FlipDirection(position.InterestDirection);
|
||||
}
|
||||
|
||||
// 获取利率
|
||||
decimal rate = Math.Round(GetFixedRate(position, unwindDate), InterestCalculationPrecision, MidpointRounding.AwayFromZero); // 做精度调整 原数据有精度误差
|
||||
decimal floatRate = GetFloatRate(position, preEodPosition, td.StartDate.Value, endDate, interestPeriod, swap, positionClone);
|
||||
|
||||
// 根据场景计算利息
|
||||
if (MarginModes.Contains(position.InterestMode))
|
||||
{
|
||||
// 保证金腿(5/6):专属计息,notional 直接取保证金余额,无融资腿差分公式与 orginPv 维度 hack
|
||||
interests.Add(CalcMarginInterest(td, valueDate, endDate, positionClone, rate, closePrincipal, posiPrincipal,
|
||||
newClosePercent, annualDays, calcFirst, calcLast||newCalcLast, preEodPosition, eventType, add, settment, swap));
|
||||
}
|
||||
else if (settment)
|
||||
if (settment)
|
||||
{
|
||||
// 收盘归档场景,使用 CalcEodInterest
|
||||
interests.Add(CalcEodInterest(td, valueDate, positionClone, rate, floatRate, closePrincipal, posiPrincipal, annualDays, calcFirst, calcLast, preEodPosition, eventType, add));
|
||||
@@ -717,7 +713,7 @@ namespace YLErp.Modules.SwapModule
|
||||
: 0m;
|
||||
interests.Add(CalcUnwindInterest(td, valueDate, endDate, positionClone, rate, floatRate, posiPrincipal,
|
||||
closePrincipal, newClosePercent, annualDays, preEodPosition, eventType, add, swap, orginPv, calcFirst,
|
||||
calcLast||newCalcLast, consumedInterest));
|
||||
calcLast || newCalcLast, consumedInterest));
|
||||
}
|
||||
}
|
||||
//当日有平仓或互换记录时,避免重复结算
|
||||
@@ -807,7 +803,9 @@ namespace YLErp.Modules.SwapModule
|
||||
protected virtual decimal GetPreEodDividendSum(int tradeId, long positionId, DateTime dealDate)
|
||||
{
|
||||
var preEod = GetPreEodPositionByDate(tradeId, positionId, dealDate);
|
||||
return preEod == null ? 0m : preEod.PosiDividendSum;
|
||||
var sum = preEod == null ? 0m : preEod.PosiDividendSum;
|
||||
Logger.Info($"[分红-读取] GetPreEodDividendSum tradeId={tradeId} positionId={positionId} dealDate={dealDate:yyyy-MM-dd} 取EOD日期={(preEod?.ValueDate):yyyy-MM-dd} PosiDividendSum={sum}");
|
||||
return sum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -821,6 +819,7 @@ namespace YLErp.Modules.SwapModule
|
||||
.Where(x => x.ValueDate <= dealDate)
|
||||
.OrderByDescending(o => o.ValueDate).FirstOrDefault();
|
||||
var preEodDate = lastEod == null ? dealDate.AddDays(-1) : lastEod.ValueDate;
|
||||
Logger.Info($"[分红-快照定位] GetPreEodPositionByDate tradeId={tradeId} positionId={positionId} dealDate={dealDate:yyyy-MM-dd} 取<=当日EOD, 命中日期={(lastEod?.ValueDate):yyyy-MM-dd}, 回退={lastEod == null}");
|
||||
return QueryPreEodPosition(tradeId, positionId, preEodDate);
|
||||
}
|
||||
|
||||
@@ -953,6 +952,21 @@ namespace YLErp.Modules.SwapModule
|
||||
/// 本方法内部按保证金维度计算(PreviousBalance),消除原 InitSwapDealInterest 的外部维度 hack
|
||||
/// (融资腿 orginPv=浮动端名义本金)。保留累计语义(priorAccrued + 增量),满足下游字段契约。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 前提(由前端保证金表单 + SwapTradeService 构造保证):
|
||||
/// 1. <b>InterestType=单利</b>。本方法恒走 SimpleInterestAccrual 单利,不查 InterestType;
|
||||
/// 若库内 InterestMode=5/6 且 InterestType=复利(脏数据),会与旧 CalcEodInterest 复利分支不一致。
|
||||
/// 2. <b>rate 由 GetFixedRate 提供</b>(SwapDealService.cs:866)——从 SwapIntervalList 取 <c>Date ≤ unwindDate</c> 最近段的 Rate,
|
||||
/// 空表/单段时返回 InterestRateDefault。SwapIntervalList 是"互换观察日排期"(阶梯利率表 + 结息日历,非 FR007 浮动——
|
||||
/// 浮动由 FloatRateUnderlyingCode + interest_rest_days 独立驱动);保证金前端亦开放"设置观察日"分段录入。
|
||||
/// 盘中用该 rate 覆盖全程,与旧 CalcDailySimpleInterest 完全一致(BuildSegmentRates 的 spread 同样是 GetFixedRate 单一值全程,
|
||||
/// 不按 SwapIntervalList 切段)——SwapIntervalList 阶梯利率在盘中半路变更的精细处理是既有未覆盖口径,非本次引入;
|
||||
/// EOD 路径因每日重取 GetFixedRate(valueDate) 故能正确反映阶梯。
|
||||
/// 契约与副作用:
|
||||
/// 3. <b>position.InterestDirection 须已由调用方翻转</b>(GetInterests:742 FlipDirection);本方法不翻转。
|
||||
/// 4. <b>preEod 在 id==0 时被就地修改</b>(设 TdInterestPrincipal/PosiNotionalValue/FloatRate),与旧 CalcEodInterest 一致。
|
||||
/// 定位:SwapCalcTrace 落盘 AccrueEod/AccrualPeriod 的 notional/days/rate/accrued;盘中 accrualBasis 可从 trace 的 notional 反推。
|
||||
/// </remarks>
|
||||
/// <param name="settment">true=收盘归档(EOD),false=盘中平仓/互换。</param>
|
||||
/// <param name="swap">互换事件(仅盘中生效,true 时利息归零,同 InitSwapDealInterest)。</param>
|
||||
public swap_flow_event CalcMarginInterest(
|
||||
@@ -1076,7 +1090,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
return InitSwapDealInterest(td, valueDate, endDate, rate, position, add, swap, posiPrincipal,
|
||||
closePrincipal, closePercent, annualDays, eventType, preEod, false,
|
||||
closePrincipal, closePercent, annualDays, eventType, preEod,
|
||||
orginPv, calcFirst, calcLast, consumedInterest);
|
||||
}
|
||||
/// <summary>
|
||||
@@ -1131,7 +1145,6 @@ namespace YLErp.Modules.SwapModule
|
||||
int annualDays,
|
||||
int eventType,
|
||||
eod_swap_position preEodPosition,
|
||||
bool needPrice,
|
||||
decimal orginPv,
|
||||
bool calcFirst,
|
||||
bool calcLast,
|
||||
@@ -1206,6 +1219,12 @@ namespace YLErp.Modules.SwapModule
|
||||
consumedInterest, resetCarryInterest);
|
||||
if (preEodPosition.id != 0 && closePrecent == 1m)
|
||||
{
|
||||
// 【全平专属分支触发标记】(快速定位):设计意图=真全平(尾差一次带走)与观察日恒1全额结息。
|
||||
// ⚠️ 契约修复暂缓期间,普通部分平仓经 EOD 恒1惯例【仍会进入本分支】(重算结果已被裁决
|
||||
// 证为不落库/不动钱/不进资金,零生产后果);修复落地后部分平仓不再进入——本行日志届时
|
||||
// 兼作落地验证哨兵(部分平仓出现在此=修复未生效/被回退)。
|
||||
Logger.Info($"[利息-全平专属分支] tradeId={td.id} posiId={position.id} valueDate={valueDate:yyyy-MM-dd} " +
|
||||
$"closePrecent={closePrecent} preEod.InterestIncomeSum={preEodPosition.InterestIncomeSum}");
|
||||
// 最终全平只重放上一日终之后的新增利息;历史部分平仓的两位结算尾差已在日终待实现中。
|
||||
// InterestAmount 是本次最终应结金额;TdInterestAmount 是不按关闭比例缩放的参考累计值。
|
||||
// 二者在全平时都以上一日 InterestIncomeSum 为起点,保证之前攒下的尾差最后一次带走。
|
||||
@@ -1435,7 +1454,7 @@ namespace YLErp.Modules.SwapModule
|
||||
SwapCalcTrace.Write(interestTrace);
|
||||
|
||||
// flowEvent.InterestPrincipal:当日计息基数(已按平仓比例缩放)——下游 EOD 用它播种次日 TdInterestPrincipal。
|
||||
// 复用 CompoundEodBasis 单一真相源(与 AccrueCompoundEod 内部同一公式)。
|
||||
// 复用 CompoundEodBasis 单一真相源(与 CompoundInterestAccrual.AccrueEod 内部同一公式,见其 EodBasis 调用)。
|
||||
flowEvent.InterestPrincipal = CompoundInterestAccrual.EodBasis(
|
||||
isResetDay, posiPrincipal, preEodPosition.InterestProfitSum, remainingFraction,
|
||||
preEodPosition.TdInterestPrincipal) * closePercent;
|
||||
@@ -1549,7 +1568,7 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
unwindPriceFee = decimal.Parse(unwindPriceFee.ToString("F10"));
|
||||
var td = DbContext.trade.Find(tradeid);
|
||||
var positions = DbContext.swap_position.Where(x => x.SwapTradeId == td.id && !x.Invalid);
|
||||
var positions = DbContext.swap_position.ActiveByTrade(td.id);
|
||||
List<int> eventTypes = new List<int>() { (int)SwapEventTypeEnum.平仓, (int)SwapEventTypeEnum.互换 };
|
||||
var dealDate = valueDate;
|
||||
var tradeExtend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == td.id);
|
||||
@@ -1675,153 +1694,6 @@ namespace YLErp.Modules.SwapModule
|
||||
return data.ValueAddedTax ?? 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 衡泰新增平仓事件
|
||||
/// </summary>
|
||||
/// <param name="td"></param>
|
||||
/// <param name="valueDate"></param>
|
||||
/// <param name="markClosePnl"></param>
|
||||
/// <param name="unwindQty"></param>
|
||||
/// <param name="allClose"></param>
|
||||
public void AutoSwapUnwindFromConsumer(trade td, DateTime valueDate, DateTime payDate, decimal markClosePnl, decimal tradeinfFee, decimal interestAmount, decimal fee, decimal unwindQty, bool allClose)
|
||||
{
|
||||
List<int> eventTypes = new List<int>() { (int)SwapEventTypeEnum.平仓, (int)SwapEventTypeEnum.互换 };
|
||||
var dealDate = valueDate;
|
||||
var tradeExtend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == td.id);
|
||||
td.trade_extend = tradeExtend;
|
||||
var position = DbContext.swap_position.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode) && x.IsInitial && !x.Invalid).FirstOrDefault();
|
||||
var preDealDate = GetPreDealDate(td.id, dealDate, eventTypes);
|
||||
swap_flow_event floatEvent = new swap_flow_event();
|
||||
UnwindData unwindData = new UnwindData();
|
||||
unwindData.CloseType = 2;
|
||||
unwindData.StartDate = td.TradeDate.Value;
|
||||
if (preDealDate.HasValue)
|
||||
{
|
||||
unwindData.StartDate = preDealDate.Value;
|
||||
}
|
||||
unwindData.ValueDate = dealDate;
|
||||
floatEvent.EventDate = dealDate;
|
||||
unwindData.UnwindDate = QdpCalendarHelper.GetNonHoliday(dealDate.AddDays(1));
|
||||
floatEvent.UnwindDate = unwindData.UnwindDate;
|
||||
floatEvent.PayDate = payDate;
|
||||
unwindData.PayDate = floatEvent.PayDate;
|
||||
floatEvent.SwapTradeId = td.id;
|
||||
floatEvent.SwapTradeNo = td.TradeNumber;
|
||||
unwindData.SwapTradeId = td.id;
|
||||
unwindData.StructureType = td.StructureType;
|
||||
unwindData.NotionalValue = Convert.ToDecimal(td.OriginalStockEqvNotional ?? 0);
|
||||
unwindData.NotionalQty = position.PosiQuantity;
|
||||
unwindData.PosiNotionalValue = Convert.ToDecimal(td.StockEqvNotional);
|
||||
unwindData.PositionQty = Convert.ToDecimal(td.TradeAmount);
|
||||
unwindData.AnnualDays = tradeExtend == null ? 365 : tradeExtend.ExtendObj.AnnualDays;
|
||||
unwindData.CloseMethod = allClose ? (int)CloseMethodEnum.全部平仓 : (int)CloseMethodEnum.部分平仓;
|
||||
unwindData.ClosePercent = allClose ? 1 : unwindQty / unwindData.NotionalQty;
|
||||
unwindData.CloseNotionalValue = allClose ? unwindData.PosiNotionalValue : unwindQty;
|
||||
unwindData.CloseQty = allClose ? unwindData.PositionQty : unwindQty;
|
||||
if (position != null)
|
||||
{
|
||||
decimal floatRatio = position.PosiDirection == 1 ? 1m : -1m;
|
||||
floatEvent.PositionId = position.id;
|
||||
floatEvent.EventType = (int)SwapEventTypeEnum.平仓;
|
||||
floatEvent.EventReason = "接口合约终止交易";
|
||||
floatEvent.DividendIn = 0;
|
||||
floatEvent.UnderlyingCode = position.UnderlyingCode;
|
||||
floatEvent.UnderlyingInstrumentType = position.UnderlyingInstrumentType;
|
||||
floatEvent.CloseFee = 0;
|
||||
floatEvent.BeforeCloseFee = position.PosiTradingFee + position.PosiTradingFeePending;
|
||||
floatEvent.PayDirection = position.PosiDirection;
|
||||
floatEvent.PosiGrossPrice = position.PosiGrossPrice;
|
||||
floatEvent.PosiNetPrice = position.PosiNetPrice;
|
||||
floatEvent.TradingAmountNetAvg = position.PosiNetNoFeePrice;
|
||||
floatEvent.TradingFeePending = position.PosiTradingFeePending * unwindData.ClosePercent;
|
||||
floatEvent.TradingFee = tradeinfFee - floatEvent.TradingFeePending;
|
||||
floatEvent.MarkClosePnl = markClosePnl;
|
||||
floatEvent.TradingAmount = floatEvent.Quantity * floatEvent.ContractSize;
|
||||
floatEvent.PositionType = position.PositionType;
|
||||
floatEvent.Quantity = position.PosiQuantity;
|
||||
floatEvent.PositionQty = 0;
|
||||
floatEvent.ContractSize = position.ContractSize;
|
||||
floatEvent.DataState = (int)SwapFlowDateStateEnum.完成;
|
||||
floatEvent.InterestMode = position.InterestMode;
|
||||
floatEvent.TradingAmount = unwindData.CloseQty;
|
||||
floatEvent.ClientId = td.ClientId;
|
||||
floatEvent.OptLog = "衡泰同步";
|
||||
floatEvent.SetOpt(UserInfo);
|
||||
}
|
||||
unwindData.FlowEvents.Add(floatEvent);
|
||||
var interestPositions = GetUnwindInterestsByHT(unwindData, td, interestAmount, fee);
|
||||
unwindData.FlowEvents.AddRange(interestPositions);
|
||||
CalcCloseAmount(unwindData);
|
||||
DealUnwind(unwindData, td, "合约终止接口回执");
|
||||
}
|
||||
private List<swap_flow_event> GetUnwindInterestsByHT(UnwindData unwindData, trade td, decimal interestAmount, decimal fee)
|
||||
{
|
||||
List<swap_flow_event> interests = new List<swap_flow_event>();
|
||||
var allpositions = DbContext.swap_position.Where(x => x.SwapTradeId == unwindData.SwapTradeId && !x.Invalid && x.IsInitial && x.PosiDirection > 0).ToList();
|
||||
var position = allpositions.Where(x => ConsTrade.InterestModels.Contains(x.InterestMode)).FirstOrDefault();
|
||||
if (position == null)
|
||||
{
|
||||
return interests;
|
||||
}
|
||||
var grossPrice = allpositions.Where(x => x.PosiDirection > 0).FirstOrDefault()?.PosiGrossPrice ?? 0;
|
||||
var _closePosiNotionalValue = unwindData.CloseNotionalValue;
|
||||
var _posiNotionalValue = unwindData.PosiNotionalValue;
|
||||
var newClosePercent = unwindData.ClosePercent;
|
||||
foreach (var item in allpositions)
|
||||
{
|
||||
var positionClone = item.Clone();
|
||||
var swapIntervalToday = position.SwapIntervalList.OrderByDescending(o => o.Date).FirstOrDefault();
|
||||
if (item.InterestMode == (int)InterestModeEnum.固定值)
|
||||
{
|
||||
_closePosiNotionalValue = item.InterestPrincipalFix;
|
||||
_posiNotionalValue = item.InterestPrincipalFix;
|
||||
newClosePercent = 1m;
|
||||
}
|
||||
else if (item.InterestMode == (int)InterestModeEnum.标的期初全价)
|
||||
{
|
||||
_closePosiNotionalValue = _posiNotionalValue * grossPrice * newClosePercent;
|
||||
_posiNotionalValue = _posiNotionalValue * grossPrice;
|
||||
}
|
||||
else if (MarginModes.Contains(item.InterestMode))
|
||||
{
|
||||
_closePosiNotionalValue = 0;
|
||||
positionClone.InterestDirection = MarginCalc.FlipDirection(position.InterestDirection);
|
||||
}
|
||||
decimal rate = item.InterestRateDefault;
|
||||
if (swapIntervalToday != null)//当日无适用观察日
|
||||
{
|
||||
rate = swapIntervalToday.Rate;
|
||||
}
|
||||
swap_flow_event interest = new swap_flow_event();
|
||||
interest.SwapTradeId = td.id;
|
||||
interest.SwapTradeNo = td.TradeNumber;
|
||||
interest.EventType = (int)SwapEventTypeEnum.平仓;
|
||||
interest.EventReason = "衡泰同步平仓";
|
||||
interest.EventDate = unwindData.ValueDate;
|
||||
interest.PositionId = item.id;
|
||||
interest.InterestDirection = positionClone.InterestDirection;
|
||||
interest.InterestRate = rate;
|
||||
interest.InterestPrincipal = _closePosiNotionalValue;
|
||||
interest.InterestSwapInterval = item.InterestSwapInterval;
|
||||
interest.InterestMode = item.InterestMode;
|
||||
interest.FloatRate = item.FloatRate;
|
||||
interest.DataState = (int)SwapFlowDateStateEnum.完成;
|
||||
interest.ClientId = td.ClientId;
|
||||
interest.UnwindDate = unwindData.ValueDate;
|
||||
interest.PayDate = unwindData.PayDate;
|
||||
if (position != null && item.id == position.id)
|
||||
{
|
||||
interest.InterestAmount = interestAmount;
|
||||
interest.TdInterestAmount = interestAmount;
|
||||
interest.InterestClosePnL = interestAmount;
|
||||
interest.InterestFee = fee;
|
||||
}
|
||||
UpdateDbOption(interest);
|
||||
interests.Add(interest);
|
||||
}
|
||||
|
||||
return interests;
|
||||
}
|
||||
private void DealUnwind(UnwindData unwindData, trade td, string actionMsg = "系统操作_自动平仓")
|
||||
{
|
||||
int clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapRealizedPnL), ClientCashInCashOut.系统操作_平仓费, unwindData.ValueDate);
|
||||
|
||||
@@ -82,26 +82,42 @@ 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 转发,保持既有测试替身对该虚接缝的拦截不变。
|
||||
/// (契约修复§六暂缓中:落地时 autoSwap=false 分支改 Intraday 形状,见裁决文档与调用点注释。)
|
||||
/// </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);
|
||||
|
||||
// FindTrade 已上提到基类 SwapTradeBaseService(三子类实现一致,消除重复)
|
||||
|
||||
/// <summary>查找交易扩展(生产: DbContext.trade_extend;测试: 内存字典)</summary>
|
||||
@@ -119,7 +135,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>
|
||||
@@ -355,7 +371,7 @@ 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);
|
||||
DealInterests(interestList, eodPositions, todyEodPositions, settleDate, td, flowEvents, autoInterests, lastEodSwap, posiLongNotional + posiShortNotional, closePosiNotional, grossPrice, orginPv);
|
||||
//获取自动互换的 interval 信息,用于确定结算日期
|
||||
IntervalModel autoInterval = null;
|
||||
foreach (var interest in interestList)
|
||||
@@ -427,8 +443,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)
|
||||
@@ -439,7 +454,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)
|
||||
@@ -487,7 +502,7 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
if (!hasClose)//当日无平仓
|
||||
{
|
||||
var _autoInterests = SaveAutoEodInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, insterval, lastEodSwap, posiLongNational, posiShortNational, grossPrice, orginPv);
|
||||
var _autoInterests = SaveAutoEodInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, insterval, lastEodSwap, posiTotalNotional, grossPrice, orginPv);
|
||||
if (_autoInterests.Count > 0)
|
||||
{
|
||||
autoInterests.AddRange(_autoInterests);
|
||||
@@ -495,7 +510,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, insterval, posiTotalNotional, swapEvents, closeNational, true, grossPrice, orginPv);
|
||||
if (_autoInterests.Count > 0)
|
||||
{
|
||||
autoInterests.AddRange(_autoInterests);
|
||||
@@ -508,11 +523,11 @@ 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, insterval, posiTotalNotional, swapEvents, closeNational, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -602,7 +617,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();
|
||||
|
||||
@@ -1079,7 +1094,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}");
|
||||
|
||||
@@ -1118,7 +1133,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)
|
||||
@@ -1138,12 +1153,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);
|
||||
|
||||
@@ -1228,7 +1245,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;
|
||||
@@ -1239,8 +1256,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);
|
||||
// 首次日终结算可能包含当日收盘,因此尚无先前的日终利息持仓。
|
||||
@@ -1276,6 +1293,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))
|
||||
{
|
||||
@@ -1302,9 +1321,21 @@ namespace YLErp.Modules.SwapModule
|
||||
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);
|
||||
// 显式入口:平仓后剩余本金 + 实际平掉额 + 恒1全额结息(语义见 InterestCalcRequest.EodPostCloseSettle)。
|
||||
// 该组合触发 GetInterests 内共享计息器的模式2/9本金修正(见其"根因位置"注释,勿删)。
|
||||
// ⚠️ 契约修复暂缓中(2026-08-16 撤回):按裁决文档§六,autoSwap=false 本应改传 Intraday 形状
|
||||
// (平仓前剩余+真实比例),但该变更影响快照种子(TdInterestPrincipal等),黄金回放验收门未过前不落地;
|
||||
// 落地时见 项目文档/双入口口径裁决-复利mode2部分平仓-20260816.md §六 与已就绪的回归网
|
||||
//(GetInterestsEntrySemanticsTest.复利_mode2_部分平仓_双入口契约口径一致)。
|
||||
// 口径选择常驻记录(快速定位第一入口):出问题先看这行确认当日本次事件的金额输入,再顺着
|
||||
// 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 实际落库的手工结息,金额已按分处理。
|
||||
@@ -1484,11 +1515,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)
|
||||
{
|
||||
@@ -1506,7 +1539,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;
|
||||
@@ -1529,7 +1562,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)
|
||||
{
|
||||
@@ -1554,7 +1587,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;
|
||||
@@ -1740,6 +1773,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)
|
||||
{
|
||||
@@ -1812,11 +1850,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;
|
||||
@@ -1849,6 +1887,11 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
curretEod.PosiDividendSum = 0;
|
||||
}
|
||||
// 分红递推过程常驻记录(快速定位):当日事件路径含实现扣减(前日+新计-当日实现)
|
||||
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;
|
||||
|
||||
@@ -1871,20 +1914,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>
|
||||
@@ -2108,7 +2137,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();//持仓腿
|
||||
// 框架合约的方向约定:多头为正、空头为负;总名义本金取交易原始规模,
|
||||
@@ -2170,7 +2199,7 @@ 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);
|
||||
@@ -2221,7 +2250,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;
|
||||
@@ -2462,7 +2491,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>
|
||||
/// 获取互换交易日终持仓数据集合
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1221,7 +1221,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();
|
||||
@@ -1550,7 +1550,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 万/笔)。
|
||||
Reference in New Issue
Block a user