移除中债估值细分与操作人姓名显示,按价格行自身 DataSource 列判定:期货/股票读 DataSource(otc-marketdata 同步写系统、手工写人工);债券 update_user 非空→人工、否则→系统。筛选仅人工/系统两选项,异常来源值返回空。ResolveBondDisplaySource 精简为单参纯函数。
265 lines
12 KiB
C#
265 lines
12 KiB
C#
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.UnderlyingInstrumentType(EodPriceView 靠它选表)</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
|
||
}
|
||
}
|