Merge remote-tracking branch 'origin/glms/feature/1.4.2' into glms/feature/1.4.2

This commit is contained in:
张名锐
2026-08-12 15:10:06 +08:00
15 changed files with 446 additions and 460 deletions
@@ -13,7 +13,7 @@ using YLErp.Core.Interest;
namespace UnitTestProject.Modules.SwapModule.Accrual
{
/// <summary>
/// 影子测试:CalcDailyCompoundInterestByEod(旧逐日循环)vs FundingLegAccrual.AccrueCompoundEod(新纯函数)。
/// 影子测试:CalcDailyCompoundInterestByEod(旧逐日循环)vs CompoundInterestAccrual.AccrueEod(新纯函数)。
/// 构造同一组参数,两套实现并行跑,断言结果一致(到分)。
/// </summary>
[TestClass]
@@ -92,14 +92,14 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
decimal oldInterest = 0, oldTd = 0;
var svc = new StubSvc();
svc.CalcDailyCompoundInterestByEod(preEod, EodDate, TradeDate, position,
Notional, Notional, flowEvent, AnnualDays, false, 0m, 1m, Notional,
Notional, Notional, flowEvent, AnnualDays, false, 0m, 1m,
ref oldInterest, ref oldTd);
// 新方法
var rate = FundingLegRate.Fixed(FixedRate);
var policy = new AccrualPolicy(AccrualBoundary.Both, true, 7, AnnualDays, true);
var remainingPercent = Math.Max(0m, Math.Min(1m, Notional / Notional));
var result = FundingLegAccrual.AccrueCompoundEod(
var result = CompoundInterestAccrual.AccrueEod(
50_000m, Notional, Notional, 1m, rate, policy,
isResetDay: true, remainingPercent, EodDate);
@@ -125,13 +125,13 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
decimal oldInterest = 0, oldTd = 0;
var svc = new StubSvc();
svc.CalcDailyCompoundInterestByEod(preEod, nonResetDate, TradeDate, position,
Notional, Notional, flowEvent, AnnualDays, false, 0m, 1m, Notional,
Notional, Notional, flowEvent, AnnualDays, false, 0m, 1m,
ref oldInterest, ref oldTd);
// 新方法
var rate = FundingLegRate.Fixed(FixedRate);
var policy = new AccrualPolicy(AccrualBoundary.Both, true, 7, AnnualDays, true);
var result = FundingLegAccrual.AccrueCompoundEod(
var result = CompoundInterestAccrual.AccrueEod(
30_000m, Notional, Notional, 1m, rate, policy,
isResetDay: false, 0m, nonResetDate);
@@ -7,7 +7,7 @@ using YLErp.Modules.SwapModule.Accrual;
namespace UnitTestProject.Modules.SwapModule.Accrual
{
/// <summary>
/// 影子测试:CalcDailyCompoundInterest(旧逐日循环)vs FundingLegAccrual.AccrueCompoundPeriod(新分段纯函数)。
/// 影子测试:CalcDailyCompoundInterest(旧逐日循环)vs CompoundInterestAccrual.AccruePeriod(新分段纯函数)。
/// </summary>
[TestClass]
public class CompoundPeriodShadowTest
@@ -74,7 +74,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
decimal oldI = 0, oldTd = 0;
var svc = new StubSvc();
svc.CalcDailyCompoundInterest(EndDate, position, Notional, flowEvent,
AnnualDays, false, 0m, 1m, Notional, true, false,
AnnualDays, false, 0m, 1m, true, false,
ref oldI, ref oldTd);
// 新方法:固定利率全段相同,分段点 = PosiStartDate + k×7
@@ -86,7 +86,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
(StartDate.AddDays(7), allInRate),
(StartDate.AddDays(14), allInRate),
};
var result = FundingLegAccrual.AccrueCompoundPeriod(
var result = CompoundInterestAccrual.AccruePeriod(
notional: Notional,
segmentRates: segRates,
startDate: StartDate,
@@ -122,7 +122,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
decimal oldI = 0, oldTd = 0;
var svc = new StubSvc();
svc.CalcDailyCompoundInterest(EndDate, position, Notional * closePct, flowEvent,
AnnualDays, false, 0m, closePct, Notional, true, false,
AnnualDays, false, 0m, closePct, true, false,
ref oldI, ref oldTd, consumedInterest: consumed, resetCarryInterest: carry);
// 新方法
@@ -133,7 +133,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
(StartDate.AddDays(7), allInRate),
(StartDate.AddDays(14), allInRate),
};
var result = FundingLegAccrual.AccrueCompoundPeriod(
var result = CompoundInterestAccrual.AccruePeriod(
notional: Notional * closePct,
segmentRates: segRates,
startDate: StartDate,
@@ -166,7 +166,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
decimal oldI = 0, oldTd = 0;
var svc = new StubSvc();
svc.CalcDailyCompoundInterest(EndDate, position, Notional, flowEvent,
AnnualDays, false, 0m, 1m, Notional, true, true,
AnnualDays, false, 0m, 1m, true, true,
ref oldI, ref oldTd);
// 新方法
@@ -177,7 +177,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
(StartDate.AddDays(7), allInRate),
(StartDate.AddDays(14), allInRate),
};
var result = FundingLegAccrual.AccrueCompoundPeriod(
var result = CompoundInterestAccrual.AccruePeriod(
notional: Notional,
segmentRates: segRates,
startDate: StartDate,
@@ -12,7 +12,7 @@ using YLErp.Derivatives.Interest;
namespace UnitTestProject.Modules.SwapModule.Accrual
{
/// <summary>
/// 影子测试:CalcDailySimpleInterest(旧逐日循环)vs FundingLegAccrual.AccrueSimplePeriod(新分段纯函数)。
/// 影子测试:CalcDailySimpleInterest(旧逐日循环)vs SimpleInterestAccrual.AccruePeriod(新分段纯函数)。
/// </summary>
[TestClass]
public class SimplePeriodShadowTest
@@ -106,7 +106,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
// 旧代码差分: dynomicPrincipal = preEod.TdInterestPrincipal(=0) + posiPrincipal - orginPv = 0
var segRates = new List<(DateTime, decimal)> { (StartDate, Spread) };
var accrualPrincipal = 0m + Notional - Notional; // 差分 = 0
var result = FundingLegAccrual.AccrueSimplePeriod(
var result = SimpleInterestAccrual.AccruePeriod(
priorAccrued: 0m,
notional: 0m,
unwindFraction: 1m,
@@ -152,7 +152,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
// 差分本金 = preEod.TdInterestPrincipal + posiPrincipal - orginPv
var accrualPrincipal = Notional + Notional - Notional;
var segRates = new List<(DateTime, decimal)> { (StartDate, Spread) };
var result = FundingLegAccrual.AccrueSimplePeriod(
var result = SimpleInterestAccrual.AccruePeriod(
priorAccrued: 200_000m * 0.5m,
notional: accrualPrincipal,
unwindFraction: 0.5m,
@@ -215,7 +215,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
// 差分本金 = preEod.TdInterestPrincipal + posiPrincipal - orginPv
var accrualPrincipal = Notional + Notional - Notional;
var result = FundingLegAccrual.AccrueSimplePeriod(
var result = SimpleInterestAccrual.AccruePeriod(
priorAccrued: 200_000m * closePct,
notional: accrualPrincipal,
unwindFraction: closePct,
@@ -509,7 +509,7 @@ namespace YLErp.Modules.SwapModule
decimal tdInterestAmount = 0m;
service.CalcDailyCompoundInterestByEod(preEod, resetDate, startDate, position,
principal, principal, flowEvent, AnnualDays, false, 0.013502m, 1m, principal,
principal, principal, flowEvent, AnnualDays, false, 0.013502m, 1m,
ref interestAmount, ref tdInterestAmount);
AssertDecimal(principal + pendingInterest, flowEvent.InterestPrincipal,
@@ -1725,14 +1725,14 @@ namespace YLErp.Modules.SwapModule
decimal expectedTdAmountAtEnd = 0m;
dealService.CalcDailyCompoundInterest(
finalCloseDate, position, remainingNotional, expectedEndFlow, AnnualDays, false,
intermediateEod.FloatRate, 1m, originalNotional, true, false,
intermediateEod.FloatRate, 1m, true, false,
ref expectedAmountAtEnd, ref expectedTdAmountAtEnd);
var expectedPreviousFlow = new swap_flow_event { InterestRate = spread };
decimal expectedAmountAtPreviousEod = 0m;
decimal expectedTdAmountAtPreviousEod = 0m;
dealService.CalcDailyCompoundInterest(
intermediateDate, position, remainingNotional, expectedPreviousFlow, AnnualDays, false,
intermediateEod.FloatRate, 1m, originalNotional, true, true,
intermediateEod.FloatRate, 1m, true, true,
ref expectedAmountAtPreviousEod, ref expectedTdAmountAtPreviousEod);
var expectedFinalInterest = intermediateEod.InterestIncomeSum
+ expectedAmountAtEnd - expectedAmountAtPreviousEod;
@@ -8,7 +8,7 @@ namespace YLErp.Modules.SwapModule
/// 【同日多次部分平仓 · unwind 基数滚动表征测试】
/// ============================================================================
/// 背景:unwind 计息基数公式 basis = priorNotional + notional - baseNotional
/// (FundingLegAccrual / CalcDailyCompoundInterestByEod 同源),其中
/// (CompoundInterestAccrual / CalcDailyCompoundInterestByEod 同源),其中
/// - priorNotional = 上一日终归档 eod_swap_position.TdInterestPrincipal
/// - baseNotional = orginPv = ResolveUnwindPreviousNotional(lastEod)(上一日终浮动端名义本金)
/// - notional = 当前持仓名义本金(posiNotionalValue,来自实时持仓)
@@ -0,0 +1,132 @@
using YLErp.Core.Interest;
using YLErp.Derivatives.Interest;
namespace YLErp.Modules.SwapModule.Accrual;
/// <summary>
/// 复利计息纯函数——EOD 单日 + intraday 多日。
/// 复利特征:每个重置日把累计利息并入本金(basis = notional + accrued)。
/// </summary>
public static class CompoundInterestAccrual
{
private const int Precision = SwapInterest.FundingLegPrecision;
/// <summary>复利日终计息基数(单一真相源,纯函数与调用方共用):
/// 重置日 = notional + 累计利息×剩余比例(利息并入本金);非重置日 = priorNotional(昨日滚动基数)。
/// remainingFraction 对齐 legacy 钳制到 [0,1]。</summary>
public static decimal EodBasis(
bool isResetDay, decimal notional, decimal priorAccrued, decimal remainingFraction, decimal priorNotional)
=> isResetDay
? notional + priorAccrued * Math.Max(0m, Math.Min(1m, remainingFraction))
: priorNotional;
/// <summary>
/// 复利日终计息(替换 CalcDailyCompoundInterestByEod 的纯数学部分)。
/// 重置日:basis = notional + priorAccrued × remainingFraction(利息并入本金)。
/// 非重置日:basis = priorNotional(昨日终滚动计息基数)。
/// </summary>
public static InterestResult AccrueEod(
decimal priorAccrued,
decimal priorNotional,
decimal notional,
decimal unwindFraction,
FundingLegRate rate,
AccrualPolicy policy,
bool isResetDay,
decimal remainingFraction,
DateTime eodDate,
AccrualTrace? trace = null)
{
var basis = EodBasis(isResetDay, notional, priorAccrued, remainingFraction, priorNotional);
var displayBasis = basis * unwindFraction;
var allInRate = rate.AllInRate;
trace?.EodContext(eodDate, isResetDay, unwindFraction, priorAccrued, priorNotional, notional, remainingFraction);
var dayInterest = displayBasis * allInRate;
var tdInterest = basis * allInRate;
if (policy.IsAnnualized)
{
dayInterest /= policy.AnnualDays;
tdInterest /= policy.AnnualDays;
}
var totalAccrued = priorAccrued * unwindFraction + dayInterest;
var result = new InterestResult(
SwapInterest.Round(totalAccrued, Precision),
SwapInterest.Round(tdInterest, Precision));
trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued);
trace?.MarkEnd(result.Accrued, result.AccruedToday);
return result;
}
/// <summary>
/// 复利多日计息(替换 CalcDailyCompoundInterest 的纯数学部分)。
/// 从 startDate 到 endDate 全程重放,每个重置日把累计利息并入本金。
/// </summary>
public static InterestResult AccruePeriod(
decimal notional,
IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
DateTime startDate,
DateTime endDate,
AccrualBoundary boundary,
int annualDays,
bool isAnnualized,
decimal resetCarryInterest,
decimal realizedInterest,
decimal unwindFraction,
out decimal finalBasis,
AccrualTrace? trace = null)
{
decimal accrualBasis = notional;
decimal accrued = 0m;
trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized);
for (int si = 0; si < segmentRates.Count; si++)
{
var isLastSegment = si == segmentRates.Count - 1;
var segEnd = isLastSegment
? endDate
: segmentRates[si + 1].StartDate;
// 重置日并本金
accrualBasis = si == 0 ? notional : notional + accrued;
// 末日恰好是重置日且 carry 非零:用存量替代(旧代码 i%interestPeriod==0 && accrueDate==endDate)。
var usedCarry = false;
if (isLastSegment && si > 0 && resetCarryInterest != 0m
&& segmentRates[si].StartDate == endDate)
{
accrualBasis = notional + resetCarryInterest;
usedCarry = true;
}
if (si > 0)
trace?.Rollover(segmentRates[si].StartDate, usedCarry ? resetCarryInterest : accrued, accrualBasis);
var segIncludeStart = (si == 0) ? boundary.IncludeStart : true;
var segIncludeEnd = isLastSegment ? boundary.IncludeEnd : false;
var days = SwapInterest.AccrualDays(segmentRates[si].StartDate, segEnd,
AccrualBoundary.Of(segIncludeStart, segIncludeEnd));
if (days <= 0) continue;
var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate;
var segInterest = accrualBasis * dailyRate * days;
accrued += segInterest;
trace?.Segment(si, segmentRates[si].StartDate, segEnd, days, segmentRates[si].Rate, accrualBasis, segInterest, accrued);
}
finalBasis = accrualBasis;
if (realizedInterest != 0m)
trace?.Unwind(endDate, unwindFraction, realizedInterest * unwindFraction, accrued - realizedInterest * unwindFraction);
accrued -= realizedInterest * unwindFraction;
var result = new InterestResult(
SwapInterest.Round(accrued, Precision),
SwapInterest.Round(accrued, Precision));
trace?.MarkEnd(result.Accrued, result.AccruedToday);
return result;
}
}
@@ -1,239 +0,0 @@
using YLErp.Core.Interest;
using YLErp.Derivatives.Interest;
namespace YLErp.Modules.SwapModule.Accrual;
/// <summary>
/// 融资腿计息编排层——纯数学部分(替换 SwapDealService 内 CalcDaily* 家族的纯计算)。
///
/// 命名规范(对齐 QuantLib / Strata):
/// - notional → 计息名义本金(不用 principalswap leg 用 notional 是业界标准)
/// - accrued → 累计应计利息
/// - unwindFraction → 平仓比例(0~1
/// - realizedInterest → 历史已结利息(legacy: consumedInterest
/// - priorNotional → 昨日终滚动计息基数(legacy: TdInterestPrincipal / dynomicPrincipal
/// </summary>
public static class FundingLegAccrual
{
private const int Precision = SwapInterest.FundingLegPrecision;
/// <summary>复利日终计息基数(单一真相源,纯函数与调用方共用):
/// 重置日 = notional + 累计利息×剩余比例(利息并入本金);非重置日 = priorNotional(昨日滚动基数)。
/// remainingFraction 对齐 legacy 钳制到 [0,1]。</summary>
public static decimal CompoundEodBasis(
bool isResetDay, decimal notional, decimal priorAccrued, decimal remainingFraction, decimal priorNotional)
=> isResetDay
? notional + priorAccrued * Math.Max(0m, Math.Min(1m, remainingFraction))
: priorNotional;
/// <summary>
/// 单利日终计息(替换 CalcDailySimpleInterestByEod 的纯数学部分)。
/// EOD 无差分:basis = priorNotional(昨日终滚动计息基数)。
/// </summary>
public static InterestResult AccrueSimpleEod(
decimal priorAccrued,
decimal priorNotional,
decimal unwindFraction,
FundingLegRate rate,
AccrualPolicy policy,
DateTime eodDate,
AccrualTrace? trace = null)
{
var basis = priorNotional;
var displayBasis = basis * unwindFraction;
var allInRate = rate.AllInRate;
var dayInterest = displayBasis * allInRate;
var tdInterest = basis * allInRate;
if (policy.IsAnnualized)
{
dayInterest /= policy.AnnualDays;
tdInterest /= policy.AnnualDays;
}
var totalAccrued = priorAccrued + dayInterest;
var result = new InterestResult(
SwapInterest.Round(totalAccrued, Precision),
SwapInterest.Round(tdInterest, Precision));
trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued);
trace?.MarkEnd(result.Accrued, result.AccruedToday);
return result;
}
/// <summary>
/// 复利日终计息(替换 CalcDailyCompoundInterestByEod 的纯数学部分)。
/// 重置日:basis = notional + priorAccrued × remainingFraction(利息并入本金)。
/// 非重置日:basis = priorNotional(昨日终滚动计息基数)。
/// </summary>
public static InterestResult AccrueCompoundEod(
decimal priorAccrued,
decimal priorNotional,
decimal notional,
decimal unwindFraction,
FundingLegRate rate,
AccrualPolicy policy,
bool isResetDay,
decimal remainingFraction,
DateTime eodDate,
AccrualTrace? trace = null)
{
var basis = CompoundEodBasis(isResetDay, notional, priorAccrued, remainingFraction, priorNotional);
var displayBasis = basis * unwindFraction;
var allInRate = rate.AllInRate;
trace?.EodContext(eodDate, isResetDay, unwindFraction, priorAccrued, priorNotional, notional, remainingFraction);
var dayInterest = displayBasis * allInRate;
var tdInterest = basis * allInRate;
if (policy.IsAnnualized)
{
dayInterest /= policy.AnnualDays;
tdInterest /= policy.AnnualDays;
}
var totalAccrued = priorAccrued * unwindFraction + dayInterest;
var result = new InterestResult(
SwapInterest.Round(totalAccrued, Precision),
SwapInterest.Round(tdInterest, Precision));
trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued);
trace?.MarkEnd(result.Accrued, result.AccruedToday);
return result;
}
/// <summary>
/// 单利多日计息(替换 CalcDailySimpleInterest 的纯数学部分)。
/// 本金全程恒定,按重置日分段取利率。
/// Accrued = 缩放累计(InterestAmount)AccruedToday = 未缩放累计(TdInterestAmount)。
/// </summary>
public static InterestResult AccrueSimplePeriod(
decimal priorAccrued,
decimal notional,
decimal unwindFraction,
IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
DateTime startDate,
DateTime endDate,
DateTime priorValueDate,
AccrualBoundary boundary,
int annualDays,
bool isAnnualized,
AccrualTrace? trace = null)
{
var displayBasis = notional * unwindFraction;
decimal accrued = priorAccrued; // 缩放累计 → InterestAmount
decimal accruedUnscaled = priorAccrued; // 未缩放累计 → TdInterestAmount
trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized);
var segStart = startDate;
for (int si = 0; si < segmentRates.Count; si++)
{
var segEnd = si < segmentRates.Count - 1
? segmentRates[si + 1].StartDate
: endDate;
var effectiveStart = segStart > priorValueDate ? segStart : priorValueDate.AddDays(1);
if (effectiveStart > segEnd) { segStart = segEnd; continue; }
// calcFirst 只跳过 startDate 本身;其余天(含重置日、ValueDate+1)只要 > ValueDate 恒纳入。
// 与旧逐日循环一致:if (!calcFirst && accrueDate == startDate) continue 是唯一的首日跳过。
// 中间段的 segIncludeStart 被 days<=0 跳过后误置 false,此处按 startDate 判定而非继承标记。
var includeStart = effectiveStart == startDate ? boundary.IncludeStart : true;
// calcLast 只影响 endDate 本身——只有真正的末段(si==Count-1)才算尾,
// 不能用 segEnd==endDate 判断(interestPeriod=1 时中间段 segEnd 也可能==endDate)。
var isLastSegment = si == segmentRates.Count - 1;
var segBoundary = AccrualBoundary.Of(includeStart, isLastSegment && boundary.IncludeEnd);
var days = SwapInterest.AccrualDays(effectiveStart, segEnd, segBoundary);
if (days <= 0) { segStart = segEnd; continue; }
var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate;
var segInterest = displayBasis * dailyRate * days;
accrued += segInterest;
accruedUnscaled += notional * dailyRate * days;
trace?.Segment(si, effectiveStart, segEnd, days, segmentRates[si].Rate, displayBasis, segInterest, accrued);
segStart = segEnd;
}
var result = new InterestResult(
SwapInterest.Round(accrued, Precision),
SwapInterest.Round(accruedUnscaled, Precision));
trace?.MarkEnd(result.Accrued, result.AccruedToday);
return result;
}
/// <summary>
/// 复利多日计息(替换 CalcDailyCompoundInterest 的纯数学部分)。
/// 从 startDate 到 endDate 全程重放,每个重置日把累计利息并入本金。
/// </summary>
public static InterestResult AccrueCompoundPeriod(
decimal notional,
IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
DateTime startDate,
DateTime endDate,
AccrualBoundary boundary,
int annualDays,
bool isAnnualized,
decimal resetCarryInterest,
decimal realizedInterest,
decimal unwindFraction,
out decimal finalBasis,
AccrualTrace? trace = null)
{
decimal accrualBasis = notional;
decimal accrued = 0m;
trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized);
for (int si = 0; si < segmentRates.Count; si++)
{
var isLastSegment = si == segmentRates.Count - 1;
var segEnd = isLastSegment
? endDate
: segmentRates[si + 1].StartDate;
// 重置日并本金
accrualBasis = si == 0 ? notional : notional + accrued;
// 末日恰好是重置日且 carry 非零:用存量替代(旧代码 i%interestPeriod==0 && accrueDate==endDate)。
// 注意:必须同时判断 startDate==endDate——endDate 非重置日时最后一段起点 < endDate,不应触发。
var usedCarry = false;
if (isLastSegment && si > 0 && resetCarryInterest != 0m
&& segmentRates[si].StartDate == endDate)
{
accrualBasis = notional + resetCarryInterest;
usedCarry = true;
}
// 复利每段起点:记录并本金瞬间(非首段 = 利息滚入计息基数)
if (si > 0)
trace?.Rollover(segmentRates[si].StartDate, usedCarry ? resetCarryInterest : accrued, accrualBasis);
// 半开区间:重置日归下一段(旧代码逐日循环中重置日先更新本金再算息)
var segIncludeStart = (si == 0) ? boundary.IncludeStart : true;
var segIncludeEnd = isLastSegment ? boundary.IncludeEnd : false;
var days = SwapInterest.AccrualDays(segmentRates[si].StartDate, segEnd,
AccrualBoundary.Of(segIncludeStart, segIncludeEnd));
if (days <= 0) continue;
var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate;
var segInterest = accrualBasis * dailyRate * days;
accrued += segInterest;
trace?.Segment(si, segmentRates[si].StartDate, segEnd, days, segmentRates[si].Rate, accrualBasis, segInterest, accrued);
}
finalBasis = accrualBasis;
// 扣除历史已结利息
if (realizedInterest != 0m)
trace?.Unwind(endDate, unwindFraction, realizedInterest * unwindFraction, accrued - realizedInterest * unwindFraction);
accrued -= realizedInterest * unwindFraction;
var result = new InterestResult(
SwapInterest.Round(accrued, Precision),
SwapInterest.Round(accrued, Precision));
trace?.MarkEnd(result.Accrued, result.AccruedToday);
return result;
}
}
@@ -0,0 +1,106 @@
using YLErp.Core.Interest;
using YLErp.Derivatives.Interest;
namespace YLErp.Modules.SwapModule.Accrual;
/// <summary>
/// 单利计息纯函数——EOD 单日 + intraday 多日。
/// 单利特征:本金全程恒定(无并本金),按重置日分段取利率。
/// </summary>
public static class SimpleInterestAccrual
{
private const int Precision = SwapInterest.FundingLegPrecision;
/// <summary>
/// 单利日终计息(替换 CalcDailySimpleInterestByEod 的纯数学部分)。
/// EOD 无差分:basis = priorNotional(昨日终滚动计息基数)。
/// </summary>
public static InterestResult AccrueEod(
decimal priorAccrued,
decimal priorNotional,
decimal unwindFraction,
FundingLegRate rate,
AccrualPolicy policy,
DateTime eodDate,
AccrualTrace? trace = null)
{
var basis = priorNotional;
var displayBasis = basis * unwindFraction;
var allInRate = rate.AllInRate;
var dayInterest = displayBasis * allInRate;
var tdInterest = basis * allInRate;
if (policy.IsAnnualized)
{
dayInterest /= policy.AnnualDays;
tdInterest /= policy.AnnualDays;
}
var totalAccrued = priorAccrued + dayInterest;
var result = new InterestResult(
SwapInterest.Round(totalAccrued, Precision),
SwapInterest.Round(tdInterest, Precision));
trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued);
trace?.MarkEnd(result.Accrued, result.AccruedToday);
return result;
}
/// <summary>
/// 单利多日计息(替换 CalcDailySimpleInterest 的纯数学部分)。
/// 本金全程恒定,按重置日分段取利率。
/// Accrued = 缩放累计(InterestAmount)AccruedToday = 未缩放累计(TdInterestAmount)。
/// </summary>
public static InterestResult AccruePeriod(
decimal priorAccrued,
decimal notional,
decimal unwindFraction,
IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
DateTime startDate,
DateTime endDate,
DateTime priorValueDate,
AccrualBoundary boundary,
int annualDays,
bool isAnnualized,
AccrualTrace? trace = null)
{
var displayBasis = notional * unwindFraction;
decimal accrued = priorAccrued; // 缩放累计 → InterestAmount
decimal accruedUnscaled = priorAccrued; // 未缩放累计 → TdInterestAmount
trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized);
var segStart = startDate;
for (int si = 0; si < segmentRates.Count; si++)
{
var segEnd = si < segmentRates.Count - 1
? segmentRates[si + 1].StartDate
: endDate;
var effectiveStart = segStart > priorValueDate ? segStart : priorValueDate.AddDays(1);
if (effectiveStart > segEnd) { segStart = segEnd; continue; }
// calcFirst 只跳过 startDate 本身;其余天(含重置日、ValueDate+1)只要 > ValueDate 恒纳入。
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);
if (days <= 0) { segStart = segEnd; continue; }
var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate;
var segInterest = displayBasis * dailyRate * days;
accrued += segInterest;
accruedUnscaled += notional * dailyRate * days;
trace?.Segment(si, effectiveStart, segEnd, days, segmentRates[si].Rate, displayBasis, segInterest, accrued);
segStart = segEnd;
}
var result = new InterestResult(
SwapInterest.Round(accrued, Precision),
SwapInterest.Round(accruedUnscaled, Precision));
trace?.MarkEnd(result.Accrued, result.AccruedToday);
return result;
}
}
@@ -41,4 +41,17 @@ public static class MarginModes
/// <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>
{
(int)InterestModeEnum.,
(int)InterestModeEnum.,
(int)InterestModeEnum.,
};
/// <summary>判断 mode 是否为固定值或保证金。</summary>
public static bool IsFixedAmountOrMargin(int interestMode) => FixedAmountAndMargin.Contains(interestMode);
}
@@ -1,5 +1,6 @@
using YLErp.DBModels;
using YLErp.DBModels.Enums;
using YLErp.Modules.SwapModule.Margin;
namespace YLErp.Modules.SwapModule.ReturnLegs;
@@ -19,4 +20,17 @@ public static class DirectionRatio
/// <summary>收付方向因子。收取=+1, 支付=-1。</summary>
public static int ReceivePay(int direction)
=> direction == (int)SwapDirectionEnum. ? 1 : -1;
/// <summary>利息腿 PnL 方向因子。收取=+1/支付=-1;保证金腿翻转(利息现金流与本金方向相反)。
/// 原 7 处内联 `收取?1:-1; if(MarginModes) ratio=-ratio` 收口到此。</summary>
public static int InterestLegPnl(int interestDirection, int interestMode)
{
var ratio = ReceivePay(interestDirection);
return MarginModes.Contains(interestMode) ? -ratio : ratio;
}
/// <summary>按收付方向选汇率类型。收取→Buy, 支付→Sell。
/// 原 7 处内联 `收取 ? Buy : Sell` 收口到此。</summary>
public static CurrencyRateType RateType(int direction)
=> direction == (int)SwapDirectionEnum. ? CurrencyRateType.Buy : CurrencyRateType.Sell;
}
@@ -0,0 +1,21 @@
namespace YLErp.Modules.SwapModule.ReturnLegs;
/// <summary>
/// EOD 利息腿展示值计算——轻量单日公式(非 SwapDealService 的分段计息引擎)。
/// </summary>
public static class InterestIncomeCalc
{
/// <summary>日应计利息 = 本金 × (固定利率 + 浮动利差) ÷ 年化天数(若年化)。
/// 原 3 处内联 `principal*(rate+float); if(annualized) /=annualDays` 收口到此。</summary>
public static decimal DailyAccrual(decimal principal, decimal rate, decimal floatRate, bool isAnnualized, int annualDays)
{
var amount = principal * (rate + floatRate);
return isAnnualized ? amount / annualDays : amount;
}
/// <summary>已实现利息滚存。(prev + today×ratio, prevFee + todayFee)。
/// 原 4 处内联 2 行赋值收口到此。</summary>
public static (decimal Interest, decimal Fee) RollRealized(
decimal prevInterest, decimal prevFee, decimal closeInterest, decimal closeInterestFee, int ratio)
=> (prevInterest + closeInterest * ratio, prevFee + closeInterestFee);
}
@@ -19,4 +19,12 @@ public static class MtmCalc
/// <param name="ratio">收取=1, 支付=-1。</param>
public static decimal UnrealizedPnl(decimal price, decimal costGrossPrice, decimal qty, decimal contractSize, int shortRatio, int ratio)
=> (price - costGrossPrice) * qty * contractSize * shortRatio * ratio;
/// <summary>浮动端总未实现盈亏 = 盯市盈亏 + 分红 + 待结费用。原 4 处内联收口到此。</summary>
public static decimal ReturnLegProfitSum(decimal mtmPnl, decimal dividendSum, decimal feePending)
=> mtmPnl + dividendSum + feePending;
/// <summary>加权均价混合:(昨日均价×昨日量 + 今日∑(量×均额)) / 总量。原 4 处内联收口到此。</summary>
public static decimal BlendPrice(decimal prevPrice, decimal prevQty, decimal sumQtyTimesPrice, decimal totalQty)
=> (prevPrice * prevQty + sumQtyTimesPrice) / totalQty;
}
+47 -76
View File
@@ -1096,12 +1096,12 @@ namespace YLErp.Modules.SwapModule
if (position.InterestType == (int)InterestTypeEnum.)
{
// 复利计算
CalcDailyCompoundInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, false, eodFloatRate, 1m, posiPrincipal, ref interestAmount, ref tdInterestAmount);
CalcDailyCompoundInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, false, eodFloatRate, 1m, ref interestAmount, ref tdInterestAmount);
}
else
{
// 单利计算
CalcDailySimpleInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, false, eodFloatRate, 1m, posiPrincipal, ref interestAmount, ref tdInterestAmount);
CalcDailySimpleInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, false, eodFloatRate, 1m, ref interestAmount, ref tdInterestAmount);
}
}
@@ -1272,7 +1272,7 @@ namespace YLErp.Modules.SwapModule
// 它只在当前 endDate 恰好为重置日时使用,避免把同一笔历史利息重复资本化。
var resetCarryInterest = preEodPosition.InterestIncomeSum * remainingPercent;
CalcDailyCompoundInterest(endDate, position, closePosiNotionalValue, interest, annualDays, needPrice,
floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount,
floateRate, closePrecent, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount,
consumedInterest, resetCarryInterest);
if (preEodPosition.id != 0 && closePrecent == 1m)
{
@@ -1299,7 +1299,7 @@ namespace YLErp.Modules.SwapModule
}
// 计算截至本次平仓日的累计利息 amountAtEnd
CalcDailyCompoundInterest(replayEndDate, position, closePosiNotionalValue,
interestAtEnd, annualDays, needPrice, floateRate, closePrecent, orginPv,
interestAtEnd, annualDays, needPrice, floateRate, closePrecent,
calcFirst, calcLast, ref amountAtEnd, ref tdAmountAtEnd, consumedInterest);
var interestAtPreviousEod = new swap_flow_event { InterestRate = rate };
decimal amountAtPreviousEod = 0m;
@@ -1308,7 +1308,7 @@ namespace YLErp.Modules.SwapModule
// 因此此处按闭区间包含上一日终当天,避免算头不算尾时重复加入该日利息。
// 计算截至上一日终累积的利息 amountAtPreviousEod
CalcDailyCompoundInterest(preEodPosition.ValueDate, position, closePosiNotionalValue,
interestAtPreviousEod, annualDays, needPrice, floateRate, closePrecent, orginPv,
interestAtPreviousEod, annualDays, needPrice, floateRate, closePrecent,
calcFirst, true, ref amountAtPreviousEod, ref tdAmountAtPreviousEod, consumedInterest);
// 例如 00045/18 待实现 -118631.261797,加 5/19 新增约 -4648.912760
// 得到最终应结 -123280.174557,按金额两位落为 Excel BN 的 -123280.17。
@@ -1334,6 +1334,29 @@ namespace YLErp.Modules.SwapModule
return interest;
}
/// <summary>
/// 按 interest_rule 取 FR007 定盘价。无浮动标的时返回 fallback;取不到抛异常。
/// EOD 单日取率 + BuildSegmentRates 多日取率共用此方法,FR007 定盘逻辑收口到一处。
/// </summary>
private decimal ResolveFloatRate(swap_position position, DateTime date, decimal fallback)
{
if (string.IsNullOrEmpty(position.FloatRateUnderlyingCode)) return fallback;
var fixingDate = IndexFixerBase.GetFixingDate(date, position.interest_rule);
if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing))
return fixing != 0m ? fixing : fallback;
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
}
/// <summary>构造利率值对象:固定腿→Fixed(spread),浮动腿→Floating(spread+fixing)。</summary>
private static FundingLegRate BuildLegRate(swap_position position, decimal spread, decimal effectiveFloat)
=> string.IsNullOrEmpty(position.FloatRateUnderlyingCode)
? FundingLegRate.Fixed(spread)
: FundingLegRate.Floating(spread, effectiveFloat);
/// <summary>构造 EOD 计息政策(算头算尾,重置周期取 position.interest_rest_days)。</summary>
private static AccrualPolicy BuildEodPolicy(swap_position position, int annualDays, bool isCompound)
=> new AccrualPolicy(AccrualBoundary.Both, isCompound, position.interest_rest_days ?? 1, annualDays, position.IsAnnualized);
/// <summary>
/// 按重置周期切分利率段,每段记录 all-in 利率(spread+fixing)。返回 (分段列表, 末段浮动利率)。
/// fetchAfterDate: 仅该日期之后的重置日才取 FR007(单利传 ValueDate,复利传 null 全程取)。
@@ -1349,19 +1372,8 @@ namespace YLErp.Modules.SwapModule
for (int i = 0; i <= calcDays; i += interestPeriod)
{
var resetDate = startDate.AddDays(i);
if ((fetchAfterDate == null || resetDate > fetchAfterDate.Value)
&& !string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
{
var fixingDate = IndexFixerBase.GetFixingDate(resetDate, position.interest_rule);
if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing))
{
if (fixing != 0m) currentFloat = fixing;
}
else
{
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
}
}
if (fetchAfterDate == null || resetDate > fetchAfterDate.Value)
currentFloat = ResolveFloatRate(position, resetDate, currentFloat);
rates.Add((resetDate, spread + currentFloat));
}
return (rates, currentFloat);
@@ -1379,7 +1391,7 @@ namespace YLErp.Modules.SwapModule
/// <param name="annualDays">年化天数</param>
/// <returns></returns>
public void CalcDailyCompoundInterest(DateTime endDate, swap_position position, decimal principal, swap_flow_event flowEvent,
int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast,
int annualDays, bool needPrice, decimal floateRate, decimal closePercent, bool calcFirst, bool calcLast,
ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m, decimal resetCarryInterest = 0m)
{
var startDate = position.PosiStartDate;
@@ -1392,7 +1404,7 @@ namespace YLErp.Modules.SwapModule
// 纯函数复利计息:分段重置日并本金 + resetCarryInterest + 扣 consumedInterest
var interestTrace = new AccrualTrace();
var result = FundingLegAccrual.AccrueCompoundPeriod(
var result = CompoundInterestAccrual.AccruePeriod(
notional: principal,
segmentRates: segmentRates,
startDate: startDate,
@@ -1434,7 +1446,7 @@ namespace YLErp.Modules.SwapModule
// 纯函数计息:Accrued=缩放累计(InterestAmount)AccruedToday=未缩放累计(TdInterestAmount)
var interestTrace = new AccrualTrace();
var result = FundingLegAccrual.AccrueSimplePeriod(
var result = SimpleInterestAccrual.AccruePeriod(
priorAccrued: preEodPosition.InterestProfitSum * closePercent,
notional: accrualBasis,
unwindFraction: closePercent,
@@ -1468,26 +1480,14 @@ namespace YLErp.Modules.SwapModule
/// <param name="isAnnualized">是否年化</param>
/// <param name="annualDays">年化天数</param>
/// <returns></returns>
public void CalcDailyCompoundInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, ref decimal InterestAmount, ref decimal TdInterestAmount)
public void CalcDailyCompoundInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, ref decimal InterestAmount, ref decimal TdInterestAmount)
{
int interestPeriod = position.interest_rest_days ?? 1;
var isResetDay = (endDate - tradeDate).Days % interestPeriod == 0;
// 重置日按 interest_rule 重新定盘浮动利率(GLMS-JIATT-20260805 根因——
// 重置日=平仓日必须用新利率,否则沿用旧周期利率并污染后续 EOD)。非重置日沿用 floateRate。
decimal effectiveFloat = floateRate;
if (isResetDay && !string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
{
var fixingDate = IndexFixerBase.GetFixingDate(endDate, position.interest_rule);
if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing))
{
if (fixing != 0m) effectiveFloat = fixing;
}
else
{
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
}
}
var effectiveFloat = isResetDay ? ResolveFloatRate(position, endDate, floateRate) : floateRate;
flowEvent.FloatRate = effectiveFloat;
// remainingFraction:重置日把上一日终待实现利息按本次平仓基数分摊(EOD 全量为 1)。
@@ -1495,21 +1495,13 @@ namespace YLErp.Modules.SwapModule
? Math.Max(0m, Math.Min(1m, principal / posiPrincipal))
: 1m;
// 纯数学下沉至 FundingLegAccrual.AccrueCompoundEod(DDD 命名 + 末位生产精度 12 舍入)。
var isFixedLeg = string.IsNullOrEmpty(position.FloatRateUnderlyingCode);
var legRate = isFixedLeg
? FundingLegRate.Fixed(flowEvent.InterestRate)
: FundingLegRate.Floating(flowEvent.InterestRate, effectiveFloat);
var accrualPolicy = new AccrualPolicy(
convention: AccrualBoundary.Both,
isCompound: true,
resetPeriodDays: interestPeriod,
annualDays: annualDays,
isAnnualized: position.IsAnnualized);
// 纯数学下沉至 CompoundInterestAccrual.AccrueEodDDD 命名 + 末位生产精度 12 舍入)。
var legRate = BuildLegRate(position, flowEvent.InterestRate, effectiveFloat);
var accrualPolicy = BuildEodPolicy(position, annualDays, isCompound: true);
// 完整计息 trace:前后日期/基数/利率/重置标志全过程,经 SwapCalcTrace 常驻落盘(关键路径日志)。
var interestTrace = new AccrualTrace();
var result = FundingLegAccrual.AccrueCompoundEod(
var result = CompoundInterestAccrual.AccrueEod(
priorAccrued: preEodPosition.InterestProfitSum,
priorNotional: preEodPosition.TdInterestPrincipal,
notional: posiPrincipal,
@@ -1524,7 +1516,7 @@ namespace YLErp.Modules.SwapModule
// flowEvent.InterestPrincipal:当日计息基数(已按平仓比例缩放)——下游 EOD 用它播种次日 TdInterestPrincipal。
// 复用 CompoundEodBasis 单一真相源(与 AccrueCompoundEod 内部同一公式)。
flowEvent.InterestPrincipal = FundingLegAccrual.CompoundEodBasis(
flowEvent.InterestPrincipal = CompoundInterestAccrual.EodBasis(
isResetDay, posiPrincipal, preEodPosition.InterestProfitSum, remainingFraction,
preEodPosition.TdInterestPrincipal) * closePercent;
@@ -1535,7 +1527,7 @@ namespace YLErp.Modules.SwapModule
/// <summary>
/// 计算单利 收盘(按重置天数分段,每段使用对应浮动利率)
/// </summary>
public void CalcDailySimpleInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, ref decimal InterestAmount, ref decimal TdInterestAmount)
public void CalcDailySimpleInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, ref decimal InterestAmount, ref decimal TdInterestAmount)
{
// 首次操作(preEod.id == 0):计息基数按存量本金初始化——保留旧行为(含对 preEod 的就地修正)。
if (preEodPosition.id == 0)
@@ -1545,39 +1537,18 @@ namespace YLErp.Modules.SwapModule
// 取率:重置日按 interest_rule 重新定盘浮动利率(GLMS-JIATT-20260805 根因——
// 重置日=平仓日必须用新利率,否则沿用旧周期利率并污染后续 EOD)。
decimal effectiveFloat = floateRate;
int interestPeriod = position.interest_rest_days ?? 1;
if ((endDate - tradeDate).Days % interestPeriod == 0
&& !string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
{
var fixingDate = IndexFixerBase.GetFixingDate(endDate, position.interest_rule);
if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing))
{
if (fixing != 0m) effectiveFloat = fixing;
}
else
{
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
}
}
var isResetDay = (endDate - tradeDate).Days % interestPeriod == 0;
var effectiveFloat = isResetDay ? ResolveFloatRate(position, endDate, floateRate) : floateRate;
flowEvent.FloatRate = effectiveFloat;
// 纯数学下沉至 FundingLegAccrualDDD 命名 + 末位生产精度 12 舍入),行为与上版逐字对齐
// 利率构成按腿型封装:固定腿 → FixedRate;浮动腿 → Spread + IndexFixing(沿用旧实现 InterestRate+浮动利率 的口径)。
var isFixedLeg = string.IsNullOrEmpty(position.FloatRateUnderlyingCode);
var legRate = isFixedLeg
? FundingLegRate.Fixed(flowEvent.InterestRate)
: FundingLegRate.Floating(flowEvent.InterestRate, effectiveFloat);
var accrualPolicy = new AccrualPolicy(
convention: AccrualBoundary.Both,
isCompound: false,
resetPeriodDays: position.interest_rest_days ?? 1,
annualDays: annualDays,
isAnnualized: position.IsAnnualized);
// 纯数学下沉至 SimpleInterestAccrual末位生产精度 12 舍入)。
var legRate = BuildLegRate(position, flowEvent.InterestRate, effectiveFloat);
var accrualPolicy = BuildEodPolicy(position, annualDays, isCompound: false);
// 完整计息 trace:收集器由适配器创建,随后经 SwapCalcTrace 常驻落盘(关键路径日志,无条件)。
var interestTrace = new AccrualTrace();
var result = FundingLegAccrual.AccrueSimpleEod(
var result = SimpleInterestAccrual.AccrueEod(
priorAccrued: preEodPosition.InterestProfitSum,
priorNotional: preEodPosition.TdInterestPrincipal,
unwindFraction: closePercent,
@@ -662,7 +662,7 @@ namespace YLErp.Modules.SwapModule
decimal premiumTotal = 0;
premiumInterests.ForEach(x =>
{
var ratio = x.InterestDirection == (int)SwapDirectionEnum. ? -1 : 1;
var ratio = -DirectionRatio.ReceivePay(x.InterestDirection);
premiumTotal += x.InterestClosePnL * ratio;
});
unwindData.SwapMarginRebatePnl = premiumTotal;
@@ -670,7 +670,7 @@ namespace YLErp.Modules.SwapModule
decimal interestTotal = 0;
interestLegs.ForEach(x =>
{
var ratio = x.InterestDirection == (int)SwapDirectionEnum. ? 1 : -1;
var ratio = DirectionRatio.ReceivePay(x.InterestDirection);
interestTotal += x.InterestClosePnL * ratio;
});
unwindData.SwapCloseAmount = interestTotal ;
@@ -1041,11 +1041,7 @@ namespace YLErp.Modules.SwapModule
eodPayPosition.PosiMatuirityDate = td.ExerciseDate.Value;
}
var tradeExtend = td.trade_extend.ExtendObj;
decimal ratio = position.InterestDirection == (int)SwapDirectionEnum. ? 1m : -1m;//收取为正,支付为负
if (MarginModes.Contains(position.InterestMode))
{
ratio = -ratio;
}
var ratio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
if (newEodPayPosition == null)
{
newEodPayPosition = new eod_swap_position();
@@ -1085,11 +1081,9 @@ namespace YLErp.Modules.SwapModule
newEodPayPosition.TdCloseInterest = flowEvents.Sum(x => x.InterestAmount);
newEodPayPosition.TdCloseInterestFee = newEodPayPosition.TdInterestFee;
//持仓内容-利息腿-损益统计(本方视角)
var intersetAcmount = newEodPayPosition.TdInterestPrincipal * (newEodPayPosition.TdInterestRate + newEodPayPosition.FloatRate);
if (position.IsAnnualized)
{
intersetAcmount /= tradeExtend.AnnualDays;
}
var intersetAcmount = InterestIncomeCalc.DailyAccrual(
newEodPayPosition.TdInterestPrincipal, newEodPayPosition.TdInterestRate, newEodPayPosition.FloatRate,
newEodPayPosition.IsAnnualized, tradeExtend.AnnualDays);
newEodPayPosition.TdInterestIncome = intersetAcmount;// 要算一下当天产生的利息
var interestIncomeBeforeSettlement = eodPayPosition.InterestIncomeSum + newEodPayPosition.TdInterestIncome;
var interestFeeBeforeSettlement = eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee;
@@ -1115,11 +1109,12 @@ namespace YLErp.Modules.SwapModule
newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio);
//累计已实现
newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio;
newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee;
var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio);
newEodPayPosition.RealizedInterest = rolled.Interest;
newEodPayPosition.RealizedInterestFee = rolled.Fee;
SetFixedLegRealizedPnl(newEodPayPosition);
var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true,
position.InterestDirection == (int)SwapDirectionEnum. ? CurrencyRateType.Buy : CurrencyRateType.Sell);
DirectionRatio.RateType(position.InterestDirection));
newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate);
PersistEodSwapPosition(newEodPayPosition);
}
@@ -1176,11 +1171,7 @@ namespace YLErp.Modules.SwapModule
var tradeExtend = td.trade_extend.ExtendObj;
decimal posiNotionalValue = posiLongNotional + posiShortNational;
decimal closePercent = 1;
decimal ratio = position.InterestDirection == (int)SwapDirectionEnum. ? 1m : -1m;//收取为正,支付为负
if (MarginModes.Contains(position.InterestMode))
{
ratio = -ratio;
}
var ratio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
if (eodPayPosition == null)
{
eodPayPosition = new eod_swap_position();
@@ -1198,7 +1189,7 @@ namespace YLErp.Modules.SwapModule
positions.Add(position);
List<eod_swap_position> preEodPositions = new List<eod_swap_position>();
preEodPositions.Add(eodPayPosition);
var interestModes = new List<int>() { (int)InterestModeEnum., (int)InterestModeEnum., (int)InterestModeEnum. };
var interestModes = MarginModes.FixedAmountAndMargin;
if (interestModes.Contains(position.InterestMode))
{
orginPv = eodPayPosition.InterestPrincipalFix;
@@ -1258,11 +1249,12 @@ namespace YLErp.Modules.SwapModule
newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio);
//累计已实现
newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio;
newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee;
var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio);
newEodPayPosition.RealizedInterest = rolled.Interest;
newEodPayPosition.RealizedInterestFee = rolled.Fee;
SetFixedLegRealizedPnl(newEodPayPosition);
var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true,
position.InterestDirection == (int)SwapDirectionEnum. ? CurrencyRateType.Buy : CurrencyRateType.Sell);
DirectionRatio.RateType(position.InterestDirection));
newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate);
PersistEodSwapPosition(newEodPayPosition);
Log.Info($"the last newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}");
@@ -1301,11 +1293,7 @@ namespace YLErp.Modules.SwapModule
decimal oriPosiNotionalValue = posiLongNotional + posiShortNational + closeNational;
decimal posiNotionalValue = posiLongNotional + posiShortNational;
// ratio 只负责把腿内原始金额转换为本方盈亏方向,不参与计息金额本身的计算。
decimal ratio = position.InterestDirection == (int)SwapDirectionEnum. ? 1m : -1m;//收取为正,支付为负
if (MarginModes.Contains(position.InterestMode))
{
ratio = -ratio;
}
var ratio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
// 首次日终结算可能包含当日收盘,因此尚无先前的日终利息持仓。
// 部分平仓仍要续接上一日日终:CalcUnwindInterest 会将 InterestProfitSum
// 加入本次待实现,已实现字段也必须按日累计,不能从新建的临时对象重新开始。
@@ -1339,7 +1327,7 @@ namespace YLErp.Modules.SwapModule
newEodPayPosition = eodPayPosition.Clone();
newEodPayPosition.id = 0;
}
var interestModes = new List<int>() { (int)InterestModeEnum., (int)InterestModeEnum., (int)InterestModeEnum. };
var interestModes = MarginModes.FixedAmountAndMargin;
if (interestModes.Contains(position.InterestMode))
{
orginPv = eodPayPosition.InterestPrincipalFix;
@@ -1381,7 +1369,7 @@ namespace YLErp.Modules.SwapModule
var autoInterest = interests[0];
autoInterest.InterestAmount = autoSettledInterestAmount;
autoInterest.InterestClosePnL = autoSettledInterestAmount
* (autoInterest.InterestDirection == (int)SwapDirectionEnum. ? 1m : -1m);
* DirectionRatio.ReceivePay(autoInterest.InterestDirection);
}
newEodPayPosition.ValueDate = valueDate;
newEodPayPosition.PositionId = position.id;
@@ -1427,11 +1415,9 @@ namespace YLErp.Modules.SwapModule
newEodPayPosition.TdCloseInterest = manualSettledInterestAmount + autoSettledInterestAmount;
// intersetAcmount 是收盘后本金的一天应计展示值。算尾部分平仓时,下面的复利分支会改用
// 平仓前全额本金重算当天新增,但跨日携带的 TdInterestPrincipal 仍只能是剩余本金。
var intersetAcmount = newEodPayPosition.TdInterestPrincipal * (newEodPayPosition.TdInterestRate + newEodPayPosition.FloatRate);
if (position.IsAnnualized)
{
intersetAcmount /= tradeExtend.AnnualDays;
}
var intersetAcmount = InterestIncomeCalc.DailyAccrual(
newEodPayPosition.TdInterestPrincipal, newEodPayPosition.TdInterestRate, newEodPayPosition.FloatRate,
newEodPayPosition.IsAnnualized, tradeExtend.AnnualDays);
newEodPayPosition.TdInterestIncome = autoSwap
? intersetAcmount
: !hasPreviousEod
@@ -1481,12 +1467,9 @@ namespace YLErp.Modules.SwapModule
var accrualPrincipal = calcLast
? fullPrincipal
: newEodPayPosition.TdInterestPrincipal;
newEodPayPosition.TdInterestIncome = accrualPrincipal
* (newEodPayPosition.TdInterestRate + newEodPayPosition.FloatRate);
if (position.IsAnnualized)
{
newEodPayPosition.TdInterestIncome /= tradeExtend.AnnualDays;
}
newEodPayPosition.TdInterestIncome = InterestIncomeCalc.DailyAccrual(
accrualPrincipal, newEodPayPosition.TdInterestRate, newEodPayPosition.FloatRate,
newEodPayPosition.IsAnnualized, tradeExtend.AnnualDays);
}
if (!autoSwap
&& closePercent > 0m && closePercent < 1m
@@ -1532,11 +1515,12 @@ namespace YLErp.Modules.SwapModule
//累计已实现
// RealizedInterest 只增不回滚:上日累计已实现 + 当日结息按方向后的金额。
// 收取腿的 -37119.14 会把累计已实现更新为 -37119.14;后续普通 EOD 保持该值。
newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio;
newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee;
var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio);
newEodPayPosition.RealizedInterest = rolled.Interest;
newEodPayPosition.RealizedInterestFee = rolled.Fee;
SetFixedLegRealizedPnl(newEodPayPosition);
var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true,
position.InterestDirection == (int)SwapDirectionEnum. ? CurrencyRateType.Buy : CurrencyRateType.Sell);
DirectionRatio.RateType(position.InterestDirection));
newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate);
Log.Info($"即将插入数据库的 newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}");
PersistEodSwapPosition(newEodPayPosition);
@@ -1556,7 +1540,7 @@ namespace YLErp.Modules.SwapModule
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;
var interestModes = new List<int>() { (int)InterestModeEnum., (int)InterestModeEnum., (int)InterestModeEnum. };
var interestModes = MarginModes.FixedAmountAndMargin;
if (eodPayPosition == null)
{
//if (position.PosiStartDate > valueDate)
@@ -1611,11 +1595,7 @@ namespace YLErp.Modules.SwapModule
{
closePercent = 1;
}
decimal ratio = eodPayPosition.InterestDirection == (int)SwapDirectionEnum. ? 1m : -1m;//收取为正,支付为负
if (MarginModes.Contains(position.InterestMode))
{
ratio = -ratio;
}
var ratio = DirectionRatio.InterestLegPnl(eodPayPosition.InterestDirection, position.InterestMode);
List<swap_position> positions = new List<swap_position>
{
position
@@ -1654,11 +1634,12 @@ namespace YLErp.Modules.SwapModule
newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio);
//累计已实现
newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio;
newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee;
var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio);
newEodPayPosition.RealizedInterest = rolled.Interest;
newEodPayPosition.RealizedInterestFee = rolled.Fee;
SetFixedLegRealizedPnl(newEodPayPosition);
var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true,
eodPayPosition.InterestDirection == (int)SwapDirectionEnum. ? CurrencyRateType.Buy : CurrencyRateType.Sell);
DirectionRatio.RateType(eodPayPosition.InterestDirection));
newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate);
Log.Info($"the last newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}");
PersistEodSwapPosition(newEodPayPosition);
@@ -1699,7 +1680,7 @@ namespace YLErp.Modules.SwapModule
bool open)
{
payQty = Math.Abs(payQty);
int ratio = eventFlow.PayDirection == (int)SwapDirectionEnum. ? 1 : -1;//收取为正,支付为负
int ratio = DirectionRatio.ReceivePay(eventFlow.PayDirection);//收取为正,支付为负
int shortRatio = DirectionRatio.LongShort(newEodPayPosition.PositionType);
newEodPayPosition.ValueDate = eventFlow.PayDate.Value;
newEodPayPosition.PositionId = eventFlow.PositionId;
@@ -1747,7 +1728,7 @@ namespace YLErp.Modules.SwapModule
newEodPayPosition.TdPosiDividend = Math.Round(dividendIn * ratio, 2);
newEodPayPosition.PosiMtmPnL = MtmCalc.UnrealizedPnl(newEodPayPosition.UnderlyingPrice, newEodPayPosition.PosiGrossPrice, newEodPayPosition.PosiQuantity, newEodPayPosition.ContractSize, shortRatio, (int)ratio);
newEodPayPosition.PosiDividendSum = Math.Round(newEodPayPosition.TdPosiDividend - newEodPayPosition.TdCloseDividend, 2);
newEodPayPosition.PosiProfitSum = newEodPayPosition.PosiMtmPnL + newEodPayPosition.PosiDividendSum + newEodPayPosition.PosiFeePending;
newEodPayPosition.PosiProfitSum = MtmCalc.ReturnLegProfitSum(newEodPayPosition.PosiMtmPnL, newEodPayPosition.PosiDividendSum, newEodPayPosition.PosiFeePending);
//持仓价值
newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum);
@@ -1799,7 +1780,7 @@ namespace YLErp.Modules.SwapModule
}
var dealDate = curretEod.ValueDate;
int shortRatio = DirectionRatio.LongShort(eod.PositionType);
int directionRatio = eod.PosiDirection == (int)SwapDirectionEnum. ? 1 : -1;
int directionRatio = DirectionRatio.ReceivePay(eod.PosiDirection);
curretEod.PosiStatus = curretEod.PosiQuantity == 0 ? 1 : 0;
var price = GetSwapValuationPrice(eod.UnderlyingCode, dealDate, out decimal vobp);
curretEod.dv01 = Dv01Helper.CalcDv01(eod.UnderlyingCode, curretEod.PosiQuantity, eod.PosiDirection, eod.PositionType, vobp);
@@ -1820,7 +1801,7 @@ namespace YLErp.Modules.SwapModule
curretEod.PosiMtmPnL = MtmCalc.UnrealizedPnl(curretEod.UnderlyingPrice, curretEod.PosiGrossPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio, directionRatio);
//curretEod.TdPosiDividend = 0;
//curretEod.PosiDividendSum = eod.PosiDividendSum + curretEod.TdPosiDividend;
curretEod.PosiProfitSum = curretEod.PosiMtmPnL + curretEod.PosiDividendSum + curretEod.PosiFeePending;
curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending);
curretEod.TdCloseFee = 0;
curretEod.TdCloseQty = 0;
curretEod.TdCloseMtmPnl = 0;
@@ -1831,7 +1812,7 @@ namespace YLErp.Modules.SwapModule
curretEod.RealizedFee = eod.RealizedFee + curretEod.TdCloseFee;
SetFloatingRealizedPnl(curretEod);
var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, td.StartDate.Value
, seekPreday: true, currencyRateType: curretEod.PosiDirection == (int)SwapDirectionEnum. ? CurrencyRateType.Buy : CurrencyRateType.Sell);
, seekPreday: true, currencyRateType: DirectionRatio.RateType(curretEod.PosiDirection));
curretEod.TdCurrency = Convert.ToDecimal(currencyRate);
//持仓价值
curretEod.SwapPositionValue = PositionValueCalc.Calc(curretEod.InterestProfitSum, curretEod.PosiProfitSum);
@@ -1891,7 +1872,7 @@ namespace YLErp.Modules.SwapModule
}
var dealDate = curretEod.ValueDate;
int shortRatio = DirectionRatio.LongShort(eod.PositionType);
int directionRatio = eod.PosiDirection == (int)SwapDirectionEnum. ? 1 : -1;
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;
@@ -1933,13 +1914,13 @@ namespace YLErp.Modules.SwapModule
SetFloatingRealizedPnl(curretEod);
curretEod.SwapPositionValue -= curretEod.TdCloseDividend;
curretEod.PosiProfitSum = curretEod.PosiMtmPnL + curretEod.PosiDividendSum + curretEod.PosiFeePending;
curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending);
if (curretEod.PosiStatus == 1)
{
curretEod.PosiNotionalValue = 0;
}
var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, td.StartDate.Value
, seekPreday: true, currencyRateType: curretEod.PosiDirection == (int)SwapDirectionEnum. ? CurrencyRateType.Buy : CurrencyRateType.Sell);
, seekPreday: true, currencyRateType: DirectionRatio.RateType(curretEod.PosiDirection));
curretEod.TdCurrency = Convert.ToDecimal(currencyRate);
//持仓价值
curretEod.SwapPositionValue = PositionValueCalc.Calc(curretEod.InterestProfitSum, curretEod.PosiProfitSum);
@@ -1979,7 +1960,7 @@ namespace YLErp.Modules.SwapModule
return;
}
int shortRatio = DirectionRatio.LongShort(eod.PositionType);
int directionRatio = eod.PosiDirection == (int)SwapDirectionEnum. ? 1 : -1;
int directionRatio = DirectionRatio.ReceivePay(eod.PosiDirection);
var unwindFlowEvents = unwindEvents.Where(x => x.EventType == (int)SwapFlowEventTypeEnum.).ToList();
var openFlowEvents = unwindEvents.Where(x => x.EventType == (int)SwapFlowEventTypeEnum.).ToList();
decimal unwindQty = unwindFlowEvents.Sum(s => s.Quantity);
@@ -2012,16 +1993,16 @@ namespace YLErp.Modules.SwapModule
{
posiQty = 0;
}
curretEod.PosiGrossPrice = (eod.PosiGrossPrice * eod.PosiQuantity + openFlowEvents.Sum(a => a.Quantity * a.TradingAmountAvg)) / (eod.PosiQuantity + openQty);
curretEod.PosiGrossPrice = MtmCalc.BlendPrice(eod.PosiGrossPrice, eod.PosiQuantity, openFlowEvents.Sum(a => a.Quantity * a.TradingAmountAvg), eod.PosiQuantity + openQty);
curretEod.PosiGrossPrice = Math.Round(
curretEod.PosiGrossPrice,
GetStorageDeliveryPriceRound(curretEod.UnderlyingInstrumentType, curretEod.UnderlyingCode),
MidpointRounding.AwayFromZero);
curretEod.PosiNetPrice = (eod.PosiNetPrice * eod.PosiQuantity + openFlowEvents.Sum(a => a.Quantity * a.TradingAmountFeeAvg)) / (eod.PosiQuantity + openQty);
curretEod.PosiNetPrice = MtmCalc.BlendPrice(eod.PosiNetPrice, eod.PosiQuantity, openFlowEvents.Sum(a => a.Quantity * a.TradingAmountFeeAvg), eod.PosiQuantity + openQty);
curretEod.PosiNetPrice = Math.Round(curretEod.PosiNetPrice, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
curretEod.PosiNetNoFeePrice = (eod.PosiNetNoFeePrice * eod.PosiQuantity + openFlowEvents.Sum(a => a.Quantity * a.TradingAmountNetAvg)) / (eod.PosiQuantity + openQty);
curretEod.PosiNetNoFeePrice = MtmCalc.BlendPrice(eod.PosiNetNoFeePrice ?? 0m, eod.PosiQuantity, openFlowEvents.Sum(a => a.Quantity * (a.TradingAmountNetAvg ?? 0m)), eod.PosiQuantity + openQty);
curretEod.PosiNetNoFeePrice = Math.Round(curretEod.PosiNetNoFeePrice ?? 0, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
curretEod.PosiNetFeePrice = (eod.PosiNetFeePrice * eod.PosiQuantity + openFlowEvents.Sum(a => a.Quantity * a.TradingAmountNetFeeAvg)) / (eod.PosiQuantity + openQty);
curretEod.PosiNetFeePrice = MtmCalc.BlendPrice(eod.PosiNetFeePrice ?? 0m, eod.PosiQuantity, openFlowEvents.Sum(a => a.Quantity * (a.TradingAmountNetFeeAvg ?? 0m)), eod.PosiQuantity + openQty);
curretEod.PosiNetFeePrice = Math.Round(curretEod.PosiNetFeePrice ?? 0, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
}
curretEod.PosiNotionalValue = curretEod.PosiGrossPrice * curretEod.PosiQuantity * curretEod.ContractSize;
@@ -2060,7 +2041,7 @@ namespace YLErp.Modules.SwapModule
curretEod.PositionId = position.id;
curretEod.ClientId = td.ClientId;
int shortRatio = DirectionRatio.LongShort(position.PositionType);
int directionRatio = position.PosiDirection == (int)SwapDirectionEnum. ? 1 : -1;
int directionRatio = DirectionRatio.ReceivePay(position.PosiDirection);
curretEod.PositionType = position.PositionType;
var eod = new eod_swap_position()
{
@@ -2106,7 +2087,7 @@ namespace YLErp.Modules.SwapModule
curretEod.UnderlyingMarketValue = MtmCalc.MarketValue(curretEod.UnderlyingPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio);
curretEod.PosiMtmPnL = MtmCalc.UnrealizedPnl(curretEod.UnderlyingPrice, curretEod.PosiGrossPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio, directionRatio);
curretEod.PosiProfitSum = curretEod.PosiMtmPnL + curretEod.PosiDividendSum + curretEod.PosiFeePending;
curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending);
curretEod.RealizedMtmPnL = curretEod.TdCloseMtmPnl;
curretEod.RealizedDividend = curretEod.TdCloseDividend;
curretEod.RealizedFee = curretEod.TdCloseFee;
@@ -2119,7 +2100,7 @@ namespace YLErp.Modules.SwapModule
//持仓价值
curretEod.SwapPositionValue = PositionValueCalc.Calc(curretEod.InterestProfitSum, curretEod.PosiProfitSum);
var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, td.StartDate.Value
, seekPreday: true, currencyRateType: curretEod.PosiDirection == (int)SwapDirectionEnum. ? CurrencyRateType.Buy : CurrencyRateType.Sell);
, seekPreday: true, currencyRateType: DirectionRatio.RateType(curretEod.PosiDirection));
curretEod.TdCurrency = Convert.ToDecimal(currencyRate);
UpdateDbOption(curretEod);
curretEod.Invalid = false;
@@ -2194,8 +2175,6 @@ namespace YLErp.Modules.SwapModule
var positions = eodSwapPositions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//持仓腿
// 框架合约的方向约定:多头为正、空头为负;总名义本金取交易原始规模,
// 不能直接用多空腿相加,否则会把对冲方向误当成合约规模变化。
eod_Swap.NotionalValueLong = Math.Round(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
eod_Swap.NotionalValueShort = Math.Round(-Math.Abs(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue)), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
eod_Swap.NotionalValue = Math.Round(Convert.ToDecimal(td.OriginalStockEqvNotional ?? td.StockEqvNotional), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
eod_Swap.SwapTradeId = td.id;
eod_Swap.SwapTradeNo = td.TradeNumber;
@@ -2203,23 +2182,8 @@ namespace YLErp.Modules.SwapModule
eod_Swap.BookId = td.AssetId;
eod_Swap.ValueDate = settleDate;
eod_Swap.StructureType = td.StructureType;
eod_Swap.MarketValueLong = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.UnderlyingMarketValue);
eod_Swap.MarketValueShort = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.UnderlyingMarketValue);
eod_Swap.FloatingPnL = positions.Sum(s => s.PosiProfitSum);
eod_Swap.dv01 = positions.Sum(s => s.dv01 ?? 0);
decimal interestPnL = 0;
// 利息腿按我方视角归集。保证金腿的利息现金流方向与普通利息腿相反,
// 因此保证金腿需要额外反转符号,确保 InterestPnL 表示我方的合约利率端收益。
interestPositions.ForEach(x =>
{
decimal ratio = x.InterestDirection == (int)SwapDirectionEnum. ? 1 : -1;//收取为正,支付为负
if (MarginModes.Contains(x.InterestMode))
{
ratio = -ratio;
}
interestPnL += x.InterestProfitSum * ratio;
});
eod_Swap.InterestPnL = interestPnL;
FillPositionLegSummary(eod_Swap, positions);
eod_Swap.InterestPnL = SumInterestPnL(interestPositions);
eod_Swap.PostionValue = eodSwapPositions.Sum(s => s.SwapPositionValue);
// 保证金腿的利息现金流方向与保证金本金方向相反。
// 不能直接汇总 RealizedPnl,否则“收取客户保证金”的腿会把应支付给客户的
@@ -2272,33 +2236,15 @@ namespace YLErp.Modules.SwapModule
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);
eod_Swap.NotionalValueLong = Math.Round(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
eod_Swap.NotionalValueShort = Math.Round(-Math.Abs(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue)), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
eod_Swap.MarketValueLong = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.UnderlyingMarketValue);
eod_Swap.MarketValueShort = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.UnderlyingMarketValue);
eod_Swap.FloatingPnL = positions.Sum(s => s.PosiProfitSum);
eod_Swap.dv01 = positions.Sum(s => s.dv01 ?? 0);
interestPositions.ForEach(x =>
{
decimal ratio = x.InterestDirection == (int)SwapDirectionEnum. ? 1 : -1;//收取为正,支付为负
if (MarginModes.Contains(x.InterestMode))
{
ratio = -ratio;
}
eod_Swap.InterestPnL += x.InterestProfitSum * ratio;
});
FillPositionLegSummary(eod_Swap, positions);
eod_Swap.InterestPnL += SumInterestPnL(interestPositions);
eodSwapPositions.ForEach(x =>
{
decimal ratio = x.InterestDirection == (int)SwapDirectionEnum. ? 1 : -1;//收取为正,支付为负
if (MarginModes.Contains(x.InterestMode))
{
ratio = -ratio;
}
var ratio = DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode);
eod_Swap.TdRealizedPnL += x.TdCloseMtmPnl + x.TdCloseDividend + x.TdCloseFee + x.TdCloseInterest * ratio + x.TdCloseInterestFee;
});
eod_Swap.PostionValue = eodSwapPositions.Sum(s => s.SwapPositionValue);
eod_Swap.RealizedPnL = eodSwapPositions.Sum(CalculateSwapRealizedPnl);
eod_Swap.TdCloseQty = positions.Sum(s => s.TdCloseQty);
var tradeInitMarginObj = DbContext.trade_initial_margin.FirstOrDefault(x => x.TradeId == td.id);
var initMarginList = interestPositions.Where(x => x.InterestMode == (int)InterestModeEnum. && x.HappenDate == settleDate).ToList();
var addMarginList = interestPositions.Where(x => x.InterestMode == (int)InterestModeEnum. && x.HappenDate == settleDate).ToList();
@@ -2319,11 +2265,7 @@ namespace YLErp.Modules.SwapModule
/// </summary>
public static decimal CalculateSwapRealizedPnl(eod_swap_position position)
{
var interestRatio = position.InterestDirection == (int)SwapDirectionEnum. ? 1m : -1m;
if (MarginModes.Contains(position.InterestMode))
{
interestRatio = -interestRatio;
}
var interestRatio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
return position.RealizedMtmPnL
+ position.RealizedDividend
@@ -2332,6 +2274,28 @@ namespace YLErp.Modules.SwapModule
+ position.RealizedInterestFee;
}
/// <summary>填充框架合约的持仓腿汇总字段(多空名义本金/市值/浮动盈亏/dv01/平仓量)。
/// SaveEodSwap 与 UpdateEodSwap 共用,消除 ~10 行重复。</summary>
private static void FillPositionLegSummary(eod_swap eod_Swap, List<eod_swap_position> positions)
{
eod_Swap.NotionalValueLong = Math.Round(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
eod_Swap.NotionalValueShort = Math.Round(-Math.Abs(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue)), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
eod_Swap.MarketValueLong = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.UnderlyingMarketValue);
eod_Swap.MarketValueShort = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.UnderlyingMarketValue);
eod_Swap.FloatingPnL = positions.Sum(s => s.PosiProfitSum);
eod_Swap.dv01 = positions.Sum(s => s.dv01 ?? 0);
eod_Swap.TdCloseQty = positions.Sum(s => s.TdCloseQty);
}
/// <summary>利息腿 PnL 汇总(按方向比例 + 保证金翻转)。原 SaveEodSwap/UpdateEodSwap 各一段 ForEach。</summary>
private static decimal SumInterestPnL(List<eod_swap_position> interestPositions)
{
decimal interestPnL = 0;
foreach (var x in interestPositions)
interestPnL += x.InterestProfitSum * DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode);
return interestPnL;
}
/// <summary>
/// 风险报表符号归一化:把历史两种符号口径的 TdCloseInterest/RealizedInterest
/// 统一按"绝对金额 × 业务方向"重写。普通利息腿收取为正、支付为负;
@@ -2343,15 +2307,11 @@ namespace YLErp.Modules.SwapModule
{
if (position.InterestDirection <= 0) return;
var interestRatio = position.InterestDirection == (int)SwapDirectionEnum. ? 1m : -1m;
if (MarginModes.Contains(position.InterestMode))
{
interestRatio = -interestRatio;
}
else if (position.InterestMode == (int)InterestModeEnum.)
if (position.InterestMode == (int)InterestModeEnum.)
{
return;
}
var interestRatio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
position.TdCloseInterest = Math.Abs(position.TdCloseInterest) * interestRatio;
position.RealizedInterest = Math.Abs(position.RealizedInterest) * interestRatio;
// 兼容修复前已落库的利息腿:当时只累计了明细字段,未同步写入 RealizedPnl。
@@ -2845,11 +2805,11 @@ namespace YLErp.Modules.SwapModule
var floatRateInterest = eodInterests.Where(x => !string.IsNullOrEmpty(x.FloatRateUnderlyingCode)).FirstOrDefault();
item.position.FloatRateUnderlyingCode = floatRateInterest?.FloatRateUnderlyingCode;
item.position.FloatRate = floatRateInterest?.FloatRate ?? 0;
item.OpenMarginAmount = initialMargins.Sum(s => s.InterestPrincipalFix * (s.InterestDirection == (int)SwapDirectionEnum. ? 1 : -1));
item.OpenMarginAmount = initialMargins.Sum(s => s.InterestPrincipalFix * DirectionRatio.ReceivePay(s.InterestDirection));
item.OpenMarginRate = CalculateWeightedMarginRate(tradeMargins);
item.AdditionalMarginAmount = additionalMargins.Sum(s => s.InterestPrincipalFix * (s.InterestDirection == (int)SwapDirectionEnum. ? 1 : -1));
item.AdditionalMarginAmount = additionalMargins.Sum(s => s.InterestPrincipalFix * DirectionRatio.ReceivePay(s.InterestDirection));
item.MarginInterestAmount = CalculateWeightedMarginInterest(eodMargins);
item.InterestAmount = eodInterests.Sum(s => s.InterestIncomeSum * (s.InterestDirection == (int)SwapDirectionEnum. ? -1 : 1));
item.InterestAmount = eodInterests.Sum(s => s.InterestIncomeSum * (-DirectionRatio.ReceivePay(s.InterestDirection)));
item.InterestRate = eodInterests.Sum(s => s.InterestRateDefault);
// 到期轧差才把期间付息/分红并入净额结算;派息日支付已在现金流层独立结算,不能重复计入估值。
var nettingDividend = (tradeExtend?.ExtendObj?.DividendPayDate ?? 1) == 0 ? pendingDividend : 0m;
@@ -2892,7 +2852,7 @@ namespace YLErp.Modules.SwapModule
public static decimal CalculateWeightedMarginInterest(IEnumerable<eod_swap_position> margins)
{
return margins.Sum(x =>
x.InterestIncomeSum * (x.InterestDirection == (int)SwapDirectionEnum. ? 1 : -1));
x.InterestIncomeSum * DirectionRatio.ReceivePay(x.InterestDirection));
}
/// <summary>