新增4个ComposePage场景测试(参考testable分支ComposePageScenarioTest): - CP_001: 空事件→不创建持仓 - CP_002: 单条开仓→创建1条持仓,均价=开仓价 - CP_003: 两条开仓(同标的)→加权均价 - CP_004: 开仓+平仓→平仓扣减数量+产生资金记录 改动(SwapEodPositionService.cs): - SaveEodPosition改为protected virtual(可override绕过UpdateSwapPosition连库) - SaveEodPosition内部DbContext.Add替换为PersistEodSwapPosition StubEodPositionService override 12个虚方法: FindTrade/FindTradeExtend/FindEodSwapPositions/FindSwapPositions/ FindEodSwap/AddSwapEvent/AddClientCash/SaveEodSwapRecord/ ClearSwapPositionsForCompose/PersistEodSwapPosition/SaveAllChanges/ GetCurrencyRate + SaveEodPosition 验证: 100+4=104全通过(T0/T1=89+DealInterests=7+ConsumedInterest=4+ComposePage=4)。
271 lines
12 KiB
C#
271 lines
12 KiB
C#
using YLErp.DBModels;
|
|
using YLErp.DBModels.Enums;
|
|
|
|
namespace YLErp.Modules.SwapModule
|
|
{
|
|
/// <summary>
|
|
/// ComposePage 流水合成持仓 - 合成单元测试
|
|
/// ============================================================================
|
|
/// 验证 swap_flow_event(开仓/平仓事件)→ eod_swap_position(持仓)的转换。
|
|
/// ComposePage 是每笔开仓/平仓/互换都要经过的核心逻辑。
|
|
///
|
|
/// 场景参考 testable 分支 ComposePageScenarioTest,简化为最核心的 3 个:
|
|
/// ① 空事件直接返回
|
|
/// ② 单条开仓 → 创建1条持仓,均价=开仓价
|
|
/// ③ 两条开仓(同标的) → 加权均价
|
|
/// ============================================================================
|
|
[TestClass]
|
|
public class ComposePageScenarioTest
|
|
{
|
|
private const int SwapTradeId = 100;
|
|
private static readonly DateTime TradeDate = new(2026, 4, 27);
|
|
|
|
#region Stub
|
|
|
|
private sealed class StubEodService : SwapEodPositionService
|
|
{
|
|
public List<eod_swap_position> CreatedEodPositions { get; } = new();
|
|
public int ClientCashCallCount { get; private set; }
|
|
private int _nextId = 1;
|
|
|
|
public StubEodService() : base(new OptUserInfo(0, nameof(ComposePageScenarioTest), OptUserFrom.UnitTest))
|
|
{
|
|
}
|
|
|
|
// 内存数据
|
|
public Dictionary<int, trade> Trades { get; set; } = new();
|
|
public Dictionary<int, trade_extend> Extends { get; set; } = new();
|
|
public List<swap_position> Positions { get; set; } = new();
|
|
public List<eod_swap_position> EodPositions { get; set; } = new();
|
|
public eod_swap LastEodSwap { get; set; }
|
|
|
|
protected override trade FindTrade(int swapTradeId)
|
|
=> Trades.TryGetValue(swapTradeId, out var t) ? t : null;
|
|
|
|
protected override trade_extend FindTradeExtend(int tradeId)
|
|
=> Extends.TryGetValue(tradeId, out var e) ? e : null;
|
|
|
|
protected override List<eod_swap_position> FindEodSwapPositions(int swapTradeId, DateTime preSettleDate)
|
|
=> EodPositions.Where(x => x.SwapTradeId == swapTradeId && !x.Invalid && x.ValueDate >= preSettleDate).ToList();
|
|
|
|
protected override List<swap_position> FindSwapPositions(int swapTradeId)
|
|
=> Positions.Where(x => x.SwapTradeId == swapTradeId && !x.Invalid).ToList();
|
|
|
|
protected override eod_swap FindEodSwap(int swapTradeId, DateTime valueDate)
|
|
=> LastEodSwap?.SwapTradeId == swapTradeId ? LastEodSwap : null;
|
|
|
|
protected override swap_event AddSwapEvent(DateTime tradeDate, int swapTradeId, int eventType, string data, int clientCashId, bool save, string reason)
|
|
{
|
|
return new swap_event { id = _nextId++, SwapTradeId = swapTradeId, EventType = eventType, ValueDate = tradeDate, EventData = data };
|
|
}
|
|
|
|
protected override int AddClientCash(trade td, double amount, string action, DateTime valueDate)
|
|
{
|
|
ClientCashCallCount++;
|
|
return _nextId++;
|
|
}
|
|
|
|
protected override void SaveEodSwapRecord(trade td, DateTime settleDate, DateTime preSettleDate)
|
|
{
|
|
// 不做任何事(测试不验证框架合约汇总)
|
|
}
|
|
|
|
protected override void ClearSwapPositionsForCompose(trade td, DateTime tradeDate, List<int> eventTypes)
|
|
{
|
|
// 不做任何事(测试无历史事件需清理)
|
|
}
|
|
|
|
protected override void PersistEodSwapPosition(eod_swap_position position)
|
|
{
|
|
if (position.id == 0) position.id = _nextId++;
|
|
CreatedEodPositions.Add(position);
|
|
}
|
|
|
|
protected override void SaveAllChanges() { }
|
|
|
|
protected override double GetCurrencyRate(string quoteCurrency, string settlementCurrency, DateTime valueDate, bool seekPreday, CurrencyRateType currencyRateType)
|
|
=> 1.0;
|
|
|
|
// override SaveEodPosition:捕获生成的 eod,绕过 UpdateSwapPosition 连库
|
|
protected override decimal SaveEodPosition(eod_swap_position newEodPayPosition,
|
|
trade td, swap_flow_event eventFlow,
|
|
decimal netPrice, decimal grossPrice, decimal netFeePrice, decimal netNoFeePrice,
|
|
decimal payQty, decimal tradingFee, decimal posiNotionalValue,
|
|
decimal dividendIn, decimal tdDividendIn,
|
|
decimal closeQty, decimal closeFee, decimal closeMtmPnl,
|
|
int posiType, bool isNewPosition)
|
|
{
|
|
// 设置关键字段(模拟生产逻辑的输出)
|
|
newEodPayPosition.PosiNetPrice = netPrice;
|
|
newEodPayPosition.PosiGrossPrice = grossPrice;
|
|
newEodPayPosition.PosiQuantity = payQty;
|
|
newEodPayPosition.PosiNotionalValue = posiNotionalValue;
|
|
newEodPayPosition.SwapTradeId = td.id;
|
|
newEodPayPosition.ClientId = td.ClientId;
|
|
PersistEodSwapPosition(newEodPayPosition);
|
|
return 0m; // 开仓费(测试不关心)
|
|
}
|
|
|
|
public void ExecuteComposePage(int swapTradeId, List<swap_flow_event> flowEvents, DateTime tradeDate)
|
|
{
|
|
// needTrans=false 跳过事务
|
|
ComposePage(swapTradeId, flowEvents, tradeDate, needTrans: false);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 数据构建
|
|
|
|
private static trade CreateTrade()
|
|
{
|
|
return new trade
|
|
{
|
|
id = SwapTradeId, TradeNumber = "UT-COMPOSE-001", ClientId = 999998,
|
|
TradeType = "收益互换", TradeDate = TradeDate, StartDate = TradeDate,
|
|
ExerciseDate = new DateTime(2027, 4, 27), TradeStatus = "确认成交",
|
|
ValidState = "Valid", StructureType = "单标的",
|
|
QuoteCurrency = "CNY", SettlementCurrency = "CNY",
|
|
trade_extend = new trade_extend { TradeId = SwapTradeId }
|
|
};
|
|
}
|
|
|
|
private static swap_position CreateFloatPosition(int positionId = 1, int positionType = 1)
|
|
{
|
|
return new swap_position
|
|
{
|
|
id = positionId, SwapTradeId = SwapTradeId,
|
|
PosiDirection = 2, PositionType = positionType,
|
|
UnderlyingCode = "210210.IB", ContractSize = 1m,
|
|
PosiQuantity = 0, PosiNotionalValue = 0,
|
|
PosiNetPrice = 0, PosiGrossPrice = 0,
|
|
IsInitial = true, Invalid = false
|
|
};
|
|
}
|
|
|
|
private static swap_flow_event CreateOpenEvent(int positionId, decimal qty, decimal feeAvg, decimal avg, int positionType = 1)
|
|
{
|
|
return new swap_flow_event
|
|
{
|
|
SwapTradeId = SwapTradeId, EventType = (int)SwapFlowEventTypeEnum.开仓,
|
|
PositionId = positionId, Quantity = qty,
|
|
TradingAmountFeeAvg = feeAvg, TradingAmountAvg = avg,
|
|
TradingAmountNetFeeAvg = feeAvg, TradingAmountNetAvg = avg,
|
|
ContractSize = 1m, PositionType = positionType,
|
|
MarkClosePnl = 0, DividendIn = 0, CloseFee = 0, TradingFeePending = 0,
|
|
UnwindDate = TradeDate, EventDate = TradeDate, PayDate = TradeDate,
|
|
DataState = (int)SwapFlowDateStateEnum.等待完成
|
|
};
|
|
}
|
|
|
|
private 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}");
|
|
}
|
|
|
|
private static StubEodService CreateService()
|
|
{
|
|
var svc = new StubEodService();
|
|
svc.Trades[SwapTradeId] = CreateTrade();
|
|
svc.Extends[SwapTradeId] = CreateTrade().trade_extend;
|
|
svc.Positions.Add(CreateFloatPosition());
|
|
return svc;
|
|
}
|
|
|
|
#endregion
|
|
|
|
// ================================================================
|
|
// 场景1:空事件 → 直接返回,不创建任何持仓
|
|
// ================================================================
|
|
|
|
[TestMethod]
|
|
public void CP_001_空事件不创建持仓()
|
|
{
|
|
var service = CreateService();
|
|
service.ExecuteComposePage(SwapTradeId, new List<swap_flow_event>(), TradeDate);
|
|
Assert.AreEqual(0, service.CreatedEodPositions.Count, "无事件不应创建持仓");
|
|
}
|
|
|
|
// ================================================================
|
|
// 场景2:单条开仓 → 创建1条持仓,均价=开仓价
|
|
// ================================================================
|
|
|
|
[TestMethod]
|
|
public void CP_002_单条开仓创建一条持仓()
|
|
{
|
|
var service = CreateService();
|
|
var events = new List<swap_flow_event>
|
|
{
|
|
CreateOpenEvent(positionId: 1, qty: 1000, feeAvg: 1.0050m, avg: 1.0020m)
|
|
};
|
|
|
|
service.ExecuteComposePage(SwapTradeId, events, TradeDate);
|
|
|
|
Assert.AreEqual(1, service.CreatedEodPositions.Count, "应创建1条持仓");
|
|
var pos = service.CreatedEodPositions[0];
|
|
Assert.AreEqual(1000m, pos.PosiQuantity, "持仓数量=1000");
|
|
AssertDecimalEqual(1.0050m, pos.PosiNetPrice, 0.0001m, "含费均价");
|
|
AssertDecimalEqual(1.0020m, pos.PosiGrossPrice, 0.0001m, "不含费均价");
|
|
Assert.AreEqual((int)SwapFlowDateStateEnum.完成, events[0].DataState, "事件应标记完成");
|
|
}
|
|
|
|
// ================================================================
|
|
// 场景3:两条开仓(同标的) → 加权均价
|
|
// ================================================================
|
|
|
|
[TestMethod]
|
|
public void CP_003_两条开仓加权均价()
|
|
{
|
|
var service = CreateService();
|
|
var events = new List<swap_flow_event>
|
|
{
|
|
CreateOpenEvent(positionId: 1, qty: 600, feeAvg: 1.0040m, avg: 1.0010m),
|
|
CreateOpenEvent(positionId: 1, qty: 400, feeAvg: 1.0060m, avg: 1.0030m)
|
|
};
|
|
|
|
service.ExecuteComposePage(SwapTradeId, events, TradeDate);
|
|
|
|
Assert.AreEqual(1, service.CreatedEodPositions.Count);
|
|
var pos = service.CreatedEodPositions[0];
|
|
|
|
// 加权均价: netPrice = (1.0040*600 + 1.0060*400) / 1000 = 1.0048
|
|
AssertDecimalEqual(1.0048m, pos.PosiNetPrice, 0.0001m, "加权含费均价");
|
|
// grossPrice = (1.0010*600 + 1.0030*400) / 1000 = 1.0018
|
|
AssertDecimalEqual(1.0018m, pos.PosiGrossPrice, 0.0001m, "加权不含费均价");
|
|
}
|
|
|
|
// ================================================================
|
|
// 场景4:一条开仓+一条平仓 → 验证平仓扣减数量
|
|
// ================================================================
|
|
|
|
[TestMethod]
|
|
public void CP_004_开仓后平仓扣减数量()
|
|
{
|
|
var service = CreateService();
|
|
var events = new List<swap_flow_event>
|
|
{
|
|
CreateOpenEvent(positionId: 1, qty: 1000, feeAvg: 1.0050m, avg: 1.0020m),
|
|
new swap_flow_event
|
|
{
|
|
SwapTradeId = SwapTradeId, EventType = (int)SwapFlowEventTypeEnum.平仓,
|
|
PositionId = 1, Quantity = 400,
|
|
TradingAmountFeeAvg = 1.0050m, TradingAmountAvg = 1.0020m,
|
|
ContractSize = 1m, PositionType = 1,
|
|
MarkClosePnl = 100m, DividendIn = 0, CloseFee = 5m, TradingFeePending = 0,
|
|
UnwindDate = TradeDate, EventDate = TradeDate, PayDate = TradeDate,
|
|
DataState = (int)SwapFlowDateStateEnum.等待完成
|
|
}
|
|
};
|
|
|
|
service.ExecuteComposePage(SwapTradeId, events, TradeDate);
|
|
|
|
Assert.AreEqual(1, service.CreatedEodPositions.Count);
|
|
var pos = service.CreatedEodPositions[0];
|
|
// 开仓1000 - 平仓400 = 剩余600
|
|
Assert.AreEqual(600m, pos.PosiQuantity, "开仓1000-平仓400=剩余600");
|
|
Assert.IsTrue(service.ClientCashCallCount > 0, "平仓应产生资金记录");
|
|
}
|
|
}
|
|
}
|