- Determinism_NoTail_EventFloatRateIndependentOfPublishTime:尾日价缺(上午) vs 有(下午)两世界,金额与事件 FloatRate 完全一致(=末段已消费利率0.01425) - CloseOnly_平仓日为重置日_剩余持仓快照再定盘(+非重置日反例):经 DealInterests 真路由断言快照 FloatRate=当日新定盘(RefixCalls 计数)——载体职责显式化验收 - CalcFirst 用例去掉 closeRate 免疫 hack:priorValueDate 根治后首重置日恒取价 - CI_007 更名+断言翻转:平仓日=重置日+不算尾 → 事件利率=末段已消费利率(旧); 原断言(取新定盘)正是被替换的旧口径契约,根因注释留痕 GLMS-JIATT-20260805 - CI_008:本金结转断言保留,利率断言改为末段旧利率 验证:ConsumedInterestScenario 9/9、InterestEodTailSnapshot+Fr007 36/36、 全量 976 例 145 败与基线 diff=0
507 lines
26 KiB
C#
507 lines
26 KiB
C#
using Newtonsoft.Json;
|
||
using YLErp.DBModels;
|
||
using YLErp.DBModels.Enums;
|
||
|
||
namespace YLErp.Modules.SwapModule
|
||
{
|
||
/// <summary>
|
||
/// 复利 consumedInterest 扣除 - 合成单元测试
|
||
/// ============================================================================
|
||
/// 验证 c6adb3bb 的修复:复利路径平仓时,扣除历史已通过互换结出的利息。
|
||
///
|
||
/// 核心场景:
|
||
/// 一笔复利交易,N天后做了互换结算(已结N天利息),之后再平仓。
|
||
/// 平仓默认值应 = 从头算的全程利息 - 已结利息(consumedInterest)。
|
||
/// 如果不扣(bug),平仓默认值 = 全程利息(偏大)。
|
||
/// 如果多扣(之前单利的错误),平仓默认值 = 0或负(偏小)。
|
||
///
|
||
/// 模仿 GetInterestsUnitTest_T0 的 StubSwapDealService 模式。
|
||
/// ============================================================================
|
||
[TestClass]
|
||
public class ConsumedInterestScenarioTest
|
||
{
|
||
#region 常量
|
||
|
||
private const decimal Principal = 1000m;
|
||
private const decimal FixedRate = 0.0025m; // 加点利率
|
||
private const double FloatRate = 0.001; // FR007
|
||
private const decimal TotalRate = FixedRate + (decimal)FloatRate; // 综合年化利率
|
||
private const int AnnualDays = 365;
|
||
private const int ResetPeriod = 3;
|
||
private static readonly DateTime StartDate = new(2026, 4, 27);
|
||
private static readonly DateTime ExerciseDate = new(2027, 4, 27);
|
||
|
||
#endregion
|
||
|
||
#region Stub:内存 SwapDealService + consumedInterest 注入
|
||
|
||
/// <summary>
|
||
/// 继承 SwapDealService,override 两个虚方法:
|
||
/// - TryGetFloatRate:返回固定浮动利率(不连库)
|
||
/// - GetConsumedInterest:返回注入的历史已结利息(不连库)
|
||
/// </summary>
|
||
private sealed class StubSwapDealService : SwapDealService
|
||
{
|
||
private readonly double _floatRate;
|
||
private readonly decimal _consumedInterest;
|
||
private readonly Func<DateTime, double> _floatRateByDate;
|
||
|
||
public StubSwapDealService(OptUserInfo optUser, double floatRate, decimal consumedInterest)
|
||
: base(optUser)
|
||
{
|
||
_floatRate = floatRate;
|
||
_consumedInterest = consumedInterest;
|
||
_floatRateByDate = null;
|
||
}
|
||
|
||
/// <summary>按查询日期返回不同浮动利率(用于复现重置日取价 bug)</summary>
|
||
public StubSwapDealService(OptUserInfo optUser, Func<DateTime, double> floatRateByDate, decimal consumedInterest = 0m)
|
||
: base(optUser)
|
||
{
|
||
_floatRate = 0;
|
||
_consumedInterest = consumedInterest;
|
||
_floatRateByDate = floatRateByDate;
|
||
}
|
||
|
||
protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate)
|
||
{
|
||
rate = _floatRateByDate != null ? _floatRateByDate(valueDate) : _floatRate;
|
||
return true;
|
||
}
|
||
|
||
public override decimal GetConsumedInterest(int tradeId, long positionId, DateTime beforeDate)
|
||
{
|
||
return _consumedInterest; // 返回注入值
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 数据构建
|
||
|
||
private static trade CreateTrade()
|
||
{
|
||
return new trade
|
||
{
|
||
id = 1, TradeNumber = "UT-CONSUMED-001", ClientId = 999998,
|
||
TradeType = "收益互换", TradeDate = StartDate, StartDate = StartDate,
|
||
ExerciseDate = ExerciseDate, TradeStatus = "确认成交", ValidState = "Valid",
|
||
StructureType = "单标的", QuoteCurrency = "CNY", SettlementCurrency = "CNY",
|
||
trade_extend = new trade_extend
|
||
{
|
||
TradeId = 1,
|
||
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||
{
|
||
AnnualDays = AnnualDays,
|
||
InterestCalcMode = "10", // 算头不算尾
|
||
SettlementRules = 0
|
||
})
|
||
}
|
||
};
|
||
}
|
||
|
||
private static swap_position CreateCompoundPosition()
|
||
{
|
||
return new swap_position
|
||
{
|
||
id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown,
|
||
InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.标的期初全价,
|
||
InterestRateDefault = FixedRate, InterestPrincipalFix = Principal,
|
||
PosiStartDate = StartDate, PosiMatuirityDate = ExerciseDate,
|
||
IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.复利,
|
||
IsAnnualized = true, interest_rest_days = ResetPeriod, interest_rule = 0,
|
||
FloatRateUnderlyingCode = "FR007",
|
||
InterestSwapInterval = JsonConvert.SerializeObject(new List<IntervalModel>
|
||
{
|
||
new IntervalModel { Date = ExerciseDate, Rate = FixedRate, Settlement = 0 }
|
||
})
|
||
};
|
||
}
|
||
|
||
/// <summary>调用 GetInterests 获取复利利息(统一调用入口,settment:false走盘中平仓路径)</summary>
|
||
private static swap_flow_event CalcCompoundUnwind(StubSwapDealService service, DateTime unwindDate, decimal closePercent = 1m)
|
||
{
|
||
var td = CreateTrade();
|
||
var position = CreateCompoundPosition();
|
||
var interests = service.GetInterests(td, td.trade_extend, unwindDate, unwindDate,
|
||
new List<eod_swap_position>(), new List<swap_position> { position },
|
||
Principal, Principal, closePercent,
|
||
(int)SwapEventTypeEnum.平仓, false, Principal,
|
||
add: false, settment: false, newCalcLast: false);
|
||
Assert.AreEqual(1, interests.Count);
|
||
return interests[0];
|
||
}
|
||
|
||
private static StubSwapDealService CreateService(decimal consumedInterest)
|
||
{
|
||
return new StubSwapDealService(
|
||
new OptUserInfo(0, nameof(ConsumedInterestScenarioTest), OptUserFrom.UnitTest),
|
||
FloatRate, consumedInterest);
|
||
}
|
||
|
||
private static void AssertDecimal(decimal expected, decimal actual, string message = "")
|
||
{
|
||
var tolerance = 1m / (decimal)Math.Pow(10, ConsGlobal.PriceRound - 2);
|
||
Assert.IsTrue(Math.Abs(expected - actual) <= tolerance,
|
||
$"{message} Expected: {expected}, Actual: {actual}, Diff: {expected - actual}");
|
||
}
|
||
|
||
#endregion
|
||
|
||
// ================================================================
|
||
// 场景1:基线——无历史互换(consumedInterest=0),拿到全程复利利息
|
||
// ================================================================
|
||
|
||
/// <summary>
|
||
/// [CI_001] 无历史互换结清,复利平仓利息基线
|
||
/// ---------------------------------------------------------------
|
||
/// consumedInterest=0,平仓利息=从头算的全程复利利息。
|
||
/// 此值作为后续场景的参照基线(避免独立复利计算的精度匹配问题)。
|
||
/// ---------------------------------------------------------------
|
||
/// </summary>
|
||
[TestMethod]
|
||
public void CI_001_无历史互换平仓利息基线()
|
||
{
|
||
var unwindDate = StartDate.AddDays(10); // 4/27+10=5/7,算头不算尾约9天
|
||
var service = CreateService(consumedInterest: 0m);
|
||
var result = CalcCompoundUnwind(service, unwindDate);
|
||
|
||
Assert.IsTrue(result.InterestAmount > 0, "无互换时复利利息应>0");
|
||
Console.WriteLine($"基线(consumedInterest=0): InterestAmount={result.InterestAmount:F6}");
|
||
}
|
||
|
||
// ================================================================
|
||
// 场景2:consumedInterest>0 → 平仓利息=基线-consumedInterest
|
||
// ================================================================
|
||
|
||
/// <summary>
|
||
/// [CI_002] 注入consumedInterest后,平仓利息应=基线-consumedInterest
|
||
/// ---------------------------------------------------------------
|
||
/// 用相同参数但注入不同的consumedInterest,验证:
|
||
/// 利息(有consumed) = 利息(无consumed) - consumedInterest
|
||
/// 这是验证cs:793 `interest -= consumedInterest` 的直接方式。
|
||
/// ---------------------------------------------------------------
|
||
/// </summary>
|
||
[TestMethod]
|
||
public void CI_002_consumedInterest正确扣除()
|
||
{
|
||
var unwindDate = StartDate.AddDays(10);
|
||
|
||
// 基线:consumedInterest=0
|
||
var baselineResult = CalcCompoundUnwind(CreateService(0m), unwindDate);
|
||
decimal baseline = baselineResult.InterestAmount;
|
||
|
||
// 注入consumedInterest=基线的50%
|
||
decimal consumed = baseline * 0.5m;
|
||
var consumedResult = CalcCompoundUnwind(CreateService(consumed), unwindDate);
|
||
|
||
// 期望 = 基线 - consumed
|
||
decimal expected = baseline - consumed;
|
||
AssertDecimal(expected, consumedResult.InterestAmount,
|
||
$"平仓利息应=基线({baseline:F6})-consumed({consumed:F6})={expected:F6}");
|
||
Console.WriteLine($"基线={baseline:F6}, consumed={consumed:F6}");
|
||
Console.WriteLine($"平仓利息={consumedResult.InterestAmount:F6}, 期望={expected:F6} ✅");
|
||
}
|
||
|
||
// ================================================================
|
||
// 场景3:守恒——consumed + 平仓利息 = 基线
|
||
// ================================================================
|
||
|
||
/// <summary>
|
||
/// [CI_003] 守恒:consumedInterest + 平仓利息(扣后) = 基线(无consumed)
|
||
/// ---------------------------------------------------------------
|
||
/// 注入任意consumedInterest,验证 consumed + 利息 = 基线。
|
||
/// 如果扣多了(守恒不成立→合计<基线)或没扣(合计>基线),测试失败。
|
||
/// ---------------------------------------------------------------
|
||
/// </summary>
|
||
[TestMethod]
|
||
public void CI_003_守恒consumed加平仓等于基线()
|
||
{
|
||
var unwindDate = StartDate.AddDays(10);
|
||
decimal baseline = CalcCompoundUnwind(CreateService(0m), unwindDate).InterestAmount;
|
||
|
||
// 注入不同的consumedInterest验证守恒
|
||
decimal[] testConsumed = { baseline * 0.3m, baseline * 0.5m, baseline * 0.8m };
|
||
foreach (var consumed in testConsumed)
|
||
{
|
||
var result = CalcCompoundUnwind(CreateService(consumed), unwindDate);
|
||
decimal actual = consumed + result.InterestAmount;
|
||
AssertDecimal(baseline, actual,
|
||
$"守恒: consumed({consumed:F6}) + 利息({result.InterestAmount:F6}) = {actual:F6} 应=基线({baseline:F6})");
|
||
Console.WriteLine($"consumed={consumed:F6} + 利息={result.InterestAmount:F6} = {actual:F6} = 基线{baseline:F6} ✅");
|
||
}
|
||
}
|
||
|
||
// ================================================================
|
||
// 场景4:consumedInterest=全部基线 → 平仓利息≈0,不为负
|
||
// ================================================================
|
||
|
||
/// <summary>
|
||
/// [CI_004] 全部利息已结清(consumedInterest=基线),再平仓利息应≈0
|
||
/// ---------------------------------------------------------------
|
||
/// 验证不会扣过头变成负数(之前单利双重扣减的错误)。
|
||
/// 复利从头算全程 - 全程consumed = 0,应精确归零或微小正值。
|
||
/// ---------------------------------------------------------------
|
||
/// </summary>
|
||
[TestMethod]
|
||
public void CI_004_全部已结再平仓利息不为负()
|
||
{
|
||
var unwindDate = StartDate.AddDays(10);
|
||
decimal baseline = CalcCompoundUnwind(CreateService(0m), unwindDate).InterestAmount;
|
||
|
||
// consumedInterest=全部基线
|
||
var result = CalcCompoundUnwind(CreateService(baseline), unwindDate);
|
||
|
||
Console.WriteLine($"基线={baseline:F6}, consumed={baseline:F6}, 平仓利息={result.InterestAmount:F6}");
|
||
Assert.IsTrue(result.InterestAmount >= -0.01m,
|
||
$"全部已结再平仓利息应≈0(实际={result.InterestAmount:F6}),不应为负");
|
||
Console.WriteLine($"全部已结平仓≈0({result.InterestAmount:F6})✅");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void CI_005_partialClose_scalesConsumedInterest()
|
||
{
|
||
var unwindDate = StartDate.AddDays(10);
|
||
const decimal closePercent = 0.4m;
|
||
const decimal consumed = 100m;
|
||
|
||
var baseline = CalcCompoundUnwind(CreateService(0m), unwindDate, closePercent).InterestAmount;
|
||
var result = CalcCompoundUnwind(CreateService(consumed), unwindDate, closePercent).InterestAmount;
|
||
|
||
AssertDecimal(baseline - consumed * closePercent, result,
|
||
$"partial close should deduct consumed interest by closePercent ({closePercent})");
|
||
}
|
||
|
||
// ================================================================
|
||
// 场景7:平仓日=重置日 + calcLast=false 的事件利率口径(EQD-6968 自洽化后)
|
||
// ================================================================
|
||
|
||
/// <summary>
|
||
/// [CI_007] 平仓日=重置日 + calcLast=false:排除日不取价,事件利率=末段已消费利率(确定性)
|
||
/// ----------------------------------------------------------------
|
||
/// 历史(GLMS-JIATT-20260805):原缺陷是重置日取价被 calcLast 跳过 → flowEvent.FloatRate 停留旧值
|
||
/// → 落库后传染 EOD。当时的修复=排除日"有价则取新定盘",事件利率因而取决于平仓时刻
|
||
/// (上午=旧/下午=新),与金额实际使用的利率脱钩。
|
||
///
|
||
/// EQD-6968 自洽化后的新契约:
|
||
/// ① 已平部分:排除日一概不取价(有价也不取),事件 FloatRate=末段已消费利率(rateOld),
|
||
/// 与金额同源、与平仓时刻无关;
|
||
/// ② 剩余持仓的新周期利率:由 EOD 快照"重置日再定盘"显式获取
|
||
/// (InterestEodTailSnapshotTest.CloseOnly_平仓日为重置日_剩余持仓快照再定盘)。
|
||
///
|
||
/// 构造(避开周末,period=7):PosiStartDate=6/1(周一), 平仓日=6/8(周一,重置日,7%7=0);
|
||
/// FR007 分界:取价日>=6/8 返回 rateNew,否则 rateOld。
|
||
/// ----------------------------------------------------------------
|
||
/// </summary>
|
||
[TestMethod]
|
||
public void CI_007_平仓日等于重置日_calcLast_false_事件利率为末段已消费利率()
|
||
{
|
||
const double rateOld = 0.001;
|
||
const double rateNew = 0.002;
|
||
// 用 6 月日期避开五一/周末:PosiStartDate=6/1(周一), period=7, 平仓日=6/8(周一,重置日)
|
||
DateTime posiStart = new DateTime(2026, 6, 1);
|
||
DateTime unwindDate = new DateTime(2026, 6, 8); // (6/8-6/1)=7, 7%7=0 重置日
|
||
DateTime newRateFrom = new DateTime(2026, 6, 8); // 6/8(查询日,周一工作日)起为新利率
|
||
|
||
StubSwapDealService ServiceByDate() => new StubSwapDealService(
|
||
new OptUserInfo(0, nameof(ConsumedInterestScenarioTest), OptUserFrom.UnitTest),
|
||
d => d >= newRateFrom ? rateNew : rateOld);
|
||
|
||
var td = new trade
|
||
{
|
||
id = 1, TradeNumber = "UT-CI007", ClientId = 999998,
|
||
TradeType = "收益互换", TradeDate = posiStart, StartDate = posiStart,
|
||
ExerciseDate = ExerciseDate, TradeStatus = "确认成交", ValidState = "Valid",
|
||
StructureType = "单标的", QuoteCurrency = "CNY", SettlementCurrency = "CNY",
|
||
trade_extend = new trade_extend
|
||
{
|
||
TradeId = 1,
|
||
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||
{
|
||
AnnualDays = AnnualDays, InterestCalcMode = "10", SettlementRules = 0
|
||
})
|
||
}
|
||
};
|
||
var position = new swap_position
|
||
{
|
||
id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown,
|
||
InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.标的期初全价,
|
||
InterestRateDefault = FixedRate, InterestPrincipalFix = Principal,
|
||
PosiStartDate = posiStart, PosiMatuirityDate = ExerciseDate,
|
||
IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.复利,
|
||
IsAnnualized = true, interest_rest_days = 7, interest_rule = 0,
|
||
FloatRateUnderlyingCode = "FR007",
|
||
InterestSwapInterval = JsonConvert.SerializeObject(new List<IntervalModel>
|
||
{
|
||
new IntervalModel { Date = ExerciseDate, Rate = FixedRate, Settlement = 0 }
|
||
})
|
||
};
|
||
|
||
var interests = ServiceByDate().GetInterests(td, td.trade_extend, unwindDate, unwindDate,
|
||
new List<eod_swap_position>(), new List<swap_position> { position },
|
||
Principal, Principal, 1m,
|
||
(int)SwapEventTypeEnum.平仓, false, Principal,
|
||
add: false, settment: false, newCalcLast: false);
|
||
|
||
Assert.AreEqual(1, interests.Count);
|
||
var result = interests[0];
|
||
Console.WriteLine($"6/8(重置日,周一)平仓:FloatRate={result.FloatRate} Amount={result.InterestAmount:F6}");
|
||
Console.WriteLine($" 新口径期望 FloatRate={rateOld}(排除日不取价,事件利率=末段已消费利率)");
|
||
|
||
// 核心断言(EQD-6968 自洽化契约):排除日(不计息)一概不取价——即使 6/8 新定盘已发布,
|
||
// 事件 FloatRate 也必须是末段已消费利率 rateOld,与金额同源、与平仓时刻无关。
|
||
// 剩余持仓的新周期利率由 EOD 快照"重置日再定盘"显式获取(见 InterestEodTailSnapshotTest)。
|
||
Assert.IsTrue(Math.Abs((result.FloatRate ?? 0) - (decimal)rateOld) < 0.0001m,
|
||
$"排除日不取价:FloatRate 应=末段已消费利率 {rateOld}。实际={result.FloatRate}," +
|
||
$"若={rateNew} 说明排除日仍在取价(旧口径:记录利率取决于平仓时刻)");
|
||
|
||
var interestBeforeResetDate = Principal * (FixedRate + (decimal)rateOld) * 7m / AnnualDays;
|
||
AssertDecimal(Principal + interestBeforeResetDate, result.InterestPrincipal,
|
||
"calcLast=false 的重置日仍应将前 7 天复利并入本金");
|
||
AssertDecimal(interestBeforeResetDate, result.InterestAmount,
|
||
"calcLast=false 不应计入重置日当天利息");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void CI_008_ResetDayPartialCloseCarriesRemainingInterestIntoPrincipal()
|
||
{
|
||
const decimal previousPrincipal = 50061728.39m;
|
||
const decimal remainingPrincipal = 30037037.04m;
|
||
const decimal previousInterest = 7425.050203320057m;
|
||
const decimal fixedRate = 0.001234m;
|
||
const double oldFloatRate = 0.0123;
|
||
const double newFloatRate = 0.0213;
|
||
var startDate = new DateTime(2026, 7, 28);
|
||
var resetDate = new DateTime(2026, 8, 4);
|
||
var service = new StubSwapDealService(
|
||
new OptUserInfo(0, nameof(ConsumedInterestScenarioTest), OptUserFrom.UnitTest),
|
||
d => d >= resetDate ? newFloatRate : oldFloatRate);
|
||
var td = CreateTrade();
|
||
td.StartDate = startDate;
|
||
td.TradeDate = startDate;
|
||
var position = new swap_position
|
||
{
|
||
id = 1001, SwapTradeId = td.id,
|
||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||
InterestMode = (int)InterestModeEnum.标的期初全价,
|
||
InterestType = (int)InterestTypeEnum.复利,
|
||
InterestRateDefault = fixedRate,
|
||
PosiStartDate = startDate, PosiMatuirityDate = ExerciseDate,
|
||
IsAnnualized = true, interest_rest_days = 7, interest_rule = 0,
|
||
FloatRateUnderlyingCode = "FR007",
|
||
InterestSwapInterval = JsonConvert.SerializeObject(new List<IntervalModel>
|
||
{
|
||
new IntervalModel { Date = ExerciseDate, Rate = fixedRate, Settlement = 0 }
|
||
})
|
||
};
|
||
var preEod = new eod_swap_position
|
||
{
|
||
id = 1, PositionId = position.id, ValueDate = resetDate.AddDays(-1),
|
||
TdInterestPrincipal = previousPrincipal,
|
||
InterestIncomeSum = previousInterest,
|
||
InterestProfitSum = previousInterest,
|
||
FloatRate = (decimal)oldFloatRate
|
||
};
|
||
|
||
var result = service.GetInterests(td, td.trade_extend, resetDate, resetDate,
|
||
new List<eod_swap_position> { preEod }, new List<swap_position> { position },
|
||
remainingPrincipal, remainingPrincipal, 1m,
|
||
(int)SwapEventTypeEnum.平仓, true, remainingPrincipal,
|
||
add: false, settment: false, newCalcLast: false).Single();
|
||
|
||
var remainingInterest = previousInterest * remainingPrincipal / previousPrincipal;
|
||
var expectedPrincipal = remainingPrincipal + remainingInterest;
|
||
AssertDecimal(expectedPrincipal, result.InterestPrincipal);
|
||
// EQD-6968 自洽化:排除日(平仓日=重置日)不取价,事件利率=末段已消费利率(旧)——与金额同源、
|
||
// 与平仓时刻无关。剩余持仓的新周期利率由 EOD 快照"重置日再定盘"显式获取
|
||
// (InterestEodTailSnapshotTest.CloseOnly_平仓日为重置日_剩余持仓快照再定盘)。
|
||
AssertDecimal((decimal)oldFloatRate, result.FloatRate.Value,
|
||
"排除日不取价:事件 FloatRate 应=末段已消费旧利率");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void CI_009_NonResetUnwindWithCalcLastFalseUsesPreviousEodPendingInterest()
|
||
{
|
||
const decimal pendingInterest = 10019.043756537721m;
|
||
const decimal remainingPrincipal = 30041492.070122881942m;
|
||
var startDate = new DateTime(2026, 7, 28);
|
||
var unwindDate = new DateTime(2026, 8, 7);
|
||
var td = CreateTrade();
|
||
td.StartDate = startDate;
|
||
td.TradeDate = startDate;
|
||
var position = new swap_position
|
||
{
|
||
id = 1001, SwapTradeId = td.id,
|
||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||
InterestMode = (int)InterestModeEnum.标的期初全价,
|
||
InterestType = (int)InterestTypeEnum.复利,
|
||
InterestRateDefault = 0.001234m,
|
||
PosiStartDate = startDate, PosiMatuirityDate = ExerciseDate,
|
||
IsAnnualized = true, interest_rest_days = 7,
|
||
FloatRateUnderlyingCode = "FR007"
|
||
};
|
||
var preEod = new eod_swap_position
|
||
{
|
||
id = 1, PositionId = position.id, ValueDate = unwindDate.AddDays(-1),
|
||
TdInterestPrincipal = remainingPrincipal,
|
||
InterestIncomeSum = pendingInterest,
|
||
InterestProfitSum = pendingInterest,
|
||
FloatRate = 0.0213m
|
||
};
|
||
var service = new StubSwapDealService(
|
||
new OptUserInfo(0, nameof(ConsumedInterestScenarioTest), OptUserFrom.UnitTest), 0.0213, 0m);
|
||
|
||
var result = service.GetInterests(td, td.trade_extend, unwindDate, unwindDate,
|
||
new List<eod_swap_position> { preEod }, new List<swap_position> { position },
|
||
remainingPrincipal, remainingPrincipal, 1m,
|
||
(int)SwapEventTypeEnum.平仓, false, remainingPrincipal,
|
||
add: false, settment: false, newCalcLast: false).Single();
|
||
|
||
AssertDecimal(pendingInterest, result.InterestAmount,
|
||
"calcLast=false must not accrue unwind-date interest after the previous EOD");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void CI_010_EodResetWithoutCloseCarriesFullPendingInterest()
|
||
{
|
||
const decimal principal = 303139117.80m;
|
||
const decimal previousBase = 303230391.742592383565m;
|
||
const decimal pendingInterest = 184331.611361300669m;
|
||
var startDate = new DateTime(2026, 4, 21);
|
||
var resetDate = new DateTime(2026, 4, 28);
|
||
var position = new swap_position
|
||
{
|
||
PosiStartDate = startDate,
|
||
InterestType = (int)InterestTypeEnum.复利,
|
||
InterestRateDefault = 0.0025m,
|
||
InterestSwapInterval = JsonConvert.SerializeObject(new List<IntervalModel>()),
|
||
IsAnnualized = true,
|
||
interest_rest_days = 7,
|
||
FloatRateUnderlyingCode = "FR007"
|
||
};
|
||
var preEod = new eod_swap_position
|
||
{
|
||
id = 1,
|
||
ValueDate = resetDate.AddDays(-1),
|
||
TdInterestPrincipal = previousBase,
|
||
InterestIncomeSum = pendingInterest,
|
||
InterestProfitSum = pendingInterest,
|
||
FloatRate = 0.013502m
|
||
};
|
||
var flowEvent = new swap_flow_event { InterestRate = 0.0025m };
|
||
var service = new StubSwapDealService(
|
||
new OptUserInfo(0, nameof(ConsumedInterestScenarioTest), OptUserFrom.UnitTest),
|
||
d => 0.0139);
|
||
decimal interestAmount = 0m;
|
||
decimal tdInterestAmount = 0m;
|
||
|
||
service.CalcDailyCompoundInterestByEod(preEod, resetDate, startDate, position,
|
||
principal, principal, flowEvent, AnnualDays, 0.013502m, 1m,
|
||
ref interestAmount, ref tdInterestAmount);
|
||
|
||
AssertDecimal(principal + pendingInterest, flowEvent.InterestPrincipal,
|
||
"无平仓重置日必须完整并入上一期累计待实现利息");
|
||
}
|
||
}
|
||
}
|