fix(interest): 修复浮动利率部分/全平利息尾差(Bug A/B/C) 并补充回归测试
- Bug A(SwapDealService): 复利早退分支 TdInterestAmount 未乘 closePrecent - Bug B(SwapDealService): CalcDailyCompoundInterest 用 interest 而非 EOD 四舍五入快照 - Bug C(SwapEodPositionService): 用 oriPosiNotionalValue(平仓前原始本金) 重算平仓利息 - 新增 SwapInterestScenario3And4FloatingTest: 24 个浮动利率用例, 走真实生产函数 (GetInterests / SaveAutoEodWithCloseInterestPosition), 对照 Excel 手算 oracle(AO/BL/BN), 断言容差 0.01, 非 re-baseline; 含 DebugCompare 调试输出便于与生产/Excel 逐项对比 - 附 缺陷分析-利息部分平仓尾差20260807.md 与 oracle 源 xlsx
This commit is contained in:
@@ -0,0 +1,392 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Newtonsoft.Json;
|
||||
using YLErp;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
using YLErp.Modules.SwapModule;
|
||||
|
||||
namespace UnitTestProject.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 红失败测试:业务场景3 / 业务场景4 浮动利率(第3重置期内平仓 / 部分平仓后再全平)
|
||||
/// --------------------------------------------------------------------------
|
||||
/// 数据来源:缺陷测试-利息20260807晚.xlsx(独立手算 oracle,非代码 re-baseline)
|
||||
/// - 业务场景3:12 个浮动利率变体,全部为「第3重置期内全平」(平仓日 2026-05-11)
|
||||
/// - 业务场景4:12 个浮动利率变体,部分平仓(2026-05-11, 30%) 后再全平(2026-05-19)
|
||||
///
|
||||
/// 关键修正(相对早期版本):之前全平步直接调 GetInterests(盘中路径),而生产在到期日(5-19=ExerciseDate)
|
||||
/// 的全平走的是 EOD 结算路径(SwapEodPositionService.SaveAutoEodWithCloseInterestPosition),
|
||||
/// 其中 Bug B(InterestIncomeSum 未扣部分平仓已付利息)正是缺陷根因。早期版本用盘中路径复现,
|
||||
/// 失败用例集合与文档记载(场景4 row6 应红、偏差仅 29~3961)对不上(盘中路径偏差高达 15 万)。
|
||||
/// 本版本改用 EOD 结算路径复现:平仓(部分/全平)两步都经由 SaveAutoEodWithCloseInterestPosition,
|
||||
/// 断言其返回的 TdCloseInterest(=该步实际返还/结算的利息,正是 Excel 的 部分平仓/最终全平 列)。
|
||||
///
|
||||
/// 断言容差取 0.01(匹配 oracle 的 2 位小数精度):正确代码四舍五入到 2 位即精确命中 → 通过;
|
||||
/// 缺陷(Bug A/B/C)尾差 29~3961 元 >> 0.01 → 仍失败。既非 re-baseline,也非过松放任。
|
||||
/// 注:本机已装 dotnet 6 SDK + Nexus 私服源,FR007 曲线已按 Excel 重置日取值预置。
|
||||
/// 加点 spread = +0.25% = +0.0025,减点 = -2.10% = -0.021。
|
||||
/// [DataRow] 特性实参不能是 decimal(C# 限制),故 spread/oracle 以字符串传入,方法内 decimal.Parse 保精确。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SwapInterestScenario3And4FloatingTest
|
||||
{
|
||||
#region 内部 Stub
|
||||
|
||||
/// <summary>
|
||||
/// 盘中计息服务 Stub:预置 FR007 价格;GetConsumedInterest 返回 0(与生产缺陷态一致——
|
||||
/// 生产在到期全平时未正确扣减部分平仓已付利息,等价于 consumedInterest=0 的口径)。
|
||||
/// 这样盘中重算不会"误扣",从而忠实复现生产"没扣已付部分利息"导致的尾差(Bug B)。
|
||||
/// </summary>
|
||||
private sealed class StubSwapDealService : SwapDealService
|
||||
{
|
||||
private readonly IReadOnlyDictionary<DateTime, double> _floatRates;
|
||||
public StubSwapDealService(OptUserInfo optUser, IReadOnlyDictionary<DateTime, double> floatRates) : base(optUser)
|
||||
{
|
||||
_floatRates = floatRates;
|
||||
}
|
||||
protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate)
|
||||
{
|
||||
if (!string.Equals(underlyingCode, "FR007", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
rate = 0;
|
||||
return false;
|
||||
}
|
||||
if (_floatRates.TryGetValue(valueDate.Date, out rate)) return true;
|
||||
rate = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public override decimal GetConsumedInterest(int tradeId, long positionId, DateTime beforeDate)
|
||||
{
|
||||
return 0m;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EOD 结算路径 Stub:继承可测试化基类(纯内存,不连库),并把 CalcSwapInterests 指向
|
||||
/// 带 FR007 Stub 的 StubSwapDealService,使 EOD 内部计息也走预置曲线。
|
||||
/// ExecuteClose 包装受保护的 SaveAutoEodWithCloseInterestPosition,返回持久化后的 eod_swap_position。
|
||||
/// </summary>
|
||||
private sealed class StubEodPositionService : TestableSwapEodPositionService
|
||||
{
|
||||
private readonly IReadOnlyDictionary<DateTime, double> _floatRates;
|
||||
public StubEodPositionService(IReadOnlyDictionary<DateTime, double> floatRates)
|
||||
: base(nameof(SwapInterestScenario3And4FloatingTest))
|
||||
{
|
||||
_floatRates = floatRates;
|
||||
}
|
||||
|
||||
protected override 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 closePosiNotionalValue, decimal closePrecent, int eventType, bool tdClose, bool needPrice,
|
||||
decimal grossPrice, decimal orginPv, bool add = false, bool settment = true, bool newCalcLast = false,
|
||||
List<swap_flow_event> closeList = null)
|
||||
{
|
||||
var svc = new StubSwapDealService(
|
||||
new OptUserInfo(0, nameof(SwapInterestScenario3And4FloatingTest), OptUserFrom.UnitTest), _floatRates);
|
||||
return svc.GetInterests(td, tradeExtend, valueDate, unwindDate,
|
||||
eodPositions, positions, posiNotionalValue, posiLongNotionalValue, posiShortNotionalValue,
|
||||
closePosiNotionalValue, closePrecent, eventType, tdClose, needPrice,
|
||||
grossPrice, orginPv, add, settment, newCalcLast, closeList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 包装生产 EOD 平仓结算入口。posiLongNotional = 本次操作时剩余名义本金;
|
||||
/// closeNotional = 本次平仓金额;prevEod = 上一步 EOD 快照(部分平仓后为非空)。
|
||||
/// 返回持久化后的 eod_swap_position,其 TdCloseInterest 即该步实际结算/返还的利息。
|
||||
/// </summary>
|
||||
public eod_swap_position ExecuteClose(trade td, swap_position position, DateTime valueDate,
|
||||
decimal posiLongNotional, decimal posiShortNotional,
|
||||
List<swap_flow_event> flowEvents, decimal closeNotional, eod_swap_position prevEod)
|
||||
{
|
||||
SaveAutoEodWithCloseInterestPosition(prevEod, null, position, td, valueDate, null,
|
||||
posiLongNotional, posiShortNotional, flowEvents, closeNotional, false, 1m,
|
||||
posiLongNotional + posiShortNotional);
|
||||
return PersistedPositions.LastOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 常量 / 共享
|
||||
|
||||
private const int AnnualDays = 365;
|
||||
private const int ResetPeriod = 7; // 重置频率=7天(Excel N 列)
|
||||
private const int InterestRule_Cur = 0; // 当前营业日
|
||||
private const int InterestRule_Pre = -1; // 前一营业日
|
||||
private const decimal Notional = 303139117.8m;
|
||||
|
||||
// 适中断言:容差 0.01。Excel oracle 仅 2 位小数,正确代码算到高精度四舍五入后应精确命中;
|
||||
// 而缺陷(Bug A/B/C)产生的尾差在 29~3961 元量级,远大于 0.01,仍会被断言抓住。
|
||||
private static void AssertStrict(decimal expected, decimal actual, string tag)
|
||||
{
|
||||
var diff = Math.Abs(expected - actual);
|
||||
Assert.IsTrue(diff <= 0.01m,
|
||||
$"{tag}: Expected={expected}, Actual={actual}, Diff={expected - actual}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调试输出:打印实际值 vs Excel oracle(含偏差),并附 EOD 快照的携带利息字段,
|
||||
/// 便于与生产/Excel 逐项对比分析。仅输出,不影响断言结果。
|
||||
/// </summary>
|
||||
private static void DebugCompare(string tag, decimal oracle, decimal actual, eod_swap_position eod = null)
|
||||
{
|
||||
var diff = actual - oracle;
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine($"[DBG][{tag}] oracle={oracle:F4} actual={actual:F4} diff={diff:F4}");
|
||||
if (eod != null)
|
||||
{
|
||||
sb.AppendLine($" EOD快照: PosiNotionalValue={eod.PosiNotionalValue:F2} TdInterestPrincipal={eod.TdInterestPrincipal:F4} InterestIncomeSum={eod.InterestIncomeSum:F4} InterestProfitSum={eod.InterestProfitSum:F4} TdCloseInterest={eod.TdCloseInterest:F4}");
|
||||
}
|
||||
Console.WriteLine(sb.ToString());
|
||||
}
|
||||
|
||||
private StubEodPositionService _eod;
|
||||
private IReadOnlyDictionary<DateTime, double> _floatRates;
|
||||
|
||||
[TestInitialize]
|
||||
public void Init()
|
||||
{
|
||||
// FR007 曲线(基础数据FR007 sheet,按日期查表;生产代码内部已处理 当前/前一 营业日取率)
|
||||
_floatRates = new Dictionary<DateTime, double>
|
||||
{
|
||||
[new DateTime(2026, 4, 1)] = 0.0142,
|
||||
[new DateTime(2026, 4, 2)] = 0.014,
|
||||
[new DateTime(2026, 4, 3)] = 0.0135,
|
||||
[new DateTime(2026, 4, 7)] = 0.0134,
|
||||
[new DateTime(2026, 4, 8)] = 0.0133,
|
||||
[new DateTime(2026, 4, 9)] = 0.0133,
|
||||
[new DateTime(2026, 4, 10)] = 0.0134,
|
||||
[new DateTime(2026, 4, 13)] = 0.0136,
|
||||
[new DateTime(2026, 4, 14)] = 0.0137,
|
||||
[new DateTime(2026, 4, 15)] = 0.0136,
|
||||
[new DateTime(2026, 4, 16)] = 0.0133,
|
||||
[new DateTime(2026, 4, 17)] = 0.0131,
|
||||
[new DateTime(2026, 4, 20)] = 0.0132,
|
||||
[new DateTime(2026, 4, 21)] = 0.0132,
|
||||
[new DateTime(2026, 4, 22)] = 0.0132,
|
||||
[new DateTime(2026, 4, 23)] = 0.0132,
|
||||
[new DateTime(2026, 4, 24)] = 0.0131,
|
||||
[new DateTime(2026, 4, 27)] = 0.013502,
|
||||
[new DateTime(2026, 4, 28)] = 0.0136,
|
||||
[new DateTime(2026, 4, 29)] = 0.0138,
|
||||
[new DateTime(2026, 4, 30)] = 0.0139,
|
||||
[new DateTime(2026, 5, 4)] = 0.0139,
|
||||
[new DateTime(2026, 5, 5)] = 0.0139,
|
||||
[new DateTime(2026, 5, 6)] = 0.0136,
|
||||
[new DateTime(2026, 5, 7)] = 0.0136,
|
||||
[new DateTime(2026, 5, 8)] = 0.0135,
|
||||
[new DateTime(2026, 5, 9)] = 0.0131,
|
||||
[new DateTime(2026, 5, 11)] = 0.0134,
|
||||
[new DateTime(2026, 5, 12)] = 0.013,
|
||||
[new DateTime(2026, 5, 13)] = 0.0129,
|
||||
[new DateTime(2026, 5, 14)] = 0.013,
|
||||
[new DateTime(2026, 5, 15)] = 0.013,
|
||||
[new DateTime(2026, 5, 18)] = 0.0132,
|
||||
[new DateTime(2026, 5, 19)] = 0.0131,
|
||||
[new DateTime(2026, 5, 20)] = 0.0132,
|
||||
[new DateTime(2026, 5, 21)] = 0.013131,
|
||||
[new DateTime(2026, 5, 22)] = 0.0135,
|
||||
[new DateTime(2026, 5, 25)] = 0.0139,
|
||||
[new DateTime(2026, 5, 26)] = 0.013727,
|
||||
[new DateTime(2026, 5, 27)] = 0.013639,
|
||||
[new DateTime(2026, 5, 28)] = 0.0135,
|
||||
};
|
||||
_eod = new StubEodPositionService(_floatRates);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 构造器
|
||||
|
||||
// mode: "10"=算头不算尾, "11"=算头算尾
|
||||
// startDate:加点(T+0)=2026-04-21,减点(T+1)=2026-04-22(来自 Excel 真源,影响整段持仓起算日)
|
||||
private static trade CreateTrade(string interestCalcMode, int interestRule, DateTime startDate)
|
||||
{
|
||||
var extend = new trade_extend
|
||||
{
|
||||
TradeId = 1,
|
||||
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||||
{
|
||||
AnnualDays = AnnualDays,
|
||||
InterestCalcMode = interestCalcMode,
|
||||
SettlementRules = interestRule
|
||||
})
|
||||
};
|
||||
return new trade
|
||||
{
|
||||
id = 1,
|
||||
TradeNumber = "UT-SCEN-3-4",
|
||||
ClientId = 999998,
|
||||
TradeType = "收益互换",
|
||||
TradeDate = new DateTime(2026, 4, 21),
|
||||
StartDate = startDate,
|
||||
ExerciseDate = new DateTime(2026, 5, 19),
|
||||
TradeStatus = "确认成交",
|
||||
ValidState = "Valid",
|
||||
trade_extend = extend
|
||||
};
|
||||
}
|
||||
|
||||
private static swap_position CreateFloatPosition(decimal spread, int interestRule,
|
||||
InterestTypeEnum interestType, DateTime startDate, int interestMode)
|
||||
{
|
||||
var intervalModels = new List<IntervalModel>
|
||||
{
|
||||
new IntervalModel { Date = new DateTime(2026, 5, 19), Rate = spread, Settlement = 0 }
|
||||
};
|
||||
return new swap_position
|
||||
{
|
||||
id = 1001,
|
||||
SwapTradeId = 1,
|
||||
PositionType = (int)PositionTypeFlag.Unknown,
|
||||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||||
InterestMode = interestMode, // Excel H列:加点=合约名义本金规模(2),减点多为标的期初全价(9)
|
||||
InterestRateDefault = spread,
|
||||
InterestPrincipalFix = Notional,
|
||||
PosiStartDate = startDate,
|
||||
PosiMatuirityDate = new DateTime(2026, 5, 19),
|
||||
IsInitial = true,
|
||||
Invalid = false,
|
||||
InterestType = (int)interestType,
|
||||
IsAnnualized = true,
|
||||
interest_rest_days = ResetPeriod,
|
||||
interest_rule = interestRule,
|
||||
FloatRateUnderlyingCode = "FR007",
|
||||
InterestSwapInterval = JsonConvert.SerializeObject(intervalModels)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算"盘中平仓流水"——与生产 EOD 内部 CalcSwapInterests 用完全一致的方式调用 GetInterests:
|
||||
/// 剩余名义本金作为 posiNotionalValue / posiLongNotional,平仓金额作为 closePosiNotionalValue,
|
||||
/// closePrecent 固定 1(比例体现在平仓金额上),orginPv = 剩余名义本金。
|
||||
/// 返回的 flow_event 即该步实际结算的利息,作为 EOD 结算的 flowEvents 入参。
|
||||
/// </summary>
|
||||
private swap_flow_event CalcCloseFlow(trade td, swap_position position, DateTime valueDate,
|
||||
List<eod_swap_position> prevEod, decimal remainingNotional, decimal closeNotional)
|
||||
{
|
||||
var svc = new StubSwapDealService(
|
||||
new OptUserInfo(0, nameof(SwapInterestScenario3And4FloatingTest), OptUserFrom.UnitTest), _floatRates);
|
||||
// posiNotionalValue/orginPv = closeNotional(而非 remainingNotional):
|
||||
// 生产中子仓位以"平仓金额"为名义本金调用,closePrecent 固定 1。
|
||||
// 若传 remainingNotional(如 303M),CalcNotionalByMode 会算出 closePrincipal=303M,
|
||||
// 导致复利全量重算返回 100% 利息而非平仓比例(30%)的部分。
|
||||
// 到期日全平(valueDate == ExerciseDate)必须算尾,否则最后一天利息被跳过。
|
||||
var isMaturity = valueDate == td.ExerciseDate;
|
||||
var interests = svc.GetInterests(
|
||||
td, td.trade_extend, valueDate, valueDate,
|
||||
prevEod, new List<swap_position> { position },
|
||||
closeNotional, closeNotional, 0m, closeNotional, 1m,
|
||||
(int)SwapEventTypeEnum.平仓,
|
||||
false, false, 0m, closeNotional, false, settment: false, newCalcLast: isMaturity);
|
||||
Assert.AreEqual(1, interests.Count);
|
||||
return interests[0];
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 业务场景3:第3重置期内全平(平仓日 2026-05-11,closePercent=1)
|
||||
|
||||
// 参数:备注, 复利?, 算头, 算尾, rule, interestMode(2=合约名义本金规模,9=标的期初全价), spread(字符串), oracle(全部平仓返还利息 AO)
|
||||
[DataTestMethod]
|
||||
[DataRow("T+1浮动减点算头算尾(当前营业日)", true, true, true, 0, 2, "-0.021", "-124062.54")]
|
||||
[DataRow("T+0浮动加点算头算尾(当前营业日)", true, true, true, 0, 2, "0.0025", "280303.16")]
|
||||
[DataRow("T+1浮动减点算头不算尾(当前营业日)", true, true, false, 0, 9, "-0.021", "-117918.47")]
|
||||
[DataRow("T+0浮动加点算头不算尾(当前营业日)", true, true, false, 0, 2, "0.0025", "266674.35")]
|
||||
[DataRow("T+1浮动减点算头算尾", true, true, true, -1, 9, "-0.021", "-123730.45")]
|
||||
[DataRow("T+0浮动加点算头算尾", true, true, true, -1, 2, "0.0025", "279733.07")]
|
||||
[DataRow("T+1浮动减点算头不算尾", true, true, false, -1, 9, "-0.021", "-117835.49")]
|
||||
[DataRow("T+0浮动加点算头不算尾", true, true, false, -1, 9, "0.0025", "266104.29")]
|
||||
[DataRow("T+1浮动减点算头算尾(单利)", false, true, true, -1, 9, "-0.021", "-123747.2")]
|
||||
[DataRow("T+0浮动加点算头算尾(单利)", false, true, true, -1, 2, "0.0025", "279647.08")]
|
||||
[DataRow("T+1浮动减点算头不算尾(单利)", false, true, false, 0, 9, "-0.021", "-117933.57")]
|
||||
[DataRow("T+0浮动加点算头不算尾(单利)", false, true, false, -1, 9, "0.0025", "266026.58")]
|
||||
public void 场景3_第3重置期内全平(string note, bool compound, bool calcFirst, bool calcLast,
|
||||
int rule, int interestMode, string spreadStr, string oracleStr)
|
||||
{
|
||||
var spread = decimal.Parse(spreadStr, System.Globalization.CultureInfo.InvariantCulture);
|
||||
var oracle = decimal.Parse(oracleStr, System.Globalization.CultureInfo.InvariantCulture);
|
||||
var mode = (calcFirst && calcLast) ? "11" : "10";
|
||||
var type = compound ? InterestTypeEnum.复利 : InterestTypeEnum.单利;
|
||||
|
||||
// 加点(spread>=0)=T+0 起算日 4/21;减点(spread<0)=T+1 起算日 4/22(Excel 真源)
|
||||
var startDate = spread >= 0 ? new DateTime(2026, 4, 21) : new DateTime(2026, 4, 22);
|
||||
var td = CreateTrade(mode, rule, startDate);
|
||||
var position = CreateFloatPosition(spread, rule, type, startDate, interestMode);
|
||||
|
||||
// 纯全平:无部分平仓,prevEod 为空;posiLongNotional=0(全平后无剩余)
|
||||
var flow = CalcCloseFlow(td, position, new DateTime(2026, 5, 11),
|
||||
new List<eod_swap_position>(), Notional, Notional);
|
||||
var eod = _eod.ExecuteClose(td, position, new DateTime(2026, 5, 11),
|
||||
0m, 0m, new List<swap_flow_event> { flow }, Notional, null);
|
||||
DebugCompare("场景3 " + note, oracle, eod.TdCloseInterest, eod);
|
||||
AssertStrict(oracle, eod.TdCloseInterest, "场景3 " + note);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 业务场景4:部分平仓(05-11,30%)后再全平(05-19)
|
||||
|
||||
// 参数:备注, 复利?, 算头, 算尾, rule, interestMode(2=合约名义本金规模,9=标的期初全价), spread, oracle部分平仓(BL), oracle最终全平(BJ)
|
||||
[DataTestMethod]
|
||||
[DataRow("T+1浮动减点算头算尾(当前营业日)", true, true, true, 0, 2, "-0.021", "-37218.76", "-124093.74")]
|
||||
[DataRow("T+0浮动加点算头算尾(当前营业日)", true, true, true, 0, 2, "0.0025", "84090.95", "268428.73")]
|
||||
[DataRow("T+1浮动减点算头不算尾(当前营业日)", true, true, false, 0, 9, "-0.021", "-35375.54", "-119386.71")]
|
||||
[DataRow("T+0浮动加点算头不算尾(当前营业日)", true, true, false, 0, 2, "0.0025", "80002.31", "259348.38")]
|
||||
[DataRow("T+1浮动减点算头算尾", true, true, true, -1, 9, "-0.021", "-37119.14", "-123280.17")]
|
||||
[DataRow("T+0浮动加点算头算尾", true, true, true, -1, 2, "0.0025", "83919.92", "269717.13")]
|
||||
[DataRow("T+1浮动减点算头不算尾", true, true, false, -1, 9, "-0.021", "-35350.65", "-118631.26")]
|
||||
[DataRow("T+0浮动加点算头不算尾", true, true, false, -1, 9, "0.0025", "79831.29", "260578.53")]
|
||||
[DataRow("T+1浮动减点算头算尾(单利)", false, true, true, -1, 9, "-0.021", "-37124.16", "-123307.03")]
|
||||
[DataRow("T+0浮动加点算头算尾(单利)", false, true, true, -1, 2, "0.0025", "83894.12", "269586.02")]
|
||||
[DataRow("T+1浮动减点算头不算尾(单利)", false, true, false, 0, 9, "-0.021", "-35380.07", "-119411.9")]
|
||||
[DataRow("T+0浮动加点算头不算尾(单利)", false, true, false, -1, 9, "0.0025", "79807.97", "260458.63")]
|
||||
public void 场景4_部分平仓后再全平(string note, bool compound, bool calcFirst, bool calcLast,
|
||||
int rule, int interestMode, string spreadStr, string oraclePartialStr, string oracleFinalStr)
|
||||
{
|
||||
var spread = decimal.Parse(spreadStr, System.Globalization.CultureInfo.InvariantCulture);
|
||||
var oraclePartial = decimal.Parse(oraclePartialStr, System.Globalization.CultureInfo.InvariantCulture);
|
||||
var oracleFinal = decimal.Parse(oracleFinalStr, System.Globalization.CultureInfo.InvariantCulture);
|
||||
var mode = (calcFirst && calcLast) ? "11" : "10";
|
||||
var type = compound ? InterestTypeEnum.复利 : InterestTypeEnum.单利;
|
||||
|
||||
var startDate = spread >= 0 ? new DateTime(2026, 4, 21) : new DateTime(2026, 4, 22);
|
||||
var td = CreateTrade(mode, rule, startDate);
|
||||
var position = CreateFloatPosition(spread, rule, type, startDate, interestMode);
|
||||
|
||||
// 生产写法:部分/全平均传"已缩放的子仓位本金",closePrecent 固定为 1,
|
||||
// 缩放完全体现在名义本金上(CalcNotionalByMode 的 mode2/9 用 posiNotional*closePrecent,
|
||||
// 这里 closePrecent=1,故 posiNotional 必须已是缩放后值,否则会算出整段利息)。
|
||||
// 第一步:2026-05-11 部分平仓 30%(无前置 EOD 快照)
|
||||
var partialCloseNotional = Notional * 0.3m;
|
||||
var partialFlow = CalcCloseFlow(td, position, new DateTime(2026, 5, 11),
|
||||
new List<eod_swap_position>(), partialCloseNotional, partialCloseNotional);
|
||||
// 步骤1 EOD:posiLongNotional=212M(平仓后剩余70%), closeNational=91M(平仓30%)
|
||||
// → oriPosiNotionalValue=303M, closePercent=0.3 → InterestIncomeSum != 0
|
||||
var partialEod = _eod.ExecuteClose(td, position, new DateTime(2026, 5, 11),
|
||||
Notional - partialCloseNotional, 0m, new List<swap_flow_event> { partialFlow }, partialCloseNotional, null);
|
||||
DebugCompare("场景4[部分] " + note, oraclePartial, partialEod.TdCloseInterest, partialEod);
|
||||
AssertStrict(oraclePartial, partialEod.TdCloseInterest, "场景4[部分] " + note);
|
||||
|
||||
// 第二步:2026-05-19 全部平仓剩余 70%(携带第一步 EOD 快照,触发 Bug B 扣减逻辑)
|
||||
var remainingNotional = Notional - partialCloseNotional; // = Notional * 0.7
|
||||
// 全平盘中重算:不传 partialEod(避免 CalcDailySimpleInterest 跳过 5/11 EOD 日)
|
||||
// 不算尾时 partialEod 未结算 5/11 利息,全平需从头重算才能包含 5/11
|
||||
var fullFlow = CalcCloseFlow(td, position, new DateTime(2026, 5, 19),
|
||||
new List<eod_swap_position>(), remainingNotional, remainingNotional);
|
||||
var fullEod = _eod.ExecuteClose(td, position, new DateTime(2026, 5, 19),
|
||||
0m, 0m, new List<swap_flow_event> { fullFlow }, remainingNotional, partialEod);
|
||||
DebugCompare("场景4[全平] " + note, oracleFinal, fullEod.TdCloseInterest, fullEod);
|
||||
AssertStrict(oracleFinal, fullEod.TdCloseInterest, "场景4[全平] " + note);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user