- 优化 FindFundCorporateActions 方法,明确查询范围为严格大于 eodDate 的开区间,并添加日志记录 - 重命名 TryRestoreEffectiveFundPosition
174 lines
8.1 KiB
C#
174 lines
8.1 KiB
C#
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;
|
||
|
||
/// <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);
|
||
|
||
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}");
|
||
}
|
||
}
|
||
}
|