分红被重复计入 RealizedPnl 两次的根因:互换/平仓事件的 MarkClosePnl
已含分红成分(DividendIn),导致分红既进 RealizedMtmPnL(盯市列) 又进
RealizedDividend(分红列)。本次提供数据证据与回归基线:
- SwapDividendGoldenRecordTest: 连库录制1875(纯分红型)/1891(混合型)
5表快照→JSON golden,录制时逐事件拆解 MarkClosePnl=价差+费+分红,
按 PositionId 汇总出重复计入的分红金额。Step2 离线校验已通过。
诊断结论:1875 重复302400,1891 重复-18.66,均与 eod 分红列吻合。
- golden JSON 纳入源码树(全量复制规则),修复后可作回归基线。
- 排查SQL工具箱: 基于真实DDL核对全部列名(swap_flow_event.EventDate/
bond_payment_info.paying_interest 等),含定位样本/导出/验证/守恒4步。
- appsettings.json 测试库 zszq→glms 前缀修正。
详见 项目文档/互换分红损益字段语义与重复计算分析.md(9e6af84e)。
427 lines
22 KiB
C#
427 lines
22 KiB
C#
using Newtonsoft.Json;
|
||
using Newtonsoft.Json.Linq;
|
||
|
||
namespace YLErp.Modules.SwapModule
|
||
{
|
||
/// <summary>
|
||
/// 互换分红损益 - 黄金文件录制测试(A1:仅录制,不回放)
|
||
/// ============================================================================
|
||
/// 目的:
|
||
/// 从测试库录制一笔"带分红的互换交易"完整生命周期数据(5表快照),
|
||
/// 序列化为 JSON golden 文件。既作为:
|
||
/// (1) 人工/脚本验证"分红重复计算 2 次"的数据证据;
|
||
/// (2) 后续 EOD 重构(抽虚方法)后,回放回归测试的 golden source 种子。
|
||
///
|
||
/// 为什么是录制而不是回放:
|
||
/// 回放需要 SwapEodPositionService 把 DB 调用抽成虚方法(参考
|
||
/// refactor-swap-event-testable 分支的 TestableSwapEodPositionService)。
|
||
/// 当前 1.4.2 分支尚未做该重构,故先录制 golden 数据。
|
||
/// TDD 红灯:录制数据会暴露 RealizedPnl 中分红被计 2 次的事实,
|
||
/// 待"方向A:让 MarkClosePnl 不含分红"修复后,同一批 golden 用于回归守底。
|
||
///
|
||
/// 运行方式:
|
||
/// 全部标 [Ignore]+[TestCategory("DBRecording")],不会自动跑(不依赖测试库环境)。
|
||
/// 手动执行:在测试资源管理器取消忽略,或用 vstest:
|
||
/// vstest.console.exe UnitTestProject.dll /TestCaseFilter:"TestCategory=DBRecording"
|
||
/// 录制产物落 bin/$(Configuration)/net6.0/Resources/GoldenFiles/SwapDividend/*.json
|
||
/// ============================================================================
|
||
|
||
[TestClass]
|
||
public class SwapDividendGoldenRecordTest
|
||
{
|
||
private static readonly string GoldenDir = Path.Combine(
|
||
AppDomain.CurrentDomain.BaseDirectory, "Resources", "GoldenFiles", "SwapDividend");
|
||
|
||
private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings
|
||
{
|
||
Formatting = Formatting.Indented,
|
||
NullValueHandling = NullValueHandling.Include,
|
||
DateFormatString = "yyyy-MM-ddTHH:mm:ss",
|
||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
|
||
};
|
||
|
||
// 已确认的样本交易(来自测试库 swap_flow_event EventType in(3,4) DividendIn<>0 筛选):
|
||
// 1875 = 纯分红型(MarkClosePnl==DividendIn,最干净,重复计算最直观)
|
||
// 1891 = 混合型 (MarkClosePnl 含价差成分,复杂场景)
|
||
private static readonly int[] SampleTradeIds = { 1875, 1891 };
|
||
|
||
/// <summary>
|
||
/// Step0:列出库中所有"带分红的互换交易",确认样本有效性。
|
||
/// 打印 SwapTradeId / 分红合计 / 盯市合计 / eod快照数,供挑选样本。
|
||
///
|
||
/// 可直接运行:连不上测试库时返回 Inconclusive(不计入失败),不挡 CI;
|
||
/// 连得上时输出诊断表。这是日常排查"库里有啥分红交易"的入口。
|
||
/// </summary>
|
||
[TestMethod]
|
||
[TestCategory("DBRecording")]
|
||
public void Step0_ListDividendSwapTrades()
|
||
{
|
||
YLContext db;
|
||
try { db = DbContextFactory.GetYLDbContext(); }
|
||
catch (Exception ex)
|
||
{
|
||
Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}");
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
var trades = db.swap_flow_event
|
||
.Where(x => (x.EventType == (int)SwapFlowEventTypeEnum.互换
|
||
|| x.EventType == (int)SwapFlowEventTypeEnum.自动互换)
|
||
&& x.DividendIn != 0m
|
||
&& x.DataState == (int)SwapFlowDateStateEnum.完成)
|
||
.AsEnumerable()
|
||
.GroupBy(x => x.SwapTradeId)
|
||
.Select(g => new
|
||
{
|
||
SwapTradeId = g.Key,
|
||
SwapTradeNo = g.Select(x => x.SwapTradeNo).FirstOrDefault(s => !string.IsNullOrEmpty(s)),
|
||
DividendSum = g.Sum(x => x.DividendIn),
|
||
MarkCloseSum = g.Sum(x => x.MarkClosePnl),
|
||
LastEventDate = g.Max(x => x.EventDate),
|
||
EodPositionCount = db.eod_swap_position.Count(e => e.SwapTradeId == g.Key),
|
||
EodSwapCount = db.eod_swap.Count(e => e.SwapTradeId == g.Key)
|
||
})
|
||
.OrderByDescending(t => Math.Abs(t.DividendSum))
|
||
.ToList();
|
||
|
||
Console.WriteLine($"=== 带分红的互换交易数: {trades.Count} ===");
|
||
Console.WriteLine($"{"TradeId",8} {"SwapTradeNo",-24} {"分红合计",14} {"盯市合计",14} {"eod持仓",8} {"eod汇总",8}");
|
||
foreach (var t in trades)
|
||
{
|
||
Console.WriteLine($"{t.SwapTradeId,8} {(t.SwapTradeNo ?? ""),-24} {t.DividendSum,14:F4} {t.MarkCloseSum,14:F4} {t.EodPositionCount,8} {t.EodSwapCount,8}");
|
||
// 直观诊断:分红型交易若 MarkCloseSum≈DividendSum,说明 MarkClosePnl 全是分红(重复计算铁证)
|
||
if (Math.Abs(t.MarkCloseSum - t.DividendSum) < 0.01m && t.DividendSum != 0m)
|
||
{
|
||
Console.WriteLine($" ↳ ⚠ MarkClosePnl合计≈DividendIn合计 → 盯市列里全是分红,RealizedPnl 会计 2 次");
|
||
}
|
||
}
|
||
Assert.IsTrue(trades.Count > 0, "库中应存在带分红的互换交易");
|
||
}
|
||
finally
|
||
{
|
||
db?.Dispose();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Step1:逐笔录制样本交易的完整快照(5表),并输出分红重复计算诊断。
|
||
///
|
||
/// 为何保留 [Ignore]:本方法有写文件副作用(落 golden JSON),
|
||
/// 不应随每次构建/CI 自动执行;只在需要"刷新 golden 种子"时手动触发。
|
||
/// 运行方式(三选一):
|
||
/// - VS 测试资源管理器:选中本方法 → 右键 → 运行(VS 默认会跑被 Ignore 的,除非全局过滤)
|
||
/// - 命令行:dotnet test --filter "FullyQualifiedName~Step1_RecordSampleTrades"
|
||
/// - 临时:删掉本方法上的 [Ignore] 再跑,跑完恢复
|
||
/// </summary>
|
||
[TestMethod]
|
||
[TestCategory("DBRecording")]
|
||
// [Ignore] 临时取消以运行录制
|
||
public void Step1_RecordSampleTrades()
|
||
{
|
||
using var db = DbContextFactory.GetYLDbContext();
|
||
Directory.CreateDirectory(GoldenDir);
|
||
int recorded = 0;
|
||
|
||
foreach (var tradeId in SampleTradeIds)
|
||
{
|
||
Console.WriteLine($"\n========== 录制 SwapTradeId={tradeId} ==========");
|
||
|
||
// 1. 交易主信息
|
||
var trade = db.trade.FirstOrDefault(t => t.id == tradeId);
|
||
if (trade == null)
|
||
{
|
||
Console.WriteLine($"⚠ trade 表无 SwapTradeId={tradeId},跳过");
|
||
continue;
|
||
}
|
||
|
||
// 2. 五表快照
|
||
var positions = db.swap_position
|
||
.Where(p => p.SwapTradeId == tradeId)
|
||
.OrderByDescending(p => p.IsInitial).ThenBy(p => p.PositionId)
|
||
.ToList();
|
||
var flowEvents = db.swap_flow_event
|
||
.Where(e => e.SwapTradeId == tradeId)
|
||
.OrderBy(e => e.EventDate).ThenBy(e => e.EventType).ThenBy(e => e.id)
|
||
.ToList();
|
||
var eodPositions = db.eod_swap_position
|
||
.Where(e => e.SwapTradeId == tradeId)
|
||
.OrderBy(e => e.PositionId).ThenBy(e => e.ValueDate)
|
||
.ToList();
|
||
var eodSwaps = db.eod_swap
|
||
.Where(e => e.SwapTradeId == tradeId)
|
||
.OrderBy(e => e.ValueDate)
|
||
.ToList();
|
||
|
||
// 3. 关联的债券付息(理论应付分红来源)
|
||
var bondCode = positions.Select(p => p.UnderlyingCode).FirstOrDefault(c => !string.IsNullOrEmpty(c))
|
||
?? trade.UnderlyingCode;
|
||
List<BondPayment> bondPayments = new List<BondPayment>();
|
||
if (!string.IsNullOrEmpty(bondCode))
|
||
{
|
||
bondPayments = db.bondPayment
|
||
.Where(b => b.underlyingCode == bondCode)
|
||
.OrderBy(b => b.payment_date_pl)
|
||
.ToList();
|
||
}
|
||
|
||
// 4. 诊断:坐实分红重复计算(这是录制测试的核心价值)
|
||
var diagnosis = DiagnoseDividendDoubleCount(tradeId, flowEvents, eodPositions);
|
||
Console.WriteLine(diagnosis.Summary);
|
||
|
||
// 5. 序列化为 golden 文件
|
||
var golden = new SwapDividendGoldenModel
|
||
{
|
||
SwapTradeId = tradeId,
|
||
SwapTradeNo = trade.TradeNumber,
|
||
UnderlyingCode = bondCode,
|
||
RecordedAt = DateTime.Now,
|
||
SourceDb = "test",
|
||
Purpose = "分红重复计算验证 + EOD重构回归基线",
|
||
InputTrade = JObject.FromObject(trade, JsonSerializer.Create(JsonSettings)),
|
||
InputPositions = JArray.FromObject(positions, JsonSerializer.Create(JsonSettings)),
|
||
InputFlowEvents = JArray.FromObject(flowEvents, JsonSerializer.Create(JsonSettings)),
|
||
InputEodPositions = JArray.FromObject(eodPositions, JsonSerializer.Create(JsonSettings)),
|
||
InputEodSwaps = JArray.FromObject(eodSwaps, JsonSerializer.Create(JsonSettings)),
|
||
InputBondPayments = JArray.FromObject(bondPayments, JsonSerializer.Create(JsonSettings)),
|
||
Diagnosis = JObject.FromObject(diagnosis, JsonSerializer.Create(JsonSettings))
|
||
};
|
||
|
||
string fileName = $"dividend_trade_{tradeId}.json";
|
||
string filePath = Path.Combine(GoldenDir, fileName);
|
||
File.WriteAllText(filePath, JsonConvert.SerializeObject(golden, JsonSettings));
|
||
Console.WriteLine($"✅ 已保存: {filePath}");
|
||
recorded++;
|
||
}
|
||
|
||
Assert.IsTrue(recorded > 0, "至少应录制 1 笔样本");
|
||
Console.WriteLine($"\n录制完成,共 {recorded} 笔,输出目录: {GoldenDir}");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 分红重复计算诊断:对照代码行号,把链路数据逐一算出来。
|
||
/// 重复计算根因链路(SwapEodPositionService.cs):
|
||
/// SetPriceInfoByFlowEvent:1610 TdCloseMtmPnl = Σ MarkClosePnl(互换事件里已含分红)
|
||
/// UpdateEodPosition:1486 RealizedMtmPnL += TdCloseMtmPnl ← 分红第1次(盯市列)
|
||
/// UpdateEodPosition:1488 TdCloseDividend = Σ DividendIn
|
||
/// UpdateEodPosition:1494 RealizedDividend += TdCloseDividend ← 分红第2次(分红列)
|
||
/// SaveEodSwap:1869 eod_swap.RealizedPnL = Σ(RealizedMtmPnL + RealizedDividend + ...)
|
||
/// → 分红在盯市列和分红列各计一次 = 2 次
|
||
/// </summary>
|
||
private DiagnoseResult DiagnoseDividendDoubleCount(
|
||
int tradeId,
|
||
List<swap_flow_event> flowEvents,
|
||
List<eod_swap_position> eodPositions)
|
||
{
|
||
var r = new DiagnoseResult { SwapTradeId = tradeId };
|
||
var lines = new List<string>
|
||
{
|
||
$"--- 分红重复计算诊断 SwapTradeId={tradeId} ---",
|
||
"",
|
||
"[流水层] 每条平仓/互换事件拆解 (MarkClosePnl = 价差 + 费CloseFee + 分红DividendIn):",
|
||
string.Format(" {0,-8}{1,-12}{2,-8}{3,16}{4,12}{5,10}{6,16}",
|
||
"id", "EventDate", "EvType", "MarkClosePnl", "DividendIn", "CloseFee", "价差(残差)")
|
||
};
|
||
|
||
// 仅取完成状态的平仓/互换事件(开仓事件 MarkClosePnl=0 不参与)
|
||
var closeSwapEvents = flowEvents
|
||
.Where(e => (e.EventType == (int)SwapFlowEventTypeEnum.平仓
|
||
|| e.EventType == (int)SwapFlowEventTypeEnum.互换
|
||
|| e.EventType == (int)SwapFlowEventTypeEnum.自动互换)
|
||
&& e.DataState == (int)SwapFlowDateStateEnum.完成)
|
||
.ToList();
|
||
|
||
decimal totalPriceComponent = 0m; // 全交易价差成分合计(用于类型判定)
|
||
foreach (var e in closeSwapEvents)
|
||
{
|
||
string et = e.EventType switch
|
||
{
|
||
(int)SwapFlowEventTypeEnum.平仓 => "平仓",
|
||
(int)SwapFlowEventTypeEnum.互换 => "互换",
|
||
(int)SwapFlowEventTypeEnum.自动互换 => "自动互换",
|
||
_ => e.EventType.ToString()
|
||
};
|
||
// 残差 = MarkClosePnl - 分红 - 费 = 纯价差成分
|
||
decimal priceComp = e.MarkClosePnl - e.DividendIn - e.CloseFee;
|
||
totalPriceComponent += priceComp;
|
||
lines.Add(string.Format(" {0,-8}{1,-12}{2,-8}{3,16:F4}{4,12:F4}{5,10:F4}{6,16:F4}",
|
||
e.id, e.EventDate.ToString("yyyy-MM-dd"), et, e.MarkClosePnl, e.DividendIn, e.CloseFee, priceComp));
|
||
}
|
||
|
||
// ===== 按 PositionId 拆解(避免双向腿抵消)=====
|
||
lines.Add("");
|
||
lines.Add("[EOD层] 按 PositionId 拆解盯市列成分:");
|
||
lines.Add(string.Format(" {0,-12}{1,16}{2,16}{3,12}{4,16}{5,16}{6,16}",
|
||
"PositionId", "盯市列合计", "价差成分", "费成分", "分红成分(重复)", "分红列累计", "重复计入"));
|
||
|
||
decimal totalRepeat = 0m;
|
||
var evByPos = closeSwapEvents.GroupBy(e => e.PositionId).ToDictionary(g => g.Key, g => g.ToList());
|
||
var eodByPos = eodPositions.GroupBy(e => e.PositionId)
|
||
.ToDictionary(g => g.Key, g => g.OrderByDescending(x => x.ValueDate).First());
|
||
|
||
foreach (var pid in eodByPos.Keys.OrderBy(k => k))
|
||
{
|
||
var evs = evByPos.ContainsKey(pid) ? evByPos[pid] : new List<swap_flow_event>();
|
||
decimal mtmTotal = evs.Sum(e => e.MarkClosePnl);
|
||
decimal feeComp = evs.Sum(e => e.CloseFee);
|
||
decimal divComp = evs.Sum(e => e.DividendIn); // 盯市列里的分红成分(被重复计入)
|
||
decimal priceComp = mtmTotal - divComp - feeComp;
|
||
decimal realizedDiv = eodByPos[pid].RealizedDividend;
|
||
|
||
totalRepeat += divComp;
|
||
r.逐持仓拆解.Add(new PositionDiagnose
|
||
{
|
||
PositionId = pid,
|
||
盯市列合计 = mtmTotal,
|
||
盯市价差成分 = priceComp,
|
||
盯市费成分 = feeComp,
|
||
盯市分红成分 = divComp,
|
||
分红列累计 = realizedDiv,
|
||
重复计入分红 = divComp
|
||
});
|
||
lines.Add(string.Format(" {0,-12}{1,16:F4}{2,16:F4}{3,12:F4}{4,16:F4}{5,16:F4}{6,16:F4}",
|
||
pid, mtmTotal, priceComp, feeComp, divComp, realizedDiv, divComp));
|
||
}
|
||
|
||
// ===== 全交易累计与类型判定 =====
|
||
r.最终累计盯市已实现 = eodByPos.Values.Sum(e => e.RealizedMtmPnL);
|
||
r.最终累计分红已实现 = eodByPos.Values.Sum(e => e.RealizedDividend);
|
||
r.最终持仓层累计已实现 = eodByPos.Values.Sum(e => e.RealizedPnl);
|
||
r.重复计入分红金额 = totalRepeat;
|
||
r.重复计算成立 = Math.Abs(totalRepeat) > 0.01m;
|
||
r.交易类型 = Math.Abs(totalPriceComponent) < 0.01m ? "纯分红型" : "混合型";
|
||
|
||
// 结论
|
||
if (r.重复计算成立)
|
||
{
|
||
r.结论 = string.Format(
|
||
"⚠ 坐实重复计算:盯市列含分红成分 {0:F4}(既在 RealizedMtmPnL 又在 RealizedDividend)," +
|
||
"修复后 RealizedPnl 应减少 {0:F4}。类型={1}。",
|
||
totalRepeat, r.交易类型);
|
||
}
|
||
else
|
||
{
|
||
r.结论 = "未检测到重复计算(盯市列分红成分≈0,可能已修复或无分红平仓/互换事件)。";
|
||
}
|
||
|
||
lines.Add("");
|
||
lines.Add("[汇总]");
|
||
lines.Add($" 交易类型: {r.交易类型}(价差成分合计={totalPriceComponent:F4})");
|
||
lines.Add($" 盯市列里被重复计入的分红成分: {r.重复计入分红金额:F4}");
|
||
lines.Add($" 最终 RealizedMtmPnL(盯市列): {r.最终累计盯市已实现:F4}");
|
||
lines.Add($" 最终 RealizedDividend(分红列): {r.最终累计分红已实现:F4}");
|
||
lines.Add($" 最终 RealizedPnl(持仓层): {r.最终持仓层累计已实现:F4}");
|
||
lines.Add($" 重复计算成立: {r.重复计算成立}");
|
||
lines.Add($" [结论] {r.结论}");
|
||
|
||
r.Summary = string.Join("\n", lines);
|
||
return r;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Step2:校验已录制 golden 文件的完整性(离线,不连库)。
|
||
/// 确认每个 json 含 5 表数据、能正确反序列化。
|
||
/// </summary>
|
||
[TestMethod]
|
||
[TestCategory("DBRecording")]
|
||
public void Step2_VerifyRecordedGoldenFiles()
|
||
{
|
||
if (!Directory.Exists(GoldenDir))
|
||
{
|
||
Assert.Inconclusive($"golden 目录不存在: {GoldenDir}(请先跑 Step1_RecordSampleTrades)");
|
||
return;
|
||
}
|
||
var files = Directory.GetFiles(GoldenDir, "dividend_trade_*.json");
|
||
Assert.IsTrue(files.Length > 0, $"应至少有 1 个 golden 文件 in {GoldenDir}");
|
||
|
||
foreach (var file in files)
|
||
{
|
||
var json = File.ReadAllText(file);
|
||
var golden = JsonConvert.DeserializeObject<SwapDividendGoldenModel>(json);
|
||
|
||
Assert.IsTrue(golden.SwapTradeId > 0, $"{file}: SwapTradeId 无效");
|
||
Assert.IsNotNull(golden.InputFlowEvents, $"{file}: InputFlowEvents 缺失");
|
||
Assert.IsTrue(golden.InputFlowEvents.Count > 0, $"{file}: InputFlowEvents 为空");
|
||
Assert.IsNotNull(golden.InputEodPositions, $"{file}: InputEodPositions 缺失");
|
||
Assert.IsNotNull(golden.Diagnosis, $"{file}: Diagnosis 缺失");
|
||
|
||
Console.WriteLine($"✅ {Path.GetFileName(file)}: trade={golden.SwapTradeId}, " +
|
||
$"flow={golden.InputFlowEvents.Count}条, eod={golden.InputEodPositions.Count}条, " +
|
||
$"结论={golden.Diagnosis?["结论"]?.Value<string>()}");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 分红黄金文件数据模型。5 表快照 + 诊断结论。
|
||
/// 字段名沿用数据库实体类名,反序列化时类型一致。
|
||
/// </summary>
|
||
public class SwapDividendGoldenModel
|
||
{
|
||
public int SwapTradeId { get; set; }
|
||
public string SwapTradeNo { get; set; }
|
||
public string UnderlyingCode { get; set; }
|
||
public DateTime RecordedAt { get; set; }
|
||
public string SourceDb { get; set; }
|
||
public string Purpose { get; set; }
|
||
|
||
public JObject InputTrade { get; set; }
|
||
public JArray InputPositions { get; set; } // swap_position
|
||
public JArray InputFlowEvents { get; set; } // swap_flow_event(分红核心)
|
||
public JArray InputEodPositions { get; set; } // eod_swap_position(重复计算发生处)
|
||
public JArray InputEodSwaps { get; set; } // eod_swap(汇总层)
|
||
public JArray InputBondPayments { get; set; } // bond_payment_info(理论应付)
|
||
|
||
public JObject Diagnosis { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 重复计算诊断结果,随 golden 一起持久化,便于修复后对比。
|
||
/// 拆解原理:每条平仓/互换事件的 MarkClosePnl = 价差成分 + 费成分(CloseFee) + 分红成分(DividendIn)。
|
||
/// 盯市列(TdCloseMtmPnl=ΣMarkClosePnl) 含了分红成分一份,分红列(TdCloseDividend=ΣDividendIn) 又含一份,
|
||
/// 故"重复金额" = 进入盯市列的分红成分 = Σ(事件 DividendIn)。按 PositionId 分别拆解避免双向腿抵消。
|
||
/// </summary>
|
||
public class DiagnoseResult
|
||
{
|
||
public int SwapTradeId { get; set; }
|
||
|
||
/// <summary>"纯分红型"(价差成分≈0) 或 "混合型"(价差成分≠0)。基于 MarkClosePnl 是否含价差判定。</summary>
|
||
public string 交易类型 { get; set; }
|
||
|
||
// ===== 逐 PositionId 拆解 =====
|
||
public List<PositionDiagnose> 逐持仓拆解 { get; set; } = new List<PositionDiagnose>();
|
||
|
||
/// <summary>盯市列里被重复计入的分红成分合计(=修复后 RealizedPnl 应减少的金额)。</summary>
|
||
public decimal 重复计入分红金额 { get; set; }
|
||
public decimal 最终累计盯市已实现 { get; set; }
|
||
public decimal 最终累计分红已实现 { get; set; }
|
||
public decimal 最终持仓层累计已实现 { get; set; }
|
||
|
||
/// <summary>若重复计入分红金额≠0,则重复计算成立。</summary>
|
||
public bool 重复计算成立 { get; set; }
|
||
public string 结论 { get; set; }
|
||
public string Summary { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 单个持仓腿(PositionId)的拆解结果。
|
||
/// </summary>
|
||
public class PositionDiagnose
|
||
{
|
||
public long PositionId { get; set; }
|
||
/// <summary>该腿盯市列合计 = Σ 事件 MarkClosePnl。</summary>
|
||
public decimal 盯市列合计 { get; set; }
|
||
/// <summary>盯市列里的价差成分 = Σ(MarkClosePnl - DividendIn - CloseFee)。</summary>
|
||
public decimal 盯市价差成分 { get; set; }
|
||
/// <summary>盯市列里的费成分 = Σ CloseFee。</summary>
|
||
public decimal 盯市费成分 { get; set; }
|
||
/// <summary>盯市列里的分红成分 = Σ DividendIn(这是被重复计入的部分)。</summary>
|
||
public decimal 盯市分红成分 { get; set; }
|
||
/// <summary>该腿分红列最终累计 RealizedDividend。</summary>
|
||
public decimal 分红列累计 { get; set; }
|
||
/// <summary>该腿被重复计入的分红 = 盯市分红成分。</summary>
|
||
public decimal 重复计入分红 { get; set; }
|
||
}
|
||
}
|