Files
zszq-trs/UnitTestProject/Modules/SwapModule/TestableSwapDealService.cs
T
张名锐 b2f4e16782 feat(bond): 支持股票和基金现金分红纳入债券支付计算 - 收紧过度开发行为
- 实现股票/基金现金分红数据从 ex_dividend_info 同步到 BondPayment
- 新增公司行为去重机制,避免镜像任务完成后重复计息
- 统一现金分红存储口径为"每 10 份派现金额",保持与同步任务一致性
- 修改 CalcPayment 方法,股票/基金分红需除以 10 转换实际现金金额
- 添加单元测试验证债券票息和股票/基金分红的不同计算方式
- 更新文档注释说明"每 10 份派现金额"存储规范
- 修复公司行为生效日处理逻辑,确保正确应用除权系数
- 扩展测试覆盖股票类证券的公司行为处理场景
2026-08-20 13:30:36 +08:00

160 lines
7.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Newtonsoft.Json;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// SwapDealService 的可测试化子类(共享 stub)。
/// 继承 SwapDealServiceoverride 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 decimal GetFundCorporateActionClosePrice(
ex_dividend_info dividendInfo,
decimal fallbackPrice)
=> fallbackPrice;
public bool RestoreEffectiveFundPositionForTest(UnwindData unwindData, DateTime valueDate)
=> TryRestoreEffectiveFundPosition(unwindData, valueDate);
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}");
}
}
}