- 添加保证金返息去重机制,防止平仓时返息被重复计入资金记录 - 新增 GetMainCashRealizedPnL 方法专门处理返息剔除逻辑 - 更新 DealUnwind 和 ApproveSwapTrade 方法使用统一的返息处理口径 - 修改 ReleaseMarginByFundTag 方法增强标签分流功能 - 添加多个单元测试验证返息处理的正确性 - 将 SaveAllChanges 设为可测试化接缝便于测试验证 - 调整 FundTag 分流逻辑支持授信和现金腿分别处理
209 lines
10 KiB
C#
209 lines
10 KiB
C#
using Newtonsoft.Json;
|
||
using YLErp.DBModels;
|
||
using YLErp.DBModels.Enums;
|
||
using YLErp.Modules.SwapModule.Margin;
|
||
|
||
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;
|
||
|
||
/// <summary>Fund 盤中基线测试输入;生产服务通过数据库查询同名 seam。</summary>
|
||
public swap_position RealtimeFloatPosition { get; set; }
|
||
public eod_swap_position LatestFundEodPosition { get; set; }
|
||
public bool HasCompletedFlowAfterLatestFundEod { get; set; }
|
||
public List<swap_position> ActiveSwapPositions { get; set; } = new();
|
||
public List<ex_dividend_info> ExDividendInfos { get; } = new();
|
||
|
||
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 List<swap_position> FindActiveSwapPositions(int tradeId)
|
||
=> ActiveSwapPositions;
|
||
|
||
protected override swap_position FindRealtimeFloatPosition(UnwindData unwindData)
|
||
=> RealtimeFloatPosition;
|
||
|
||
protected override eod_swap_position FindLatestFundEodPosition(int tradeId, long positionId, DateTime valueDate)
|
||
=> LatestFundEodPosition;
|
||
|
||
protected override bool HasCompletedFlowAfterFundEod(int tradeId, long positionId, DateTime eodDate, DateTime valueDate)
|
||
=> HasCompletedFlowAfterLatestFundEod;
|
||
|
||
protected override ex_dividend_info FindFundCorporateAction(string underlyingCode, DateTime valueDate)
|
||
=> ExDividendInfos.FirstOrDefault(x => x.ValidStatus
|
||
&& x.UnderlyingCode == underlyingCode
|
||
&& x.EffectiveDate == valueDate.Date);
|
||
|
||
protected override List<ex_dividend_info> FindFundCorporateActions(
|
||
string underlyingCode,
|
||
DateTime eodDate,
|
||
DateTime valueDate)
|
||
=> ExDividendInfos
|
||
.Where(x => x.ValidStatus
|
||
&& x.UnderlyingCode == underlyingCode
|
||
&& x.EffectiveDate.HasValue
|
||
&& x.EffectiveDate.Value.Date > eodDate.Date
|
||
&& x.EffectiveDate.Value.Date <= valueDate.Date)
|
||
.OrderBy(x => x.EffectiveDate)
|
||
.ThenBy(x => x.id)
|
||
.ToList();
|
||
|
||
protected override decimal GetFundCorporateActionClosePrice(
|
||
ex_dividend_info dividendInfo,
|
||
decimal fallbackPrice)
|
||
=> fallbackPrice;
|
||
|
||
public bool RestoreEffectiveFundPositionForTest(UnwindData unwindData, DateTime valueDate)
|
||
=> TryRestoreAndValidateUnwindData(unwindData, valueDate);
|
||
|
||
public void DealUnwindForTest(UnwindData unwindData, string actionMsg = "系统操作_自动平仓")
|
||
=> DealUnwind(unwindData, _trade, actionMsg);
|
||
|
||
/// <summary>
|
||
/// FundTag 分流 stub 的可配置返回值:非 null 时直接返回(模拟授信/现金腿拆分,
|
||
/// 见 UW_003B);null 时保持旧行为——传入金额全额现金
|
||
/// (CashMargin=marginAmount, CashRebate=marginRebate),等价于"全现金/无标签"场景。
|
||
/// </summary>
|
||
public UnwindTagSplit ReleaseMarginByFundTagResult { get; set; }
|
||
|
||
/// <summary>保留已忽略的历史互换场景测试所需的兼容属性。</summary>
|
||
public List<bool> ReleaseMarginPrincipalFlags { get; } = new();
|
||
|
||
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++;
|
||
}
|
||
|
||
// R4 按标签分流释放:纯内存测试不连库(生产 GetSettlements 查 swap_position 标签),
|
||
// 默认 stub 为全现金(与既有断言语义一致);ReleaseMarginByFundTagResult 非 null 时
|
||
// 返回它以模拟授信/现金拆分。
|
||
protected override UnwindTagSplit ReleaseMarginByFundTag(trade td, DateTime valueDate, List<swap_flow_event> interestEvents,
|
||
decimal marginAmount, decimal marginRebate)
|
||
{
|
||
var marginLegs = interestEvents
|
||
.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)
|
||
&& (x.InterestMode == (int)InterestModeEnum.初始预付金
|
||
|| x.InterestMode == (int)InterestModeEnum.追加预付金))
|
||
.ToList();
|
||
return ReleaseMarginByFundTagResult
|
||
?? new UnwindTagSplit
|
||
{
|
||
CashMargin = Convert.ToDouble(marginAmount),
|
||
CashRebate = Convert.ToDouble(marginLegs.Any()
|
||
? marginLegs.Sum(x => x.InterestClosePnL)
|
||
: marginRebate)
|
||
};
|
||
}
|
||
|
||
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}");
|
||
}
|
||
}
|
||
}
|