Files
zszq-trs/UnitTestProject/Modules/EodModule/EodPriceGoldenReplayTest.cs
T
hjhan 0fda5fd1cc fix: 日终价格「数据来源」改为价格行自身语义(人工/系统两档)
移除中债估值细分与操作人姓名显示,按价格行自身 DataSource 列判定:期货/股票读 DataSource(otc-marketdata 同步写系统、手工写人工);债券 update_user 非空→人工、否则→系统。筛选仅人工/系统两选项,异常来源值返回空。ResolveBondDisplaySource 精简为单参纯函数。
2026-07-13 15:42:28 +08:00

265 lines
12 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;
namespace YLErp.Modules.EodModule
{
#region Golden 数据模型
/// <summary>
/// 日终价格"标的种类 + 数据来源"golden 场景模型。
/// 每个 JSON 文件存:一组原始输入行 + 每行的期望输出(种类中文/来源/路由键)。
/// 结构与 SwapModule 的 GoldenScenarioModel 对齐(Scenario/Description/Source + Rows)。
/// </summary>
public class EodPriceGoldenModel
{
public string Scenario { get; set; }
public string Description { get; set; }
/// <summary>synthetic(合成 Mock) / recorded(真实库录制)</summary>
public string Source { get; set; } = "synthetic";
public DateTime? RecordedAt { get; set; }
public List<EodPriceGoldenRow> Rows { get; set; } = new();
}
public class EodPriceGoldenRow
{
public string UnderlyingCode { get; set; }
/// <summary>存储表路由键 = DTO.UnderlyingInstrumentTypeEodPriceView 靠它选表)</summary>
public string RouteKey { get; set; }
/// <summary>真实标的种类 = underlying_manager.UnderlyingInstrumentType</summary>
public string RealInstrumentType { get; set; }
public bool IsBond { get; set; }
/// <summary>期望的"标的种类"列显示值</summary>
public string ExpectedTypeCn { get; set; }
/// <summary>期望的"数据来源"(仅债券行断言)</summary>
public string ExpectedDataSource { get; set; }
}
#endregion
/// <summary>
/// 日终价格 Golden 回放测试
/// ============================================================================
/// 仿 SwapModule/DealInterestsGoldenReplayTest
/// - Record_* :连真实库拉数据生成 golden JSON(标 [Ignore],手动跑)
/// - Replay_* :读 Mock/录制 JSON 重放并逐行断言(进 CI,不碰库)
///
/// 守护点(回放时任何一行不符即失败):
/// 1. 标的种类按真实类型显示(现券→信用债、贵金属→黄金现货…),不再一律"商品期货";
/// 2. 路由键 UnderlyingInstrumentType 保持不变(保证"查看"不串表);
/// 3. 债券数据来源固定为中债估值(聚源仅转发,无人手工维护,不随 JSID 变化)。
/// ============================================================================
/// </summary>
[TestClass]
public class EodPriceGoldenReplayTest
{
private static readonly string GoldenDir = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory, "Resources", "GoldenFiles", "EodPriceGolden");
#region 回放:读 golden 重放 + 逐行断言(进 CI
[TestMethod]
public void Replay_AllGoldenFiles()
{
if (!Directory.Exists(GoldenDir))
{
Assert.Inconclusive($"golden 目录不存在: {GoldenDir}");
return;
}
var files = Directory.GetFiles(GoldenDir, "*.json").OrderBy(f => f).ToArray();
Assert.IsTrue(files.Length > 0, "应至少有 1 个 golden 文件");
int rowsChecked = 0;
foreach (var file in files)
{
var golden = JsonConvert.DeserializeObject<EodPriceGoldenModel>(File.ReadAllText(file));
Console.WriteLine($"\n回放: {Path.GetFileName(file)} - {golden.Scenario} [{golden.Source}]");
foreach (var row in golden.Rows)
{
// 用原始输入重建 DTO(等价于 SearchUnderlyingList 的投影结果)
var dto = new EodUnderlyingPriceDto
{
UnderlyingCode = row.UnderlyingCode,
UnderlyingInstrumentType = row.RouteKey, // 路由键
RealInstrumentType = row.RealInstrumentType, // 真实类型
IsBond = row.IsBond
};
// 债券来源:自动同步(中债)→系统(等价 SearchUnderlyingList 后处理赋值;synthetic 无 UpdateUser 故为系统)
if (dto.IsBond)
{
dto.DataSource = EodPriceBase.系统;
}
// 守护点1:显示按真实类型
Assert.AreEqual(row.ExpectedTypeCn, dto.UnderlyingInstrumentTypeCn,
$"[{row.UnderlyingCode}] 标的种类显示不符");
// 守护点2:路由键不变
Assert.AreEqual(row.RouteKey, dto.UnderlyingInstrumentType,
$"[{row.UnderlyingCode}] 路由键被改动,会导致查看串表");
// 守护点3:债券来源
if (row.IsBond)
{
Assert.AreEqual(row.ExpectedDataSource, dto.DataSource,
$"[{row.UnderlyingCode}] 债券数据来源判定不符");
}
rowsChecked++;
Console.WriteLine($" ✅ {row.UnderlyingCode}: {dto.UnderlyingInstrumentTypeCn}" +
(row.IsBond ? $" / {dto.DataSource}" : ""));
}
}
Console.WriteLine($"\n回放完成,共校验 {rowsChecked} 行");
Assert.IsTrue(rowsChecked > 0, "至少应校验 1 行");
}
#endregion
#region 录制:连真实库拉数据生成 golden(标 [Ignore],手动跑)
/// <summary>
/// 从真实库拉一批 underlying_manager + china_bond_valuation
/// 按当前生产逻辑生成 recorded golden JSON。
/// 手动取消 [Ignore] 运行;生成后复制到 Resources/GoldenFiles/EodPriceGolden/ 持久化。
/// </summary>
[TestMethod]
[Ignore]
[TestCategory("GoldenRecord")]
public void Record_FromRealDb()
{
Directory.CreateDirectory(GoldenDir);
var golden = new EodPriceGoldenModel
{
Scenario = "标的种类与来源(真实库录制)",
Description = "从 underlying_manager/china_bond_valuation 采样,快照当前生产映射",
Source = "recorded",
RecordedAt = DateTime.Now
};
using (var db = DbContextFactory.GetYLDbContext())
{
// 采样若干上线标的(含真实类型)
var uns = db.underlying_manager
.Where(x => x.LaunchState == "1")
.Select(x => new { x.UnderlyingCode, x.UnderlyingInstrumentType })
.Take(30).ToList();
// 债券估值采样(来源:自动同步→系统,手工改过→人工)
var bonds = db.china_bond_valuation
.Select(b => new { b.bond_id })
.Take(200).ToList();
var bondCodes = new HashSet<string>(bonds.Select(b => b.bond_id));
foreach (var un in uns)
{
bool isBond = bondCodes.Contains(un.UnderlyingCode);
// 路由键:债券走真实类型,其余按来源表默认(这里录制以真实类型近似,
// 因为 recorded 主要用于快照真实分布;CI 用 synthetic 覆盖精确路由)。
string routeKey = isBond
? un.UnderlyingInstrumentType
: ConsGlobal.InstrumentType.CommodityFutures;
golden.Rows.Add(new EodPriceGoldenRow
{
UnderlyingCode = un.UnderlyingCode,
RouteKey = routeKey,
RealInstrumentType = un.UnderlyingInstrumentType,
IsBond = isBond,
ExpectedTypeCn = ConsGlobal.InstrumentType.GetDesc(un.UnderlyingInstrumentType),
ExpectedDataSource = isBond ? EodPriceBase.系统 : null
});
}
}
var path = Path.Combine(GoldenDir, "golden_标的种类与来源_recorded.json");
File.WriteAllText(path, JsonConvert.SerializeObject(golden, Formatting.Indented));
Console.WriteLine($"✅ 录制 {golden.Rows.Count} 行 -> {path}");
}
#endregion
#region 回归:新增日终价格可见性(连真实库,标 [Ignore] 手动跑)
/// <summary>
/// 回归"新增日终价格后是否查得出",直接跑生产查询 SearchUnderlyingList。
/// 守护点(与之前"新增后查不出"的修复一一对应):
/// (a) 今天 + 已上市(LaunchState=1) 标的 → 查得出;
/// (b) 估值日期=0001(未填) → 落在列表默认"仅今天"窗口外 → 查不出;
/// (c) 标的未上市(LaunchState!=1) → 被 inner join(underlying_manager.LaunchState=="1") 过滤 → 查不出。
/// 复用库中已有标的(不新建 underlying_manager,避免触碰该表约束),只插入/清理临时债券估值行。
/// </summary>
[TestMethod]
[Ignore]
[TestCategory("EodVisibility")]
[Description("新增日终价格可见性:(a)今天+已上市可查 (b)日期0001查不出 (c)未上市查不出")]
public void Record_NewRecordVisibility()
{
using (var db = DbContextFactory.GetYLDbContext())
{
var svc = new EodPriceService(OptUserInfo.SystemUser);
var today = DateTime.Today;
var req = new EodCommodityFuturePriceReq { ValueDateStart = today, ValueDateEnd = today };
// 取一个已上市的债券类标的(正向用例);退而求其次取任意已上市标的
var listedBond = db.underlying_manager
.FirstOrDefault(x => x.LaunchState == "1" && x.UnderlyingInstrumentType == ConsGlobal.InstrumentType.CreditBonds)
?? db.underlying_manager.FirstOrDefault(x => x.LaunchState == "1");
Assert.IsNotNull(listedBond, "需存在一个 LaunchState=1 的标的用于正向回归");
// 取一个未上市的标的(负向用例)
var unlisted = db.underlying_manager.FirstOrDefault(x => x.LaunchState != "1");
Assert.IsNotNull(unlisted, "需存在一个 LaunchState!=1 的标的用于负向回归");
var insertedIds = new List<long>();
try
{
// (a) 今天 + 已上市 → 查得出
var a = new ChinaBondValuation { bond_id = listedBond.UnderlyingCode, valuation_date = today, dirty_price_close = 100, net_price = 100, yield = 3 };
db.china_bond_valuation.Add(a);
db.SaveChanges();
insertedIds.Add(a.id);
var rA = svc.SearchUnderlyingList(req);
Assert.IsTrue(rA.rows.Any(x => x.id == a.id), "(a) 今天+已上市债券应查得出");
// (b) 日期=0001(未填) → 落在仅今天窗口外,查不出
var b = new ChinaBondValuation { bond_id = listedBond.UnderlyingCode, valuation_date = DateTime.MinValue, dirty_price_close = 100, net_price = 100, yield = 3 };
db.china_bond_valuation.Add(b);
db.SaveChanges();
insertedIds.Add(b.id);
var rB = svc.SearchUnderlyingList(req);
Assert.IsFalse(rB.rows.Any(x => x.id == b.id), "(b) 日期0001 应查不出");
// (c) 未上市标的 → 被 inner join 过滤,查不出
var c = new ChinaBondValuation { bond_id = unlisted.UnderlyingCode, valuation_date = today, dirty_price_close = 100, net_price = 100, yield = 3 };
db.china_bond_valuation.Add(c);
db.SaveChanges();
insertedIds.Add(c.id);
var rC = svc.SearchUnderlyingList(req);
Assert.IsFalse(rC.rows.Any(x => x.id == c.id), "(c) 未上市标的应查不出");
}
finally
{
foreach (var id in insertedIds)
{
var e = db.china_bond_valuation.Find(id);
if (e != null) db.china_bond_valuation.Remove(e);
}
db.SaveChanges();
}
}
}
#endregion
}
}