Files
zszq-trs/UnitTestProject/Modules/SwapModule/DealFloatPositionsScenarioTest.cs
T
hjhan 7b007bddfa refactor(swap-test): 抽取 TestableSwapEodPositionService 公共基类收敛重复 Stub
- 新建 TestableSwapEodPositionService 收敛 8/8 Stub 重复的高频 override
  (PersistEodSwapPosition/SaveAllChanges/GetCurrencyRate/AddClientCash)
  + 统一 OptUserInfo 构造 + PersistedPositions/SaveChangesCount 输出捕获
- SwapEodPositionService.DealInterests 改 protected virtual(行为零变化)
- 8 个 ScenarioTest 改为继承基类,删除重复 override
- 消灭 DealInterestsScenarioTest/DealInterestsGoldenReplayTest 的反射调用
  (typeof().GetMethod().Invoke → 直接调用 DealInterests)

验证:dotnet build 0 错误;dotnet test SwapModule 284通过/6跳过/0失败
2026-07-23 11:16:11 +08:00

416 lines
19 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 YLErp.DBModels;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// DealFloatPositions 浮动腿收盘归档 - 合成单元测试
/// ============================================================================
/// DealFloatPositions 处理浮动腿(标的持仓)的日终归档,三个分支:
/// ① 无前日eod → SaveCurrentEodInitalPosi(首日初始化,纯计算)
/// ② 有eod无平仓 → CopyEodPosition(复制+更新价格,依赖外部数据源)
/// ③ 有eod有平仓 → UpdateEodPosition(更新持仓,依赖外部数据源)
///
/// 当前可测范围:
/// - 分支选择逻辑(DealFloatPositions 调度层,纯内存)
/// - SaveCurrentEodInitalPosi(首日初始化,纯计算,无外部依赖)
/// CopyEodPosition/UpdateEodPosition 需额外接缝(UnderlyingCodePrice等),留后续。
/// ============================================================================
[TestClass]
public class DealFloatPositionsScenarioTest
{
private const int SwapTradeId = 200;
private static readonly DateTime TradeDate = new(2026, 4, 28);
private static readonly DateTime PreSettleDate = new(2026, 4, 27);
#region Stub
private sealed class StubEodService : TestableSwapEodPositionService
{
// 可注入的外部数据
public decimal UnderlyingPrice { get; set; } = 1.00m;
public decimal Vobp { get; set; } = 0m;
public decimal BondPayment { get; set; } = 0m;
public decimal TaxRate { get; set; } = 0m;
public string UnderlyingCode { get; set; } = "210210.IB";
public StubEodService() : base(nameof(DealFloatPositionsScenarioTest))
{
}
// override 外部依赖
protected override decimal GetUnderlyingPrice(string code, DateTime settleDate, out decimal vobp)
{
vobp = Vobp;
return UnderlyingPrice;
}
protected override underlying_manager GetUnderlyingData(string underlyingCode)
{
return new underlying_manager { ValueAddedTax = TaxRate };
}
protected override decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate, decimal qty, int shortRatio, int directionRatio)
{
return BondPayment;
}
// DealFloatPositions 和子方法都是 protected,通过 public 包装暴露
public List<eod_swap_position> ExecuteDealFloatPositions(
List<swap_position> posiList, List<swap_position> realPosiList,
List<eod_swap_position> eodPositions, List<eod_swap_position> todyEodPositions,
DateTime settleDate, trade td, DateTime preSettleDate, List<swap_flow_event> flowEvents)
{
return DealFloatPositions(posiList, realPosiList, eodPositions, todyEodPositions,
settleDate, td, preSettleDate, flowEvents);
}
public eod_swap_position ExecuteSaveCurrentEodInitalPosi(
swap_position position, trade td, DateTime settleDate, DateTime preSettleDate,
List<swap_flow_event> unwindEvents)
{
return SaveCurrentEodInitalPosi(position, td, settleDate, preSettleDate, unwindEvents);
}
public eod_swap_position ExecuteCopyEodPosition(
eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate)
{
return CopyEodPosition(eod, curretEod, td, valueDate, preSettleDate);
}
}
#endregion
#region 数据构建
private static trade CreateTrade()
{
return new trade
{
id = SwapTradeId, TradeNumber = "UT-FLOAT-001", ClientId = 999998,
TradeType = "收益互换", TradeDate = PreSettleDate, StartDate = PreSettleDate,
ExerciseDate = new DateTime(2027, 4, 27), TradeStatus = "确认成交",
ValidState = "Valid", StructureType = "单标的",
QuoteCurrency = "CNY", SettlementCurrency = "CNY",
OriginalStockEqvNotional = 10000
};
}
private static swap_position CreateFloatPosition(int id = 3001, decimal qty = 10000m)
{
return new swap_position
{
id = id, SwapTradeId = SwapTradeId,
PosiDirection = 2, PositionType = (int)PositionTypeFlag.Long,
UnderlyingCode = "210210.IB", ContractSize = 1m,
PosiQuantity = qty, PosiNotionalValue = qty,
PosiNetPrice = 1.005m, PosiGrossPrice = 1.002m,
PosiNetFeePrice = 1.004m, PosiNetNoFeePrice = 1.001m,
IsInitial = true, Invalid = false,
PosiTradingFee = 0, PosiTradingFeePending = 0
};
}
private static swap_flow_event CreateCloseEvent(int positionId, decimal qty, decimal markClosePnl = 100m)
{
return new swap_flow_event
{
SwapTradeId = SwapTradeId, EventType = (int)SwapFlowEventTypeEnum.平仓,
PositionId = positionId, Quantity = qty,
MarkClosePnl = markClosePnl, DividendIn = 0, CloseFee = 5m,
TradingFeePending = 0, TradingAmount = qty * 1.002m,
UnwindDate = TradeDate, EventDate = TradeDate, PayDate = TradeDate,
DataState = (int)SwapFlowDateStateEnum.完成
};
}
#endregion
// ================================================================
// 场景1:空持仓列表 → 返回空列表
// ================================================================
[TestMethod]
public void DF_001_空持仓返回空列表()
{
var service = new StubEodService();
var result = service.ExecuteDealFloatPositions(
new List<swap_position>(), new List<swap_position>(),
new List<eod_swap_position>(), new List<eod_swap_position>(),
TradeDate, CreateTrade(), PreSettleDate, new List<swap_flow_event>());
Assert.AreEqual(0, result.Count, "空持仓应返回空列表");
}
// ================================================================
// 场景2:首日无前日eod → 走 SaveCurrentEodInitalPosi 分支
// ================================================================
/// <summary>
/// 无前日eodeodPositions 不含该持仓),应走 SaveCurrentEodInitalPosi。
/// SaveCurrentEodInitalPosi 是纯计算,验证基本字段正确。
/// </summary>
[TestMethod]
public void DF_002_首日无eod走初始化分支()
{
var service = new StubEodService();
var td = CreateTrade();
var position = CreateFloatPosition();
var result = service.ExecuteDealFloatPositions(
new List<swap_position> { position },
new List<swap_position> { position },
new List<eod_swap_position>(), // 无前日eod
new List<eod_swap_position>(), // 无当日eod
TradeDate, td, PreSettleDate,
new List<swap_flow_event>()); // 无平仓事件
Assert.AreEqual(1, result.Count, "应生成1条浮动腿eod");
var eod = result[0];
Assert.AreEqual(position.id, eod.PositionId, "PositionId应匹配");
Assert.AreEqual(SwapTradeId, eod.SwapTradeId);
Assert.AreEqual(TradeDate, eod.ValueDate);
Console.WriteLine($"首日初始化: PosiQuantity={eod.PosiQuantity}, PosiNetPrice={eod.PosiNetPrice}");
}
// ================================================================
// 场景3SaveCurrentEodInitalPosi 直接验证(纯计算方法)
// ================================================================
/// <summary>
/// 直接测 SaveCurrentEodInitalPosi,验证它正确初始化 eod 的关键字段。
/// 这个方法无外部依赖(纯计算),可以精确验证值。
/// </summary>
[TestMethod]
public void DF_003_首日初始化字段正确()
{
var service = new StubEodService();
var td = CreateTrade();
var position = CreateFloatPosition(qty: 10000m);
var eod = service.ExecuteSaveCurrentEodInitalPosi(
position, td, TradeDate, PreSettleDate, new List<swap_flow_event>());
// 验证关键字段
Assert.AreEqual(10000m, eod.PosiQuantity, "持仓数量应=初始数量");
Assert.AreEqual(1.005m, eod.PosiNetPrice, "含费均价应=持仓均价");
Assert.AreEqual(1.002m, eod.PosiGrossPrice, "不含费均价");
Assert.AreEqual((int)PositionTypeFlag.Long, eod.PositionType, "持仓类型");
Assert.AreEqual(SwapTradeId, eod.SwapTradeId, "交易ID");
Assert.AreEqual(td.ClientId, eod.ClientId, "客户ID");
Assert.AreEqual(0, eod.TdCloseQty, "首日无平仓数量");
Assert.AreEqual(0, eod.TdCloseMtmPnl, "首日无平仓盈亏");
Console.WriteLine($"首日初始化 eod: Qty={eod.PosiQuantity}, NetPrice={eod.PosiNetPrice}, GrossPrice={eod.PosiGrossPrice} ✅");
}
// ================================================================
// 场景4:有前日eod无平仓 → 走 CopyEodPosition 分支
// ================================================================
/// <summary>
/// 有前日eod但无平仓事件,应走 CopyEodPosition 分支。
/// CopyEodPosition 依赖外部数据源(DataCacheProvider/UnderlyingCodePrice),
/// 测试验证分支选择正确(不验证值),且不抛异常。
/// </summary>
[TestMethod]
public void DF_004_有eod无平仓走Copy分支()
{
var service = new StubEodService();
var td = CreateTrade();
var position = CreateFloatPosition();
var preEod = new eod_swap_position
{
id = 5001, SwapTradeId = SwapTradeId, PositionId = position.id,
ValueDate = PreSettleDate, PosiQuantity = 10000m,
PosiNetPrice = 1.005m, PosiGrossPrice = 1.002m,
UnderlyingCode = "210210.IB", ContractSize = 1m,
PositionType = (int)PositionTypeFlag.Long, PosiDirection = 2
};
// CopyEodPosition 内部调 DataCacheProvider/UnderlyingCodePrice
// 这些连缓存可能返回null → 方法 cs:1492 if(um==null) return curretEod
// 所以即使缓存没数据,也不会抛异常,只是字段不更新
try
{
var result = service.ExecuteDealFloatPositions(
new List<swap_position> { position },
new List<swap_position> { position },
new List<eod_swap_position> { preEod },
new List<eod_swap_position>(),
TradeDate, td, PreSettleDate,
new List<swap_flow_event>()); // 无平仓
Assert.AreEqual(1, result.Count, "应生成1条eod");
// um==null时 CopyEodPosition 直接返回 clone,字段不变
Assert.AreEqual(10000m, result[0].PosiQuantity, "无缓存时数量应=前日值");
Console.WriteLine($"Copy分支(无缓存): PosiQuantity={result[0].PosiQuantity}(保持前日值)");
}
catch (Exception ex)
{
Assert.Inconclusive($"CopyEodPosition 依赖外部数据源,需额外接缝。异常: {ex.Message}");
}
}
// ================================================================
// 场景5CopyEodPosition 盯市盈亏计算(注入固定标的价格)
// ================================================================
/// <summary>
/// [DF_005] 无平仓日,标的价格变动 → PosiMtmPnL 正确反映浮动盈亏
/// ---------------------------------------------------------------
/// 前日持仓全价=1.002,当日标的价格=1.010(涨了)。
/// 多头收取方向,PosiMtmPnL = (1.010 - 1.002) × 10000 × 1(ContractSize) × 1(shortRatio) × 1(directionRatio)
/// = 0.008 × 10000 = 80
/// </summary>
[TestMethod]
public void DF_005_Copy分支盯市盈亏计算()
{
var service = new StubEodService();
service.UnderlyingPrice = 1.010m; // 当日标的价格
service.TaxRate = 0m;
service.BondPayment = 0m;
var td = CreateTrade();
var position = CreateFloatPosition();
var preEod = new eod_swap_position
{
id = 5001, SwapTradeId = SwapTradeId, PositionId = position.id,
ValueDate = PreSettleDate, PosiQuantity = 10000m,
PosiGrossPrice = 1.002m, PosiNetPrice = 1.005m,
UnderlyingCode = "210210.IB", ContractSize = 1m,
PositionType = (int)PositionTypeFlag.Long, PosiDirection = (int)SwapDirectionEnum.收取,
PosiDividendSum = 0m, PosiFeePending = 0m, PosiProfitSum = 0m
};
var result = service.ExecuteCopyEodPosition(preEod, null, td, TradeDate, PreSettleDate);
// PosiMtmPnL = (price - grossPrice) × qty × contractSize × shortRatio × directionRatio
// 收取方向 directionRatio=1, 多头 shortRatio=1
// = (1.010 - 1.002) × 10000 × 1 × 1 × 1 = 80
AssertDecimalEqual(80m, result.PosiMtmPnL, 0.01m, "盯市盈亏");
Console.WriteLine($"Copy分支盯市: PosiMtmPnL={result.PosiMtmPnL}(1.010-1.002)×10000=80)✅");
}
// ================================================================
// 场景6CopyEodPosition 分红计算(注入固定付息)
// ================================================================
/// <summary>
/// [DF_006] 无平仓日,债券付息 → TdPosiDividend 和 PosiDividendSum 正确
/// ---------------------------------------------------------------
/// 注入 BondPayment=100(付息总额),增值税率=0。
/// TdPosiDividend = 100 / (1+0) × (1-0) = 100。
/// PosiDividendSum = preEod.PosiDividendSum(0) + TdPosiDividend(100) = 100。
/// </summary>
[TestMethod]
public void DF_006_Copy分支分红计算()
{
var service = new StubEodService();
service.UnderlyingPrice = 1.002m; // 价格不变
service.TaxRate = 0m;
service.BondPayment = 100m; // 付息100
var td = CreateTrade();
var position = CreateFloatPosition();
var preEod = new eod_swap_position
{
id = 5001, SwapTradeId = SwapTradeId, PositionId = position.id,
ValueDate = PreSettleDate, PosiQuantity = 10000m,
PosiGrossPrice = 1.002m, PosiNetPrice = 1.005m,
UnderlyingCode = "210210.IB", ContractSize = 1m,
PositionType = (int)PositionTypeFlag.Long, PosiDirection = (int)SwapDirectionEnum.收取,
PosiDividendSum = 0m, PosiFeePending = 0m, PosiProfitSum = 0m
};
var result = service.ExecuteCopyEodPosition(preEod, null, td, TradeDate, PreSettleDate);
// TdPosiDividend = payment / (1+tax) × (1-tax) = 100 / 1 × 1 = 100
AssertDecimalEqual(100m, result.TdPosiDividend, 0.01m, "当日分红");
// PosiDividendSum = 前日(0) + 当日(100) = 100
AssertDecimalEqual(100m, result.PosiDividendSum, 0.01m, "待实现分红累计");
Console.WriteLine($"Copy分支分红: TdPosiDividend={result.TdPosiDividend}, PosiDividendSum={result.PosiDividendSum} ✅");
}
// ================================================================
// 场景7:分红增值税调整(税率≠0)
// ================================================================
/// <summary>
/// [DF_007] 分红含增值税调整:BondPayment=100, tax=6%(0.06)
/// ---------------------------------------------------------------
/// TdPosiDividend = 100 / (1+0.06) × (1-0.06) = 100/1.06×0.94 ≈ 88.68
/// 验证增值税调整公式 cs:1508 payment / (1+tax) * (1-tax)。
/// </summary>
[TestMethod]
public void DF_007_分红增值税调整()
{
var service = new StubEodService();
service.UnderlyingPrice = 1.002m;
service.TaxRate = 0.06m; // 增值税率6%
service.BondPayment = 100m;
var td = CreateTrade();
var position = CreateFloatPosition();
var preEod = new eod_swap_position
{
id = 5001, SwapTradeId = SwapTradeId, PositionId = position.id,
ValueDate = PreSettleDate, PosiQuantity = 10000m,
PosiGrossPrice = 1.002m, PosiNetPrice = 1.005m,
UnderlyingCode = "210210.IB", ContractSize = 1m,
PositionType = (int)PositionTypeFlag.Long, PosiDirection = 2,
PosiDividendSum = 0m, PosiFeePending = 0m, PosiProfitSum = 0m
};
var result = service.ExecuteCopyEodPosition(preEod, null, td, TradeDate, PreSettleDate);
// TdPosiDividend = 100 / 1.06 × 0.94 = 88.6792...
decimal expected = Math.Round(100m / 1.06m * 0.94m, 2);
AssertDecimalEqual(expected, result.TdPosiDividend, 0.01m, "增值税调整后分红");
Console.WriteLine($"分红增值税调整: 付息100, 税率6% → TdPosiDividend={result.TdPosiDividend}(期望{expected})✅");
}
[TestMethod]
public void DF_008_CopyBranch_RealizedPnlIncludesRealizedFee()
{
var service = new StubEodService
{
UnderlyingPrice = 1.002m,
TaxRate = 0m,
BondPayment = 0m
};
var preEod = new eod_swap_position
{
id = 5001,
SwapTradeId = SwapTradeId,
PositionId = 3001,
ValueDate = PreSettleDate,
PosiQuantity = 10000m,
PosiGrossPrice = 1.002m,
PosiNetPrice = 1.005m,
UnderlyingCode = "210210.IB",
ContractSize = 1m,
PositionType = (int)PositionTypeFlag.Long,
PosiDirection = (int)SwapDirectionEnum.收取,
RealizedMtmPnL = 98000000m,
RealizedDividend = 0m,
RealizedFee = 100m,
RealizedPnl = 98000000m
};
var result = service.ExecuteCopyEodPosition(preEod, null, CreateTrade(), TradeDate, PreSettleDate);
Assert.AreEqual(98000100m, result.RealizedPnl);
}
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}");
}
}
}