refactor(test): SwapDealSettlementTest拆分为SwapUnwind/SwapIncome+提取共享stub
按业务拆分原 SwapDealSettlementTest(8场景)为3个文件,提升可读性: - TestableSwapDealService.cs: 共享stub(7个seam override)+工厂方法(CreateTrade/CreateUnwindData) - SwapUnwindScenarioTest.cs: 平仓全流程6场景(UW_001~006: 全平/部分平/含预付金/含费价重算/审核/提交) - SwapIncomeScenarioTest.cs: 结息2场景(SI_001~002: 正常结息/含预付金返息) 命名对齐 testable 分支风格(SwapUnwindScenarioTest),场景前缀改 UW_/SI_ 更直观。 删除 SwapDealSettlementTest.cs(内容已拆分迁移)。 SwapModule 173测试全绿,无回归。
This commit is contained in:
@@ -1,426 +0,0 @@
|
||||
using Newtonsoft.Json;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// SwapDealService 手动结算(SwapIncome/SwapUnwind)内存单元测试
|
||||
/// ============================================================================
|
||||
/// 背景:SwapIncome/SwapUnwind 是写客户资金流水(ClientCashInCashOut)的核心入口,
|
||||
/// 此前零单元测试(仅 DBRecording,CI 不跑)。本测试通过 7 个 virtual seam
|
||||
/// 把 DB/事务/外部服务打桩,在纯内存下验证控制流、资金流水金额、持仓状态变更。
|
||||
///
|
||||
/// 命名规范说明(见《互换价格字段命名规范决策文档》):
|
||||
/// 本测试引用现状字段(如 PosiGrossPrice/PosiNetPrice)时加对照注释,
|
||||
/// 标明其真实含义与规范名,让测试可读、可作规范示范。
|
||||
/// - PosiGrossPrice 现状名,实为"期初全价不含费",规范名 EntryDirtyPrice
|
||||
/// - PosiNetPrice 现状名,实为"期初全价含费"(非净价!),规范名 EntryDirtyFeePrice
|
||||
/// ============================================================================
|
||||
[TestClass]
|
||||
public class SwapDealSettlementTest
|
||||
{
|
||||
private const int SwapTradeId = 7700;
|
||||
private static readonly DateTime ValueDate = new(2026, 6, 15);
|
||||
private static readonly DateTime UnwindDate = new(2026, 6, 16);
|
||||
|
||||
#region Stub
|
||||
|
||||
/// <summary>
|
||||
/// 继承 SwapDealService,override 7 个 seam,把 DB/事务/外部服务替换为内存收集器。
|
||||
/// 生产路径零改动(seam 生产实现 = 原逻辑),测试可纯内存运行。
|
||||
/// </summary>
|
||||
private sealed class StubDealService : SwapDealService
|
||||
{
|
||||
private readonly trade _trade;
|
||||
private readonly Dictionary<int, swap_event> _swapEvents;
|
||||
private readonly Dictionary<long, List<swap_flow_event>> _flowEventsByEventId;
|
||||
public List<(double amount, string action, DateTime date)> ClientCashCalls = new();
|
||||
public List<(UnwindData data, int eventType, int clientCashId)> SaveSwapDealCalls = new();
|
||||
public int SaveAllChangesCount;
|
||||
public int CloseReCheckCallCount;
|
||||
|
||||
public StubDealService(trade td,
|
||||
Dictionary<int, swap_event> swapEvents = null,
|
||||
Dictionary<long, List<swap_flow_event>> flowEventsByEventId = null)
|
||||
: base(new OptUserInfo(0, nameof(SwapDealSettlementTest), OptUserFrom.UnitTest))
|
||||
{
|
||||
_trade = td;
|
||||
_swapEvents = swapEvents ?? new Dictionary<int, swap_event>();
|
||||
_flowEventsByEventId = flowEventsByEventId ?? new Dictionary<long, List<swap_flow_event>>();
|
||||
}
|
||||
|
||||
protected override trade FindTrade(int tradeId) => tradeId == _trade.id ? _trade : null;
|
||||
|
||||
protected override int AddClientCash(trade td, double amount, string action, DateTime valueDate)
|
||||
{
|
||||
ClientCashCalls.Add((amount, action, valueDate));
|
||||
return ClientCashCalls.Count; // 返回自增 id
|
||||
}
|
||||
|
||||
// 整体 override SaveSwapDeal:收集入参,规避内部 new SwapEventService 连库
|
||||
protected override long SaveSwapDeal(UnwindData unwindData, int eventType, int clientCashId, string eventResason = "", bool approve = false)
|
||||
{
|
||||
SaveSwapDealCalls.Add((unwindData, eventType, clientCashId));
|
||||
return SaveSwapDealCalls.Count; // 返回自增 eventId
|
||||
}
|
||||
|
||||
// ApproveSwapTrade 查待审核事件:从内存字典取(key=eventType)
|
||||
protected override swap_event FindSwapEvent(int tradeId, int eventType)
|
||||
{
|
||||
return _swapEvents.TryGetValue(eventType, out var evt) ? evt : null;
|
||||
}
|
||||
|
||||
// ApproveSwapTrade 查事件关联流水:从内存字典取
|
||||
protected override List<swap_flow_event> FindFlowEventsByEventId(long eventId)
|
||||
{
|
||||
return _flowEventsByEventId.TryGetValue(eventId, out var list) ? list : new List<swap_flow_event>();
|
||||
}
|
||||
|
||||
// ApplySwapTrade 的前置校验:计数,不实际执行
|
||||
protected override void CloseReCheckSetTrade(int swapTradeId, bool isSwap, bool needCheck)
|
||||
{
|
||||
CloseReCheckCallCount++;
|
||||
}
|
||||
|
||||
protected override void SaveAllChanges() { SaveAllChangesCount++; }
|
||||
protected override void ExecuteInTransaction(Action action) => action(); // 不包事务,直接执行
|
||||
protected override void CallSaveSwapTradeClientCash(trade td, DateTime valueDate) { } // 空操作
|
||||
protected override void TriggerRealtimeSwapPosition() { } // 空操作
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 数据构建
|
||||
|
||||
private static trade CreateTrade()
|
||||
{
|
||||
return new trade
|
||||
{
|
||||
id = SwapTradeId, TradeNumber = "UT-SD-001", ClientId = 888888,
|
||||
TradeType = "收益互换", StartDate = new DateTime(2026, 1, 5),
|
||||
ExerciseDate = new DateTime(2026, 6, 14), // 已到期边界(SwapIncome 判断用)
|
||||
TradeStatus = "确认成交", ValidState = "Valid",
|
||||
Notional = 1000000, StockEqvNotional = 1000000, TradeAmount = 10000
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>构造结息/平仓的 UnwindData(金额由前端算好传入,后端直接用)</summary>
|
||||
private static UnwindData CreateUnwindData(decimal swapRealizedPnL, decimal swapMarginRebatePnl = 0m,
|
||||
decimal swapMarginAmount = 0m, int closeMethod = 0, decimal closePercent = 0m,
|
||||
decimal closeQty = 0m, decimal closeNotionalValue = 0m, decimal positionQty = 0m)
|
||||
{
|
||||
return new UnwindData
|
||||
{
|
||||
SwapTradeId = SwapTradeId,
|
||||
SwapRealizedPnL = swapRealizedPnL,
|
||||
SwapMarginRebatePnl = swapMarginRebatePnl,
|
||||
SwapMarginAmount = swapMarginAmount,
|
||||
SwapCloseAmount = swapRealizedPnL,
|
||||
CloseMethod = closeMethod,
|
||||
ClosePercent = closePercent,
|
||||
CloseQty = closeQty,
|
||||
CloseNotionalValue = closeNotionalValue,
|
||||
PositionQty = positionQty,
|
||||
ValueDate = ValueDate,
|
||||
UnwindDate = UnwindDate,
|
||||
StartDate = new DateTime(2026, 1, 5)
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// ================================================================
|
||||
// SD_001:SwapIncome 正常结息 —— 验证资金流水金额正确
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// [SD_001] SwapIncome 正常结息:SwapRealizedPnL=1000 → 客户资金流水金额=-1000
|
||||
/// ------------------------------------------------------------
|
||||
/// 后端 SwapDealService.cs:1553 直接用前端传入的 SwapRealizedPnL 记账:
|
||||
/// AddClientCash(td, -SwapRealizedPnL, 系统操作_互换, ValueDate)
|
||||
/// 本测试锁定:资金流水金额 = -SwapRealizedPnL,事件类型 = 互换(3)。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void SD_001_SwapIncome_正常结息_资金流水金额正确()
|
||||
{
|
||||
var td = CreateTrade();
|
||||
td.ExerciseDate = new DateTime(2026, 12, 31); // 未到期,不走"已到期"分支
|
||||
var service = new StubDealService(td);
|
||||
var unwindData = CreateUnwindData(swapRealizedPnL: 1000m);
|
||||
|
||||
service.SwapIncome(unwindData);
|
||||
|
||||
Assert.AreEqual(1, service.ClientCashCalls.Count, "应生成1条资金流水(互换)");
|
||||
Assert.AreEqual(-1000.0, service.ClientCashCalls[0].amount, 0.001, "资金流水金额 = -SwapRealizedPnL");
|
||||
Assert.AreEqual(ClientCashInCashOut.系统操作_互换, service.ClientCashCalls[0].action, "操作类型=系统操作_互换");
|
||||
Assert.AreEqual(1, service.SaveSwapDealCalls.Count, "应调用 SaveSwapDeal 1次");
|
||||
Assert.AreEqual((int)SwapEventTypeEnum.互换, service.SaveSwapDealCalls[0].eventType, "事件类型=互换(3)");
|
||||
Console.WriteLine($"SD_001 通过:资金流水金额={service.ClientCashCalls[0].amount},事件类型=互换 ✅");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// SD_002:SwapIncome 含预付金返息 —— 两条资金流水
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// [SD_002] SwapIncome 含预付金返息:SwapRealizedPnL=1000, SwapMarginRebatePnl=200
|
||||
/// → 生成2条资金流水(互换 + 预付金返息),金额分别为 -1000、-200
|
||||
/// 后端 SwapDealService.cs:1556 条件:SwapMarginRebatePnl != 0 时追加预付金返息流水。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void SD_002_SwapIncome_含预付金返息_两条资金流水()
|
||||
{
|
||||
var td = CreateTrade();
|
||||
td.ExerciseDate = new DateTime(2026, 12, 31);
|
||||
var service = new StubDealService(td);
|
||||
var unwindData = CreateUnwindData(swapRealizedPnL: 1000m, swapMarginRebatePnl: 200m);
|
||||
|
||||
service.SwapIncome(unwindData);
|
||||
|
||||
Assert.AreEqual(2, service.ClientCashCalls.Count, "应生成2条资金流水(互换+预付金返息)");
|
||||
Assert.AreEqual(-1000.0, service.ClientCashCalls[0].amount, 0.001, "第1条=互换金额 -SwapRealizedPnL");
|
||||
Assert.AreEqual(ClientCashInCashOut.系统操作_互换, service.ClientCashCalls[0].action);
|
||||
Assert.AreEqual(-200.0, service.ClientCashCalls[1].amount, 0.001, "第2条=预付金返息 -SwapMarginRebatePnl");
|
||||
Assert.AreEqual(ClientCashInCashOut.系统操作_预付金返息, service.ClientCashCalls[1].action);
|
||||
Console.WriteLine($"SD_002 通过:2条资金流水,互换={service.ClientCashCalls[0].amount},预付金返息={service.ClientCashCalls[1].amount} ✅");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// SD_003:SwapUnwind 全平仓 —— 持仓归零、资金流水、状态变更
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// [SD_003] SwapUnwind 全平仓:ClosePercent=1 → TradeStatus=已平仓、持仓扣减、资金流水正确
|
||||
/// 后端 SwapDealService.cs SwapUnwind:全平时 TradeStatus=已平仓,StockEqvNotional/TradeAmount 扣减。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void SD_003_SwapUnwind_正常平仓_资金流水与持仓状态正确()
|
||||
{
|
||||
var td = CreateTrade();
|
||||
var service = new StubDealService(td);
|
||||
// 全平:ClosePercent=1, CloseQty=10000, CloseNotionalValue=1000000
|
||||
var unwindData = CreateUnwindData(
|
||||
swapRealizedPnL: 5000m, swapMarginAmount: 0m,
|
||||
closeMethod: (int)CloseMethodEnum.全部平仓, closePercent: 1m,
|
||||
closeQty: 10000m, closeNotionalValue: 1000000m, positionQty: 10000m);
|
||||
|
||||
service.SwapUnwind(unwindData);
|
||||
|
||||
// 资金流水:平仓费 = -SwapRealizedPnL
|
||||
Assert.AreEqual(1, service.ClientCashCalls.Count, "全平无预付金时应1条资金流水");
|
||||
Assert.AreEqual(-5000.0, service.ClientCashCalls[0].amount, 0.001, "资金流水=-SwapRealizedPnL");
|
||||
Assert.AreEqual(ClientCashInCashOut.系统操作_平仓费, service.ClientCashCalls[0].action);
|
||||
// 持仓状态
|
||||
Assert.AreEqual("已平仓", td.TradeStatus, "全平仓 TradeStatus=已平仓");
|
||||
// 全平仓走"已平仓"分支,不设 HasPartialUnWind(仅部分平仓才设=1)
|
||||
Assert.AreNotEqual(1, td.HasPartialUnWind, "全平仓不应设 HasPartialUnWind(仅部分平仓设=1)");
|
||||
// 持仓扣减:原 StockEqvNotional=1000000 - CloseNotionalValue=1000000 = 0
|
||||
Assert.AreEqual(0.0, td.StockEqvNotional, 0.001, "StockEqvNotional 扣减后=0");
|
||||
Assert.AreEqual(0.0, td.TradeAmount, 0.001, "TradeAmount 扣减后=0");
|
||||
// 事件类型
|
||||
Assert.AreEqual((int)SwapEventTypeEnum.平仓, service.SaveSwapDealCalls[0].eventType, "事件类型=平仓(2)");
|
||||
Console.WriteLine($"SD_003 通过:TradeStatus={td.TradeStatus},StockEqvNotional={td.StockEqvNotional} ✅");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// SD_007:SwapUnwind 部分平仓 —— HasPartialUnWind=1,TradeStatus 不变
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// [SD_007] SwapUnwind 部分平仓(借鉴 testable SwapUnwindScenarioTest.Scenario2)
|
||||
/// ------------------------------------------------------------
|
||||
/// 部分平仓(CloseMethod≠全部平仓 且 ClosePercent≠1)走 else 分支:
|
||||
/// td.HasPartialUnWind = 1,TradeStatus 保持"确认成交"不变
|
||||
/// StockEqvNotional/TradeAmount 仍按 CloseNotionalValue/CloseQty 扣减
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void SD_007_SwapUnwind_部分平仓_设HasPartialUnWind且TradeStatus不变()
|
||||
{
|
||||
var td = CreateTrade();
|
||||
var service = new StubDealService(td);
|
||||
// 部分平仓:ClosePercent=0.5, CloseQty=5000, CloseNotionalValue=500000
|
||||
var unwindData = CreateUnwindData(
|
||||
swapRealizedPnL: 3000m, swapMarginAmount: 0m,
|
||||
closeMethod: (int)CloseMethodEnum.部分平仓, closePercent: 0.5m,
|
||||
closeQty: 5000m, closeNotionalValue: 500000m, positionQty: 10000m);
|
||||
|
||||
service.SwapUnwind(unwindData);
|
||||
|
||||
// 部分平仓:设 HasPartialUnWind=1,TradeStatus 不变
|
||||
Assert.AreEqual(1, td.HasPartialUnWind, "部分平仓应设 HasPartialUnWind=1");
|
||||
Assert.AreEqual("确认成交", td.TradeStatus, "部分平仓 TradeStatus 保持不变");
|
||||
// 持仓扣减:原 StockEqvNotional=1000000 - 500000 = 500000
|
||||
Assert.AreEqual(500000.0, td.StockEqvNotional, 0.001, "StockEqvNotional 扣减后=500000");
|
||||
Assert.AreEqual(5000.0, td.TradeAmount, 0.001, "TradeAmount 扣减后=5000");
|
||||
// 资金流水仍有(部分平仓也产生平仓费流水)
|
||||
Assert.AreEqual(1, service.ClientCashCalls.Count, "部分平仓应1条资金流水");
|
||||
Assert.AreEqual(-3000.0, service.ClientCashCalls[0].amount, 0.001, "资金流水=-SwapRealizedPnL");
|
||||
Console.WriteLine($"SD_007 通过:HasPartialUnWind={td.HasPartialUnWind},TradeStatus={td.TradeStatus},StockEqvNotional={td.StockEqvNotional} ✅");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// SD_008:SwapUnwind 含预付金 —— 两条资金流水(平仓费+应付预付金)
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// [SD_008] SwapUnwind 含预付金返还(SwapMarginAmount≠0)
|
||||
/// ------------------------------------------------------------
|
||||
/// SwapUnwind 内部:SwapMarginAmount≠0 时追加一条"应付预付金"资金流水。
|
||||
/// 验证:2条流水(平仓费 + 应付预付金),金额和操作类型正确。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void SD_008_SwapUnwind_含预付金_两条资金流水()
|
||||
{
|
||||
var td = CreateTrade();
|
||||
var service = new StubDealService(td);
|
||||
var unwindData = CreateUnwindData(
|
||||
swapRealizedPnL: 5000m, swapMarginAmount: 2000m,
|
||||
closeMethod: (int)CloseMethodEnum.全部平仓, closePercent: 1m,
|
||||
closeQty: 10000m, closeNotionalValue: 1000000m, positionQty: 10000m);
|
||||
|
||||
service.SwapUnwind(unwindData);
|
||||
|
||||
// 2条资金流水:平仓费 + 应付预付金
|
||||
Assert.AreEqual(2, service.ClientCashCalls.Count, "含预付金时应2条资金流水");
|
||||
Assert.AreEqual(-5000.0, service.ClientCashCalls[0].amount, 0.001, "第1条=平仓费 -SwapRealizedPnL");
|
||||
Assert.AreEqual(ClientCashInCashOut.系统操作_平仓费, service.ClientCashCalls[0].action);
|
||||
Assert.AreEqual(2000.0, service.ClientCashCalls[1].amount, 0.001, "第2条=应付预付金 SwapMarginAmount");
|
||||
Assert.AreEqual(ClientCashInCashOut.系统操作_应付预付金, service.ClientCashCalls[1].action);
|
||||
Console.WriteLine($"SD_008 通过:2条流水,平仓费={service.ClientCashCalls[0].amount},应付预付金={service.ClientCashCalls[1].amount} ✅");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// SD_004:DealFloatPosition 含费价重算正确(后端唯二真做计算的地方)
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// [SD_004] DealFloatPosition 含费价重算(SwapDealService.cs:1713-1725)
|
||||
/// ------------------------------------------------------------
|
||||
/// 平仓事件重算三个字段(规范语义,见命名文档):
|
||||
/// TradingAmountFeeAvg(ExitDirtyFeePrice)= TradingAmountAvg(ExitDirtyPrice) + TradingFeePending/CloseQty × shortRatio
|
||||
/// TradingAmountNetFeeAvg(ExitCleanFeePrice)= TradingAmountNetAvg(ExitCleanPrice) + TradingFeePending/CloseQty × shortRatio
|
||||
/// TradingAmount = TradingAmountAvg × CloseQty
|
||||
/// 这是后端少数真正做计算(而非透传前端值)的地方,需锁住。
|
||||
///
|
||||
/// 手算:ExitDirtyPrice=1.02, TradingFeePending=50, CloseQty=1000, Long(多头,shortRatio=-1)
|
||||
/// ExitDirtyFeePrice = 1.02 + 50/1000 × (-1) = 1.02 - 0.05 = 0.97
|
||||
/// ExitCleanFeePrice = 1.00 + 50/1000 × (-1) = 1.00 - 0.05 = 0.95
|
||||
/// TradingAmount = 1.02 × 1000 = 1020
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void SD_004_DealFloatPosition_含费价重算正确()
|
||||
{
|
||||
var td = CreateTrade();
|
||||
var service = new StubDealService(td);
|
||||
|
||||
// 构造平仓事件(PositionType>0 触发重算)
|
||||
var closeEvent = new swap_flow_event
|
||||
{
|
||||
EventType = (int)SwapEventTypeEnum.平仓,
|
||||
PositionType = (int)PositionTypeFlag.Long, // 多头,shortRatio=-1
|
||||
// TradingAmountAvg 现状名,实为"期末全价不含费",规范名 ExitDirtyPrice
|
||||
TradingAmountAvg = 1.02m,
|
||||
// TradingAmountNetAvg 现状名,实为"期末净价不含费",规范名 ExitCleanPrice
|
||||
TradingAmountNetAvg = 1.00m,
|
||||
TradingFeePending = 50m,
|
||||
};
|
||||
var unwindData = CreateUnwindData(swapRealizedPnL: 0m, closeQty: 1000m);
|
||||
unwindData.FlowEvents.Add(closeEvent);
|
||||
|
||||
service.SwapUnwind(unwindData);
|
||||
|
||||
// ExitDirtyFeePrice(TradingAmountFeeAvg)= 1.02 + 50/1000×(-1) = 0.97
|
||||
Assert.AreEqual(0.97m, closeEvent.TradingAmountFeeAvg, 0.0001m,
|
||||
$"TradingAmountFeeAvg(ExitDirtyFeePrice) 应=ExitDirtyPrice(1.02)+Fee/CloseQty×(-1)=0.97,实际={closeEvent.TradingAmountFeeAvg}");
|
||||
// ExitCleanFeePrice(TradingAmountNetFeeAvg)= 1.00 + 50/1000×(-1) = 0.95
|
||||
Assert.AreEqual(0.95m, closeEvent.TradingAmountNetFeeAvg ?? 0m, 0.0001m,
|
||||
$"TradingAmountNetFeeAvg(ExitCleanFeePrice) 应=ExitCleanPrice(1.00)+Fee/CloseQty×(-1)=0.95,实际={closeEvent.TradingAmountNetFeeAvg}");
|
||||
// TradingAmount = ExitDirtyPrice × CloseQty = 1.02 × 1000 = 1020
|
||||
Assert.AreEqual(1020m, closeEvent.TradingAmount, 0.0001m,
|
||||
$"TradingAmount 应=ExitDirtyPrice(1.02)×CloseQty(1000)=1020,实际={closeEvent.TradingAmount}");
|
||||
Console.WriteLine($"SD_004 通过:ExitDirtyFeePrice={closeEvent.TradingAmountFeeAvg},ExitCleanFeePrice={closeEvent.TradingAmountNetFeeAvg},TradingAmount={closeEvent.TradingAmount} ✅");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// SD_005:ApproveSwapTrade 审核通过 —— 反序列化事件、资金流水、持仓状态
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// [SD_005] ApproveSwapTrade 审核通过全部平仓
|
||||
/// ------------------------------------------------------------
|
||||
/// 后端 SwapDealService.ApproveSwapTrade:从 swap_event.EventData 反序列化 UnwindData,
|
||||
/// 据此生成资金流水 + 更新持仓状态。
|
||||
/// 借鉴 testable 分支 SwapUnwindScenarioTest.Scenario4,验证:
|
||||
/// - SwapRealizedPnL 从事件反序列化正确(EventData JSON)
|
||||
/// - 资金流水金额 = -SwapRealizedPnL
|
||||
/// - 全平仓 → TradeStatus=已平仓
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void SD_005_ApproveSwapTrade_全平仓审核_反序列化事件并记账()
|
||||
{
|
||||
var td = CreateTrade();
|
||||
// 构造待审核事件:EventData 里序列化了 UnwindData(含 SwapRealizedPnL=8000)
|
||||
var unwindData = CreateUnwindData(swapRealizedPnL: 8000m,
|
||||
closeMethod: (int)CloseMethodEnum.全部平仓, closePercent: 1m,
|
||||
closeQty: 10000m, closeNotionalValue: 1000000m);
|
||||
var swapEvent = new swap_event
|
||||
{
|
||||
id = 1, SwapTradeId = SwapTradeId,
|
||||
EventType = (int)SwapEventTypeEnum.平仓, Invalid = false,
|
||||
EventData = JsonConvert.SerializeObject(unwindData)
|
||||
};
|
||||
var flowEvents = new Dictionary<long, List<swap_flow_event>>
|
||||
{
|
||||
[1] = new List<swap_flow_event> { new swap_flow_event { id = 1, EventId = 1, PositionId = 1 } }
|
||||
};
|
||||
var service = new StubDealService(td,
|
||||
swapEvents: new Dictionary<int, swap_event> { [(int)SwapEventTypeEnum.平仓] = swapEvent },
|
||||
flowEventsByEventId: flowEvents);
|
||||
|
||||
service.ApproveSwapTrade(td, (int)SwapEventTypeEnum.平仓);
|
||||
|
||||
// 资金流水:从反序列化的 SwapRealizedPnL(8000) 记账 → -8000
|
||||
Assert.AreEqual(1, service.ClientCashCalls.Count, "全平仓无预付金时应1条资金流水");
|
||||
Assert.AreEqual(-8000.0, service.ClientCashCalls[0].amount, 0.001, "资金流水=-反序列化的SwapRealizedPnL");
|
||||
// 持仓状态
|
||||
Assert.AreEqual("已平仓", td.TradeStatus, "审核全平仓 TradeStatus=已平仓");
|
||||
Console.WriteLine($"SD_005 通过:审核反序列化 SwapRealizedPnL=8000,资金流水={service.ClientCashCalls[0].amount},TradeStatus={td.TradeStatus} ✅");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// SD_006:ApplySwapTrade 提交审核 —— 前置校验 + 保存事件
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// [SD_006] ApplySwapTrade 提交审核
|
||||
/// ------------------------------------------------------------
|
||||
/// 后端 SwapDealService.ApplySwapTrade:调 CloseReCheckSetTrade 前置校验 + SaveSwapDeal(approve=true)。
|
||||
/// 借鉴 testable 分支 SwapUnwindScenarioTest.Scenario5,验证:
|
||||
/// - CloseReCheckSetTrade 被调用1次
|
||||
/// - SaveSwapDeal 以 approve=true 调用(事件类型正确)
|
||||
/// - SwapRealizedPnL = SwapCloseAmount(ApplySwapTrade 内部赋值)
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void SD_006_ApplySwapTrade_提交审核_前置校验与保存事件()
|
||||
{
|
||||
var td = CreateTrade();
|
||||
var service = new StubDealService(td);
|
||||
// 前端提交时 SwapCloseAmount=6000(前端算好的总额),SwapRealizedPnL 初始可能为0
|
||||
var unwindData = CreateUnwindData(swapRealizedPnL: 0m);
|
||||
unwindData.SwapCloseAmount = 6000m; // 模拟前端传入的平仓总额
|
||||
|
||||
service.ApplySwapTrade(unwindData, (int)SwapEventTypeEnum.平仓);
|
||||
|
||||
// 前置校验被调用
|
||||
Assert.AreEqual(1, service.CloseReCheckCallCount, "应调用 CloseReCheckSetTrade 1次");
|
||||
// SaveSwapDeal 以 approve=true 调用
|
||||
Assert.AreEqual(1, service.SaveSwapDealCalls.Count, "应调用 SaveSwapDeal 1次");
|
||||
Assert.AreEqual((int)SwapEventTypeEnum.平仓, service.SaveSwapDealCalls[0].eventType, "事件类型=平仓");
|
||||
// SwapRealizedPnL 应被赋值为 SwapCloseAmount(ApplySwapTrade 内部 cs:1631)
|
||||
Assert.AreEqual(6000m, service.SaveSwapDealCalls[0].data.SwapRealizedPnL, 0.001m,
|
||||
"SwapRealizedPnL 应=SwapCloseAmount(6000)");
|
||||
Console.WriteLine($"SD_006 通过:CloseReCheck 调用{service.CloseReCheckCallCount}次,SwapRealizedPnL={service.SaveSwapDealCalls[0].data.SwapRealizedPnL} ✅");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 互换结息(SwapIncome)测试
|
||||
/// ============================================================================
|
||||
/// 借鉴 testable 分支命名,基于当前分支 TestableSwapDealService 共享 stub。
|
||||
/// SwapIncome 是写客户资金流水(ClientCashInCashOut)的核心入口之一。
|
||||
/// ============================================================================
|
||||
[TestClass]
|
||||
public class SwapIncomeScenarioTest
|
||||
{
|
||||
// ================================================================
|
||||
// 场景1:SwapIncome 正常结息 —— 资金流水金额正确
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// SwapIncome 正常结息:SwapRealizedPnL=1000 → 客户资金流水金额=-1000。
|
||||
/// 后端 SwapDealService SwapIncome 直接用前端传入的 SwapRealizedPnL 记账。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void SI_001_SwapIncome_正常结息_资金流水金额正确()
|
||||
{
|
||||
var td = SwapDealTestFactory.CreateTrade();
|
||||
td.ExerciseDate = new DateTime(2026, 12, 31); // 未到期,不走"已到期"分支
|
||||
var service = new TestableSwapDealService(td);
|
||||
var unwindData = SwapDealTestFactory.CreateUnwindData(swapRealizedPnL: 1000m);
|
||||
|
||||
service.SwapIncome(unwindData);
|
||||
|
||||
Assert.AreEqual(1, service.ClientCashCalls.Count, "应生成1条资金流水(互换)");
|
||||
Assert.AreEqual(-1000.0, service.ClientCashCalls[0].amount, 0.001, "资金流水金额 = -SwapRealizedPnL");
|
||||
Assert.AreEqual(ClientCashInCashOut.系统操作_互换, service.ClientCashCalls[0].action, "操作类型=系统操作_互换");
|
||||
Assert.AreEqual(1, service.SaveSwapDealCalls.Count, "应调用 SaveSwapDeal 1次");
|
||||
Assert.AreEqual((int)SwapEventTypeEnum.互换, service.SaveSwapDealCalls[0].eventType, "事件类型=互换(3)");
|
||||
Console.WriteLine($"SI_001: 资金流水={service.ClientCashCalls[0].amount}, 事件类型=互换 ✅");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 场景2:SwapIncome 含预付金返息 —— 两条资金流水
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// SwapIncome 含预付金返息:SwapRealizedPnL=1000, SwapMarginRebatePnl=200
|
||||
/// → 生成2条资金流水(互换 + 预付金返息),金额分别为 -1000、-200。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void SI_002_SwapIncome_含预付金返息_两条资金流水()
|
||||
{
|
||||
var td = SwapDealTestFactory.CreateTrade();
|
||||
td.ExerciseDate = new DateTime(2026, 12, 31);
|
||||
var service = new TestableSwapDealService(td);
|
||||
var unwindData = SwapDealTestFactory.CreateUnwindData(swapRealizedPnL: 1000m, swapMarginRebatePnl: 200m);
|
||||
|
||||
service.SwapIncome(unwindData);
|
||||
|
||||
Assert.AreEqual(2, service.ClientCashCalls.Count, "应生成2条资金流水(互换+预付金返息)");
|
||||
Assert.AreEqual(-1000.0, service.ClientCashCalls[0].amount, 0.001, "第1条=互换金额");
|
||||
Assert.AreEqual(ClientCashInCashOut.系统操作_互换, service.ClientCashCalls[0].action);
|
||||
Assert.AreEqual(-200.0, service.ClientCashCalls[1].amount, 0.001, "第2条=预付金返息");
|
||||
Assert.AreEqual(ClientCashInCashOut.系统操作_预付金返息, service.ClientCashCalls[1].action);
|
||||
Console.WriteLine($"SI_002: 互换={service.ClientCashCalls[0].amount}, 预付金返息={service.ClientCashCalls[1].amount} ✅");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
using Newtonsoft.Json;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 互换平仓全流程测试(SwapUnwind/ApproveSwapTrade/ApplySwapTrade/DealFloatPosition)
|
||||
/// ============================================================================
|
||||
/// 借鉴 testable 分支 SwapUnwindScenarioTest,基于当前分支 TestableSwapDealService 共享 stub。
|
||||
/// 命名规范说明(见《互换价格字段命名规范决策文档》):
|
||||
/// PosiGrossPrice 现状名,实为"期初全价不含费",规范名 EntryDirtyPrice
|
||||
/// TradingAmountAvg 现状名,实为"期末全价不含费",规范名 ExitDirtyPrice
|
||||
/// ============================================================================
|
||||
[TestClass]
|
||||
public class SwapUnwindScenarioTest
|
||||
{
|
||||
// ================================================================
|
||||
// 场景1:SwapUnwind 全平仓 —— 持仓归零、TradeStatus=已平仓
|
||||
// ================================================================
|
||||
|
||||
[TestMethod]
|
||||
public void UW_001_SwapUnwind_全平仓_持仓归零且资金流水正确()
|
||||
{
|
||||
var td = SwapDealTestFactory.CreateTrade();
|
||||
var service = new TestableSwapDealService(td);
|
||||
var unwindData = SwapDealTestFactory.CreateUnwindData(
|
||||
swapRealizedPnL: 5000m, swapMarginAmount: 0m,
|
||||
closeMethod: (int)CloseMethodEnum.全部平仓, closePercent: 1m,
|
||||
closeQty: 10000m, closeNotionalValue: 1000000m, positionQty: 10000m);
|
||||
|
||||
service.SwapUnwind(unwindData);
|
||||
|
||||
Assert.AreEqual(1, service.ClientCashCalls.Count, "全平无预付金时应1条资金流水");
|
||||
Assert.AreEqual(-5000.0, service.ClientCashCalls[0].amount, 0.001, "资金流水=-SwapRealizedPnL");
|
||||
Assert.AreEqual(ClientCashInCashOut.系统操作_平仓费, service.ClientCashCalls[0].action);
|
||||
Assert.AreEqual("已平仓", td.TradeStatus, "全平仓 TradeStatus=已平仓");
|
||||
Assert.AreNotEqual(1, td.HasPartialUnWind, "全平仓不应设 HasPartialUnWind");
|
||||
Assert.AreEqual(0.0, td.StockEqvNotional, 0.001, "StockEqvNotional 扣减后=0");
|
||||
Assert.AreEqual(0.0, td.TradeAmount, 0.001, "TradeAmount 扣减后=0");
|
||||
Assert.AreEqual((int)SwapEventTypeEnum.平仓, service.SaveSwapDealCalls[0].eventType, "事件类型=平仓(2)");
|
||||
Console.WriteLine($"UW_001: TradeStatus={td.TradeStatus}, StockEqvNotional={td.StockEqvNotional} ✅");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 场景2:SwapUnwind 部分平仓 —— HasPartialUnWind=1,TradeStatus 不变
|
||||
// ================================================================
|
||||
|
||||
[TestMethod]
|
||||
public void UW_002_SwapUnwind_部分平仓_设HasPartialUnWind且TradeStatus不变()
|
||||
{
|
||||
var td = SwapDealTestFactory.CreateTrade();
|
||||
var service = new TestableSwapDealService(td);
|
||||
var unwindData = SwapDealTestFactory.CreateUnwindData(
|
||||
swapRealizedPnL: 3000m, swapMarginAmount: 0m,
|
||||
closeMethod: (int)CloseMethodEnum.部分平仓, closePercent: 0.5m,
|
||||
closeQty: 5000m, closeNotionalValue: 500000m, positionQty: 10000m);
|
||||
|
||||
service.SwapUnwind(unwindData);
|
||||
|
||||
Assert.AreEqual(1, td.HasPartialUnWind, "部分平仓应设 HasPartialUnWind=1");
|
||||
Assert.AreEqual("确认成交", td.TradeStatus, "部分平仓 TradeStatus 保持不变");
|
||||
Assert.AreEqual(500000.0, td.StockEqvNotional, 0.001, "StockEqvNotional 扣减后=500000");
|
||||
Assert.AreEqual(5000.0, td.TradeAmount, 0.001, "TradeAmount 扣减后=5000");
|
||||
Assert.AreEqual(1, service.ClientCashCalls.Count, "部分平仓应1条资金流水");
|
||||
Assert.AreEqual(-3000.0, service.ClientCashCalls[0].amount, 0.001, "资金流水=-SwapRealizedPnL");
|
||||
Console.WriteLine($"UW_002: HasPartialUnWind={td.HasPartialUnWind}, TradeStatus={td.TradeStatus} ✅");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 场景3:SwapUnwind 含预付金 —— 两条资金流水
|
||||
// ================================================================
|
||||
|
||||
[TestMethod]
|
||||
public void UW_003_SwapUnwind_含预付金_两条资金流水()
|
||||
{
|
||||
var td = SwapDealTestFactory.CreateTrade();
|
||||
var service = new TestableSwapDealService(td);
|
||||
var unwindData = SwapDealTestFactory.CreateUnwindData(
|
||||
swapRealizedPnL: 5000m, swapMarginAmount: 2000m,
|
||||
closeMethod: (int)CloseMethodEnum.全部平仓, closePercent: 1m,
|
||||
closeQty: 10000m, closeNotionalValue: 1000000m, positionQty: 10000m);
|
||||
|
||||
service.SwapUnwind(unwindData);
|
||||
|
||||
Assert.AreEqual(2, service.ClientCashCalls.Count, "含预付金时应2条资金流水");
|
||||
Assert.AreEqual(-5000.0, service.ClientCashCalls[0].amount, 0.001, "第1条=平仓费");
|
||||
Assert.AreEqual(ClientCashInCashOut.系统操作_平仓费, service.ClientCashCalls[0].action);
|
||||
Assert.AreEqual(2000.0, service.ClientCashCalls[1].amount, 0.001, "第2条=应付预付金");
|
||||
Assert.AreEqual(ClientCashInCashOut.系统操作_应付预付金, service.ClientCashCalls[1].action);
|
||||
Console.WriteLine($"UW_003: 平仓费={service.ClientCashCalls[0].amount}, 应付预付金={service.ClientCashCalls[1].amount} ✅");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 场景4:DealFloatPosition 含费价重算(后端唯二真做计算的地方)
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 平仓事件重算三字段(SwapDealService DealFloatPosition):
|
||||
/// TradingAmountFeeAvg(ExitDirtyFeePrice) = TradingAmountAvg(ExitDirtyPrice) + Fee/CloseQty × shortRatio
|
||||
/// TradingAmountNetFeeAvg(ExitCleanFeePrice) = TradingAmountNetAvg(ExitCleanPrice) + Fee/CloseQty × shortRatio
|
||||
/// TradingAmount = TradingAmountAvg × CloseQty
|
||||
/// 手算:ExitDirtyPrice=1.02, Fee=50, CloseQty=1000, Long(shortRatio=-1)
|
||||
/// ExitDirtyFeePrice = 1.02 + 50/1000×(-1) = 0.97
|
||||
/// ExitCleanFeePrice = 1.00 + 50/1000×(-1) = 0.95
|
||||
/// TradingAmount = 1.02 × 1000 = 1020
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void UW_004_DealFloatPosition_含费价重算正确()
|
||||
{
|
||||
var td = SwapDealTestFactory.CreateTrade();
|
||||
var service = new TestableSwapDealService(td);
|
||||
var closeEvent = new swap_flow_event
|
||||
{
|
||||
EventType = (int)SwapEventTypeEnum.平仓,
|
||||
PositionType = (int)PositionTypeFlag.Long,
|
||||
TradingAmountAvg = 1.02m, // ExitDirtyPrice
|
||||
TradingAmountNetAvg = 1.00m, // ExitCleanPrice
|
||||
TradingFeePending = 50m,
|
||||
};
|
||||
var unwindData = SwapDealTestFactory.CreateUnwindData(swapRealizedPnL: 0m, closeQty: 1000m);
|
||||
unwindData.FlowEvents.Add(closeEvent);
|
||||
|
||||
service.SwapUnwind(unwindData);
|
||||
|
||||
Assert.AreEqual(0.97m, closeEvent.TradingAmountFeeAvg, 0.0001m,
|
||||
$"TradingAmountFeeAvg(ExitDirtyFeePrice)=ExitDirtyPrice+Fee/Qty×(-1)=0.97");
|
||||
Assert.AreEqual(0.95m, closeEvent.TradingAmountNetFeeAvg ?? 0m, 0.0001m,
|
||||
$"TradingAmountNetFeeAvg(ExitCleanFeePrice)=ExitCleanPrice+Fee/Qty×(-1)=0.95");
|
||||
Assert.AreEqual(1020m, closeEvent.TradingAmount, 0.0001m,
|
||||
$"TradingAmount=ExitDirtyPrice×CloseQty=1020");
|
||||
Console.WriteLine($"UW_004: ExitDirtyFeePrice={closeEvent.TradingAmountFeeAvg}, TradingAmount={closeEvent.TradingAmount} ✅");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 场景5:ApproveSwapTrade 审核通过全平仓 —— 反序列化事件并记账
|
||||
// ================================================================
|
||||
|
||||
[TestMethod]
|
||||
public void UW_005_ApproveSwapTrade_全平仓审核_反序列化事件并记账()
|
||||
{
|
||||
var td = SwapDealTestFactory.CreateTrade();
|
||||
td.ExerciseDate = new DateTime(2026, 12, 31);
|
||||
var unwindData = SwapDealTestFactory.CreateUnwindData(swapRealizedPnL: 8000m,
|
||||
closeMethod: (int)CloseMethodEnum.全部平仓, closePercent: 1m,
|
||||
closeQty: 10000m, closeNotionalValue: 1000000m);
|
||||
var swapEvent = new swap_event
|
||||
{
|
||||
id = 1, SwapTradeId = SwapDealTestFactory.SwapTradeId,
|
||||
EventType = (int)SwapEventTypeEnum.平仓, Invalid = false,
|
||||
EventData = JsonConvert.SerializeObject(unwindData)
|
||||
};
|
||||
var flowEvents = new Dictionary<long, List<swap_flow_event>>
|
||||
{
|
||||
[1] = new List<swap_flow_event> { new swap_flow_event { id = 1, EventId = 1, PositionId = 1 } }
|
||||
};
|
||||
var service = new TestableSwapDealService(td,
|
||||
swapEvents: new Dictionary<int, swap_event> { [(int)SwapEventTypeEnum.平仓] = swapEvent },
|
||||
flowEventsByEventId: flowEvents);
|
||||
|
||||
service.ApproveSwapTrade(td, (int)SwapEventTypeEnum.平仓);
|
||||
|
||||
Assert.AreEqual(1, service.ClientCashCalls.Count, "全平仓无预付金时应1条资金流水");
|
||||
Assert.AreEqual(-8000.0, service.ClientCashCalls[0].amount, 0.001, "资金流水=-反序列化的SwapRealizedPnL");
|
||||
Assert.AreEqual("已平仓", td.TradeStatus, "审核全平仓 TradeStatus=已平仓");
|
||||
Console.WriteLine($"UW_005: 反序列化SwapRealizedPnL=8000, 资金流水={service.ClientCashCalls[0].amount}, TradeStatus={td.TradeStatus} ✅");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 场景6:ApplySwapTrade 提交审核 —— 前置校验与保存事件
|
||||
// ================================================================
|
||||
|
||||
[TestMethod]
|
||||
public void UW_006_ApplySwapTrade_提交审核_前置校验与保存事件()
|
||||
{
|
||||
var td = SwapDealTestFactory.CreateTrade();
|
||||
var service = new TestableSwapDealService(td);
|
||||
var unwindData = SwapDealTestFactory.CreateUnwindData(swapRealizedPnL: 0m);
|
||||
unwindData.SwapCloseAmount = 6000m;
|
||||
|
||||
service.ApplySwapTrade(unwindData, (int)SwapEventTypeEnum.平仓);
|
||||
|
||||
Assert.AreEqual(1, service.CloseReCheckCallCount, "应调用 CloseReCheckSetTrade 1次");
|
||||
Assert.AreEqual(1, service.SaveSwapDealCalls.Count, "应调用 SaveSwapDeal 1次");
|
||||
Assert.AreEqual((int)SwapEventTypeEnum.平仓, service.SaveSwapDealCalls[0].eventType, "事件类型=平仓");
|
||||
Assert.AreEqual(6000m, service.SaveSwapDealCalls[0].data.SwapRealizedPnL, 0.001m,
|
||||
"SwapRealizedPnL 应=SwapCloseAmount(6000)");
|
||||
Console.WriteLine($"UW_006: CloseReCheck={service.CloseReCheckCallCount}次, SwapRealizedPnL={service.SaveSwapDealCalls[0].data.SwapRealizedPnL} ✅");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using Newtonsoft.Json;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// SwapDealService 的可测试化子类(共享 stub)。
|
||||
/// 继承 SwapDealService,override seam 把 DB/事务/外部服务替换为内存收集器。
|
||||
/// 被 SwapUnwindScenarioTest / SwapIncomeScenarioTest 共用,避免重复。
|
||||
/// </summary>
|
||||
public class TestableSwapDealService : SwapDealService
|
||||
{
|
||||
private readonly trade _trade;
|
||||
private readonly Dictionary<int, swap_event> _swapEvents;
|
||||
private readonly Dictionary<long, List<swap_flow_event>> _flowEventsByEventId;
|
||||
|
||||
/// <summary>捕获 AddClientCash 的每次调用(金额, 操作, 日期)</summary>
|
||||
public List<(double amount, string action, DateTime date)> ClientCashCalls { get; } = new();
|
||||
|
||||
/// <summary>捕获 SaveSwapDeal 的每次调用(unwindData, eventType, clientCashId)</summary>
|
||||
public List<(UnwindData data, int eventType, int clientCashId)> SaveSwapDealCalls { get; } = new();
|
||||
|
||||
public int SaveAllChangesCount;
|
||||
public int CloseReCheckCallCount;
|
||||
|
||||
public TestableSwapDealService(trade td,
|
||||
Dictionary<int, swap_event> swapEvents = null,
|
||||
Dictionary<long, List<swap_flow_event>> flowEventsByEventId = null)
|
||||
: base(new OptUserInfo(0, nameof(TestableSwapDealService), OptUserFrom.UnitTest))
|
||||
{
|
||||
_trade = td;
|
||||
_swapEvents = swapEvents ?? new Dictionary<int, swap_event>();
|
||||
_flowEventsByEventId = flowEventsByEventId ?? new Dictionary<long, List<swap_flow_event>>();
|
||||
}
|
||||
|
||||
protected override trade FindTrade(int tradeId) => tradeId == _trade.id ? _trade : null;
|
||||
|
||||
protected override int AddClientCash(trade td, double amount, string action, DateTime valueDate)
|
||||
{
|
||||
ClientCashCalls.Add((amount, action, valueDate));
|
||||
return ClientCashCalls.Count; // 返回自增 id
|
||||
}
|
||||
|
||||
// 整体 override SaveSwapDeal:收集入参,规避内部 new SwapEventService 连库
|
||||
protected override long SaveSwapDeal(UnwindData unwindData, int eventType, int clientCashId, string eventResason = "", bool approve = false)
|
||||
{
|
||||
SaveSwapDealCalls.Add((unwindData, eventType, clientCashId));
|
||||
return SaveSwapDealCalls.Count; // 返回自增 eventId
|
||||
}
|
||||
|
||||
// ApproveSwapTrade 查待审核事件:从内存字典取(key=eventType)
|
||||
protected override swap_event FindSwapEvent(int tradeId, int eventType)
|
||||
{
|
||||
return _swapEvents.TryGetValue(eventType, out var evt) ? evt : null;
|
||||
}
|
||||
|
||||
// ApproveSwapTrade 查事件关联流水:从内存字典取
|
||||
protected override List<swap_flow_event> FindFlowEventsByEventId(long eventId)
|
||||
{
|
||||
return _flowEventsByEventId.TryGetValue(eventId, out var list) ? list : new List<swap_flow_event>();
|
||||
}
|
||||
|
||||
// ApplySwapTrade 的前置校验:计数,不实际执行
|
||||
protected override void CloseReCheckSetTrade(int swapTradeId, bool isSwap, bool needCheck)
|
||||
{
|
||||
CloseReCheckCallCount++;
|
||||
}
|
||||
|
||||
protected override void SaveAllChanges() { SaveAllChangesCount++; }
|
||||
protected override void ExecuteInTransaction(Action action) => action(); // 不包事务,直接执行
|
||||
protected override void CallSaveSwapTradeClientCash(trade td, DateTime valueDate) { } // 空操作
|
||||
protected override void TriggerRealtimeSwapPosition() { } // 空操作
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SwapDealService 测试的共享工厂方法(TestableSwapDealService + UnwindData 构造)。
|
||||
/// 被 SwapUnwindScenarioTest / SwapIncomeScenarioTest 共用。
|
||||
/// </summary>
|
||||
public static class SwapDealTestFactory
|
||||
{
|
||||
public const int SwapTradeId = 7700;
|
||||
public static readonly DateTime ValueDate = new(2026, 6, 15);
|
||||
public static readonly DateTime UnwindDate = new(2026, 6, 16);
|
||||
|
||||
public static trade CreateTrade()
|
||||
{
|
||||
return new trade
|
||||
{
|
||||
id = SwapTradeId, TradeNumber = "UT-SD-001", ClientId = 888888,
|
||||
TradeType = "收益互换", StartDate = new DateTime(2026, 1, 5),
|
||||
ExerciseDate = new DateTime(2026, 6, 14), // 已到期边界(SwapIncome 判断用)
|
||||
TradeStatus = "确认成交", ValidState = "Valid",
|
||||
Notional = 1000000, StockEqvNotional = 1000000, TradeAmount = 10000
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>构造结息/平仓的 UnwindData(金额由前端算好传入,后端直接用)</summary>
|
||||
public static UnwindData CreateUnwindData(decimal swapRealizedPnL, decimal swapMarginRebatePnl = 0m,
|
||||
decimal swapMarginAmount = 0m, int closeMethod = 0, decimal closePercent = 0m,
|
||||
decimal closeQty = 0m, decimal closeNotionalValue = 0m, decimal positionQty = 0m)
|
||||
{
|
||||
return new UnwindData
|
||||
{
|
||||
SwapTradeId = SwapTradeId,
|
||||
SwapRealizedPnL = swapRealizedPnL,
|
||||
SwapMarginRebatePnl = swapMarginRebatePnl,
|
||||
SwapMarginAmount = swapMarginAmount,
|
||||
SwapCloseAmount = swapRealizedPnL,
|
||||
CloseMethod = closeMethod,
|
||||
ClosePercent = closePercent,
|
||||
CloseQty = closeQty,
|
||||
CloseNotionalValue = closeNotionalValue,
|
||||
PositionQty = positionQty,
|
||||
ValueDate = ValueDate,
|
||||
UnwindDate = UnwindDate,
|
||||
StartDate = new DateTime(2026, 1, 5)
|
||||
};
|
||||
}
|
||||
|
||||
public static void AssertDecimalEqual(decimal expected, decimal actual, decimal tolerance, string message = "")
|
||||
{
|
||||
Assert.IsTrue(Math.Abs(expected - actual) <= tolerance,
|
||||
$"{message} Expected: {expected}, Actual: {actual}, Diff: {expected - actual}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user