Merge branch 'glms/feature/1.4.2' of http://git.yiliantech.com/gitlab/otc-dev/zszq-trs into glms/feature/1.4.2
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using YLErp.DBModels;
|
||||
@@ -20,6 +21,7 @@ namespace YLErp.Modules.SwapModule
|
||||
public void RegistrationSnapshot_IsPending_AndKeepsBeforeFields()
|
||||
{
|
||||
var info = CreateAction(77, ConsGlobal.InstrumentType.Stock);
|
||||
info.GiveShareAmount = 1m;
|
||||
var before = CreateEodPosition(9, info.UnderlyingCode, 1000m, 100m);
|
||||
|
||||
var snapshot = SwapEodPositionService.BuildCorporateActionEventData(
|
||||
@@ -37,8 +39,9 @@ namespace YLErp.Modules.SwapModule
|
||||
Assert.IsFalse(snapshot.Applied);
|
||||
|
||||
var reason = SwapEventService.BuildCorporateActionEventReason(snapshot);
|
||||
StringAssert.Contains(reason, "BeforeQuantity=1000");
|
||||
StringAssert.Contains(reason, "AfterQuantity=0");
|
||||
StringAssert.Contains(reason, "股权登记日:2026-08-14 发生公司行为(送股)");
|
||||
StringAssert.Contains(reason, "调整前:名义本金:100000 期初标的价格:100 持仓数量:1000");
|
||||
StringAssert.Contains(reason, "调整后:名义本金:0 期初标的价格:0 持仓数量:0");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -65,6 +68,23 @@ namespace YLErp.Modules.SwapModule
|
||||
Assert.IsFalse(SwapEodPositionService.IsCorporateActionInstrument(ConsGlobal.InstrumentType.TBonds));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CorporateActionReason_ShowsOneActionTypeOrActualCashDividend()
|
||||
{
|
||||
AssertActionDescription(
|
||||
new CorporateActionEventData { ExDividendDate = RecordDate, RationedSharesAmount = 1m, GiveShareAmount = 1m, Split = 2m, GiveCashAmount = 10m, CashFlowChange = 1000m },
|
||||
"发生公司行为(配股)");
|
||||
AssertActionDescription(
|
||||
new CorporateActionEventData { ExDividendDate = RecordDate, GiveShareAmount = 1m, Split = 2m, GiveCashAmount = 10m, CashFlowChange = 1000m },
|
||||
"发生公司行为(送股)");
|
||||
AssertActionDescription(
|
||||
new CorporateActionEventData { ExDividendDate = RecordDate, Split = 2m, GiveCashAmount = 10m, CashFlowChange = 1000m },
|
||||
"发生公司行为(拆分)");
|
||||
AssertActionDescription(
|
||||
new CorporateActionEventData { ExDividendDate = RecordDate, GiveCashAmount = 11m, CashFlowChange = 220000m },
|
||||
"发生公司行为(产生分红:220000)");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Rerun_DoesNotCreateDuplicateCorporateActionEvent()
|
||||
{
|
||||
@@ -113,7 +133,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void OperationHistory_PreservesPendingCorporateActionForAudit()
|
||||
public void OperationHistory_HidesPendingCorporateActionUntilItIsApplied()
|
||||
{
|
||||
var info = CreateAction(80, ConsGlobal.InstrumentType.Stock);
|
||||
var pendingData = SwapEodPositionService.BuildCorporateActionEventData(
|
||||
@@ -133,12 +153,17 @@ namespace YLErp.Modules.SwapModule
|
||||
new swap_event { id = 3, EventType = (int)SwapEventTypeEnum.互换, EventData = "{}" }
|
||||
};
|
||||
|
||||
// 操作历史不再隐藏登记日待生效事件;Applied=false 是事件状态,不是展示过滤条件。
|
||||
Assert.AreEqual(3, events.Count);
|
||||
Assert.IsTrue(SwapEventService.TryDeserializeCorporateActionEventData(events[0], out var pendingSnapshot));
|
||||
Assert.IsFalse(pendingSnapshot.Applied);
|
||||
Assert.IsTrue(SwapEventService.TryDeserializeCorporateActionEventData(events[1], out var appliedSnapshot));
|
||||
Assert.IsTrue(appliedSnapshot.Applied);
|
||||
var filter = typeof(SwapEventService).GetMethod(
|
||||
"FilterOperationHistory",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
Assert.IsNotNull(filter, "操作历史必须过滤登记日创建的待生效公司行为事件。");
|
||||
|
||||
var visibleEvents = (List<swap_event>)filter.Invoke(null, new object[] { events });
|
||||
|
||||
Assert.AreEqual(2, visibleEvents.Count);
|
||||
Assert.IsFalse(visibleEvents.Any(x => x.id == 1));
|
||||
Assert.IsTrue(visibleEvents.Any(x => x.id == 2));
|
||||
Assert.IsTrue(visibleEvents.Any(x => x.id == 3));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -283,6 +308,11 @@ namespace YLErp.Modules.SwapModule
|
||||
};
|
||||
}
|
||||
|
||||
private static void AssertActionDescription(CorporateActionEventData data, string expected)
|
||||
{
|
||||
StringAssert.Contains(SwapEventService.BuildCorporateActionEventReason(data), expected);
|
||||
}
|
||||
|
||||
private static eod_swap_position CreateEodPosition(long positionId, string code, decimal quantity, decimal price)
|
||||
{
|
||||
return new eod_swap_position
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
|
||||
namespace YLErp.Modules.SwapModule;
|
||||
|
||||
/// <summary>
|
||||
/// EQD-7084 新“框架合约”Tab 的口径测试。
|
||||
/// 纯计算测试不依赖数据库,直接锁定 EodPnlCalculator 的新口径。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SwapEodPositionRiskNewTabTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void 新口径_普通利息排除保证金_浮动收益剔除费用并保留估值总额()
|
||||
{
|
||||
var floating = new[]
|
||||
{
|
||||
// EOD 归一后,费用已经按我方收益视角落库;支付费用为负数。
|
||||
FloatingLeg("600000", 1, 100m, 0m, -12m, "普通收益互换")
|
||||
};
|
||||
var interests = new[]
|
||||
{
|
||||
InterestLeg(1, (int)InterestModeEnum.固定值, 80m, 0.02m, 0.02m),
|
||||
InterestLeg(1, (int)InterestModeEnum.初始预付金, 20m, 0.01m, 0.01m)
|
||||
};
|
||||
|
||||
var fields = InvokeCalculation(
|
||||
floating,
|
||||
interests,
|
||||
structureType: "普通收益互换",
|
||||
notionalValue: 1_000m,
|
||||
startDate: new DateTime(2026, 1, 1),
|
||||
maturityDate: new DateTime(2026, 12, 31),
|
||||
periodAmount: 5m,
|
||||
dividendPayDate: 0);
|
||||
|
||||
Assert.AreEqual(100m, GetDecimal(fields, "FloatingUnrealizedPnl"), 0.0001m,
|
||||
"新浮动端待实现收益应排除 PosiFeePending:PosiProfitSum(88) - PosiFeePending(-12) = 100");
|
||||
Assert.AreEqual(-12m, GetDecimal(fields, "OpeningClosingFee"), 0.0001m,
|
||||
"开平仓费用直接使用 EOD 已归一的 PosiFeePending");
|
||||
Assert.AreEqual(80m, GetDecimal(fields, "OrdinaryInterestPnl"), 0.0001m,
|
||||
"利息端待实现收益应排除初始/维持保证金腿");
|
||||
Assert.AreEqual(-20m, GetDecimal(fields, "MarginInterestAmount"), 0.0001m,
|
||||
"保证金利息仍应按保证金腿方向计入估值");
|
||||
Assert.AreEqual(153m, GetDecimal(fields, "MaturityNettingValuation"), 0.0001m,
|
||||
"估值应保持旧口径:100 - 12 + 80 - 20 + 5 = 153;费用只计一次");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 新口径_当前利率合计使用普通利息腿TdInterestRate_并识别FR007()
|
||||
{
|
||||
var fr007Leg = InterestLeg(2, (int)InterestModeEnum.合约名义本金规模, 20m, 0.03m, 0.018m);
|
||||
fr007Leg.FloatRateUnderlyingCode = "FR007";
|
||||
fr007Leg.FloatRate = 0.015m;
|
||||
var fields = InvokeCalculation(
|
||||
new[] { FloatingLeg("600001", 2, 100m, 0m, 0m, "普通收益互换") },
|
||||
new[]
|
||||
{
|
||||
InterestLeg(1, (int)InterestModeEnum.固定值, 10m, 0.02m, 0.0125m),
|
||||
fr007Leg
|
||||
},
|
||||
structureType: "普通收益互换",
|
||||
notionalValue: 100m,
|
||||
startDate: new DateTime(2026, 2, 1),
|
||||
maturityDate: new DateTime(2026, 8, 1),
|
||||
periodAmount: 0m,
|
||||
dividendPayDate: 1);
|
||||
|
||||
Assert.AreEqual(0.0305m, GetDecimal(fields, "InterestRatePrice"), 0.0000001m,
|
||||
"利率端价格应为普通利息腿当前 TdInterestRate 合计,而非默认利差合计");
|
||||
Assert.AreEqual("FR007", GetString(fields, "InterestBenchmark"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 新口径_普通利息腿无FR007时基准为固定利率()
|
||||
{
|
||||
var fields = InvokeCalculation(
|
||||
new[] { FloatingLeg("600002", 1, 100m, 0m, 0m, "普通收益互换") },
|
||||
new[] { InterestLeg(1, (int)InterestModeEnum.固定值, 10m, 0.02m, 0.0125m) },
|
||||
structureType: "普通收益互换",
|
||||
notionalValue: 100m,
|
||||
startDate: new DateTime(2026, 2, 1),
|
||||
maturityDate: new DateTime(2026, 8, 1),
|
||||
periodAmount: 0m,
|
||||
dividendPayDate: 1);
|
||||
|
||||
Assert.AreEqual("固定利率", GetString(fields, "InterestBenchmark"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 新口径_债券期初价格按风险页约定放大百分价格_并保留合同字段()
|
||||
{
|
||||
var fields = InvokeCalculation(
|
||||
new[] { FloatingLeg("110000", 1, 99.12m, 0m, 0m, "普通债券类收益互换", "Bond") },
|
||||
new[] { InterestLeg(1, (int)InterestModeEnum.固定值, 1m, 0.01m, 0.01m) },
|
||||
structureType: "普通债券类收益互换",
|
||||
notionalValue: 9_900m,
|
||||
startDate: new DateTime(2026, 3, 1),
|
||||
maturityDate: new DateTime(2027, 3, 1),
|
||||
periodAmount: 0m,
|
||||
dividendPayDate: 1);
|
||||
|
||||
Assert.AreEqual(99.12m, GetDecimal(fields, "InitialPrice"), 0.0001m,
|
||||
"债券日终 PosiGrossPrice 已由 SetPosiPrice 按风险页口径缩放,新接口不能再次乘 100");
|
||||
Assert.AreEqual(9_900m, GetDecimal(fields, "NotionalQuantity"), 0.0001m);
|
||||
Assert.AreEqual("多头", GetString(fields, "UnderlyingDirection"));
|
||||
Assert.AreEqual("110000", GetString(fields, "UnderlyingCode"));
|
||||
Assert.AreEqual("Bond", GetString(fields, "UnderlyingInstrumentType"));
|
||||
Assert.AreEqual(new DateTime(2026, 3, 1), GetDate(fields, "ContractStartDate"));
|
||||
Assert.AreEqual(new DateTime(2027, 3, 1), GetDate(fields, "ContractMaturityDate"));
|
||||
}
|
||||
|
||||
private static object InvokeCalculation(
|
||||
IEnumerable<eod_swap_position> floating,
|
||||
IEnumerable<eod_swap_position> interests,
|
||||
string structureType,
|
||||
decimal notionalValue,
|
||||
DateTime startDate,
|
||||
DateTime maturityDate,
|
||||
decimal periodAmount,
|
||||
int dividendPayDate)
|
||||
{
|
||||
return EodPnlCalculator.CalculateEodSwapRiskNewFields(
|
||||
floating,
|
||||
interests,
|
||||
structureType,
|
||||
notionalValue,
|
||||
startDate,
|
||||
maturityDate,
|
||||
periodAmount,
|
||||
dividendPayDate);
|
||||
}
|
||||
|
||||
private static decimal GetDecimal(object fields, string name)
|
||||
=> Convert.ToDecimal(fields.GetType().GetProperty(name)!.GetValue(fields));
|
||||
|
||||
private static string GetString(object fields, string name)
|
||||
=> (string)fields.GetType().GetProperty(name)!.GetValue(fields)!;
|
||||
|
||||
private static DateTime GetDate(object fields, string name)
|
||||
=> (DateTime)fields.GetType().GetProperty(name)!.GetValue(fields)!;
|
||||
|
||||
private static eod_swap_position FloatingLeg(
|
||||
string code,
|
||||
int positionType,
|
||||
decimal mtm,
|
||||
decimal dividend,
|
||||
decimal fee,
|
||||
string structureType,
|
||||
string instrumentType = null)
|
||||
=> new()
|
||||
{
|
||||
UnderlyingCode = code,
|
||||
UnderlyingInstrumentType = instrumentType ?? structureType,
|
||||
PositionType = positionType,
|
||||
PosiGrossPrice = mtm,
|
||||
PosiMtmPnL = mtm,
|
||||
PosiDividendSum = dividend,
|
||||
PosiFeePending = fee,
|
||||
PosiProfitSum = mtm + dividend + fee,
|
||||
PosiNotionalValue = 100m
|
||||
};
|
||||
|
||||
private static eod_swap_position InterestLeg(
|
||||
int direction,
|
||||
int mode,
|
||||
decimal profit,
|
||||
decimal defaultRate,
|
||||
decimal currentRate)
|
||||
=> new()
|
||||
{
|
||||
InterestDirection = direction,
|
||||
InterestMode = mode,
|
||||
InterestProfitSum = profit,
|
||||
InterestRateDefault = defaultRate,
|
||||
TdInterestRate = currentRate
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user