#EQD-6948 国联民生-实现保证金规则(2)追保金额的产生与收盘计算 收盘计算和授信占用
This commit is contained in:
@@ -259,6 +259,11 @@ namespace YLErp.DBModels
|
||||
public const string 系统操作_互换 = "系统操作-互换";
|
||||
public const string 系统操作_应付预付金 = "系统操作-应付预付金";
|
||||
public const string 系统操作_预付金返息 = "系统操作-预付金返息";
|
||||
/// <summary>
|
||||
/// 阶段四 §4.1 合约维度(MarginWatchRule==0)EOD 结算产生的追加保证金资金记录:
|
||||
/// 交易级单条累计值(TradeId+Action+Deal=0 幂等 upsert),负数=客户应付追加;写入见 SwapAdditionalMarginService。
|
||||
/// </summary>
|
||||
public const string 系统操作_追加保证金 = "系统操作-追加保证金";
|
||||
public const string 人工操作_其他 = "人工操作-其他";
|
||||
public const string 人工操作_预付金 = "人工操作-预付金";
|
||||
public const string 系统 = "系统";
|
||||
@@ -319,6 +324,10 @@ namespace YLErp.DBModels
|
||||
{
|
||||
action = "预付金返息";
|
||||
}
|
||||
else if (Action == "系统操作-追加保证金")
|
||||
{
|
||||
action = "追加保证金";
|
||||
}
|
||||
if (action != "")
|
||||
{
|
||||
return $"{ action}{(Money > 0 ? "收入" : "支出")}";
|
||||
@@ -372,6 +381,10 @@ namespace YLErp.DBModels
|
||||
{
|
||||
action = "预付金返息";
|
||||
}
|
||||
else if (Action == "系统操作-追加保证金")
|
||||
{
|
||||
action = "追加保证金";
|
||||
}
|
||||
if (action != "")
|
||||
{
|
||||
return $"{ action}{(Money > 0 ? "支出" : "收入")}";
|
||||
|
||||
@@ -57,17 +57,17 @@ namespace YLErp.DBModels
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取期限的显示名称
|
||||
/// 获取期限的显示名称(与设置页 bondTermLabel 同口径的开闭区间文案:≤5y / (5y-10y] / (10y-30y] / >30y;空=全部兜底档)
|
||||
/// </summary>
|
||||
public static string GetDisplayName(string term)
|
||||
{
|
||||
return term switch
|
||||
{
|
||||
UnderFiveYear => "<5y",
|
||||
FiveToTenYear => "5y-10y",
|
||||
TenToThirtyYear => "10y-30y",
|
||||
UnderFiveYear => "≤5y",
|
||||
FiveToTenYear => "(5y-10y]",
|
||||
TenToThirtyYear => "(10y-30y]",
|
||||
OverThirtyYear => ">30y",
|
||||
_ => term ?? "<5y"
|
||||
_ => string.IsNullOrEmpty(term) ? "全部" : term
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace YLErp.DBModels
|
||||
public double? MaxCreditUseRatio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已使用授信(R4,非持久化:授信出入表 SUM(占用)−SUM(释放)+SUM(调整),列表展示用)
|
||||
/// 已使用授信(R4,非持久化:授信出入表 Σ(amount),占用记正/释放记负,列表展示用)
|
||||
/// </summary>
|
||||
[DisplayName("已使用授信")]
|
||||
[NotMapped]
|
||||
|
||||
@@ -223,8 +223,22 @@ namespace YLErp.Models
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 已使用授信(R2/R4,阶段三 §3.2):授信出入表 client_credit_inout 按 Σ(amount) 汇总(与资金流水同号口径),
|
||||
/// 估值报告展示与可用资金公式共用。
|
||||
/// </summary>
|
||||
public double UsedCredit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 互换初始保证金(R2,阶段三 §3.2,净收取为正):客户 应付预付金 流水收付净额(平仓返还自动冲减)。
|
||||
/// </summary>
|
||||
public double SwapInitMargin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 交易维度追加保证金合计(R2/R3 合约维度公式输入):Σ(维持保证金 − 累计保证金),
|
||||
/// 累计保证金=该交易 应付预付金+追加保证金 流水收付净额;阶段四产生追加保证金资金记录后口径自然完整。
|
||||
/// </summary>
|
||||
public double SwapAdditionalMarginTotal { get; set; }
|
||||
|
||||
|
||||
private double _AvailableAmount;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using YLErp.Core.Helpers;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Consts;
|
||||
@@ -63,6 +63,53 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator
|
||||
return !isOurLong;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 阶段三 §3.3 决策①:方案B手工录入结构直接赋值(不再用 100∓n×(x−y) 推导)。
|
||||
/// 模板追保表 4 行 ↔ SpanConfig 4 层:多头 第1层=[Lower,+∞)(模板行0只用下限)、第n层=[Lower,Upper);
|
||||
/// 空头 第1层=(−∞,Upper](模板行0只用上限)、第n层=(Lower,Upper];
|
||||
/// 追保金额比例 = AmountRate×100(累计到该层的追保比例,直接取值),金额文字口径 n%×期初全价×券面总额 由模板静态文字承载;
|
||||
/// 预警线/平仓线直接取录入值,未录时按 docx 口径取边界(平仓线=最深层边界、预警线=次深层边界);
|
||||
/// 配置层不足 4 层时余下行置空;返还追保事件表与追保事件同值(模板静态文字区分方向措辞)。
|
||||
/// </summary>
|
||||
private static void FillSpanConfigLines(Dictionary<string, JToken> dic, SpanConfig spanCfg, bool posiLong)
|
||||
{
|
||||
var tiers = (posiLong ? spanCfg.LongSpans : spanCfg.ShortSpans) ?? new List<SpanTierConfig>();
|
||||
var valid = tiers.Where(t => t != null && (t.Lower.HasValue || t.Upper.HasValue)).ToList();
|
||||
var last = valid.LastOrDefault();
|
||||
|
||||
var warnLine = spanCfg.WarnLine;
|
||||
var closeLine = spanCfg.CloseLine;
|
||||
if (last != null)
|
||||
{
|
||||
closeLine ??= posiLong ? last.Lower : last.Upper;
|
||||
warnLine ??= posiLong ? last.Upper : last.Lower;
|
||||
}
|
||||
dic["预警线"] = warnLine.HasValue ? (warnLine.Value * 100).ToString("0.##") : "";
|
||||
dic["平仓线"] = closeLine.HasValue ? (closeLine.Value * 100).ToString("0.##") : "";
|
||||
|
||||
//档位n = 第n层触发线(多头取各层 Lower、空头取各层 Upper),现用模板无档位占位符,保留赋值兼容旧模板
|
||||
for (int n = 1; n <= 4; n++)
|
||||
{
|
||||
var tier = n <= valid.Count ? valid[n - 1] : null;
|
||||
var line = tier == null ? null : (posiLong ? tier.Lower : tier.Upper);
|
||||
dic[$"档位{n}"] = line.HasValue ? (line.Value * 100).ToString("0.##") : "";
|
||||
}
|
||||
|
||||
//追保事件/返还追保事件区间表:行0=第1层,行1..3=第2..4层;
|
||||
//与上方档位循环同用过滤后的 valid 列表(BUG-26:配置中间夹空层时两类占位符不错位)
|
||||
for (int i = 0; i <= 3; i++)
|
||||
{
|
||||
var tier = i < valid.Count ? valid[i] : null;
|
||||
var hasTier = tier != null && (tier.Lower.HasValue || tier.Upper.HasValue);
|
||||
dic[$"追保区间{i}下限"] = hasTier && tier.Lower.HasValue ? (tier.Lower.Value * 100).ToString("0.##") : "";
|
||||
dic[$"追保区间{i}上限"] = hasTier && tier.Upper.HasValue ? (tier.Upper.Value * 100).ToString("0.##") : "";
|
||||
dic[$"追保金额比例{i}"] = hasTier && tier.AmountRate.HasValue ? (tier.AmountRate.Value * 100).ToString("0.##") : "";
|
||||
dic[$"返还区间{i}下限"] = dic[$"追保区间{i}下限"];
|
||||
dic[$"返还区间{i}上限"] = dic[$"追保区间{i}上限"];
|
||||
dic[$"返还金额比例{i}"] = dic[$"追保金额比例{i}"];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取收益互换模板路径
|
||||
/// </summary>
|
||||
@@ -254,158 +301,142 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator
|
||||
: swapRatePosition != null ? "" : "0.0000";
|
||||
|
||||
// 获取客户适用的保证金率
|
||||
//保证金模板V2迁移:优先按交易绑定的预付金模板V2(区间追保结构)取 初始率x/维持率y;
|
||||
//取不到(未绑定/模板无效/无匹配明细)时回退旧的互换预付金率维护(client_margin_config)链路
|
||||
//阶段三 §3.3 决策①:档位/预警线/平仓线/追保区间统一按交易绑定的预付金模板V2参数赋值——
|
||||
//方案B手工录入结构(SpanConfig.WarnLine/CloseLine/LongSpans/ShortSpans)直接取录入值,不再用 100∓n×(x−y) 推导;
|
||||
//旧互换预付金率维护(client_margin_config / UnderlyingHelper.GetApplicableMarginRate)回退链路已移除
|
||||
//(本注释保留一个观察周期后删除):取不到模板参数时率显示 0、档位与区间置空。
|
||||
var templateRate = MarginTemplateV2RateHelper.GetTradeMarginRate(
|
||||
trade.id, swapPosition.UnderlyingCode, swapPosition.UnderlyingInstrumentType, trade.TradeDate ?? DateTime.Now);
|
||||
|
||||
var clientMarginRate = templateRate != null
|
||||
? new client_margin_detail { init_rate = templateRate.InitRate ?? 0, maintain_rate = templateRate.MaintainRate ?? 0 }
|
||||
: UnderlyingHelper.GetApplicableMarginRate(
|
||||
client.id, swapPosition.UnderlyingCode, trade.TradeDate ?? DateTime.Now);
|
||||
var initRate = templateRate?.InitRate ?? 0m;
|
||||
var maintainRate = templateRate?.MaintainRate ?? 0m;
|
||||
var spanCfg = templateRate?.Detail?.SpanConfig;
|
||||
|
||||
if (clientMarginRate != null)
|
||||
{
|
||||
dic["初始保障金率"] = ((double)clientMarginRate.init_rate * 100).ToString("N4");
|
||||
dic["维持保障金率"] = ((double)clientMarginRate.maintain_rate * 100).ToString("N4");
|
||||
dic["初始保障金率"] = ((double)initRate * 100).ToString("N4");
|
||||
dic["维持保障金率"] = ((double)maintainRate * 100).ToString("N4");
|
||||
|
||||
// 期初预付比例和金额
|
||||
dic["期初预付比例"] = ((double)clientMarginRate.init_rate * 100).ToString("0.##");
|
||||
dic["期初预付金额"] = ((trade.OriginalStockEqvNotional ?? 0) * (double)clientMarginRate.init_rate).ToString("N2");
|
||||
}
|
||||
else
|
||||
{
|
||||
dic["初始保障金率"] = "0.0000";
|
||||
dic["维持保障金率"] = "0.0000";
|
||||
dic["期初预付比例"] = "0";
|
||||
dic["期初预付金额"] = "0.00";
|
||||
}
|
||||
// 期初预付比例和金额
|
||||
dic["期初预付比例"] = ((double)initRate * 100).ToString("0.##");
|
||||
dic["期初预付金额"] = ((trade.OriginalStockEqvNotional ?? 0) * (double)initRate).ToString("N2");
|
||||
bool posiLong = IsCustomerLong(swapPosition);
|
||||
// 计算平仓线、预警线、档位值(利率债TRS,固定4档)
|
||||
// A = 维持保证金率(%),B = 初始保证金率(%),diff = B - A
|
||||
var maintainRatePercent = (double)(clientMarginRate?.maintain_rate ?? 0) * 100; // A(%)
|
||||
var initRatePercent = (double)(clientMarginRate?.init_rate ?? 0) * 100; // B(%)
|
||||
var diff = initRatePercent - maintainRatePercent; // B - A
|
||||
if (maintainRatePercent > 0)
|
||||
var maintainRatePercent = (double)maintainRate * 100; // A(%)
|
||||
|
||||
if (YLErp.Modules.SwapModule.Margin.SwapSpanMarginCalc.HasSpanConfig(spanCfg))
|
||||
{
|
||||
if (posiLong)
|
||||
{
|
||||
// 客户看多:档位n = 100 - n * (B-A)
|
||||
dic["档位1"] = (100 - 1 * diff).ToString("0.##");
|
||||
dic["档位2"] = (100 - 2 * diff).ToString("0.##");
|
||||
dic["档位3"] = (100 - 3 * diff).ToString("0.##");
|
||||
dic["档位4"] = (100 - 4 * diff).ToString("0.##");
|
||||
dic["预警线"] = dic["档位4"]; // 预警线 = 最高档位
|
||||
dic["平仓线"] = (100 - 5 * diff).ToString("0.##"); // 平仓线 = 最高档位 - (B-A)
|
||||
}
|
||||
else
|
||||
{
|
||||
// 客户看空:档位n = 100 + n * (B-A)
|
||||
dic["档位1"] = (100 + 1 * diff).ToString("0.##");
|
||||
dic["档位2"] = (100 + 2 * diff).ToString("0.##");
|
||||
dic["档位3"] = (100 + 3 * diff).ToString("0.##");
|
||||
dic["档位4"] = (100 + 4 * diff).ToString("0.##");
|
||||
dic["预警线"] = dic["档位4"]; // 预警线 = 最高档位
|
||||
dic["平仓线"] = (100 + 5 * diff).ToString("0.##"); // 平仓线 = 最高档位 + (B-A)
|
||||
}
|
||||
//方案B:直接取录入的预警/平仓线与多空区间(未录线时按 docx 口径取最深/次深档位边界)
|
||||
FillSpanConfigLines(dic, spanCfg, posiLong);
|
||||
}
|
||||
else
|
||||
{
|
||||
dic["平仓线"] = "";
|
||||
dic["预警线"] = "";
|
||||
dic["档位1"] = "";
|
||||
dic["档位2"] = "";
|
||||
dic["档位3"] = "";
|
||||
dic["档位4"] = "";
|
||||
}
|
||||
|
||||
// 追保事件/返还追保事件 价格区间和金额比例
|
||||
if (maintainRatePercent > 0)
|
||||
{
|
||||
if (posiLong)
|
||||
//存量 x/y 配置:维持 100∓n×(B−A) 推导(B=初始率,A=维持率)
|
||||
var initRatePercent = (double)initRate * 100; // B(%)
|
||||
var diff = initRatePercent - maintainRatePercent; // B - A
|
||||
if (maintainRatePercent > 0)
|
||||
{
|
||||
// === 客户看多 追保事件 ===
|
||||
// 区间0:当前净价 ≥ {100-(B-A)}% × 期初净价,追保0%
|
||||
dic["追保区间0下限"] = (100 - 1 * diff).ToString("0.##");
|
||||
dic["追保金额比例0"] = "0";
|
||||
// 区间1:{100-2(B-A)}% ≤ 当前净价 < {100-(B-A)}%,追保{B-A}%
|
||||
dic["追保区间1下限"] = (100 - 2 * diff).ToString("0.##");
|
||||
dic["追保区间1上限"] = (100 - 1 * diff).ToString("0.##");
|
||||
dic["追保金额比例1"] = (1 * diff).ToString("0.##");
|
||||
// 区间2:{100-3(B-A)}% ≤ 当前净价 < {100-2(B-A)}%,追保{2(B-A)}%
|
||||
dic["追保区间2下限"] = (100 - 3 * diff).ToString("0.##");
|
||||
dic["追保区间2上限"] = (100 - 2 * diff).ToString("0.##");
|
||||
dic["追保金额比例2"] = (2 * diff).ToString("0.##");
|
||||
// 区间3:{100-4(B-A)}% ≤ 当前净价 < {100-3(B-A)}%,追保{3(B-A)}%
|
||||
dic["追保区间3下限"] = (100 - 4 * diff).ToString("0.##");
|
||||
dic["追保区间3上限"] = (100 - 3 * diff).ToString("0.##");
|
||||
dic["追保金额比例3"] = (3 * diff).ToString("0.##");
|
||||
|
||||
// === 客户看多 返还追保事件 ===
|
||||
// 区间0:当前净价 ≥ {100-(B-A)}% × 期初净价,返还0%
|
||||
dic["返还区间0下限"] = (100 - 1 * diff).ToString("0.##");
|
||||
dic["返还金额比例0"] = "0";
|
||||
// 区间1:{100-2(B-A)}% ≤ 当前净价 < {100-(B-A)}%,返还{B-A}%
|
||||
dic["返还区间1下限"] = (100 - 2 * diff).ToString("0.##");
|
||||
dic["返还区间1上限"] = (100 - 1 * diff).ToString("0.##");
|
||||
dic["返还金额比例1"] = (1 * diff).ToString("0.##");
|
||||
// 区间2:{100-3(B-A)}% ≤ 当前净价 < {100-2(B-A)}%,返还{2(B-A)}%
|
||||
dic["返还区间2下限"] = (100 - 3 * diff).ToString("0.##");
|
||||
dic["返还区间2上限"] = (100 - 2 * diff).ToString("0.##");
|
||||
dic["返还金额比例2"] = (2 * diff).ToString("0.##");
|
||||
// 区间3:{100-4(B-A)}% ≤ 当前净价 < {100-3(B-A)}%,返还{3(B-A)}%
|
||||
dic["返还区间3下限"] = (100 - 4 * diff).ToString("0.##");
|
||||
dic["返还区间3上限"] = (100 - 3 * diff).ToString("0.##");
|
||||
dic["返还金额比例3"] = (3 * diff).ToString("0.##");
|
||||
if (posiLong)
|
||||
{
|
||||
// 客户看多:档位n = 100 - n * (B-A)
|
||||
dic["档位1"] = (100 - 1 * diff).ToString("0.##");
|
||||
dic["档位2"] = (100 - 2 * diff).ToString("0.##");
|
||||
dic["档位3"] = (100 - 3 * diff).ToString("0.##");
|
||||
dic["档位4"] = (100 - 4 * diff).ToString("0.##");
|
||||
dic["预警线"] = dic["档位4"]; // 预警线 = 最高档位
|
||||
dic["平仓线"] = (100 - 5 * diff).ToString("0.##"); // 平仓线 = 最高档位 - (B-A)
|
||||
}
|
||||
else
|
||||
{
|
||||
// 客户看空:档位n = 100 + n * (B-A)
|
||||
dic["档位1"] = (100 + 1 * diff).ToString("0.##");
|
||||
dic["档位2"] = (100 + 2 * diff).ToString("0.##");
|
||||
dic["档位3"] = (100 + 3 * diff).ToString("0.##");
|
||||
dic["档位4"] = (100 + 4 * diff).ToString("0.##");
|
||||
dic["预警线"] = dic["档位4"]; // 预警线 = 最高档位
|
||||
dic["平仓线"] = (100 + 5 * diff).ToString("0.##"); // 平仓线 = 最高档位 + (B-A)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// === 客户看空 追保事件 ===
|
||||
// 区间0:当前净价 ≤ {100+(B-A)}% × 期初净价,追保0%
|
||||
dic["追保区间0上限"] = (100 + 1 * diff).ToString("0.##");
|
||||
dic["追保金额比例0"] = "0";
|
||||
// 区间1:{100+(B-A)}% < 当前净价 ≤ {100+2(B-A)}%,追保{B-A}%
|
||||
dic["追保区间1下限"] = (100 + 1 * diff).ToString("0.##");
|
||||
dic["追保区间1上限"] = (100 + 2 * diff).ToString("0.##");
|
||||
dic["追保金额比例1"] = (1 * diff).ToString("0.##");
|
||||
// 区间2:{100+2(B-A)}% < 当前净价 ≤ {100+3(B-A)}%,追保{2(B-A)}%
|
||||
dic["追保区间2下限"] = (100 + 2 * diff).ToString("0.##");
|
||||
dic["追保区间2上限"] = (100 + 3 * diff).ToString("0.##");
|
||||
dic["追保金额比例2"] = (2 * diff).ToString("0.##");
|
||||
// 区间3:{100+3(B-A)}% < 当前净价 ≤ {100+4(B-A)}%,追保{3(B-A)}%
|
||||
dic["追保区间3下限"] = (100 + 3 * diff).ToString("0.##");
|
||||
dic["追保区间3上限"] = (100 + 4 * diff).ToString("0.##");
|
||||
dic["追保金额比例3"] = (3 * diff).ToString("0.##");
|
||||
|
||||
// === 客户看空 返还追保事件 ===
|
||||
// 区间0:当前净价 ≤ {100+(B-A)}% × 期初净价,返还0%
|
||||
dic["返还区间0上限"] = (100 + 1 * diff).ToString("0.##");
|
||||
dic["返还金额比例0"] = "0";
|
||||
// 区间1:{100+(B-A)}% < 当前净价 ≤ {100+2(B-A)}%,返还{B-A}%
|
||||
dic["返还区间1下限"] = (100 + 1 * diff).ToString("0.##");
|
||||
dic["返还区间1上限"] = (100 + 2 * diff).ToString("0.##");
|
||||
dic["返还金额比例1"] = (1 * diff).ToString("0.##");
|
||||
// 区间2:{100+2(B-A)}% < 当前净价 ≤ {100+3(B-A)}%,返还{2(B-A)}%
|
||||
dic["返还区间2下限"] = (100 + 2 * diff).ToString("0.##");
|
||||
dic["返还区间2上限"] = (100 + 3 * diff).ToString("0.##");
|
||||
dic["返还金额比例2"] = (2 * diff).ToString("0.##");
|
||||
// 区间3:{100+3(B-A)}% < 当前净价 ≤ {100+4(B-A)}%,返还{3(B-A)}%
|
||||
dic["返还区间3下限"] = (100 + 3 * diff).ToString("0.##");
|
||||
dic["返还区间3上限"] = (100 + 4 * diff).ToString("0.##");
|
||||
dic["返还金额比例3"] = (3 * diff).ToString("0.##");
|
||||
dic["平仓线"] = "";
|
||||
dic["预警线"] = "";
|
||||
dic["档位1"] = "";
|
||||
dic["档位2"] = "";
|
||||
dic["档位3"] = "";
|
||||
dic["档位4"] = "";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 清空追保/返还相关字典值
|
||||
for (int i = 0; i <= 3; i++)
|
||||
|
||||
if (maintainRatePercent > 0)
|
||||
{
|
||||
dic[$"追保区间{i}下限"] = "";
|
||||
dic[$"追保区间{i}上限"] = "";
|
||||
dic[$"追保金额比例{i}"] = "";
|
||||
dic[$"返还区间{i}下限"] = "";
|
||||
dic[$"返还区间{i}上限"] = "";
|
||||
dic[$"返还金额比例{i}"] = "";
|
||||
if (posiLong)
|
||||
{
|
||||
// === 客户看多 追保事件 ===
|
||||
// 区间0:当前净价 ≥ {100-(B-A)}% × 期初净价,追保0%
|
||||
dic["追保区间0下限"] = (100 - 1 * diff).ToString("0.##");
|
||||
dic["追保金额比例0"] = "0";
|
||||
// 区间1:{100-2(B-A)}% ≤ 当前净价 < {100-(B-A)}%,追保{B-A}%
|
||||
dic["追保区间1下限"] = (100 - 2 * diff).ToString("0.##");
|
||||
dic["追保区间1上限"] = (100 - 1 * diff).ToString("0.##");
|
||||
dic["追保金额比例1"] = (1 * diff).ToString("0.##");
|
||||
// 区间2:{100-3(B-A)}% ≤ 当前净价 < {100-2(B-A)}%,追保{2(B-A)}%
|
||||
dic["追保区间2下限"] = (100 - 3 * diff).ToString("0.##");
|
||||
dic["追保区间2上限"] = (100 - 2 * diff).ToString("0.##");
|
||||
dic["追保金额比例2"] = (2 * diff).ToString("0.##");
|
||||
// 区间3:{100-4(B-A)}% ≤ 当前净价 < {100-3(B-A)}%,追保{3(B-A)}%
|
||||
dic["追保区间3下限"] = (100 - 4 * diff).ToString("0.##");
|
||||
dic["追保区间3上限"] = (100 - 3 * diff).ToString("0.##");
|
||||
dic["追保金额比例3"] = (3 * diff).ToString("0.##");
|
||||
|
||||
// === 客户看多 返还追保事件 ===
|
||||
for (int n = 1; n <= 3; n++)
|
||||
{
|
||||
dic[$"返还区间{n}下限"] = (100 - (n + 1) * diff).ToString("0.##");
|
||||
dic[$"返还区间{n}上限"] = (100 - n * diff).ToString("0.##");
|
||||
dic[$"返还金额比例{n}"] = (n * diff).ToString("0.##");
|
||||
}
|
||||
dic["返还区间0下限"] = (100 - 1 * diff).ToString("0.##");
|
||||
dic["返还金额比例0"] = "0";
|
||||
}
|
||||
else
|
||||
{
|
||||
// === 客户看空 追保事件 ===
|
||||
// 区间0:当前净价 ≤ {100+(B-A)}% × 期初净价,追保0%
|
||||
dic["追保区间0上限"] = (100 + 1 * diff).ToString("0.##");
|
||||
dic["追保金额比例0"] = "0";
|
||||
// 区间1:{100+(B-A)}% < 当前净价 ≤ {100+2(B-A)}%,追保{B-A}%
|
||||
dic["追保区间1下限"] = (100 + 1 * diff).ToString("0.##");
|
||||
dic["追保区间1上限"] = (100 + 2 * diff).ToString("0.##");
|
||||
dic["追保金额比例1"] = (1 * diff).ToString("0.##");
|
||||
// 区间2:{100+2(B-A)}% < 当前净价 ≤ {100+3(B-A)}%,追保{2(B-A)}%
|
||||
dic["追保区间2下限"] = (100 + 2 * diff).ToString("0.##");
|
||||
dic["追保区间2上限"] = (100 + 3 * diff).ToString("0.##");
|
||||
dic["追保金额比例2"] = (2 * diff).ToString("0.##");
|
||||
// 区间3:{100+3(B-A)}% < 当前净价 ≤ {100+4(B-A)}%,追保{3(B-A)}%
|
||||
dic["追保区间3下限"] = (100 + 3 * diff).ToString("0.##");
|
||||
dic["追保区间3上限"] = (100 + 4 * diff).ToString("0.##");
|
||||
dic["追保金额比例3"] = (3 * diff).ToString("0.##");
|
||||
|
||||
// === 客户看空 返还追保事件 ===
|
||||
for (int n = 1; n <= 3; n++)
|
||||
{
|
||||
dic[$"返还区间{n}下限"] = (100 + n * diff).ToString("0.##");
|
||||
dic[$"返还区间{n}上限"] = (100 + (n + 1) * diff).ToString("0.##");
|
||||
dic[$"返还金额比例{n}"] = (n * diff).ToString("0.##");
|
||||
}
|
||||
dic["返还区间0上限"] = (100 + 1 * diff).ToString("0.##");
|
||||
dic["返还金额比例0"] = "0";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 清空追保/返还相关字典值
|
||||
for (int i = 0; i <= 3; i++)
|
||||
{
|
||||
dic[$"追保区间{i}下限"] = "";
|
||||
dic[$"追保区间{i}上限"] = "";
|
||||
dic[$"追保金额比例{i}"] = "";
|
||||
dic[$"返还区间{i}下限"] = "";
|
||||
dic[$"返还区间{i}上限"] = "";
|
||||
dic[$"返还金额比例{i}"] = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
using YLErp.BLL;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Enums;
|
||||
using YLErp.Modules.MarginModule;
|
||||
|
||||
namespace YLErp.Modules.CalcModules
|
||||
{
|
||||
/// <summary>
|
||||
/// R1 模板三层级找到即停回退集成测试(连 dev 库,MarginTemplateV2RateHelper.ResolveTieredTemplate/GetTradeMarginRate):
|
||||
/// 交易绑定(自定义)→ 客户默认(client_margin_template 按客户)→ 全局默认(IsDefault&&!IsForClient)。
|
||||
/// 测试数据全部带 "ZZZ-R1回退测试-" 名称前缀,TestInitialize/TestCleanup 双向清理,不触碰真实交易。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class MarginTemplateV2RateHelperFallbackTest
|
||||
{
|
||||
private const string Marker = "ZZZ-R1回退测试-";
|
||||
private const int SentinelTradeId = 1900000001;
|
||||
private const int SentinelTradeId2 = 1900000002;
|
||||
private DateTime EffectiveDate = new DateTime(2000, 1, 1);
|
||||
|
||||
private YLContext db;
|
||||
private int clientId;
|
||||
|
||||
[TestInitialize]
|
||||
public void Init()
|
||||
{
|
||||
db = new YLContext();
|
||||
Cleanup();
|
||||
//取一个真实客户做客户级绑定(只写 client_margin_template,不动客户数据)
|
||||
using (var clientDb = DbContextFactory.GetClientDbContext(OptUserInfo.SystemUser))
|
||||
{
|
||||
clientId = clientDb.client.Where(c => c.id > 0).OrderBy(c => c.id).Select(c => c.id).First();
|
||||
}
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void CleanupFixture()
|
||||
{
|
||||
Cleanup();
|
||||
db.Dispose();
|
||||
}
|
||||
|
||||
private void Cleanup()
|
||||
{
|
||||
var templateIds = db.margin_template_v2.Where(x => x.Name.StartsWith(Marker)).Select(x => x.id).ToList();
|
||||
if (templateIds.Count > 0)
|
||||
{
|
||||
db.margin_template_detail.RemoveRange(db.margin_template_detail.Where(x => templateIds.Contains(x.MarginTemplateId)));
|
||||
db.trade_margin_template.RemoveRange(db.trade_margin_template.Where(x => templateIds.Contains(x.MarginTemplateId)));
|
||||
db.client_margin_template.RemoveRange(db.client_margin_template.Where(x => templateIds.Contains(x.MarginTemplateId)));
|
||||
db.margin_template_v2.RemoveRange(db.margin_template_v2.Where(x => templateIds.Contains(x.id)));
|
||||
db.SaveChanges();
|
||||
}
|
||||
db.trade_margin_template.RemoveRange(db.trade_margin_template.Where(x => x.TradeId == SentinelTradeId || x.TradeId == SentinelTradeId2));
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
private margin_template_v2 AddTemplate(string name, bool isDefault, bool isForClient, string tradeTypes = "收益互换", bool isValid = true, int ruleType = (int)MarginRuleTypeEnum.区间追保结构)
|
||||
{
|
||||
var t = new margin_template_v2
|
||||
{
|
||||
Name = name,
|
||||
IsDefault = isDefault,
|
||||
IsForClient = isForClient,
|
||||
IsValid = isValid,
|
||||
TradeTypes = tradeTypes,
|
||||
RuleType = ruleType,
|
||||
ValueDate = EffectiveDate
|
||||
};
|
||||
db.margin_template_v2.Add(t);
|
||||
db.SaveChanges();
|
||||
return t;
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TF_001_交易绑定优先_压过客户与全局()
|
||||
{
|
||||
var custom = AddTemplate(Marker + "自定义", isDefault: false, isForClient: false);
|
||||
var clientTpl = AddTemplate(Marker + "客户", isDefault: true, isForClient: true);
|
||||
var globalTpl = AddTemplate(Marker + "全局", isDefault: true, isForClient: false);
|
||||
db.trade_margin_template.Add(new trade_margin_template { TradeId = SentinelTradeId, MarginTemplateId = custom.id, ValueDate = EffectiveDate, IsLatest = true });
|
||||
db.client_margin_template.Add(new client_margin_template { ClientId = clientId, MarginTemplateId = clientTpl.id, ValueDate = EffectiveDate, ClientLevel = "" });
|
||||
db.SaveChanges();
|
||||
|
||||
var resolved = MarginTemplateV2RateHelper.ResolveTieredTemplate(SentinelTradeId, clientId, DateTime.Today, db);
|
||||
Assert.IsNotNull(resolved);
|
||||
Assert.AreEqual(custom.id, resolved.id, "交易绑定(自定义)应找到即停,压过客户与全局默认");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TF_002_无交易绑定_落到客户默认()
|
||||
{
|
||||
var clientTpl = AddTemplate(Marker + "客户", isDefault: true, isForClient: true);
|
||||
var globalTpl = AddTemplate(Marker + "全局", isDefault: true, isForClient: false);
|
||||
db.client_margin_template.Add(new client_margin_template { ClientId = clientId, MarginTemplateId = clientTpl.id, ValueDate = EffectiveDate, ClientLevel = "" });
|
||||
db.SaveChanges();
|
||||
|
||||
var resolved = MarginTemplateV2RateHelper.ResolveTieredTemplate(tradeId: null, clientId, DateTime.Today, db);
|
||||
Assert.IsNotNull(resolved);
|
||||
Assert.AreEqual(clientTpl.id, resolved.id, "无交易绑定时应命中客户默认,压过全局默认");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TF_003_无交易无客户绑定_落到全局默认()
|
||||
{
|
||||
var globalTpl = AddTemplate(Marker + "全局", isDefault: true, isForClient: false);
|
||||
AddTemplate(Marker + "客户", isDefault: true, isForClient: true); //未绑定不应被取到
|
||||
|
||||
var resolved = MarginTemplateV2RateHelper.ResolveTieredTemplate(tradeId: null, clientId, DateTime.Today, db);
|
||||
AssertGlobal(resolved, globalTpl);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TF_004_客户绑定指向非互换模板_跳过落全局()
|
||||
{
|
||||
var globalTpl = AddTemplate(Marker + "全局", isDefault: true, isForClient: false);
|
||||
var optionTpl = AddTemplate(Marker + "期权客户模板", isDefault: true, isForClient: true, tradeTypes: "香草期权");
|
||||
db.client_margin_template.Add(new client_margin_template { ClientId = clientId, MarginTemplateId = optionTpl.id, ValueDate = EffectiveDate, ClientLevel = "" });
|
||||
db.SaveChanges();
|
||||
|
||||
var resolved = MarginTemplateV2RateHelper.ResolveTieredTemplate(tradeId: null, clientId, DateTime.Today, db);
|
||||
AssertGlobal(resolved, globalTpl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 同日并存"互换绑定+期权绑定"(页面互斥只挡适用结构重叠,此组合允许保存):
|
||||
/// 应命中互换绑定,不因先取到期权绑定被过滤而误穿透到全局。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TF_007_同日并存互换与期权绑定_命中间换绑定()
|
||||
{
|
||||
var swapTpl = AddTemplate(Marker + "客户", isDefault: true, isForClient: true);
|
||||
var optionTpl = AddTemplate(Marker + "期权客户模板", isDefault: true, isForClient: true, tradeTypes: "香草期权");
|
||||
var globalTpl = AddTemplate(Marker + "全局", isDefault: true, isForClient: false);
|
||||
db.client_margin_template.Add(new client_margin_template { ClientId = clientId, MarginTemplateId = swapTpl.id, ValueDate = EffectiveDate, ClientLevel = "" });
|
||||
db.client_margin_template.Add(new client_margin_template { ClientId = clientId, MarginTemplateId = optionTpl.id, ValueDate = EffectiveDate, ClientLevel = "" });
|
||||
db.SaveChanges();
|
||||
|
||||
var resolved = MarginTemplateV2RateHelper.ResolveTieredTemplate(tradeId: null, clientId, DateTime.Today, db);
|
||||
Assert.IsNotNull(resolved);
|
||||
Assert.AreEqual(swapTpl.id, resolved.id, "同日期权+互换绑定并存时应命中互换绑定(过滤后再取最新),而非穿透全局");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TF_005_绑定模板已失效_找到即停返回null()
|
||||
{
|
||||
var invalidTpl = AddTemplate(Marker + "已失效", isDefault: false, isForClient: false, isValid: false);
|
||||
var globalTpl = AddTemplate(Marker + "全局", isDefault: true, isForClient: false);
|
||||
db.trade_margin_template.Add(new trade_margin_template { TradeId = SentinelTradeId2, MarginTemplateId = invalidTpl.id, ValueDate = EffectiveDate, IsLatest = true });
|
||||
db.SaveChanges();
|
||||
|
||||
var resolved = MarginTemplateV2RateHelper.ResolveTieredTemplate(SentinelTradeId2, clientId, DateTime.Today, db);
|
||||
Assert.IsNull(resolved, "交易绑定指向已失效模板时应找到即停(不向下回退到全局默认)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 端到端:交易绑定(区间追保结构 + 明细 x/y)经 GetTradeMarginRate 完整取到率——
|
||||
/// 兼容回归交易级取数路径在新回退结构下行为不变。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TF_006_交易绑定端到端取率()
|
||||
{
|
||||
var custom = AddTemplate(Marker + "自定义", isDefault: false, isForClient: false);
|
||||
db.margin_template_detail.Add(new margin_template_detail
|
||||
{
|
||||
MarginTemplateId = custom.id,
|
||||
ValueDate = EffectiveDate,
|
||||
UnderlyingType = UnderlyingTypeEnum.None,
|
||||
MarginRatio1 = 0.05,
|
||||
MarginRatio2 = 0.03
|
||||
});
|
||||
db.trade_margin_template.Add(new trade_margin_template { TradeId = SentinelTradeId, MarginTemplateId = custom.id, ValueDate = EffectiveDate, IsLatest = true });
|
||||
db.SaveChanges();
|
||||
|
||||
var rate = MarginTemplateV2RateHelper.GetTradeMarginRate(SentinelTradeId, "240004.IB", "TBonds", DateTime.Today, db);
|
||||
Assert.IsNotNull(rate);
|
||||
Assert.AreEqual(custom.id, rate.Template.id);
|
||||
Assert.AreEqual(0.05m, rate.InitRate.Value);
|
||||
Assert.AreEqual(0.03m, rate.MaintainRate.Value);
|
||||
}
|
||||
|
||||
private void AssertGlobal(margin_template_v2 resolved, margin_template_v2 expected)
|
||||
{
|
||||
Assert.IsNotNull(resolved);
|
||||
if (resolved.Name.StartsWith(Marker))
|
||||
{
|
||||
Assert.AreEqual(expected.id, resolved.id, "应命中本用例创建的全局默认模板");
|
||||
}
|
||||
//dev 库存在其他真实全局默认模板时,按 ValueDate 最新者胜出,不做更严格断言
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using YLErp.Modules.MarginModule;
|
||||
|
||||
namespace YLErp.Modules.CalcModules
|
||||
{
|
||||
/// <summary>
|
||||
/// 实现方案阶段一 §1.6 测试要点:GetTradeMarginRate 分类判定钩子默认行为回归。
|
||||
/// 钩子本期默认返回 null → 走"全部/通配行"兜底,取数行为与现状一致(阶段二标签功能不改变取数链路)。
|
||||
/// 完整 GetTradeMarginRate 依赖 YLContext(MySQL),无法纯内存测试,此处覆盖钩子契约。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class MarginTemplateV2RateHelperTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void MT_001_分类判定钩子_默认返回null_走通配兜底()
|
||||
{
|
||||
Assert.IsNull(MarginTemplateV2RateHelper.GetUnderlyingCategory("019546.XSHG", null));
|
||||
Assert.IsNull(MarginTemplateV2RateHelper.GetUnderlyingCategory("200002.XSHE", "Fund"));
|
||||
Assert.IsNull(MarginTemplateV2RateHelper.GetUnderlyingCategory(null, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using YLErp.BLL;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Modules.SwapModule;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// BUG-01 方向守护(2026-08-24):授信出入表金额口径 占用记正/释放记负——
|
||||
/// 已使用授信 = SUM(amount) 随占用上升、随释放回落;可用授信 = 有效授信 − 已使用授信 随占用收缩。
|
||||
/// 连 dev 库闭环验证 Occupy/Release/GetUsedCredit(哨兵 client_id 自建自清,不触碰真实客户)。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class ClientCreditInoutSignTest
|
||||
{
|
||||
private const int SentinelClientId = 99064001; //哨兵客户号(不存在的客户,仅授信出入表按 client_id 记账)
|
||||
private YLContext db;
|
||||
private ClientCreditInoutService service;
|
||||
|
||||
[TestInitialize]
|
||||
public void Init()
|
||||
{
|
||||
db = new YLContext();
|
||||
Cleanup();
|
||||
service = new ClientCreditInoutService(OptUserInfo.SystemUser);
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void Teardown()
|
||||
{
|
||||
Cleanup();
|
||||
db.Dispose();
|
||||
}
|
||||
|
||||
private void Cleanup()
|
||||
{
|
||||
var olds = db.client_credit_inout.Where(x => x.client_id == SentinelClientId).ToList();
|
||||
if (olds.Count > 0)
|
||||
{
|
||||
db.client_credit_inout.RemoveRange(olds);
|
||||
db.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>占用记正数:占用越多已使用授信越大(修复前为负——"越占越多"BUG-01)</summary>
|
||||
[TestMethod]
|
||||
public void CR_001_占用记正_已使用授信上升()
|
||||
{
|
||||
service.Occupy(SentinelClientId, 900001L, 800001, 10000, DateTime.Today, "方向守护-占用1");
|
||||
Assert.AreEqual(10000, ClientCreditInoutService.GetUsedCredit(SentinelClientId, db), 1e-6);
|
||||
|
||||
service.Occupy(SentinelClientId, 900002L, 800002, 5000, DateTime.Today, "方向守护-占用2");
|
||||
Assert.AreEqual(15000, ClientCreditInoutService.GetUsedCredit(SentinelClientId, db), 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>释放记负数:已使用授信回落,快照 used_after 同口径</summary>
|
||||
[TestMethod]
|
||||
public void CR_002_释放记负_已使用授信回落()
|
||||
{
|
||||
service.Occupy(SentinelClientId, 900001L, 800001, 10000, DateTime.Today, "方向守护-占用");
|
||||
service.Release(SentinelClientId, 900001L, 800001, 4000, DateTime.Today, "方向守护-释放");
|
||||
|
||||
var used = ClientCreditInoutService.GetUsedCredit(SentinelClientId, db);
|
||||
Assert.AreEqual(6000, used, 1e-6);
|
||||
//快照列与求和口径一致(最后一次写完后的已使用授信)
|
||||
var last = db.client_credit_inout.Where(x => x.client_id == SentinelClientId).OrderByDescending(x => x.id).First();
|
||||
Assert.AreEqual(6000, last.used_after, 1e-6);
|
||||
Assert.AreEqual(-4000, last.amount, 1e-6, "释放行 amount 记负数");
|
||||
}
|
||||
|
||||
/// <summary>方向传导:可用授信 = 有效授信 − 已使用授信,占用使其收缩(BUG-01 修复后的业务语义)</summary>
|
||||
[TestMethod]
|
||||
public void CR_003_可用授信随占用收缩()
|
||||
{
|
||||
service.Occupy(SentinelClientId, null, 800003, 10000, DateTime.Today, "方向守护-占用");
|
||||
var used = ClientCreditInoutService.GetUsedCredit(SentinelClientId, db);
|
||||
var effective = 30000d; //有效授信给定量
|
||||
Assert.AreEqual(20000, effective - used, 1e-6, "占用 10000 后可用授信应从 30000 收缩到 20000");
|
||||
}
|
||||
|
||||
/// <summary>Occupy/Release 入口对传入符号容错(一律按数量取绝对值定方向),防调用方残留旧负号</summary>
|
||||
[TestMethod]
|
||||
public void CR_004_语义入口符号容错()
|
||||
{
|
||||
//即使调用方按旧习惯传负数,Occupy 也记正(数量口径),防止新代码库中残留旧符号调用点
|
||||
var rec = service.Occupy(SentinelClientId, null, 800004, -7000, DateTime.Today, "方向守护-负号容错");
|
||||
Assert.AreEqual(7000, rec.amount, 1e-6);
|
||||
var rel = service.Release(SentinelClientId, null, 800004, -3000, DateTime.Today, "方向守护-负号容错");
|
||||
Assert.AreEqual(-3000, rel.amount, 1e-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using YLErp.BLL;
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 新建表列名规范测试(2026-08-20 约定:新建表所有数据库列一律小写下划线)。
|
||||
/// 只构建 EF 模型(纯内存,不连库),断言映射列名不含大写字母:
|
||||
/// client_credit_inout 全表、swap_position.fund_tag、credit.original_credit/max_credit_use_ratio。
|
||||
/// 基类操作人列(OptId/OptName/OptTime)经 override+[Column] 覆写为小写,此处一并守护。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class DbColumnNamingTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void DB_001_授信出入表_全部列名小写()
|
||||
{
|
||||
using var db = new YLContext();
|
||||
var entity = db.Model.FindEntityType(typeof(client_credit_inout));
|
||||
Assert.IsNotNull(entity, "client_credit_inout 未注册到 YLContext");
|
||||
var table = StoreObjectIdentifier.Table("client_credit_inout", null);
|
||||
foreach (var property in entity.GetProperties())
|
||||
{
|
||||
var column = property.GetColumnName(table);
|
||||
Assert.IsNotNull(column, $"属性 {property.Name} 未映射列名");
|
||||
Assert.IsFalse(column.Any(char.IsUpper), $"列名应全小写: {column}(属性 {property.Name})");
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DB_002_互换持仓与授信表_新增列名小写()
|
||||
{
|
||||
using var db = new YLContext();
|
||||
AssertColumn(db, typeof(swap_position), nameof(swap_position.FundTag), "fund_tag");
|
||||
AssertColumn(db, typeof(CreditTable), nameof(CreditTable.OriginalCredit), "original_credit");
|
||||
AssertColumn(db, typeof(CreditTable), nameof(CreditTable.MaxCreditUseRatio), "max_credit_use_ratio");
|
||||
}
|
||||
|
||||
private static void AssertColumn(YLContext db, Type entityType, string propertyName, string expectedColumn)
|
||||
{
|
||||
var entity = db.Model.FindEntityType(entityType);
|
||||
Assert.IsNotNull(entity, $"{entityType.Name} 未注册到 YLContext");
|
||||
var property = entity.FindProperty(propertyName);
|
||||
Assert.IsNotNull(property, $"属性 {propertyName} 不在 {entityType.Name} 映射中");
|
||||
var table = StoreObjectIdentifier.Create(entity, StoreObjectType.Table).Value;
|
||||
var column = property.GetColumnName(table);
|
||||
Assert.AreEqual(expectedColumn, column, $"{entityType.Name}.{propertyName} 映射列名不符");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// R4 授信/现金标签 分配与返还分流纯函数测试(实现方案阶段二 §2.3/§2.4 测试要点)。
|
||||
/// 资金来源是预付金腿上的录入项(逐腿选择),覆盖:标签赋值四种情形、拆单金额核对、
|
||||
/// 平仓按标签返还、存量无标签按现金。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class FundTagCalcTest
|
||||
{
|
||||
private static LegAmount Leg(long id, double amount, bool preferCredit)
|
||||
=> new() { Leg = new swap_position { id = id }, Amount = amount, PreferCredit = preferCredit };
|
||||
|
||||
// ================================================================
|
||||
// §2.3 标签赋值四种情形(FundTagCalc.AllocateByLegPreference,逐腿)
|
||||
// ================================================================
|
||||
|
||||
[TestMethod]
|
||||
public void FT_001_腿未选资金来源_全额现金()
|
||||
{
|
||||
var plans = FundTagCalc.AllocateByLegPreference(new List<LegAmount> { Leg(101, 1000, preferCredit: false) }, 5000, ignoreMoneyCheck: false);
|
||||
Assert.AreEqual(0, plans[0].CreditAmount);
|
||||
Assert.AreEqual(1000, plans[0].CashAmount);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FT_002_腿选现金_全额现金_不消耗额度()
|
||||
{
|
||||
var plans = FundTagCalc.AllocateByLegPreference(new List<LegAmount> { Leg(101, 1000, preferCredit: false) }, 0, ignoreMoneyCheck: false);
|
||||
Assert.AreEqual(0, plans[0].CreditAmount);
|
||||
Assert.AreEqual(1000, plans[0].CashAmount);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FT_003_腿选授信_额度充足_全额授信()
|
||||
{
|
||||
var plans = FundTagCalc.AllocateByLegPreference(new List<LegAmount> { Leg(101, 1000, preferCredit: true) }, 5000, ignoreMoneyCheck: false);
|
||||
Assert.AreEqual(1000, plans[0].CreditAmount);
|
||||
Assert.AreEqual(0, plans[0].CashAmount);
|
||||
Assert.IsFalse(plans[0].NeedSplit);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FT_004_腿选授信_额度不足_拆分为剩余授信加现金差额()
|
||||
{
|
||||
var plans = FundTagCalc.AllocateByLegPreference(new List<LegAmount> { Leg(101, 1000, preferCredit: true) }, 300, ignoreMoneyCheck: false);
|
||||
Assert.AreEqual(300, plans[0].CreditAmount);
|
||||
Assert.AreEqual(700, plans[0].CashAmount);
|
||||
Assert.IsTrue(plans[0].NeedSplit);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FT_005_腿选授信_额度为零_全额现金()
|
||||
{
|
||||
var plans = FundTagCalc.AllocateByLegPreference(new List<LegAmount> { Leg(101, 1000, preferCredit: true) }, 0, ignoreMoneyCheck: false);
|
||||
Assert.AreEqual(0, plans[0].CreditAmount);
|
||||
Assert.AreEqual(1000, plans[0].CashAmount);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FT_006_特批_忽略腿上选择_全额现金不占授信()
|
||||
{
|
||||
var plans = FundTagCalc.AllocateByLegPreference(new List<LegAmount> { Leg(101, 1000, preferCredit: true) }, 5000, ignoreMoneyCheck: true);
|
||||
Assert.AreEqual(0, plans[0].CreditAmount);
|
||||
Assert.AreEqual(1000, plans[0].CashAmount);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FT_007_客户净收取的腿_即使选授信也不占用授信()
|
||||
{
|
||||
var plans = FundTagCalc.AllocateByLegPreference(new List<LegAmount> { Leg(101, -500, preferCredit: true) }, 5000, ignoreMoneyCheck: false);
|
||||
Assert.AreEqual(0, plans[0].CreditAmount);
|
||||
Assert.AreEqual(-500, plans[0].CashAmount);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FT_008_混合偏好_现金腿不消耗授信额度()
|
||||
{
|
||||
var plans = FundTagCalc.AllocateByLegPreference(new List<LegAmount>
|
||||
{
|
||||
Leg(101, 600, preferCredit: false), //现金腿
|
||||
Leg(102, 400, preferCredit: true), //授信腿
|
||||
}, 500, ignoreMoneyCheck: false);
|
||||
Assert.AreEqual(0, plans[0].CreditAmount);
|
||||
Assert.AreEqual(600, plans[0].CashAmount);
|
||||
//授信腿可用额度仍是 500(现金腿未消耗)
|
||||
Assert.AreEqual(400, plans[1].CreditAmount);
|
||||
Assert.AreEqual(0, plans[1].CashAmount);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// §2.3 拆单:多腿授信额度按顺序消耗(FundTagCalc.AllocateByLegPreference)
|
||||
// ================================================================
|
||||
|
||||
[TestMethod]
|
||||
public void FT_010_多条授信腿_额度跨在第二腿_第二腿拆单()
|
||||
{
|
||||
var plans = FundTagCalc.AllocateByLegPreference(new List<LegAmount>
|
||||
{
|
||||
Leg(101, 600, preferCredit: true),
|
||||
Leg(102, 600, preferCredit: true),
|
||||
}, creditAvailable: 1000, ignoreMoneyCheck: false);
|
||||
|
||||
Assert.AreEqual(2, plans.Count);
|
||||
//第一腿全额授信
|
||||
Assert.AreEqual(600, plans[0].CreditAmount);
|
||||
Assert.AreEqual(0, plans[0].CashAmount);
|
||||
Assert.IsFalse(plans[0].NeedSplit);
|
||||
//第二腿跨界拆单:授信400 + 现金200
|
||||
Assert.AreEqual(400, plans[1].CreditAmount);
|
||||
Assert.AreEqual(200, plans[1].CashAmount);
|
||||
Assert.IsTrue(plans[1].NeedSplit);
|
||||
//金额守恒:授信合计=可用额度,现金合计=差额
|
||||
Assert.AreEqual(1000, plans.Sum(p => p.CreditAmount));
|
||||
Assert.AreEqual(200, plans.Sum(p => p.CashAmount));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FT_011_额度耗尽后_后续授信腿全额现金()
|
||||
{
|
||||
var plans = FundTagCalc.AllocateByLegPreference(new List<LegAmount>
|
||||
{
|
||||
Leg(101, 600, preferCredit: true),
|
||||
Leg(102, 600, preferCredit: true),
|
||||
Leg(103, 600, preferCredit: true),
|
||||
}, creditAvailable: 600, ignoreMoneyCheck: false);
|
||||
|
||||
Assert.AreEqual(600, plans[0].CreditAmount);
|
||||
Assert.AreEqual(0, plans[1].CreditAmount);
|
||||
Assert.AreEqual(600, plans[1].CashAmount);
|
||||
Assert.AreEqual(0, plans[2].CreditAmount);
|
||||
Assert.AreEqual(600, plans[2].CashAmount);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// §2.4 平仓/到期按原标签返还(FundTagCalc.SplitUnwindByTag)
|
||||
// ================================================================
|
||||
|
||||
[TestMethod]
|
||||
public void FT_020_平仓返还_授信腿不产生现金_按腿生成释放明细()
|
||||
{
|
||||
var settlements = new List<MarginLegSettlement>
|
||||
{
|
||||
//授信腿:返还本金 400,返息 50 → 全部不进资金,写释放
|
||||
new() { PositionId = 101, Tag = ConsFundTag.Credit, MarginAmount = 400m, RebateAmount = 50m },
|
||||
//现金腿:返还本金 300,返息 20 → 正常资金流水
|
||||
new() { PositionId = 102, Tag = ConsFundTag.Cash, MarginAmount = 300m, RebateAmount = 20m },
|
||||
//存量无标签(EffectiveTag 后按现金)
|
||||
new() { PositionId = 103, Tag = ConsFundTag.Cash, MarginAmount = 100m, RebateAmount = 0m },
|
||||
};
|
||||
var split = FundTagCalc.SplitUnwindByTag(settlements);
|
||||
|
||||
Assert.AreEqual(300 + 100, split.CashMargin);
|
||||
Assert.AreEqual(400, split.CreditMargin);
|
||||
Assert.AreEqual(20, split.CashRebate);
|
||||
Assert.AreEqual(50, split.CreditRebate);
|
||||
//释放明细按 position_id 匹配原占用记录
|
||||
Assert.AreEqual(1, split.Releases.Count);
|
||||
Assert.AreEqual(101L, split.Releases[0].PositionId);
|
||||
Assert.AreEqual(400, split.Releases[0].Amount);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FT_021_平仓返还_全部授信_现金部分为零()
|
||||
{
|
||||
var settlements = new List<MarginLegSettlement>
|
||||
{
|
||||
new() { PositionId = 201, Tag = ConsFundTag.Credit, MarginAmount = 800m, RebateAmount = 30m },
|
||||
};
|
||||
var split = FundTagCalc.SplitUnwindByTag(settlements);
|
||||
Assert.AreEqual(0, split.CashMargin);
|
||||
Assert.AreEqual(0, split.CashRebate);
|
||||
Assert.AreEqual(800, split.CreditMargin);
|
||||
Assert.AreEqual(1, split.Releases.Count);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 平仓利息事件 → 逐腿结算额构造(MarginSettlementBuilder)
|
||||
// ================================================================
|
||||
|
||||
[TestMethod]
|
||||
public void FT_030_由利息事件构造结算额_方向比与标签过滤正确()
|
||||
{
|
||||
var events = new List<swap_flow_event>
|
||||
{
|
||||
//预付金腿(InterestMode=5,无标的代码):InterestDirection=2 → 返还本金 = principal × 1
|
||||
new() { PositionId = 101, InterestMode = (int)InterestModeEnum.初始预付金, InterestDirection = 2, InterestPrincipal = 400m, InterestClosePnL = 50m },
|
||||
//预付金腿:InterestDirection=1(收取)→ 返还本金 = principal × -1
|
||||
new() { PositionId = 102, InterestMode = (int)InterestModeEnum.初始预付金, InterestDirection = 1, InterestPrincipal = 300m, InterestClosePnL = 0m },
|
||||
//浮动腿(有标的代码):不参与
|
||||
new() { PositionId = 103, InterestMode = 0, UnderlyingCode = "123456.SH", InterestPrincipal = 999m },
|
||||
//利息腿(非预付金、无标的):不参与
|
||||
new() { PositionId = 104, InterestMode = (int)InterestModeEnum.固定值, InterestPrincipal = 555m },
|
||||
};
|
||||
var tags = new Dictionary<long, string> { { 101, ConsFundTag.Credit } };
|
||||
var settlements = MarginSettlementBuilder.Build(tags, events);
|
||||
|
||||
Assert.AreEqual(2, settlements.Count);
|
||||
Assert.AreEqual(ConsFundTag.Credit, settlements[0].Tag);
|
||||
Assert.AreEqual(400m, settlements[0].MarginAmount);
|
||||
Assert.AreEqual(50m, settlements[0].RebateAmount);
|
||||
//102 标签字典缺失(存量无标签)→ 按现金;方向比 -1
|
||||
Assert.AreEqual(ConsFundTag.Cash, settlements[1].Tag);
|
||||
Assert.AreEqual(-300m, settlements[1].MarginAmount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Modules.SwapModule;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// R3 阶段四 §4.1 合约维度追加保证金计算测试(纯函数):
|
||||
/// 目标 = 维持保证金 − 应付预付金净收额(≤0 不追);当日新增 = 目标 − 已补足(现金记录累计+授信占用累计);
|
||||
/// 授信优先分配;重跑幂等(新增=0 不写);追保回落不返还(负缺口走可用资金公式)。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SwapAdditionalMarginCalcTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void AM_001_目标计算_维持大于应付()
|
||||
{
|
||||
//维持 180 − 应付净收 100 = 80
|
||||
Assert.AreEqual(80, SwapAdditionalMarginCalc.CalcTarget(180, 100), 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AM_002_目标计算_应付覆盖维持_不追()
|
||||
{
|
||||
Assert.AreEqual(0, SwapAdditionalMarginCalc.CalcTarget(80, 100), 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AM_003_目标计算_维持为负_我方净支付方向不追()
|
||||
{
|
||||
Assert.AreEqual(0, SwapAdditionalMarginCalc.CalcTarget(-50, 100), 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AM_004_分配_授信充足_全额授信()
|
||||
{
|
||||
var (credit, cash) = SwapAdditionalMarginCalc.Allocate(80, 100);
|
||||
Assert.AreEqual(80, credit, 1e-6);
|
||||
Assert.AreEqual(0, cash, 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AM_005_分配_授信不足_拆为授信加现金()
|
||||
{
|
||||
var (credit, cash) = SwapAdditionalMarginCalc.Allocate(80, 30);
|
||||
Assert.AreEqual(30, credit, 1e-6);
|
||||
Assert.AreEqual(50, cash, 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AM_006_分配_无授信_全额现金()
|
||||
{
|
||||
var (credit, cash) = SwapAdditionalMarginCalc.Allocate(80, 0);
|
||||
Assert.AreEqual(0, credit, 1e-6);
|
||||
Assert.AreEqual(80, cash, 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AM_007_分配_新增为零或负_不产生()
|
||||
{
|
||||
Assert.AreEqual((0d, 0d), SwapAdditionalMarginCalc.Allocate(0, 100));
|
||||
Assert.AreEqual((0d, 0d), SwapAdditionalMarginCalc.Allocate(-5, 100));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 幂等与跨日增量(资金记录存累计值的口径验证):
|
||||
/// 第1日 维持180/应付100 → 新增80;第2日 维持不变、已补足80 → 新增0(重跑不写);
|
||||
/// 第3日 维持升到200 → 目标100 − 已补足80 = 新增20(记录累计值更新为100,历史增量自然承载)。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void AM_008_跨日增量与幂等()
|
||||
{
|
||||
double fundedCash = 0, fundedCredit = 0;
|
||||
var payableNet = 100d;
|
||||
|
||||
var day1 = Math.Round(SwapAdditionalMarginCalc.CalcTarget(180, payableNet) - fundedCash - fundedCredit, 2, MidpointRounding.AwayFromZero);
|
||||
Assert.AreEqual(80, day1, 1e-6);
|
||||
var (c1, m1) = SwapAdditionalMarginCalc.Allocate(day1, 30);
|
||||
fundedCredit += c1;
|
||||
fundedCash += m1;
|
||||
Assert.AreEqual(30, fundedCredit, 1e-6);
|
||||
Assert.AreEqual(50, fundedCash, 1e-6);
|
||||
|
||||
//重跑同日:目标/已补足不变 → 新增 0
|
||||
var day1Rerun = Math.Round(SwapAdditionalMarginCalc.CalcTarget(180, payableNet) - fundedCash - fundedCredit, 2, MidpointRounding.AwayFromZero);
|
||||
Assert.AreEqual(0, day1Rerun, 1e-6);
|
||||
|
||||
//第2日 维持不变 → 已补足=目标 → 新增 0
|
||||
var day2 = Math.Round(SwapAdditionalMarginCalc.CalcTarget(180, payableNet) - fundedCash - fundedCredit, 2, MidpointRounding.AwayFromZero);
|
||||
Assert.AreEqual(0, day2, 1e-6);
|
||||
|
||||
//第3日 维持上升 → 只补差额
|
||||
var day3 = Math.Round(SwapAdditionalMarginCalc.CalcTarget(200, payableNet) - fundedCash - fundedCredit, 2, MidpointRounding.AwayFromZero);
|
||||
Assert.AreEqual(20, day3, 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 追保回落(维持下降到应付之下)不返还:目标为 0 → 已补足保持,新增 0;
|
||||
/// 超付部分由可用资金公式的负缺口(Σ维持−累计)体现,不产生返还记录。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void AM_009_追保回落不返还()
|
||||
{
|
||||
var target = SwapAdditionalMarginCalc.CalcTarget(50, 100);
|
||||
Assert.AreEqual(0, target, 1e-6);
|
||||
var (credit, cash) = SwapAdditionalMarginCalc.Allocate(target - 80, 100);
|
||||
Assert.AreEqual(0, credit, 1e-6);
|
||||
Assert.AreEqual(0, cash, 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 追加保证金授信出入记录的 remark 前缀识别(RemoveByTrade 保留判定与累计口径共用)。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void AM_010_追加保证金记录识别()
|
||||
{
|
||||
Assert.IsTrue(ClientCreditInoutService.IsAdditionalMarginRecord(
|
||||
new client_credit_inout { change_type = client_credit_inout.ChangeTypeOccupy, remark = "追加保证金占用" }));
|
||||
Assert.IsFalse(ClientCreditInoutService.IsAdditionalMarginRecord(
|
||||
new client_credit_inout { change_type = client_credit_inout.ChangeTypeOccupy, remark = "簿记授信占用" }));
|
||||
Assert.IsFalse(ClientCreditInoutService.IsAdditionalMarginRecord(
|
||||
new client_credit_inout { remark = null }));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// R2 阶段三 §3.2 可用资金公式测试(需求拆分 R2 口径,实时/EOD/报告三处共用 SwapSpanBalanceCalc):
|
||||
/// 客户维度 = Max(现金结存 + 授信 − 已使用授信 + 初始保证金 − 维持保证金, 0);
|
||||
/// 合约维度 = Max(现金结存 + 授信 − 已使用授信 − 交易维度追加保证金合计, 0)。
|
||||
/// 授信额度为 credit.Credit 合计(保存时已折算比例),现金结存=期末结存(阶段二起授信不进资金)。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SwapSpanBalanceCalcTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void SB_001_客户维度_现金充足()
|
||||
{
|
||||
//现金200 + 授信100 − 已使用20 + 初始100 − 维持150 = 230
|
||||
Assert.AreEqual(230, SwapSpanBalanceCalc.CalcClientDimensionAvailable(
|
||||
cashBalance: 200, totalCredit: 100, usedCredit: 20, initialMargin: 100, maintenanceMargin: 150), 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SB_002_客户维度_现金不足_截断为0()
|
||||
{
|
||||
//现金20 + 0 − 0 + 100 − 130 = −10 → 0(追加10未补足时无可用资金)
|
||||
Assert.AreEqual(0, SwapSpanBalanceCalc.CalcClientDimensionAvailable(
|
||||
cashBalance: 20, totalCredit: 0, usedCredit: 0, initialMargin: 100, maintenanceMargin: 130), 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SB_003_客户维度_授信可用额参与覆盖()
|
||||
{
|
||||
//现金20 + 授信100 − 已使用20 + 100 − 130 = 70
|
||||
Assert.AreEqual(70, SwapSpanBalanceCalc.CalcClientDimensionAvailable(
|
||||
cashBalance: 20, totalCredit: 100, usedCredit: 20, initialMargin: 100, maintenanceMargin: 130), 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SB_004_合约维度_按交易追加合计扣减()
|
||||
{
|
||||
//现金20 + 授信100 − 已使用20 − 追加合计30 = 70
|
||||
Assert.AreEqual(70, SwapSpanBalanceCalc.CalcContractDimensionAvailable(
|
||||
cashBalance: 20, totalCredit: 100, usedCredit: 20, tradeAdditionalMarginSum: 30), 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SB_005_合约维度_扣尽截断为0()
|
||||
{
|
||||
Assert.AreEqual(0, SwapSpanBalanceCalc.CalcContractDimensionAvailable(
|
||||
cashBalance: 20, totalCredit: 0, usedCredit: 0, tradeAdditionalMarginSum: 30), 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 阶段三口径一致性:无追加保证金流水时(阶段四前),两维度公式数值一致——
|
||||
/// 交易追加合计 = Σ(维持−累计) = 维持 − 初始,与客户维度的 (初始−维持) 项互相抵消。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void SB_006_阶段三两维度数值一致()
|
||||
{
|
||||
var cash = 20d;
|
||||
var credit = 100d;
|
||||
var used = 20d;
|
||||
var initial = 100d;
|
||||
var maintenance = 130d;
|
||||
var clientDimension = SwapSpanBalanceCalc.CalcClientDimensionAvailable(cash, credit, used, initial, maintenance);
|
||||
var contractDimension = SwapSpanBalanceCalc.CalcContractDimensionAvailable(cash, credit, used, maintenance - initial);
|
||||
Assert.AreEqual(clientDimension, contractDimension, 1e-9);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 阶段四 §4.2 客户维度双向追保:正数=需追保,不以 0 截断。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void SB_010_客户维度追保金额_正数需追保()
|
||||
{
|
||||
//追保差额 (维持150 − 初始100)=50;资金 现金5+授信0−已使用0=5 → 追保 = 50 − 5 = 45
|
||||
Assert.AreEqual(45, SwapSpanBalanceCalc.CalcClientDimensionCallMargin(
|
||||
cashBalance: 5, totalCredit: 0, usedCredit: 0, initialMargin: 100, maintenanceMargin: 150), 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SB_011_客户维度追保金额_负数可返还()
|
||||
{
|
||||
//差额 (维持130−初始100)=30,资金 20+100−20=100 → 追保 = 30 − 100 = −70(可返还,不截断为 0)
|
||||
Assert.AreEqual(-70, SwapSpanBalanceCalc.CalcClientDimensionCallMargin(
|
||||
cashBalance: 20, totalCredit: 100, usedCredit: 20, initialMargin: 100, maintenanceMargin: 130), 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 客户维度追保金额与可用资金公式互为反向(去 Max 截断):追保 = −(未截断可用资金)。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void SB_012_客户维度追保与可用资金反向一致()
|
||||
{
|
||||
var cash = 20d;
|
||||
var credit = 100d;
|
||||
var used = 20d;
|
||||
var initial = 100d;
|
||||
var maintenance = 130d;
|
||||
var available = SwapSpanBalanceCalc.CalcClientDimensionAvailable(cash, credit, used, initial, maintenance);
|
||||
var callMargin = SwapSpanBalanceCalc.CalcClientDimensionCallMargin(cash, credit, used, initial, maintenance);
|
||||
var availableUnfloored = cash + credit - used + initial - maintenance;
|
||||
Assert.AreEqual(-availableUnfloored, callMargin, 1e-9);
|
||||
//可用资金被 0 截断时追保为正(需追保),两者不矛盾
|
||||
if (availableUnfloored < 0)
|
||||
{
|
||||
Assert.AreEqual(0, available, 1e-9);
|
||||
Assert.IsTrue(callMargin > 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 合约维度追保金额(需求原文 现金+授信−已使用 的应追加方向取值):账户透支为正=应补足,盈余为负。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void SB_013_合约维度追保金额_透支为正()
|
||||
{
|
||||
//现金−80 + 授信100 − 已使用30 = −10 → 追保 = 10(应补足)
|
||||
Assert.AreEqual(10, SwapSpanBalanceCalc.CalcContractDimensionCallMargin(
|
||||
cashBalance: -80, totalCredit: 100, usedCredit: 30), 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SB_014_合约维度追保金额_盈余为负()
|
||||
{
|
||||
//现金50 + 授信100 − 已使用20 = 130 → 追保 = −130(盈余可返还方向)
|
||||
Assert.AreEqual(-130, SwapSpanBalanceCalc.CalcContractDimensionCallMargin(
|
||||
cashBalance: 50, totalCredit: 100, usedCredit: 20), 1e-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// R2 阶段三 §3.1 区间追保纯函数测试:落档边界值(恰好等于档界)、看多/看空方向、
|
||||
/// 追保金额=AmountRate×期初全价×券面总额(累计到所落档位,直取)、
|
||||
/// 维持保证金=(初始+总追加)×我方净收取方向。
|
||||
/// 区间结构(docx 确认书追保表同型):多头 第1层[Lower,+∞)、第n层[Lower,Upper);空头 第1层(−∞,Upper]、第n层(Lower,Upper]。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SwapSpanMarginCalcTest
|
||||
{
|
||||
private static SpanTierConfig Tier(double? lower, double? upper, double? amountRate)
|
||||
=> new() { Lower = lower, Upper = upper, AmountRate = amountRate };
|
||||
|
||||
/// <summary>客户看多 4 层(A=3% 口径:97/94/91/88,比例 0/3%/6%/9%)</summary>
|
||||
private static List<SpanTierConfig> LongTiers() => new()
|
||||
{
|
||||
Tier(0.97, null, 0.00),
|
||||
Tier(0.94, 0.97, 0.03),
|
||||
Tier(0.91, 0.94, 0.06),
|
||||
Tier(0.88, 0.91, 0.09),
|
||||
};
|
||||
|
||||
/// <summary>客户看空 4 层(A=3% 口径:103/106/109/112,比例 0/3%/6%/9%)</summary>
|
||||
private static List<SpanTierConfig> ShortTiers() => new()
|
||||
{
|
||||
Tier(null, 1.03, 0.00),
|
||||
Tier(1.03, 1.06, 0.03),
|
||||
Tier(1.06, 1.09, 0.06),
|
||||
Tier(1.09, 1.12, 0.09),
|
||||
};
|
||||
|
||||
// ================================================================
|
||||
// HasSpanConfig:方案B结构判定(存量 x/y 配置回落旧公式)
|
||||
// ================================================================
|
||||
|
||||
[TestMethod]
|
||||
public void SS_001_空配置_视为存量xy()
|
||||
{
|
||||
Assert.IsFalse(SwapSpanMarginCalc.HasSpanConfig(null));
|
||||
Assert.IsFalse(SwapSpanMarginCalc.HasSpanConfig(new SpanConfig()));
|
||||
Assert.IsFalse(SwapSpanMarginCalc.HasSpanConfig(new SpanConfig
|
||||
{
|
||||
LongSpans = new List<SpanTierConfig> { new SpanTierConfig(), null },
|
||||
ShortSpans = new List<SpanTierConfig> { new SpanTierConfig() }
|
||||
}));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SS_002_任一方向或线有值_视为新结构()
|
||||
{
|
||||
Assert.IsTrue(SwapSpanMarginCalc.HasSpanConfig(new SpanConfig { WarnLine = 0.91 }));
|
||||
Assert.IsTrue(SwapSpanMarginCalc.HasSpanConfig(new SpanConfig { LongSpans = new List<SpanTierConfig> { Tier(0.97, null, 0) } }));
|
||||
Assert.IsTrue(SwapSpanMarginCalc.HasSpanConfig(new SpanConfig { ShortSpans = new List<SpanTierConfig> { Tier(null, 1.03, 0) } }));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// IsCustomerLong:与确认书同规则(收取端同向、支付端反向,客户取反)
|
||||
// ================================================================
|
||||
|
||||
[TestMethod]
|
||||
public void SS_003_客户方向判定()
|
||||
{
|
||||
//我方收取端:PositionType 同向 → 我方多头 → 客户看空
|
||||
Assert.IsFalse(SwapSpanMarginCalc.IsCustomerLong(posiDirection: 1, positionType: 1));
|
||||
Assert.IsTrue(SwapSpanMarginCalc.IsCustomerLong(posiDirection: 1, positionType: 2));
|
||||
//我方支付端:PositionType 反向 → 我方空头 → 客户看多
|
||||
Assert.IsTrue(SwapSpanMarginCalc.IsCustomerLong(posiDirection: 2, positionType: 1));
|
||||
Assert.IsFalse(SwapSpanMarginCalc.IsCustomerLong(posiDirection: 2, positionType: 2));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// MatchTier 多头:[Lower, Upper),第1层上不封顶;边界恰好等于档界
|
||||
// ================================================================
|
||||
|
||||
[TestMethod]
|
||||
public void SS_004_多头落档_区间内与边界值()
|
||||
{
|
||||
var tiers = LongTiers();
|
||||
//未触发(价格充足)→ 第1层
|
||||
Assert.AreSame(tiers[0], SwapSpanMarginCalc.MatchTier(tiers, true, 1.00));
|
||||
Assert.AreSame(tiers[0], SwapSpanMarginCalc.MatchTier(tiers, true, 0.97)); //恰好=第1层下界
|
||||
//第2层 [0.94, 0.97)
|
||||
Assert.AreSame(tiers[1], SwapSpanMarginCalc.MatchTier(tiers, true, 0.9699));
|
||||
Assert.AreSame(tiers[1], SwapSpanMarginCalc.MatchTier(tiers, true, 0.94)); //恰好=第2层下界
|
||||
//第3层 [0.91, 0.94)
|
||||
Assert.AreSame(tiers[2], SwapSpanMarginCalc.MatchTier(tiers, true, 0.9399));
|
||||
Assert.AreSame(tiers[2], SwapSpanMarginCalc.MatchTier(tiers, true, 0.91));
|
||||
//第4层 [0.88, 0.91)
|
||||
Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, true, 0.9099));
|
||||
Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, true, 0.88)); //恰好=最深层下界
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SS_005_多头穿出最深层_按最深层计()
|
||||
{
|
||||
var tiers = LongTiers();
|
||||
Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, true, 0.8799));
|
||||
Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, true, 0.50));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// MatchTier 空头:(Lower, Upper],第1层下不设限;边界恰好等于档界
|
||||
// ================================================================
|
||||
|
||||
[TestMethod]
|
||||
public void SS_006_空头落档_区间内与边界值()
|
||||
{
|
||||
var tiers = ShortTiers();
|
||||
Assert.AreSame(tiers[0], SwapSpanMarginCalc.MatchTier(tiers, false, 1.00));
|
||||
Assert.AreSame(tiers[0], SwapSpanMarginCalc.MatchTier(tiers, false, 1.03)); //恰好=第1层上界
|
||||
//第2层 (1.03, 1.06]
|
||||
Assert.AreSame(tiers[1], SwapSpanMarginCalc.MatchTier(tiers, false, 1.0301));
|
||||
Assert.AreSame(tiers[1], SwapSpanMarginCalc.MatchTier(tiers, false, 1.06)); //恰好=第2层上界
|
||||
//第3层 (1.06, 1.09]
|
||||
Assert.AreSame(tiers[2], SwapSpanMarginCalc.MatchTier(tiers, false, 1.0601));
|
||||
Assert.AreSame(tiers[2], SwapSpanMarginCalc.MatchTier(tiers, false, 1.09));
|
||||
//第4层 (1.09, 1.12]
|
||||
Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, false, 1.0901));
|
||||
Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, false, 1.12));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SS_007_空头穿出最深层_按最深层计()
|
||||
{
|
||||
var tiers = ShortTiers();
|
||||
Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, false, 1.1201));
|
||||
Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, false, 2.00));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SS_008_无可用层_返回null()
|
||||
{
|
||||
Assert.IsNull(SwapSpanMarginCalc.MatchTier(null, true, 0.9));
|
||||
Assert.IsNull(SwapSpanMarginCalc.MatchTier(new List<SpanTierConfig>(), false, 1.0));
|
||||
Assert.IsNull(SwapSpanMarginCalc.MatchTier(new List<SpanTierConfig> { new SpanTierConfig() }, true, 0.9));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 追保金额与维持保证金公式
|
||||
// ================================================================
|
||||
|
||||
[TestMethod]
|
||||
public void SS_009_追保金额_比例乘期初全价乘券面()
|
||||
{
|
||||
//第2层 3%:0.03 × 期初全价101.5 × 券面10000 = 30450
|
||||
Assert.AreEqual(30450, SwapSpanMarginCalc.CalcAdditionalMargin(Tier(0.94, 0.97, 0.03), 101.5, 10000), 1e-6);
|
||||
//第1层 0% → 0
|
||||
Assert.AreEqual(0, SwapSpanMarginCalc.CalcAdditionalMargin(Tier(0.97, null, 0), 101.5, 10000), 1e-6);
|
||||
//未配置比例/null 层 → 0
|
||||
Assert.AreEqual(0, SwapSpanMarginCalc.CalcAdditionalMargin(null, 101.5, 10000), 1e-6);
|
||||
Assert.AreEqual(0, SwapSpanMarginCalc.CalcAdditionalMargin(Tier(0.94, 0.97, null), 101.5, 10000), 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SS_010_维持保证金_初始加追加乘方向()
|
||||
{
|
||||
//我方净收取 +1
|
||||
Assert.AreEqual(80450, SwapSpanMarginCalc.CalcMaintenanceMargin(50000, 30450, 1), 1e-6);
|
||||
//我方净支付 −1(负值=客户应收)
|
||||
Assert.AreEqual(-80450, SwapSpanMarginCalc.CalcMaintenanceMargin(50000, 30450, -1), 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>docx 数值复算(客户看多,A=3%):期初净价98.5 → 当前净价92.0 落第3层,追加=6%×101×100000</summary>
|
||||
[TestMethod]
|
||||
public void SS_011_客户看多数值复算()
|
||||
{
|
||||
var tiers = LongTiers();
|
||||
var ratio = 92.0 / 98.5;
|
||||
var tier = SwapSpanMarginCalc.MatchTier(tiers, isCustomerLong: true, priceRatio: ratio);
|
||||
Assert.AreSame(tiers[2], tier);
|
||||
var additional = SwapSpanMarginCalc.CalcAdditionalMargin(tier, initPrice: 101.0, quantity: 100000);
|
||||
Assert.AreEqual(0.06 * 101.0 * 100000, additional, 1e-6);
|
||||
var maintenance = SwapSpanMarginCalc.CalcMaintenanceMargin(500000, additional, direction: 1);
|
||||
Assert.AreEqual(500000 + 0.06 * 101.0 * 100000, maintenance, 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>docx 数值复算(客户看空,A=3%):期初价98.5 → 收盘价104.0 落第2层,追加=3%×101×100000</summary>
|
||||
[TestMethod]
|
||||
public void SS_012_客户看空数值复算()
|
||||
{
|
||||
var tiers = ShortTiers();
|
||||
var ratio = 104.0 / 98.5;
|
||||
var tier = SwapSpanMarginCalc.MatchTier(tiers, isCustomerLong: false, priceRatio: ratio);
|
||||
Assert.AreSame(tiers[1], tier);
|
||||
var additional = SwapSpanMarginCalc.CalcAdditionalMargin(tier, initPrice: 101.0, quantity: 100000);
|
||||
Assert.AreEqual(0.03 * 101.0 * 100000, additional, 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BUG-25 回归:配置层未按浅→深排序时,穿出最深层的回落不依赖数组顺序——
|
||||
/// 多头取最小下界层、空头取最大上界层(Last() 假定有序,乱序时回落错层)。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void SS_013_穿层回落_乱序配置按边界取最深层()
|
||||
{
|
||||
//多头乱序(浅→深:0.97/0.94/0.91/0.88 → 打乱为 0.91/0.97/0.88/0.94)
|
||||
var longShuffled = new List<SpanTierConfig>
|
||||
{
|
||||
Tier(0.91, 0.94, 0.06),
|
||||
Tier(0.97, null, 0.00),
|
||||
Tier(0.88, 0.91, 0.09),
|
||||
Tier(0.94, 0.97, 0.03),
|
||||
};
|
||||
var longDeepest = SwapSpanMarginCalc.MatchTier(longShuffled, isCustomerLong: true, priceRatio: 0.85);
|
||||
Assert.AreEqual(0.88, longDeepest.Lower);
|
||||
Assert.AreEqual(0.09, longDeepest.AmountRate);
|
||||
|
||||
//空头乱序(浅→深:1.03/1.06/1.09/1.12 → 打乱为 1.09/1.03/1.12/1.06)
|
||||
var shortShuffled = new List<SpanTierConfig>
|
||||
{
|
||||
Tier(1.06, 1.09, 0.06),
|
||||
Tier(null, 1.03, 0.00),
|
||||
Tier(1.09, 1.12, 0.09),
|
||||
Tier(1.03, 1.06, 0.03),
|
||||
};
|
||||
var shortDeepest = SwapSpanMarginCalc.MatchTier(shortShuffled, isCustomerLong: false, priceRatio: 1.15);
|
||||
Assert.AreEqual(1.12, shortDeepest.Upper);
|
||||
Assert.AreEqual(0.09, shortDeepest.AmountRate);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// R2 阶段三 §3.1 引擎单笔计算场景测试(SwapSpanMarginCalc.CalcTradeMaintenanceMargin,收盘价由调用方解析后注入,
|
||||
/// 覆盖"需要收盘价"的各场景:取到价落档、未取到价兜底、试算初始、期初价缺省、方向与腿缺失)。
|
||||
/// 收盘价解析(债券中债估值净价/ETF收盘价/取不到置0)在 MarginCalculationBase.CalcSwapSpanMaintenanceMargin 胶水层,
|
||||
/// 价格源本身的读库行为见 SwapSpanPriceSourceTest。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SwapSpanMarginEngineTest
|
||||
{
|
||||
private static SpanTierConfig Tier(double? lower, double? upper, double? amountRate)
|
||||
=> new() { Lower = lower, Upper = upper, AmountRate = amountRate };
|
||||
|
||||
private static SpanConfig LongShortCfg() => new()
|
||||
{
|
||||
LongSpans = new List<SpanTierConfig>
|
||||
{
|
||||
Tier(0.97, null, 0.00), Tier(0.94, 0.97, 0.03), Tier(0.91, 0.94, 0.06), Tier(0.88, 0.91, 0.09)
|
||||
},
|
||||
ShortSpans = new List<SpanTierConfig>
|
||||
{
|
||||
Tier(null, 1.03, 0.00), Tier(1.03, 1.06, 0.03), Tier(1.06, 1.09, 0.06), Tier(1.09, 1.12, 0.09)
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>标的腿(多空),posiDirection/positionType 组合出方向</summary>
|
||||
private static swap_position UnderlyingLeg(int posiDirection, int positionType,
|
||||
decimal grossPrice, decimal? netPrice, decimal quantity, string code = "240004.IB")
|
||||
=> new()
|
||||
{
|
||||
PosiDirection = posiDirection,
|
||||
PositionType = positionType,
|
||||
UnderlyingCode = code,
|
||||
PosiGrossPrice = grossPrice,
|
||||
PosiNetNoFeePrice = netPrice,
|
||||
PosiQuantity = quantity
|
||||
};
|
||||
|
||||
/// <summary>初始预付金腿(InterestMode=5),interestDirection 1=收取 2=支付</summary>
|
||||
private static swap_position MarginLeg(int interestDirection, decimal principal)
|
||||
=> new() { InterestMode = 5, InterestDirection = interestDirection, InterestPrincipalFix = principal };
|
||||
|
||||
// ================================================================
|
||||
// 收盘价取到 → 落档计算
|
||||
// ================================================================
|
||||
|
||||
/// <summary>债券客户看多:收取端标的腿 PositionType=Short → 客户看多;净价 92.0/期初净价 98.5 落第3层</summary>
|
||||
[TestMethod]
|
||||
public void SE_001_债券客户看多_净价落第3层()
|
||||
{
|
||||
var legs = new List<swap_position>
|
||||
{
|
||||
UnderlyingLeg(posiDirection: 1, positionType: 2, grossPrice: 101.0m, netPrice: 98.5m, quantity: 100000m),
|
||||
MarginLeg(interestDirection: 1, principal: 500000m)
|
||||
};
|
||||
var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(
|
||||
tradeInitialMargin: null, spanCfg: LongShortCfg(), legs, isInitialCalc: false, closePrice: 92.0);
|
||||
//追加 = 6% × 期初全价101 × 券面100000 = 606000;维持 = 500000 + 606000
|
||||
Assert.AreEqual(1106000, maintenance.Value, 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>ETF客户看空:收取端标的腿 PositionType=Long → 客户看空;收盘 1.32/期初净价 1.25 落第2层</summary>
|
||||
[TestMethod]
|
||||
public void SE_002_ETF客户看空_收盘价落第2层()
|
||||
{
|
||||
var legs = new List<swap_position>
|
||||
{
|
||||
UnderlyingLeg(posiDirection: 1, positionType: 1, grossPrice: 1.25m, netPrice: 1.25m, quantity: 1000000m, code: "511010.SH"),
|
||||
MarginLeg(interestDirection: 1, principal: 200000m)
|
||||
};
|
||||
var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(
|
||||
tradeInitialMargin: null, spanCfg: LongShortCfg(), legs, isInitialCalc: false, closePrice: 1.32);
|
||||
//ratio=1.056 ∈ (1.03,1.06] → 追加 = 3% × 1.25 × 1000000 = 37500;维持 = 200000 + 37500
|
||||
Assert.AreEqual(237500, maintenance.Value, 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>收盘价大幅下跌穿出最深层:按最深层比例计(追加=9%),不叠加也不归零</summary>
|
||||
[TestMethod]
|
||||
public void SE_003_价格穿出最深层_按最深层计()
|
||||
{
|
||||
var legs = new List<swap_position>
|
||||
{
|
||||
UnderlyingLeg(1, 2, grossPrice: 101.0m, netPrice: 98.5m, quantity: 100000m),
|
||||
MarginLeg(1, 500000m)
|
||||
};
|
||||
var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), legs, false, closePrice: 80.0);
|
||||
Assert.AreEqual(500000 + 0.09 * 101.0 * 100000, maintenance.Value, 1e-6);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 收盘价未取到 / 试算 —— 兜底行为
|
||||
// ================================================================
|
||||
|
||||
/// <summary>当天无收盘价(closePrice=0):不抛错,追加按0、维持=初始保证金(引擎侧另记告警日志)</summary>
|
||||
[TestMethod]
|
||||
public void SE_004_无收盘价_追加为0维持等于初始()
|
||||
{
|
||||
var legs = new List<swap_position>
|
||||
{
|
||||
UnderlyingLeg(1, 2, 101.0m, 98.5m, 100000m),
|
||||
MarginLeg(1, 500000m)
|
||||
};
|
||||
var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), legs, isInitialCalc: false, closePrice: 0);
|
||||
Assert.AreEqual(500000, maintenance.Value, 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>试算初始(isInitialCalc=true):即使有收盘价也只出初始项——追加保证金为收盘后口径,试算不产出</summary>
|
||||
[TestMethod]
|
||||
public void SE_005_试算初始_不参与追加()
|
||||
{
|
||||
var legs = new List<swap_position>
|
||||
{
|
||||
UnderlyingLeg(1, 2, 101.0m, 98.5m, 100000m),
|
||||
MarginLeg(1, 500000m)
|
||||
};
|
||||
var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), legs, isInitialCalc: true, closePrice: 92.0);
|
||||
Assert.AreEqual(500000, maintenance.Value, 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>期初净价缺失:比基回落期初全价(close/initGross)</summary>
|
||||
[TestMethod]
|
||||
public void SE_006_期初净价缺省_回落期初全价()
|
||||
{
|
||||
var legs = new List<swap_position>
|
||||
{
|
||||
UnderlyingLeg(1, 2, grossPrice: 98.5m, netPrice: null, quantity: 100000m),
|
||||
MarginLeg(1, 500000m)
|
||||
};
|
||||
//close=92.0/98.5 落第3层
|
||||
var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), legs, false, closePrice: 92.0);
|
||||
Assert.AreEqual(500000 + 0.06 * 98.5 * 100000, maintenance.Value, 1e-6);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 腿数据缺失与方向
|
||||
// ================================================================
|
||||
|
||||
/// <summary>无标的腿(缺期初价):返回 null,引擎跳过该交易不产出 trade_span</summary>
|
||||
[TestMethod]
|
||||
public void SE_007_无标的腿_返回null()
|
||||
{
|
||||
var legs = new List<swap_position> { MarginLeg(1, 500000m) };
|
||||
Assert.IsNull(SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), legs, false, closePrice: 92.0));
|
||||
Assert.IsNull(SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), new List<swap_position>(), false, 92.0));
|
||||
}
|
||||
|
||||
/// <summary>我方支付初始保证金(InterestDirection=支付):方向取 −1,维持保证金为负(客户应收)</summary>
|
||||
[TestMethod]
|
||||
public void SE_008_我方支付预付金_维持为负()
|
||||
{
|
||||
var legs = new List<swap_position>
|
||||
{
|
||||
UnderlyingLeg(1, 2, 101.0m, 98.5m, 100000m),
|
||||
MarginLeg(interestDirection: 2, principal: 500000m)
|
||||
};
|
||||
var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), legs, false, closePrice: 92.0);
|
||||
Assert.AreEqual(-(500000 + 0.06 * 101.0 * 100000), maintenance.Value, 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>无预付金腿:初始保证金回落交易录入值 trade.InitialMargin(客户应付常态,方向+1)</summary>
|
||||
[TestMethod]
|
||||
public void SE_009_无预付金腿_回落交易录入初始保证金()
|
||||
{
|
||||
var legs = new List<swap_position> { UnderlyingLeg(1, 2, 101.0m, 98.5m, 100000m) };
|
||||
var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(300000, LongShortCfg(), legs, false, closePrice: 92.0);
|
||||
Assert.AreEqual(300000 + 0.06 * 101.0 * 100000, maintenance.Value, 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>多条预付金腿按收付净额定初始与方向(净支付 → −1)</summary>
|
||||
[TestMethod]
|
||||
public void SE_010_多预付金腿_按净收取定方向()
|
||||
{
|
||||
var legs = new List<swap_position>
|
||||
{
|
||||
UnderlyingLeg(1, 2, 101.0m, 98.5m, 100000m),
|
||||
MarginLeg(1, 200000m), //收取 20万
|
||||
MarginLeg(2, 500000m) //支付 50万 → 净支付 30万
|
||||
};
|
||||
var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), legs, false, closePrice: 0);
|
||||
Assert.AreEqual(-300000, maintenance.Value, 1e-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Modules.DataProviderModule;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// R2 阶段三 §3.1 收盘价取数链路冒烟测试(只读 dev 库,不构造数据):
|
||||
/// 债券净价源(china_bond_valuation,GetBondPrice 口径 SettlePrice=净价、ClosePrice=全价,≤计算日 取最近——盘中/非交易日回退到最近已有估值);
|
||||
/// 指数/ETF收盘价源(eod_stock_price)。
|
||||
/// 两个源在"价格同步作业跑完前"决定引擎行为:取不到 → 追加按0、维持=初始(见 SwapSpanMarginEngineTest.SE_004)。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SwapSpanPriceSourceTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void SP_001_债券估值净价源_可取且净价为正()
|
||||
{
|
||||
using var db = DbContextFactory.GetYLDbContext();
|
||||
var latest = db.china_bond_valuation
|
||||
.Where(x => x.net_price > 0 && x.dirty_price_close > 0)
|
||||
.OrderByDescending(x => x.valuation_date)
|
||||
.Select(x => new { x.valuation_date, x.bond_id })
|
||||
.FirstOrDefault();
|
||||
if (latest == null)
|
||||
{
|
||||
Assert.Inconclusive("dev 库无中债估值数据,跳过");
|
||||
}
|
||||
|
||||
//当日可取
|
||||
Assert.IsTrue(EodPriceQueryService.TryGetBondEodPrice(latest.valuation_date, latest.bond_id, out var price));
|
||||
Assert.IsTrue(price.SettlePrice > 0, "净价(SettlePrice)应为正");
|
||||
Assert.IsTrue(price.ClosePrice > 0, "全价(ClosePrice)应为正");
|
||||
|
||||
//≤计算日 取最近:往未来多取几天仍回退到最近一条估值(盘中跑引擎即此语义)
|
||||
Assert.IsTrue(EodPriceQueryService.TryGetBondEodPrice(latest.valuation_date.AddDays(5), latest.bond_id, out var fallback));
|
||||
Assert.AreEqual(price.SettlePrice, fallback.SettlePrice, 1e-9);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SP_002_ETF收盘价源_可取()
|
||||
{
|
||||
using var db = DbContextFactory.GetYLDbContext();
|
||||
var latest = db.eod_stock_price
|
||||
.Where(x => x.ClosePrice > 0)
|
||||
.OrderByDescending(x => x.ValueDate)
|
||||
.Select(x => new { x.ValueDate, x.UnderlyingCode })
|
||||
.FirstOrDefault();
|
||||
if (latest == null)
|
||||
{
|
||||
Assert.Inconclusive("dev 库无股票/ETF日终价格数据,跳过");
|
||||
}
|
||||
|
||||
Assert.IsTrue(EodPriceQueryService.TryGetEodPrice(latest.ValueDate, latest.UnderlyingCode, out var price));
|
||||
Assert.IsTrue(price.GetPrice(SettlementTypeEnum.ClosePrice) > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Newtonsoft.Json;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
@@ -113,6 +114,11 @@ namespace YLErp.Modules.SwapModule
|
||||
CloseReCheckCallCount++;
|
||||
}
|
||||
|
||||
// R4 按标签分流释放:纯内存测试不连库,stub 为全现金(与既有断言语义一致)
|
||||
protected override UnwindTagSplit ReleaseMarginByFundTag(trade td, DateTime valueDate, List<swap_flow_event> interestEvents,
|
||||
decimal marginAmount, decimal marginRebate)
|
||||
=> new UnwindTagSplit { CashMargin = Convert.ToDouble(marginAmount), CashRebate = Convert.ToDouble(marginRebate) };
|
||||
|
||||
protected override void SaveAllChanges() { SaveAllChangesCount++; }
|
||||
protected override void ExecuteInTransaction(Action action) => action(); // 不包事务,直接执行
|
||||
protected override void CallSaveSwapTradeClientCash(trade td, DateTime valueDate) { } // 空操作
|
||||
|
||||
@@ -122,5 +122,14 @@ namespace YLErp.Modules.SwapModule
|
||||
ClientCashCalls.Add((amount, action));
|
||||
return _nextId++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R4 自动互换返息按标签分流:纯内存测试不连库(查 swap_position 标签),
|
||||
/// stub 为全额现金返息(与既有断言语义一致)。
|
||||
/// </summary>
|
||||
protected override decimal GetAutoSwapCashRebate(trade td, List<swap_flow_event> flowEvents, decimal totalRebate)
|
||||
{
|
||||
return totalRebate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ using YLErp.Model;
|
||||
using YLErp.Models;
|
||||
using YLErp.Modules;
|
||||
using YLErp.Modules.ClientModule;
|
||||
using YLErp.Modules.SwapModule;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
namespace YLErp.BLL.EodSettlement
|
||||
{
|
||||
@@ -296,6 +298,12 @@ namespace YLErp.BLL.EodSettlement
|
||||
|
||||
var endDatas = endQuery.ToArray();
|
||||
|
||||
//阶段三 §3.2 估值报告口径输入:已使用授信(授信出入表)、互换初始保证金(应付预付金净额)、
|
||||
//交易维度追加保证金合计(Σ(维持−累计),规则15交易)——与实时口径共用同一查询(SwapSpanBalanceQueryService)
|
||||
var usedCreditDic = ClientCreditInoutService.GetUsedCreditByClients(clientIdS, db);
|
||||
var swapInitMarginDic = SwapSpanBalanceQueryService.GetSwapInitMarginByClients(clientIdS, lastDate, db);
|
||||
var swapAdditionalDic = SwapSpanBalanceQueryService.GetTradeAdditionalMarginByClients(clientIdS, lastDate, db);
|
||||
|
||||
foreach (var data in endDatas)
|
||||
{
|
||||
if (dic.TryGetValue(data.ClientId, out var balance))
|
||||
@@ -360,12 +368,49 @@ namespace YLErp.BLL.EodSettlement
|
||||
balance.VmOutFundSum = data.VmOutFundSum ?? 0;
|
||||
//预付金金额=期末结存-初始预付金金额
|
||||
balance.MarginBalance = balance.AmountFund - balance.MySideMargin;
|
||||
// 可用资金 = 期末结存 - 追保账户余额 - 初始保证金
|
||||
balance.AvailableAmount = balance.MarginBalance - (balance.VmInFundSum - balance.VmOutFundSum);
|
||||
// 是否追保=盯市金额小于维持保证金额
|
||||
balance.NeedAddMargin = balance.SwapMarketAmount < balance.MaintenanceMargin;
|
||||
// 追保金额=初始保证金金额-盯市金额
|
||||
balance.MarginByPayableMarginTotal = balance.NeedAddMargin ? (balance.MySideMargin - balance.SwapMarketAmount) : 0;
|
||||
//阶段三 §3.2:已使用授信/互换初始保证金/交易维度追加保证金(估值报告字段与可用资金公式共用输入)
|
||||
balance.UsedCredit = usedCreditDic.TryGetValue(data.ClientId, out var usedCredit) ? usedCredit : 0;
|
||||
balance.SwapInitMargin = swapInitMarginDic.TryGetValue(data.ClientId, out var swapInit) ? swapInit : 0;
|
||||
balance.SwapAdditionalMarginTotal = swapAdditionalDic.TryGetValue(data.ClientId, out var swapAdd) ? swapAdd : 0;
|
||||
// 可用资金:配置了追保规则维度的客户按维度分流(R2公式,实时/EOD三处一致);
|
||||
// 未配置(MarginWatchRule=NULL,存量客户)维持旧口径:期末结存-追保账户余额
|
||||
if (client?.MarginWatchRule == 0)
|
||||
{
|
||||
balance.AvailableAmount = SwapSpanBalanceCalc.CalcContractDimensionAvailable(
|
||||
balance.AmountFund, balance.TotalCredit, balance.UsedCredit, balance.SwapAdditionalMarginTotal);
|
||||
}
|
||||
else if (client?.MarginWatchRule == 1)
|
||||
{
|
||||
//维持保证金(净收取为正)= -MySideMargin(client_span 维持保证金反号聚合)
|
||||
balance.AvailableAmount = SwapSpanBalanceCalc.CalcClientDimensionAvailable(
|
||||
balance.AmountFund, balance.TotalCredit, balance.UsedCredit,
|
||||
balance.SwapInitMargin, -balance.MySideMargin);
|
||||
}
|
||||
else
|
||||
{
|
||||
balance.AvailableAmount = balance.MarginBalance - (balance.VmInFundSum - balance.VmOutFundSum);
|
||||
}
|
||||
// 是否追保/追保金额(阶段四 §4.2 按维度分流,允许负值=双向,不以 0 截断):
|
||||
// 客户维度(==1)= (维持−初始) − (现金+授信−已使用),负=可返还;
|
||||
// 合约维度(==0)= −(现金+授信−已使用)(需求原文公式的应追加方向取值,开放问题2,两值均有产出);
|
||||
// 未配置(NULL 存量)维持旧口径:盯市低于维持时 = 初始保证金金额−盯市金额,否则 0
|
||||
if (client?.MarginWatchRule == 1)
|
||||
{
|
||||
balance.MarginByPayableMarginTotal = SwapSpanBalanceCalc.CalcClientDimensionCallMargin(
|
||||
balance.AmountFund, balance.TotalCredit, balance.UsedCredit, balance.SwapInitMargin, -balance.MySideMargin);
|
||||
balance.NeedAddMargin = balance.MarginByPayableMarginTotal > 0;
|
||||
}
|
||||
else if (client?.MarginWatchRule == 0)
|
||||
{
|
||||
balance.MarginByPayableMarginTotal = SwapSpanBalanceCalc.CalcContractDimensionCallMargin(
|
||||
balance.AmountFund, balance.TotalCredit, balance.UsedCredit);
|
||||
balance.NeedAddMargin = balance.MarginByPayableMarginTotal > 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
balance.NeedAddMargin = balance.SwapMarketAmount < balance.MaintenanceMargin;
|
||||
balance.MarginByPayableMarginTotal = balance.NeedAddMargin ? (balance.MySideMargin - balance.SwapMarketAmount) : 0;
|
||||
}
|
||||
// 可取资金=Math.Max(期末结存-min(持仓盈亏,0)-初始保证金,0)
|
||||
balance.DesirableFund = Math.Max(balance.MarginBalance + Math.Min(balance.RoundedPositionPnl, 0), 0);
|
||||
|
||||
|
||||
@@ -234,6 +234,14 @@ namespace YLErp.BLL.Eod
|
||||
Dictionary<int, DateTime?> dicIntradayTradePosition = DbContext.intraday_trade_position.Where(x => clientIds.Contains(x.ClientId)).AsEnumerable().GroupBy(p => p.ClientId).Select(p => new { p.Key, OptDate = p.Max(d => d.OptDate) })
|
||||
.ToDictionary(p => p.Key, p => p.OptDate);
|
||||
Dictionary<int, DateTime?> dicCashInOut = DbContext.ClientCashInCashOut.Where(x => clientIds.Contains((int)x.ClientId)).AsEnumerable().GroupBy(p => (int)p.ClientId).Select(p => new { p.Key, OptDate = p.Max(d => d.OptDate) }).ToDictionary(p => p.Key, p => p.OptDate);
|
||||
|
||||
//阶段三 §3.2 估值报告口径输入(与 ClientBalanceUtility EOD/报告路径共用同一查询,保证三处一致):
|
||||
//已使用授信(授信出入表)、互换初始保证金(应付预付金净额)、交易维度追加保证金合计(Σ(维持−累计))
|
||||
var clientIdList = clientIds.ToList();
|
||||
var usedCreditDic = Modules.SwapModule.ClientCreditInoutService.GetUsedCreditByClients(clientIdList, DbContext);
|
||||
var swapInitMarginDic = SwapSpanBalanceQueryService.GetSwapInitMarginByClients(clientIdList, calcDate.Value, DbContext);
|
||||
var swapAdditionalDic = SwapSpanBalanceQueryService.GetTradeAdditionalMarginByClients(clientIdList, calcDate.Value, DbContext);
|
||||
|
||||
foreach (var item in _clientBalanceDic.Values)
|
||||
{
|
||||
#region 判断实时持仓数据是否最新(包含了最新交易操作的数据)
|
||||
@@ -269,12 +277,51 @@ namespace YLErp.BLL.Eod
|
||||
item.AvailableStockEqvNotional = item.TotalCreditStockEqvNotional - item.PositionNotionalPrincipal;
|
||||
//预付金金额=期末结存-初始预付金金额
|
||||
item.MarginBalance = item.AmountFund - item.MySideMargin;
|
||||
// 可用资金 = 期末结存 - 追保账户余额 - 初始保证金
|
||||
item.AvailableAmount = item.MarginBalance - item.FrozenMarginMoney;
|
||||
// 是否追保=盯市金额小于维持保证金额
|
||||
item.NeedAddMargin = item.SwapMarketAmount < item.MaintenanceMargin;
|
||||
// 追保金额=初始保证金金额-盯市金额
|
||||
item.MarginByPayableMarginTotal = item.NeedAddMargin ? (item.MySideMargin - item.SwapMarketAmount) : 0;
|
||||
//阶段三 §3.2:已使用授信/互换初始保证金/交易维度追加保证金(与 EOD/报告口径一致)
|
||||
item.UsedCredit = usedCreditDic.TryGetValue(clientId, out var usedCredit) ? usedCredit : 0;
|
||||
item.SwapInitMargin = swapInitMarginDic.TryGetValue(clientId, out var swapInit) ? swapInit : 0;
|
||||
item.SwapAdditionalMarginTotal = swapAdditionalDic.TryGetValue(clientId, out var swapAdd) ? swapAdd : 0;
|
||||
// 可用资金:配置了追保规则维度的客户按维度分流(R2公式,实时/EOD三处一致);
|
||||
// 未配置(MarginWatchRule=NULL,存量客户)维持旧口径:期末结存-冻结资金
|
||||
var ruleClient = DataCacheProvider.GetClientDataSource().GetData(clientId);
|
||||
if (ruleClient?.MarginWatchRule == 0)
|
||||
{
|
||||
item.AvailableAmount = SwapSpanBalanceCalc.CalcContractDimensionAvailable(
|
||||
item.AmountFund, item.TotalCredit, item.UsedCredit, item.SwapAdditionalMarginTotal);
|
||||
}
|
||||
else if (ruleClient?.MarginWatchRule == 1)
|
||||
{
|
||||
//维持保证金(净收取为正)= -MySideMargin(client_span 维持保证金反号聚合)
|
||||
item.AvailableAmount = SwapSpanBalanceCalc.CalcClientDimensionAvailable(
|
||||
item.AmountFund, item.TotalCredit, item.UsedCredit,
|
||||
item.SwapInitMargin, -item.MySideMargin);
|
||||
}
|
||||
else
|
||||
{
|
||||
item.AvailableAmount = item.MarginBalance - item.FrozenMarginMoney;
|
||||
}
|
||||
// 是否追保/追保金额(阶段四 §4.2 按维度分流,与 ClientBalanceUtility 报告口径一致):
|
||||
// 客户维度(==1)双向追保 = (维持−初始) − (现金+授信−已使用),允许负值(负=可返还);
|
||||
// 合约维度(==0)= −(现金+授信−已使用);NULL 存量维持旧口径
|
||||
if (ruleClient?.MarginWatchRule == 1)
|
||||
{
|
||||
item.MarginByPayableMarginTotal = SwapSpanBalanceCalc.CalcClientDimensionCallMargin(
|
||||
item.AmountFund, item.TotalCredit, item.UsedCredit, item.SwapInitMargin, -item.MySideMargin);
|
||||
item.NeedAddMargin = item.MarginByPayableMarginTotal > 0;
|
||||
}
|
||||
else if (ruleClient?.MarginWatchRule == 0)
|
||||
{
|
||||
item.MarginByPayableMarginTotal = SwapSpanBalanceCalc.CalcContractDimensionCallMargin(
|
||||
item.AmountFund, item.TotalCredit, item.UsedCredit);
|
||||
item.NeedAddMargin = item.MarginByPayableMarginTotal > 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 是否追保=盯市金额小于维持保证金额
|
||||
item.NeedAddMargin = item.SwapMarketAmount < item.MaintenanceMargin;
|
||||
// 追保金额=初始保证金金额-盯市金额
|
||||
item.MarginByPayableMarginTotal = item.NeedAddMargin ? (item.MySideMargin - item.SwapMarketAmount) : 0;
|
||||
}
|
||||
// 可取资金=max(期末结存+min(持仓盈亏,0)-初始保证金,0)
|
||||
item.DesirableFund =Math.Max( item.MarginBalance - item.FrozenMarginMoney + Math.Min(item.RoundedPositionPnl, 0),0);
|
||||
}
|
||||
@@ -798,7 +845,9 @@ namespace YLErp.BLL.Eod
|
||||
{
|
||||
clientEntryexitsSwap.ForEach(clientEntryexit =>
|
||||
{
|
||||
if (ClientCashInCashOut.系统操作_期权费.Equals(clientEntryexit.Action) || ClientCashInCashOut.系统操作_应付预付金.Equals(clientEntryexit.Action) || ClientCashInCashOut.系统操作_预付金返息.Equals(clientEntryexit.Action))
|
||||
if (ClientCashInCashOut.系统操作_期权费.Equals(clientEntryexit.Action) || ClientCashInCashOut.系统操作_应付预付金.Equals(clientEntryexit.Action) || ClientCashInCashOut.系统操作_预付金返息.Equals(clientEntryexit.Action)
|
||||
//阶段四 §4.1:追加保证金(EOD 结算产生的合约维度追保入金)与应付预付金同口径计入当日资金变动
|
||||
|| ClientCashInCashOut.系统操作_追加保证金.Equals(clientEntryexit.Action))
|
||||
{
|
||||
//CurrPnl += clientEntryexit.Money ?? 0.0;
|
||||
CurrChangeAmount += clientEntryexit.Money ?? 0.0;
|
||||
@@ -1227,9 +1276,12 @@ namespace YLErp.BLL.Eod
|
||||
{ //资金净流入 + 权利金收支 + 期权费收支
|
||||
var balance = _clientBalanceDic[data.ClientId];
|
||||
|
||||
if (data.action == "应收^" + ClientCashInCashOut.系统操作_期权费 || data.action == "应收^" + ClientCashInCashOut.系统操作_应付预付金 || data.action == "应收^" + ClientCashInCashOut.系统操作_预付金返息)
|
||||
if (data.action == "应收^" + ClientCashInCashOut.系统操作_期权费 || data.action == "应收^" + ClientCashInCashOut.系统操作_应付预付金 || data.action == "应收^" + ClientCashInCashOut.系统操作_预付金返息
|
||||
//阶段四 §4.1:追加保证金与应付预付金同桶(互换应收),当日已结算/已确认记录计入
|
||||
|| data.action == "应收^" + ClientCashInCashOut.系统操作_追加保证金)
|
||||
{
|
||||
balance.OptionPremiumSwap = data.moneySum;
|
||||
//同桶多 Action(应付预付金+追加保证金等)并存须累加——与多币种分支(OptionPremiumSwap +=)一致(BUG-08)
|
||||
balance.OptionPremiumSwap += data.moneySum;
|
||||
}
|
||||
else if (data.action == "应收^" + ClientCashInCashOut.系统操作_互换)
|
||||
{
|
||||
|
||||
@@ -4,9 +4,11 @@ using YLErp.Enums;
|
||||
using YLErp.Helpers;
|
||||
using YLErp.Model.Enum;
|
||||
using YLErp.Modules;
|
||||
using YLErp.DBModels.Enums;
|
||||
using YLErp.Modules.CalculationModule;
|
||||
using YLErp.Modules.DataProviderModule;
|
||||
using YLErp.Modules.MarginModule;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
using YLErp.Modules.TradeModule;
|
||||
|
||||
namespace YLErp.BLL.MarginCalculation
|
||||
@@ -366,32 +368,34 @@ namespace YLErp.BLL.MarginCalculation
|
||||
}
|
||||
|
||||
//收益互换预付金计算
|
||||
//预付金模板V2迁移:绑定了模板的互换交易按模板规则计算——无预付金=0;区间追保结构=名义本金×x(初始)/名义本金×y(持仓),
|
||||
//x/y 由 MarginTemplateV2RateHelper 按 期限档+资产类型 匹配明细取得。未绑定或绑定其他规则的互换交易维持现状(不产出 trade_span)。
|
||||
//预付金模板V2迁移:按 R1 三层级(交易绑定→客户默认→全局默认,ResolveTieredTemplates 找到即停)解析模板——
|
||||
//无预付金=0;区间追保结构=名义本金×x(初始)/名义本金×y(持仓),x/y 由 MarginTemplateV2RateHelper 按 期限档+资产类型 匹配明细取得。
|
||||
//三级均未命中或命中其他规则的互换交易维持现状(不产出 trade_span),与确认书/取数口径一致(BUG-02 修正)。
|
||||
//阶段三 §3.1:区间追保结构明细按方案B录入新区间结构(SpanConfig.LongSpans/ShortSpans)时切换新引擎——
|
||||
//每日取标的收盘价落档(债券净价/指数及ETF收盘价),维持保证金 = (初始保证金 + 总追加保证金) × 我方净收取方向;
|
||||
//存量 x/y 配置(无新区间结构)维持 名义×y 公式不变。
|
||||
private static List<trade_span> SwapTradeMarginCalculation(RunMarginCalculationReq req)
|
||||
{
|
||||
var result = new List<trade_span>();
|
||||
|
||||
var swapTradeIds = req.tradeList.Where(t => t.ParentTradeId == 0 && t.id > 0).Select(t => t.id).ToList();
|
||||
if (swapTradeIds.Count == 0)
|
||||
var calcTrades = req.tradeList.Where(t => t.ParentTradeId == 0 && t.id > 0).ToList();
|
||||
if (calcTrades.Count == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
var swapTradeIds = calcTrades.Select(t => t.id).ToList();
|
||||
|
||||
using (var db = new YLContext())
|
||||
{
|
||||
var bindings = db.trade_margin_template.AsNoTracking()
|
||||
.Where(x => swapTradeIds.Contains(x.TradeId) && x.ValueDate <= req.settleDate)
|
||||
.ToList();
|
||||
if (bindings.Count == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
//R1 三层级找到即停(BUG-02 修正):交易绑定 → 客户默认 → 全局默认 统一解析,
|
||||
//只配了客户/全局默认模板的交易与交易级绑定同权生效,与确认书/取数口径一致
|
||||
var templatesByTrade = MarginTemplateV2RateHelper.ResolveTieredTemplates(calcTrades, req.settleDate, db);
|
||||
|
||||
var templateIds = bindings.Select(x => x.MarginTemplateId).Distinct().ToList();
|
||||
var templates = db.margin_template_v2.AsNoTracking()
|
||||
.Where(x => templateIds.Contains(x.id) && x.IsValid)
|
||||
//期初腿数据:标的腿(多空)取 期初价格/数量/方向,初始预付金腿取 初始保证金与收取方向
|
||||
var swapPositions = db.swap_position.AsNoTracking()
|
||||
.Where(x => swapTradeIds.Contains(x.SwapTradeId) && !x.Invalid && x.IsInitial)
|
||||
.ToList();
|
||||
var positionsByTrade = swapPositions.GroupBy(x => x.SwapTradeId).ToDictionary(g => g.Key, g => g.ToList());
|
||||
|
||||
foreach (var trade in req.tradeList)
|
||||
{
|
||||
@@ -401,29 +405,40 @@ namespace YLErp.BLL.MarginCalculation
|
||||
continue;
|
||||
}
|
||||
|
||||
var binding = bindings.Where(x => x.TradeId == trade.id).OrderByDescending(x => x.ValueDate).FirstOrDefault();
|
||||
var template = binding == null ? null : templates.FirstOrDefault(x => x.id == binding.MarginTemplateId);
|
||||
if (template == null)
|
||||
if (!templatesByTrade.TryGetValue(trade.id, out var template))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
double rate;
|
||||
double? margin;
|
||||
if (template.RuleType == (int)MarginRuleTypeEnum.无预付金)
|
||||
{
|
||||
rate = 0;
|
||||
margin = 0;
|
||||
}
|
||||
else if (template.RuleType == (int)MarginRuleTypeEnum.区间追保结构)
|
||||
{
|
||||
var rateResult = MarginTemplateV2RateHelper.GetTradeMarginRate(trade.id, trade.UnderlyingCode, trade.UnderlyingInstrumentType, req.settleDate, db);
|
||||
var rateResult = MarginTemplateV2RateHelper.GetRateByTemplate(template, trade.UnderlyingCode, trade.UnderlyingInstrumentType, req.settleDate, db);
|
||||
if (rateResult == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
//初始预付金=名义本金×x;持仓预付金=名义本金×y
|
||||
rate = (double)(req.CalcMarginType == CalcMarginTypeEnum.InitialMargin
|
||||
? rateResult.InitRate ?? 0m
|
||||
: rateResult.MaintainRate ?? 0m);
|
||||
|
||||
var spanCfg = rateResult.Detail?.SpanConfig;
|
||||
if (SwapSpanMarginCalc.HasSpanConfig(spanCfg))
|
||||
{
|
||||
//方案B新结构:收盘价落档 → 维持保证金 = (初始 + 总追加) × 方向
|
||||
margin = CalcSwapSpanMaintenanceMargin(trade, spanCfg,
|
||||
positionsByTrade.TryGetValue(trade.id, out var legs) ? legs : new List<swap_position>(),
|
||||
req.CalcMarginType == CalcMarginTypeEnum.InitialMargin, req.settleDate);
|
||||
}
|
||||
else
|
||||
{
|
||||
//存量 x/y 配置:初始预付金=名义本金×x;持仓预付金=名义本金×y
|
||||
var rate = (double)(req.CalcMarginType == CalcMarginTypeEnum.InitialMargin
|
||||
? rateResult.InitRate ?? 0m
|
||||
: rateResult.MaintainRate ?? 0m);
|
||||
margin = rate * trade.StockEqvNotional;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -431,7 +446,12 @@ namespace YLErp.BLL.MarginCalculation
|
||||
continue;
|
||||
}
|
||||
|
||||
var margin = rate * trade.StockEqvNotional;
|
||||
if (margin == null)
|
||||
{
|
||||
//新引擎无法计算(缺标的腿等),不产出 trade_span,与未匹配明细行为一致
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(new trade_span()
|
||||
{
|
||||
TradeId = trade.id,
|
||||
@@ -442,16 +462,16 @@ namespace YLErp.BLL.MarginCalculation
|
||||
UnderlyingId = trade.UnderlyingId,
|
||||
UnderlyingCode = trade.UnderlyingCode,
|
||||
ValueDate = req.settleDate,
|
||||
Spv1 = margin,
|
||||
Spv2 = margin,
|
||||
Spv3 = margin,
|
||||
Spv4 = margin,
|
||||
Spv5 = margin,
|
||||
Spv6 = margin,
|
||||
Spv7 = margin,
|
||||
Spv8 = margin,
|
||||
Spv = margin,
|
||||
WorstCastClientPayable = margin,
|
||||
Spv1 = margin.Value,
|
||||
Spv2 = margin.Value,
|
||||
Spv3 = margin.Value,
|
||||
Spv4 = margin.Value,
|
||||
Spv5 = margin.Value,
|
||||
Spv6 = margin.Value,
|
||||
Spv7 = margin.Value,
|
||||
Spv8 = margin.Value,
|
||||
Spv = margin.Value,
|
||||
WorstCastClientPayable = margin.Value,
|
||||
Margin = 0
|
||||
});
|
||||
}
|
||||
@@ -459,5 +479,59 @@ namespace YLErp.BLL.MarginCalculation
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 阶段三 §3.1 新引擎:规则15 方案B配置的维持保证金(DB 胶水层——只负责解析收盘价,计算在 SwapSpanMarginCalc.CalcTradeMaintenanceMargin 纯函数,单测覆盖)。
|
||||
/// 收盘价口径:债券取中债估值净价(GetBondPrice 口径 SettlePrice=净价;取 ≤计算日 最近一条,盘中跑用最近已有估值),
|
||||
/// 指数/ETF取收盘价(InnerGetEodPrice 将日期规整到最近交易日);未取到 → closePrice=0(追加按0、维持=初始),仅记日志不抛错。
|
||||
/// </summary>
|
||||
private static double? CalcSwapSpanMaintenanceMargin(trade trade, SpanConfig spanCfg, List<swap_position> legs, bool isInitialCalc, DateTime settleDate)
|
||||
{
|
||||
//标的腿(多空):期初价格、数量、客户方向
|
||||
var underlyingLeg = legs.FirstOrDefault(x => x.PositionType == (int)PositionTypeFlag.Long || x.PositionType == (int)PositionTypeFlag.Short);
|
||||
if (underlyingLeg == null || underlyingLeg.PosiGrossPrice <= 0)
|
||||
{
|
||||
logger.Info($"【警告】规则15新引擎:交易{trade.id}无有效标的腿(期初价),跳过新公式计算");
|
||||
return null;
|
||||
}
|
||||
|
||||
double closePrice;
|
||||
if (isInitialCalc)
|
||||
{
|
||||
//试算初始不依赖收盘价
|
||||
closePrice = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
var underlyingCode = !string.IsNullOrEmpty(underlyingLeg.UnderlyingCode) ? underlyingLeg.UnderlyingCode : trade.UnderlyingCode;
|
||||
var isBond = DataCacheProvider.GetUnderlyingDataSource().GetData(underlyingCode)?.IsBond() ?? false;
|
||||
if (isBond)
|
||||
{
|
||||
if (!EodPriceQueryService.TryGetBondEodPrice(settleDate, underlyingCode, out var bondPrice))
|
||||
{
|
||||
logger.Info($"【警告】规则15新引擎:交易{trade.id}标的{underlyingCode}无债券估值净价,追加保证金按0计(维持=初始)");
|
||||
closePrice = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
closePrice = bondPrice.GetPrice(SettlementTypeEnum.SettlePrice);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!EodPriceQueryService.TryGetEodPrice(settleDate, underlyingCode, out var eodPrice))
|
||||
{
|
||||
logger.Info($"【警告】规则15新引擎:交易{trade.id}标的{underlyingCode}无收盘价,追加保证金按0计(维持=初始)");
|
||||
closePrice = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
closePrice = eodPrice.GetPrice(SettlementTypeEnum.ClosePrice);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SwapSpanMarginCalc.CalcTradeMaintenanceMargin(trade.InitialMargin, spanCfg, legs, isInitialCalc, closePrice);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,11 @@ namespace YLErp.Modules.ClientModule
|
||||
|
||||
//原始授信值 × 最大授信可用比例(NULL按1) 折算出最终授信值;
|
||||
//OriginalCredit 为空时不触碰 Credit,维持手工授信值(存量兼容)
|
||||
//比例服务端校验 0-1(BUG-10:此前仅前端校验,绕过前端可存负值或>1放大授信)
|
||||
if (req.MaxCreditUseRatio.HasValue && (req.MaxCreditUseRatio.Value < 0 || req.MaxCreditUseRatio.Value > 1))
|
||||
{
|
||||
throw new ServiceException("最大授信可用比例须在0-1之间");
|
||||
}
|
||||
if (req.OriginalCredit.HasValue)
|
||||
{
|
||||
req.Credit = req.OriginalCredit.Value * (req.MaxCreditUseRatio ?? 1);
|
||||
|
||||
@@ -37,6 +37,10 @@ namespace YLErp.Modules.EodModule.SettlementModule
|
||||
var balanceDate = _context.SettleDate;
|
||||
var reqClientIds = _context.Request.ClientIds;
|
||||
|
||||
//阶段四 §4.1:合约维度(MarginWatchRule==0)规则15 交易按 维持−累计 产生"追加保证金"资金记录(授信优先,幂等)。
|
||||
//必须在下方出入金查询之前执行:当日新记录计入当日结算窗口并翻"已结算",重跑时 目标/已补足 不变→同值覆盖,首跑与重跑口径一致
|
||||
new SwapAdditionalMarginService(this).SettleAdditionalMargin(balanceDate, reqClientIds?.ToList());
|
||||
|
||||
var clientDb = DbContextFactory.GetClientDbContext(OptUser);
|
||||
|
||||
var currencyCodes = _context.CurrencyCodes;
|
||||
@@ -654,7 +658,9 @@ namespace YLErp.Modules.EodModule.SettlementModule
|
||||
}
|
||||
}
|
||||
|
||||
if (ClientCashInCashOut.系统操作_期权费.Equals(clientEntryexit.Action) || ClientCashInCashOut.系统操作_应付预付金.Equals(clientEntryexit.Action) || ClientCashInCashOut.系统操作_预付金返息.Equals(clientEntryexit.Action))
|
||||
if (ClientCashInCashOut.系统操作_期权费.Equals(clientEntryexit.Action) || ClientCashInCashOut.系统操作_应付预付金.Equals(clientEntryexit.Action) || ClientCashInCashOut.系统操作_预付金返息.Equals(clientEntryexit.Action)
|
||||
//阶段四 §4.1:追加保证金(EOD 结算产生的合约维度追保入金)与应付预付金同口径计入当日资金变动
|
||||
|| ClientCashInCashOut.系统操作_追加保证金.Equals(clientEntryexit.Action))
|
||||
{
|
||||
CurrChangeAmount += clientEntryexit.Money ?? 0.0;
|
||||
OptionPremiumSwap += clientEntryexit.Money ?? 0.0;
|
||||
|
||||
@@ -4,6 +4,8 @@ using System.Linq;
|
||||
using YLErp.BLL;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Enums;
|
||||
using YLErp.Helpers;
|
||||
using YLErp.Models;
|
||||
using YLErp.Modules.UnderlyingModule;
|
||||
|
||||
namespace YLErp.Modules.MarginModule
|
||||
@@ -15,6 +17,8 @@ namespace YLErp.Modules.MarginModule
|
||||
/// </summary>
|
||||
public static class MarginTemplateV2RateHelper
|
||||
{
|
||||
private static readonly IYcLogger logger = LogFactory.GetLogger("预付金模板取数");
|
||||
|
||||
/// <summary>
|
||||
/// 取数结果。率为小数(如 0.1 表示 10%,与数据库一致),调用方直接用。
|
||||
/// </summary>
|
||||
@@ -59,25 +63,29 @@ namespace YLErp.Modules.MarginModule
|
||||
|
||||
/// <summary>
|
||||
/// 按交易绑定的模板取 初始/维持预付金率(调用方传入 DbContext,供批量场景复用连接)。
|
||||
/// R1 三层级找到即停(2026-08-21 补齐客户/全局两级):交易绑定(自定义)→ 客户默认(client_margin_template,
|
||||
/// 按客户或客户等级)→ 全局默认(margin_template_v2 IsDefault&&!IsForClient,适用结构需含收益互换);
|
||||
/// 三级均未命中返回 null(由调用方兜底,此处留日志:视为无预付金要求)。
|
||||
/// </summary>
|
||||
public static MarginRateResult GetTradeMarginRate(int tradeId, string underlyingCode, string underlyingInstrumentType, DateTime valueDate, YLContext db)
|
||||
{
|
||||
//1.交易绑定(ValueDate 最新)
|
||||
var binding = db.trade_margin_template.AsNoTracking()
|
||||
.Where(x => x.TradeId == tradeId && x.ValueDate <= valueDate)
|
||||
.OrderByDescending(x => x.ValueDate)
|
||||
.FirstOrDefault();
|
||||
if (binding == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var template = db.margin_template_v2.AsNoTracking().FirstOrDefault(x => x.id == binding.MarginTemplateId);
|
||||
if (template == null || !template.IsValid)
|
||||
//R1 三层级找到即停:交易绑定(自定义)→ 客户默认 → 全局默认
|
||||
var clientId = db.trade.AsNoTracking().Where(t => t.id == tradeId).Select(t => (int?)t.ClientId).FirstOrDefault() ?? 0;
|
||||
var template = ResolveTieredTemplate(tradeId, clientId, valueDate, db);
|
||||
if (template == null)
|
||||
{
|
||||
logger.Info($"【警告】预付金模板取数:交易{tradeId}(客户{clientId})三级(交易绑定/客户默认/全局默认)均未命中有效模板,视为无预付金要求");
|
||||
return null;
|
||||
}
|
||||
return GetRateByTemplate(template, underlyingCode, underlyingInstrumentType, valueDate, db);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 已解析模板的取率(GetTradeMarginRate 去掉三层级解析的部分;引擎批量解析后逐交易调用,避免二次解析)。
|
||||
/// 规则非 无预付金/区间追保结构 或明细无匹配行时返回 null(由调用方兜底)。
|
||||
/// </summary>
|
||||
public static MarginRateResult GetRateByTemplate(margin_template_v2 template, string underlyingCode, string underlyingInstrumentType, DateTime valueDate, YLContext db)
|
||||
{
|
||||
//2.无预付金规则:率直接为 0
|
||||
if (template.RuleType == (int)MarginRuleTypeEnum.无预付金)
|
||||
{
|
||||
@@ -149,6 +157,196 @@ namespace YLErp.Modules.MarginModule
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R1 模板三层级找到即停解析(public 供集成测试与后续消费方复用):
|
||||
/// 1.交易绑定 trade_margin_template(自定义/录入时显式选择,ValueDate 最新 ≤ 业务日)——找到即停,
|
||||
/// 绑定指向已失效模板时同样停止(不向下回退,避免"改绑定失效后悄悄换成别的规则"),返回 null 并留日志;
|
||||
/// 2.客户默认 client_margin_template(按客户 ClientId 或按客户等级 ClientId=0+ClientLevel,
|
||||
/// 客户等级经 client.LevelId → clientlevel.LevelName 解析;ValueDate 最新 ≤ 业务日),
|
||||
/// 模板须有效且适用结构含收益互换;
|
||||
/// 3.全局默认 margin_template_v2(IsDefault && !IsForClient && IsValid && 适用收益互换,ValueDate 最新)。
|
||||
/// </summary>
|
||||
public static margin_template_v2 ResolveTieredTemplate(int? tradeId, int clientId, DateTime valueDate, YLContext db)
|
||||
{
|
||||
//1.交易绑定(找到即停)
|
||||
if (tradeId > 0)
|
||||
{
|
||||
var bindingTemplateId = db.trade_margin_template.AsNoTracking()
|
||||
.Where(x => x.TradeId == tradeId.Value && x.ValueDate <= valueDate)
|
||||
.OrderByDescending(x => x.ValueDate)
|
||||
.ThenByDescending(x => x.id)
|
||||
.Select(x => (int?)x.MarginTemplateId)
|
||||
.FirstOrDefault();
|
||||
if (bindingTemplateId > 0)
|
||||
{
|
||||
var bound = db.margin_template_v2.AsNoTracking().FirstOrDefault(x => x.id == bindingTemplateId.Value && x.IsValid);
|
||||
if (bound != null)
|
||||
{
|
||||
return bound;
|
||||
}
|
||||
logger.Info($"【警告】预付金模板取数:交易{tradeId}绑定的模板{bindingTemplateId}已失效或不存在,按找到即停不向下回退");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
//2.客户默认(client_margin_template:按客户 或 按客户等级;客户表在独立客户库)
|
||||
//先按 有效+适用收益互换 过滤再取绑定日期最新——同日并存"互换绑定+期权绑定"(页面互斥只挡适用结构重叠)时,
|
||||
//若先取绑定再过滤会随机命中期权绑定而漏掉互换绑定、误穿透到全局
|
||||
if (clientId > 0)
|
||||
{
|
||||
string levelName = null;
|
||||
using (var clientDb = DbContextFactory.GetClientDbContext(OptUserInfo.SystemUser))
|
||||
{
|
||||
var levelId = clientDb.client.AsNoTracking().Where(c => c.id == clientId).Select(c => c.LevelId).FirstOrDefault();
|
||||
if (levelId > 0)
|
||||
{
|
||||
levelName = clientDb.clientlevel.AsNoTracking().Where(l => l.id == levelId).Select(l => l.LevelName).FirstOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
var clientTemplate = (from cmt in db.client_margin_template.AsNoTracking()
|
||||
join mt in db.margin_template_v2.AsNoTracking() on cmt.MarginTemplateId equals mt.id
|
||||
where cmt.ValueDate <= valueDate && cmt.MarginTemplateId > 0
|
||||
&& (cmt.ClientId == clientId || (cmt.ClientId == 0 && cmt.ClientLevel != "" && cmt.ClientLevel == levelName))
|
||||
&& mt.IsValid && mt.TradeTypes.Contains("收益互换")
|
||||
orderby cmt.ValueDate descending, cmt.id descending
|
||||
select mt).FirstOrDefault();
|
||||
if (clientTemplate != null)
|
||||
{
|
||||
return clientTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
//3.全局默认
|
||||
return db.margin_template_v2.AsNoTracking()
|
||||
.Where(x => x.IsDefault && !x.IsForClient && x.IsValid && x.TradeTypes.Contains("收益互换") && x.ValueDate <= valueDate)
|
||||
.OrderByDescending(x => x.ValueDate)
|
||||
.ThenByDescending(x => x.id)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R1 三层级找到即停解析(批量版,BUG-02 修正:EOD 引擎与结算查询按交易列表解析,
|
||||
/// 只配了客户默认/全局默认模板的交易与交易级绑定同权生效):
|
||||
/// 语义与单笔 ResolveTieredTemplate 一致——
|
||||
/// 1.交易绑定(ValueDate 最新 ≤ 业务日,同日多条按 id 最新兜底)找到即停,绑定指向失效模板同样停止(不回退);
|
||||
/// 2.客户默认(client_margin_template 按客户或按客户等级,模板须有效且适用结构含收益互换,先过滤有效再取绑定最新);
|
||||
/// 3.全局默认(IsDefault&&!IsForClient&&IsValid&&适用收益互换,ValueDate/id 最新)。
|
||||
/// 返回 tradeId → 命中模板;未命中或因失效绑定停止的交易不在结果中。
|
||||
/// </summary>
|
||||
public static Dictionary<int, margin_template_v2> ResolveTieredTemplates(List<trade> trades, DateTime valueDate, YLContext db)
|
||||
{
|
||||
var result = new Dictionary<int, margin_template_v2>();
|
||||
if (trades == null || trades.Count == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
var tradeIds = trades.Select(t => t.id).Distinct().ToList();
|
||||
|
||||
//1.交易绑定(批量取回后内存取每交易最新一条,同日多条按 id 最新兜底——与单笔解析一致)
|
||||
var bindingRows = db.trade_margin_template.AsNoTracking()
|
||||
.Where(x => tradeIds.Contains(x.TradeId) && x.ValueDate <= valueDate)
|
||||
.Select(x => new { x.TradeId, x.MarginTemplateId, x.ValueDate, x.id })
|
||||
.ToList();
|
||||
var bindingByTrade = bindingRows
|
||||
.GroupBy(x => x.TradeId)
|
||||
.ToDictionary(g => g.Key,
|
||||
g => g.OrderByDescending(x => x.ValueDate).ThenByDescending(x => x.id).First().MarginTemplateId);
|
||||
var boundTemplates = new Dictionary<int, margin_template_v2>();
|
||||
if (bindingByTrade.Values.Distinct().Any())
|
||||
{
|
||||
var boundTemplateIds = bindingByTrade.Values.Distinct().ToList();
|
||||
boundTemplates = db.margin_template_v2.AsNoTracking()
|
||||
.Where(x => boundTemplateIds.Contains(x.id) && x.IsValid)
|
||||
.ToDictionary(x => x.id);
|
||||
}
|
||||
|
||||
//2.客户默认(client_margin_template:按客户 或 按客户等级;客户表在独立客户库,等级名批量解析)
|
||||
var clientIds = trades.Select(t => t.ClientId).Where(c => c > 0).Distinct().ToList();
|
||||
var clientTemplateByClient = new Dictionary<int, margin_template_v2>();
|
||||
if (clientIds.Any())
|
||||
{
|
||||
var levelNames = new Dictionary<int, string>();
|
||||
using (var clientDb = DbContextFactory.GetClientDbContext(OptUserInfo.SystemUser))
|
||||
{
|
||||
var clientLevels = clientDb.client.AsNoTracking()
|
||||
.Where(c => clientIds.Contains(c.id) && c.LevelId > 0)
|
||||
.Select(c => new { c.id, LevelId = c.LevelId ?? 0 })
|
||||
.ToList();
|
||||
var levelIds = clientLevels.Select(c => c.LevelId).Distinct().ToList();
|
||||
var levelNameById = clientDb.clientlevel.AsNoTracking()
|
||||
.Where(l => levelIds.Contains(l.id))
|
||||
.Select(l => new { l.id, l.LevelName })
|
||||
.ToList()
|
||||
.ToDictionary(l => l.id, l => l.LevelName);
|
||||
foreach (var c in clientLevels)
|
||||
{
|
||||
levelNames[c.id] = levelNameById.TryGetValue(c.LevelId, out var name) ? name : null;
|
||||
}
|
||||
}
|
||||
|
||||
//先按 有效+适用收益互换 过滤再按绑定取最新(与单笔解析一致,同日互换/期权并存不误穿透)
|
||||
var clientBindingRows = (from cmt in db.client_margin_template.AsNoTracking()
|
||||
join mt in db.margin_template_v2.AsNoTracking() on cmt.MarginTemplateId equals mt.id
|
||||
where cmt.ValueDate <= valueDate && cmt.MarginTemplateId > 0
|
||||
&& (clientIds.Contains(cmt.ClientId) || (cmt.ClientId == 0 && cmt.ClientLevel != ""))
|
||||
&& mt.IsValid && mt.TradeTypes.Contains("收益互换")
|
||||
orderby cmt.ValueDate descending, cmt.id descending
|
||||
select new { cmt.ClientId, cmt.ClientLevel, Template = mt }).ToList();
|
||||
foreach (var clientId in clientIds)
|
||||
{
|
||||
var levelName = levelNames.TryGetValue(clientId, out var name) ? name : null;
|
||||
//每客户找到即停:按客户 或 按客户等级 同查询取绑定最新(与单笔解析一致;
|
||||
//直接绑定与等级绑定并存的优先级方案未定义(BUG-16),维持"绑定日期最新"现状不改语义)
|
||||
var hit = clientBindingRows
|
||||
.FirstOrDefault(r => r.ClientId == clientId || (r.ClientId == 0 && r.ClientLevel == levelName));
|
||||
if (hit != null)
|
||||
{
|
||||
clientTemplateByClient[clientId] = hit.Template;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//3.全局默认(全部交易共享,一次查询)
|
||||
var globalDefault = db.margin_template_v2.AsNoTracking()
|
||||
.Where(x => x.IsDefault && !x.IsForClient && x.IsValid && x.TradeTypes.Contains("收益互换") && x.ValueDate <= valueDate)
|
||||
.OrderByDescending(x => x.ValueDate)
|
||||
.ThenByDescending(x => x.id)
|
||||
.FirstOrDefault();
|
||||
|
||||
foreach (var trade in trades)
|
||||
{
|
||||
if (result.ContainsKey(trade.id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (bindingByTrade.TryGetValue(trade.id, out var templateId))
|
||||
{
|
||||
//一级找到即停:绑定指向失效模板时不向下回退(与单笔解析一致,仅留日志)
|
||||
if (boundTemplates.TryGetValue(templateId, out var bound))
|
||||
{
|
||||
result[trade.id] = bound;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Info($"【警告】预付金模板取数:交易{trade.id}绑定的模板{templateId}已失效或不存在,按找到即停不向下回退");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (clientTemplateByClient.TryGetValue(trade.ClientId, out var clientTemplate))
|
||||
{
|
||||
result[trade.id] = clientTemplate;
|
||||
continue;
|
||||
}
|
||||
if (globalDefault != null)
|
||||
{
|
||||
result[trade.id] = globalDefault;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标的细分分类判定钩子(转债ETF/科创债ETF/中债指数等),public 供单元测试回归默认行为。
|
||||
/// 本期默认返回 null → 走通配行兜底,行为与现状一致;后续需求按业务给的判定规则(代码段/标的维护字段)实现。
|
||||
|
||||
@@ -319,6 +319,19 @@ namespace YLErp.Modules.ReportModule
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 已使用授信(R2 阶段三 §3.2):授信出入表 Σ(amount)
|
||||
/// </summary>
|
||||
public double? UsedCredit { get; set; }
|
||||
|
||||
public string UsedCreditString
|
||||
{
|
||||
get
|
||||
{
|
||||
return ((UsedCredit == null || UsedCredit == -0) ? 0 : UsedCredit.Value).ToString("0.00");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 授信占用
|
||||
/// </summary>
|
||||
|
||||
@@ -107,6 +107,10 @@ namespace YLErp.Modules.ReportModule
|
||||
AvailableFund = clientBalance?.AvailableAmount ?? 0,
|
||||
TotalMargin = clientBalance?.TotalMarginTotal,
|
||||
Credit = clientBalance?.TotalCredit ?? 0,
|
||||
UsedCredit = clientBalance?.UsedCredit ?? 0,
|
||||
//阶段三 §3.2 R2 口径:初始保证金=应付预付金流水净额(净收取为正);维持保证金=client_span 维持保证金(−MySideMargin)
|
||||
MySideMargin = clientBalance?.SwapInitMargin ?? 0,
|
||||
MaintenanceMargin = -(clientBalance?.MySideMargin ?? 0),
|
||||
CreditRatio = clientBalance?.CreditUsed ?? 0,
|
||||
Margin = clientBalance?.MarginByPayableMarginTotal ?? 0,
|
||||
Amount = (PS.Config.IsPVRounded ? clientBalance?.RoundedTotalAmountTotal : clientBalance?.TotalAmountTotal) ?? 0,
|
||||
@@ -408,6 +412,10 @@ namespace YLErp.Modules.ReportModule
|
||||
case "预付金占用": return report.FundReportModel.WorstCastClientPayableString;
|
||||
case "可用资金": return report.FundReportModel.AvailableFundString;
|
||||
case "授信额度": return report.FundReportModel.CreditString;
|
||||
//阶段三 §3.2 R2 估值报告字段口径
|
||||
case "已使用授信": return report.FundReportModel.UsedCreditString;
|
||||
case "初始保证金金额": return report.FundReportModel.MySideMarginString;
|
||||
case "维持保证金金额": return report.FundReportModel.MaintenanceMarginString;
|
||||
case "应付了结交易款": return report.FundReportModel.ClosedTradePayableFundString;
|
||||
case "应付存续交易款": return report.FundReportModel.PositionTradePayableFundString;
|
||||
case "存续交易期权费净额": return report.FundReportModel.PositionPremiumNetCashString;
|
||||
|
||||
@@ -6,12 +6,20 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <summary>
|
||||
/// 客户授信出入服务(R4 授信/现金标签体系,决策④)。
|
||||
/// 授信出入表的写入口集中在 标签赋值(占用)与 平仓/到期返还(释放)两个链路内,禁止散落调用。
|
||||
/// 金额符号口径(2026-08-20 业务确认):与资金流水同号——入金为负、出金为正;
|
||||
/// 已使用授信 = SUM(amount) 直接求和(入金使已使用授信下降、可用授信=有效授信−已使用授信 上升;出金反之)。
|
||||
/// 金额符号口径(BUG-01 修正,2026-08-24):**占用记正数、释放记负数**(与 §2.2 原表"占用/调整为正"一致)——
|
||||
/// 已使用授信 = SUM(amount) 直接求和(占用使其上升、释放使其回落),可用授信 = 有效授信 − 已使用授信(占用使其收缩)。
|
||||
/// 2026-08-20 曾裁定"与资金流水同号(入金负/出金正)"——但授信不入资金流水(§2.1),同号对齐的对手方记录并不存在,
|
||||
/// 且该口径下"占用越多已使用授信越负、可用授信越大"(越占越多),已废弃;历史同号行由迁移脚本翻符号。
|
||||
/// change_type(占用/释放/调整)仅作分类审计,不参与求和方向。
|
||||
/// </summary>
|
||||
public class ClientCreditInoutService : YLBaseService
|
||||
{
|
||||
/// <summary>
|
||||
/// 追加保证金相关授信出入记录的 remark 前缀(阶段四 §4.1 EOD 占用写入"追加保证金占用";
|
||||
/// 累计保证金/缺口口径与清理链路以此识别,后续如写释放类记录沿用同一前缀)。
|
||||
/// </summary>
|
||||
public const string AdditionalMarginRemark = "追加保证金";
|
||||
|
||||
public ClientCreditInoutService(OptUserInfo userInfo) : base(userInfo)
|
||||
{
|
||||
}
|
||||
@@ -21,9 +29,19 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 客户已使用授信 = SUM(amount)(金额与资金流水同号:入金负、出金正)。
|
||||
/// 可用授信 = 有效授信 − 本值;入金使可用授信增长、出金使其收缩。
|
||||
/// 供交易确认校验(RealtimePnlCalc.TradeCanBeConfirm)等静态上下文直接调用。
|
||||
/// 是否追加保证金相关记录(remark 前缀标识,阶段四 EOD 写入)。
|
||||
/// </summary>
|
||||
public static bool IsAdditionalMarginRecord(client_credit_inout record)
|
||||
{
|
||||
return record != null && !string.IsNullOrEmpty(record.remark)
|
||||
&& record.remark.StartsWith(AdditionalMarginRemark);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 客户已使用授信 = SUM(amount)(占用为正、释放为负)。
|
||||
/// 可用授信 = 有效授信 − 本值:占用使其收缩、释放使其回升。
|
||||
/// 供交易确认校验(RealtimePnlCalc.TradeCanBeConfirm)、拆单额度(GetAvailableCredit)、
|
||||
/// 估值报告可用资金(阶段三 §3.2)等消费方统一调用。
|
||||
/// </summary>
|
||||
public static double GetUsedCredit(int clientId, YLContext db)
|
||||
{
|
||||
@@ -40,8 +58,8 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入一条授信变化记录(占用/释放/调整统一入口),并冗余记录变更后已使用授信。
|
||||
/// amount 带符号:入金(客户付)为负、出金(客户收)为正,与资金流水 Money 同号。
|
||||
/// 写入一条授信变化记录(统一入口),并冗余记录变更后已使用授信。
|
||||
/// amount 带符号直写(人工调整类可正可负);占用/释放请走 Occupy/Release 语义入口(内部定方向)。
|
||||
/// </summary>
|
||||
public client_credit_inout Record(int clientId, long? positionId, int? tradeId, int changeType,
|
||||
double amount, DateTime happenDate, string remark)
|
||||
@@ -51,7 +69,7 @@ namespace YLErp.Modules.SwapModule
|
||||
return null;
|
||||
}
|
||||
var amountRounded = Math.Round(amount, 2, MidpointRounding.AwayFromZero);
|
||||
//变更后已使用授信快照:直接求和口径(符号已含方向)
|
||||
//变更后已使用授信快照:直接求和口径(占用正/释放负,符号已含方向)
|
||||
var usedAfter = Math.Round(GetUsedCredit(clientId) + amountRounded, 2, MidpointRounding.AwayFromZero);
|
||||
var record = new client_credit_inout
|
||||
{
|
||||
@@ -74,31 +92,37 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
/// <summary>
|
||||
/// 占用:预付金腿标 Credit 的簿记入金(含拆单的授信部分;交易级占用 positionId 为空)。
|
||||
/// amount 传负数(入金方向,与资金流水同号)。
|
||||
/// amount 传正数(占用数量),内部记正——已使用授信随之上升。
|
||||
/// </summary>
|
||||
public client_credit_inout Occupy(int clientId, long? positionId, int tradeId, double amount, DateTime happenDate, string remark)
|
||||
{
|
||||
return Record(clientId, positionId, tradeId, client_credit_inout.ChangeTypeOccupy, amount, happenDate, remark);
|
||||
return Record(clientId, positionId, tradeId, client_credit_inout.ChangeTypeOccupy, Math.Abs(amount), happenDate, remark);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放:平仓/到期按原标签返还授信部分(按腿的 position_id 匹配原占用记录)。
|
||||
/// amount 传正数(出金方向,与资金流水同号)。
|
||||
/// amount 传正数(释放数量),内部记负——已使用授信随之回落。
|
||||
/// </summary>
|
||||
public client_credit_inout Release(int clientId, long? positionId, int tradeId, double amount, DateTime happenDate, string remark)
|
||||
{
|
||||
return Record(clientId, positionId, tradeId, client_credit_inout.ChangeTypeRelease, amount, happenDate, remark);
|
||||
return Record(clientId, positionId, tradeId, client_credit_inout.ChangeTypeRelease, -Math.Abs(amount), happenDate, remark);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除某交易的全部授信出入记录:与资金记录同生命周期——
|
||||
/// 删除某交易的授信出入记录:与资金记录同生命周期——
|
||||
/// 交易回退到开仓(DeleteTradeCashInCashOut)、修改清除(ClearSwapPositions)、删除交易时同步清理,
|
||||
/// 重新确认/重补时按最新标签重写,避免占用悬挂。
|
||||
/// 注意:阶段四追加保证金占用落地后,此处需区分保留追加部分。
|
||||
/// keepAdditionalMargin=true 时保留追加保证金部分(阶段四落地):重确认自愈/修改清除只重写 应付预付金 相关占用,
|
||||
/// 追加部分由 EOD 幂等维护(该两处不删除追加资金记录,授信占用须同步保留,否则已使用授信被低估);
|
||||
/// 删除交易(资金记录全删)传 false 全清。
|
||||
/// </summary>
|
||||
public void RemoveByTrade(int tradeId)
|
||||
public void RemoveByTrade(int tradeId, bool keepAdditionalMargin = false)
|
||||
{
|
||||
var records = DbContext.client_credit_inout.Where(x => x.trade_id == tradeId).ToList();
|
||||
if (keepAdditionalMargin)
|
||||
{
|
||||
records = records.Where(x => !IsAdditionalMarginRecord(x)).ToList();
|
||||
}
|
||||
DbContext.client_credit_inout.RemoveRange(records);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
namespace YLErp.Modules.SwapModule.Margin
|
||||
{
|
||||
/// <summary>
|
||||
/// R3 阶段四 §4.1 合约维度追加保证金的纯函数(金额判定与授信/现金分配,DB 胶水在 SwapAdditionalMarginService)。
|
||||
/// 口径(实现方案 §0 基线 + 需求拆分 R3):
|
||||
/// - 累计保证金(净收取为正)= 该交易 应付预付金+追加保证金 资金记录收付净额 + 追加保证金的授信占用部分(授信不进资金、无流水);
|
||||
/// - 目标追加保证金(累计到当日)= 维持保证金 − 应付预付金净收额,≤0 时为 0(追保回落不返还,负缺口在可用资金公式体现);
|
||||
/// - 已补足额 = 追加资金记录累计值(单条 upsert 存累计值,Money 为负)+ 授信占用累计(amount 为负);
|
||||
/// - 当日新增 = 目标 − 已补足,>0 时按阶段二 §2.3 授信优先逻辑分配:授信部分只写授信出入表,差额走资金记录。
|
||||
/// 幂等:同一结算日重跑时 目标/已补足 均不变 → 新增=0,不产生任何写入。
|
||||
/// </summary>
|
||||
public static class SwapAdditionalMarginCalc
|
||||
{
|
||||
/// <summary>
|
||||
/// 目标追加保证金(累计到当日,净收取为正)= 维持保证金 − 应付预付金净收额;差值 ≤0 返回 0(无需追加;
|
||||
/// 维持为负=我方净支付方向,同样不追)。
|
||||
/// </summary>
|
||||
public static double CalcTarget(double maintenanceMargin, double payableMarginNet)
|
||||
{
|
||||
return Round(Math.Max(maintenanceMargin - payableMarginNet, 0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当日新增追加保证金的授信/现金分配(阶段二 §2.3 授信优先的交易级版本):
|
||||
/// 授信部分 = min(新增, 剩余可用授信),差额为现金部分;新增 ≤0 时两者皆 0(不产生)。
|
||||
/// </summary>
|
||||
public static (double CreditAmount, double CashAmount) Allocate(double increment, double creditAvailable)
|
||||
{
|
||||
increment = Round(increment);
|
||||
if (increment <= 0)
|
||||
{
|
||||
return (0, 0);
|
||||
}
|
||||
var credit = Math.Min(increment, Round(Math.Max(creditAvailable, 0)));
|
||||
return (credit, Round(increment - credit));
|
||||
}
|
||||
|
||||
private static double Round(double value)
|
||||
{
|
||||
return Math.Round(value, 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
using YLErp.BLL;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Enums;
|
||||
using YLErp.Modules.MarginModule;
|
||||
using YLErp.Modules.TradeModule;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Margin
|
||||
{
|
||||
/// <summary>
|
||||
/// R3 阶段四 §4.1:合约维度(MarginWatchRule==0)规则15 交易日终结算产生"追加保证金"资金记录。
|
||||
/// 交易维度追加保证金 = 维持保证金(阶段三引擎 trade_span 产出)− 累计保证金(应付预付金+追加保证金 流水净额 + 追加授信占用);
|
||||
/// 现金部分为逐结算日增量记录(BUG-03 修正:每结算日一条、Money=−increment,键 TradeId+Action+Deal+HappenDate 幂等),
|
||||
/// 需求上升只增不减;授信优先(阶段二规则):授信部分只写授信出入表(remark 前缀=追加保证金,position_id 空、冗余 trade_id)。
|
||||
/// 由 EOD 在客户资金计算之前调用:当日新记录计入当日出入金窗口并翻"已结算",重跑时 目标/已补足 不变 → 新增为 0 不重复写。
|
||||
/// 客户维度(MarginWatchRule=1/NULL)不产生资金记录(§0 占用口径),不在本服务范围。
|
||||
/// </summary>
|
||||
public class SwapAdditionalMarginService : YLBaseService
|
||||
{
|
||||
public SwapAdditionalMarginService(OptUserInfo userInfo) : base(userInfo)
|
||||
{
|
||||
}
|
||||
|
||||
public SwapAdditionalMarginService(YLBaseService baseService) : base(baseService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 结算日逐客户逐交易产生追加保证金(clientFilter 为部分结算的客户过滤,与 EOD 请求一致)。
|
||||
/// </summary>
|
||||
public void SettleAdditionalMargin(DateTime settleDate, List<int> clientFilter = null)
|
||||
{
|
||||
//合约维度盯市客户
|
||||
var watchClientIds = DbContextFactory.GetClientDbContext(OptUser).client.AsNoTracking()
|
||||
.Where(t => t.ProcessStatus != "未提交" && t.MarginWatchRule == 0)
|
||||
.Select(t => t.id)
|
||||
.ToList();
|
||||
if (clientFilter != null && clientFilter.Any())
|
||||
{
|
||||
watchClientIds = watchClientIds.Where(t => clientFilter.Contains(t)).ToList();
|
||||
}
|
||||
if (watchClientIds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//存续中的互换交易(状态口径与 eodSwapQuery 一致,含当日已了结)
|
||||
var tradeStatuses = ConsTrade.TradeStatusAfterConfirmed;
|
||||
var trades = DbContext.trade.AsNoTracking()
|
||||
.Where(t => tradeStatuses.Contains(t.TradeStatus)
|
||||
&& t.ValidState != ConsGlobal.InValid
|
||||
&& t.TradeType == "收益互换"
|
||||
&& t.TradeDate <= settleDate
|
||||
&& watchClientIds.Contains(t.ClientId))
|
||||
.ToList();
|
||||
if (trades.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//规则15(区间追保结构)交易:R1 三层级解析(BUG-02 修正,与引擎/确认书同口径)——
|
||||
//交易绑定→客户默认→全局默认 找到即停,只配客户/全局默认模板的交易同样纳入追保结算
|
||||
var templatesByTrade = MarginTemplateV2RateHelper.ResolveTieredTemplates(trades, settleDate, DbContext);
|
||||
trades = trades.Where(t => templatesByTrade.TryGetValue(t.id, out var tpl)
|
||||
&& tpl.RuleType == (int)MarginRuleTypeEnum.区间追保结构).ToList();
|
||||
var tradeIds = trades.Select(t => t.id).ToList();
|
||||
if (tradeIds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//当日维持保证金(引擎产出,我方净收取为正),按交易合计(与 SwapSpanBalanceQueryService 缺口口径一致)
|
||||
var maintenanceByTrade = DbContext.trade_span.AsNoTracking()
|
||||
.Where(x => x.ValueDate == settleDate && tradeIds.Contains(x.TradeId) && x.Spv != null)
|
||||
.GroupBy(x => x.TradeId)
|
||||
.Select(g => new { TradeId = g.Key, Spv = g.Sum(x => x.Spv ?? 0d) })
|
||||
.ToDictionary(x => x.TradeId, x => x.Spv);
|
||||
|
||||
//应付预付金净收额(客户付钱记负 → 取反为正;平仓返还自动冲减;口径与 EOD canonical 一致:非作废+已确认/已结算)
|
||||
var payableNetByTrade = DbContext.ClientCashInCashOut.AsNoTracking()
|
||||
.Where(x => x.TradeId != null && tradeIds.Contains(x.TradeId ?? 0)
|
||||
&& x.Action == ClientCashInCashOut.系统操作_应付预付金
|
||||
&& x.HappenDate <= settleDate
|
||||
&& x.ValidState != ConsGlobal.InValid
|
||||
&& (x.State == ClientCashInCashOut.已确认 || x.State == ClientCashInCashOut.已结算)
|
||||
&& x.Money != null)
|
||||
.GroupBy(x => x.TradeId)
|
||||
.Select(g => new { TradeId = g.Key ?? 0, Sum = -g.Sum(x => x.Money ?? 0d) })
|
||||
.ToDictionary(x => x.TradeId, x => x.Sum);
|
||||
|
||||
//追加保证金资金记录累计值(逐日增量记录求和即累计,BUG-03;Deal=0 交易级,口径与 EOD canonical 一致:非作废+已确认/已结算)
|
||||
var addRecordByTrade = DbContext.ClientCashInCashOut.AsNoTracking()
|
||||
.Where(x => x.TradeId != null && tradeIds.Contains(x.TradeId ?? 0)
|
||||
&& x.Action == ClientCashInCashOut.系统操作_追加保证金
|
||||
&& x.ValidState != ConsGlobal.InValid
|
||||
&& (x.State == ClientCashInCashOut.已确认 || x.State == ClientCashInCashOut.已结算)
|
||||
&& x.Deal == 0
|
||||
&& x.Money != null)
|
||||
.GroupBy(x => x.TradeId)
|
||||
.Select(g => new { TradeId = g.Key ?? 0, Funded = -g.Sum(x => x.Money ?? 0d) })
|
||||
.ToDictionary(x => x.TradeId, x => x.Funded);
|
||||
|
||||
//追加保证金授信占用累计(amount 占用记正 → 直接求和(BUG-01 修正口径);remark 前缀标识,见 ClientCreditInoutService)
|
||||
var addCreditByTrade = DbContext.client_credit_inout.AsNoTracking()
|
||||
.Where(x => x.trade_id != null && tradeIds.Contains(x.trade_id ?? 0)
|
||||
&& x.remark.StartsWith(ClientCreditInoutService.AdditionalMarginRemark))
|
||||
.GroupBy(x => x.trade_id)
|
||||
.Select(g => new { TradeId = g.Key ?? 0, Funded = g.Sum(x => x.amount) })
|
||||
.ToDictionary(x => x.TradeId, x => x.Funded);
|
||||
|
||||
var fundTagService = new SwapFundTagService(this);
|
||||
var cashService = new ClientCashInCashOutService(this);
|
||||
var creditService = new ClientCreditInoutService(this);
|
||||
//客户剩余可用授信逐笔扣减缓存(同一次结算内多笔追加按顺序消耗额度,与阶段二逐腿分配同语义)
|
||||
var creditRemaining = new Dictionary<int, double>();
|
||||
|
||||
foreach (var clientGroup in trades.GroupBy(t => t.ClientId).OrderBy(g => g.Key))
|
||||
{
|
||||
foreach (var td in clientGroup.OrderBy(t => t.id))
|
||||
{
|
||||
if (!maintenanceByTrade.TryGetValue(td.id, out var maintenance) || maintenance <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var target = SwapAdditionalMarginCalc.CalcTarget(maintenance,
|
||||
payableNetByTrade.TryGetValue(td.id, out var payableNet) ? payableNet : 0);
|
||||
if (target <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var fundedCash = addRecordByTrade.TryGetValue(td.id, out var cash) ? cash : 0;
|
||||
var fundedCredit = addCreditByTrade.TryGetValue(td.id, out var credit) ? credit : 0;
|
||||
var increment = Math.Round(target - fundedCash - fundedCredit, 2, MidpointRounding.AwayFromZero);
|
||||
if (increment <= 0)
|
||||
{
|
||||
//已补足;追保回落(目标下降)不返还——负缺口在可用资金公式(Σ维持−累计)体现
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!creditRemaining.TryGetValue(td.ClientId, out var remain))
|
||||
{
|
||||
remain = fundTagService.GetAvailableCredit(td.ClientId, settleDate);
|
||||
creditRemaining[td.ClientId] = remain;
|
||||
}
|
||||
var (creditPart, cashPart) = SwapAdditionalMarginCalc.Allocate(increment, remain);
|
||||
if (creditPart > 0)
|
||||
{
|
||||
//授信部分不产生资金流水,只写授信出入表占用(占用记正数,BUG-01 修正口径)
|
||||
creditService.Occupy(td.ClientId, null, td.id, creditPart, settleDate,
|
||||
ClientCreditInoutService.AdditionalMarginRemark + "占用");
|
||||
creditRemaining[td.ClientId] = Math.Round(remain - creditPart, 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
if (cashPart > 0)
|
||||
{
|
||||
//现金部分按结算日逐笔增量记录(BUG-03 修正:每结算日一条、Money=−increment、键含日期幂等),
|
||||
//避免单条累计值覆盖 + HappenDate 前移使 EOD 差分窗口跨日全额重复计入;负数=客户应付追加
|
||||
cashService.SaveSwapTradeClientCash(td, -cashPart, settleDate, 0,
|
||||
ClientCashInCashOut.系统操作_追加保证金, matchDate: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
namespace YLErp.Modules.SwapModule.Margin
|
||||
{
|
||||
/// <summary>
|
||||
/// R2 阶段三 §3.2 可用资金公式(需求拆分 R2 口径,实时/EOD/报告三处共用保证一致)。
|
||||
/// 授信额度取 credit.Credit 合计(阶段一 §1.1 已在保存时折算 原始授信值×最大授信可用比例,消费方不再乘比例);
|
||||
/// 现金结存 = 期末结存 AmountFund(阶段二起授信不进资金,流水天然不含授信部分,无需排除);
|
||||
/// 已使用授信 = 授信出入表 Σ(amount);
|
||||
/// 初始保证金(净收取为正)= 应付预付金流水收付净额(平仓返还自动冲减);
|
||||
/// 维持保证金(净收取为正)= −MySideMargin(client_span 维持保证金写入 trade_span 后经 CalcClientMargin 反号聚合)。
|
||||
/// 公式整体待业务校验(EQD-6948),参数化集中在此便于校验后调整。
|
||||
/// </summary>
|
||||
public static class SwapSpanBalanceCalc
|
||||
{
|
||||
/// <summary>
|
||||
/// 可用资金(客户维度,MarginWatchRule=1):
|
||||
/// Max(现金结存 + 授信额度 − 已使用授信 + 初始保证金 − 维持保证金, 0)(现金结存已扣初始保证金,故加回)。
|
||||
/// maintenanceMargin/initialMargin 均为净收取为正;未配置维度的存量客户不走本公式(调用方保证)。
|
||||
/// </summary>
|
||||
public static double CalcClientDimensionAvailable(double cashBalance, double totalCredit, double usedCredit,
|
||||
double initialMargin, double maintenanceMargin)
|
||||
{
|
||||
return Math.Max(cashBalance + totalCredit - usedCredit + initialMargin - maintenanceMargin, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可用资金(合约维度,MarginWatchRule=0):
|
||||
/// Max(现金结存 + 授信额度 − 已使用授信 − 交易维度追加保证金合计, 0);
|
||||
/// 交易维度追加保证金合计 = Σ(维持保证金 − 累计保证金)(阶段三:累计=应付预付金净额;阶段四含追加保证金流水)。
|
||||
/// </summary>
|
||||
public static double CalcContractDimensionAvailable(double cashBalance, double totalCredit, double usedCredit,
|
||||
double tradeAdditionalMarginSum)
|
||||
{
|
||||
return Math.Max(cashBalance + totalCredit - usedCredit - tradeAdditionalMarginSum, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 追保金额(客户维度,MarginWatchRule=1,阶段四 §4.2 双向——不以 0 截断):
|
||||
/// (维持保证金 − 初始保证金) − (现金结存 + 授信额度 − 已使用授信),正=需追保、负=可返还。
|
||||
/// 即客户维度可用资金公式的反向值(去 Max 截断)——与需求原文"现金结存+授信额度−(维持−初始)"数值互为相反数,
|
||||
/// 此处按 MarginByPayableMarginTotal 字段既有口径(正数=应追加,估值报告"应追加预付金X元")定向。
|
||||
/// </summary>
|
||||
public static double CalcClientDimensionCallMargin(double cashBalance, double totalCredit, double usedCredit,
|
||||
double initialMargin, double maintenanceMargin)
|
||||
{
|
||||
return Math.Round(maintenanceMargin - initialMargin - (cashBalance + totalCredit - usedCredit), 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 追保金额(合约维度,MarginWatchRule=0,阶段四 §4.2):−(现金结存 + 授信额度 − 已使用授信)。
|
||||
/// 需求原文公式为 现金结存+授信额度−已使用授信(与"交易维度追加保证金"的关系为开放问题2,两值均有产出),
|
||||
/// 此处取负对齐字段"正数=应追加"口径:账户透支(现金+授信不足)为正=应补足,盈余为负。
|
||||
/// </summary>
|
||||
public static double CalcContractDimensionCallMargin(double cashBalance, double totalCredit, double usedCredit)
|
||||
{
|
||||
return Math.Round(-(cashBalance + totalCredit - usedCredit), 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using YLErp.BLL;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Enums;
|
||||
using YLErp.Modules.MarginModule;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Margin
|
||||
{
|
||||
/// <summary>
|
||||
/// R2 阶段三 §3.2 估值报告/可用资金查询输入(静态查询,供 ClientBalanceUtility 与 RealTimeClientBanlanceService 共用,保证三处口径一致)。
|
||||
/// 口径:
|
||||
/// - 互换初始保证金(净收取为正)= 客户 应付预付金 流水收付净额取反(客户应付入金记负、平仓返还为正,取负号后净收取为正);
|
||||
/// - 交易维度追加保证金(合约维度)= Σ(维持保证金 − 累计保证金):
|
||||
/// 维持保证金取当日 trade_span.Spv(区间追保结构引擎产出,我方净收取为正);
|
||||
/// 累计保证金 = 该交易 应付预付金+追加保证金 流水收付净额 + 追加保证金授信占用净额(§0 口径,阶段四 §4.1 起);
|
||||
/// 仅统计规则15(区间追保结构,R1 三层级解析,与引擎同口径)且有当日 trade_span 的交易。
|
||||
/// </summary>
|
||||
public static class SwapSpanBalanceQueryService
|
||||
{
|
||||
/// <summary>
|
||||
/// 客户维度输入:互换初始保证金(净收取为正,按客户汇总)。
|
||||
/// </summary>
|
||||
public static Dictionary<int, double> GetSwapInitMarginByClients(List<int> clientIds, DateTime valueDate, YLContext db)
|
||||
{
|
||||
if (clientIds == null || clientIds.Count == 0)
|
||||
{
|
||||
return new Dictionary<int, double>();
|
||||
}
|
||||
|
||||
var flows = db.ClientCashInCashOut.AsNoTracking()
|
||||
.Where(x => clientIds.Contains(x.ClientId ?? 0)
|
||||
&& x.Action == ClientCashInCashOut.系统操作_应付预付金
|
||||
&& x.HappenDate <= valueDate
|
||||
&& x.ValidState != ConsGlobal.InValid
|
||||
&& (x.State == ClientCashInCashOut.已确认 || x.State == ClientCashInCashOut.已结算)
|
||||
&& x.Money != null)
|
||||
.Select(x => new { ClientId = x.ClientId ?? 0, Money = x.Money ?? 0d })
|
||||
.ToList();
|
||||
|
||||
return flows.GroupBy(x => x.ClientId)
|
||||
.ToDictionary(g => g.Key, g => -g.Sum(x => x.Money));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 合约维度输入:交易维度追加保证金合计 = Σ(维持保证金 − 累计保证金),按客户汇总。
|
||||
/// </summary>
|
||||
public static Dictionary<int, double> GetTradeAdditionalMarginByClients(List<int> clientIds, DateTime valueDate, YLContext db)
|
||||
{
|
||||
var result = new Dictionary<int, double>();
|
||||
if (clientIds == null || clientIds.Count == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
//当日维持保证金(引擎产出:我方净收取为正)——先取当日有 span 的客户交易,再按规则15过滤
|
||||
var maintenance = db.trade_span.AsNoTracking()
|
||||
.Where(x => x.ValueDate == valueDate && x.Spv != null
|
||||
&& x.ClientId != null && clientIds.Contains(x.ClientId.Value))
|
||||
.Select(x => new { x.TradeId, x.ClientId, Spv = x.Spv ?? 0d })
|
||||
.ToList();
|
||||
if (maintenance.Count == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
var spanTradeIds = maintenance.Select(x => x.TradeId).Distinct().ToList();
|
||||
|
||||
//规则15(区间追保结构)交易:R1 三层级解析(BUG-02 修正,与引擎/结算判定同口径)——
|
||||
//交易绑定→客户默认→全局默认 找到即停;无预付金等其他规则产出/留存的 span 行不计入追保缺口
|
||||
var spanTrades = db.trade.AsNoTracking().Where(t => spanTradeIds.Contains(t.id)).ToList();
|
||||
var templatesByTrade = MarginTemplateV2RateHelper.ResolveTieredTemplates(spanTrades, valueDate, db);
|
||||
var rule15TradeIds = spanTrades
|
||||
.Where(t => templatesByTrade.TryGetValue(t.id, out var tpl)
|
||||
&& tpl.RuleType == (int)MarginRuleTypeEnum.区间追保结构)
|
||||
.Select(t => t.id)
|
||||
.ToHashSet();
|
||||
maintenance = maintenance.Where(x => rule15TradeIds.Contains(x.TradeId)).ToList();
|
||||
if (maintenance.Count == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
//累计保证金:该交易 应付预付金+追加保证金 流水收付净额取反(收取为正);
|
||||
//追加保证金的授信占用部分不产生资金流水(阶段二口径),阶段四 §4.1 起一并计入——
|
||||
//否则结算后缺口残留(=授信部分),与可用资金公式里的 −已使用授信 形成双扣
|
||||
var marginActions = new List<string> { ClientCashInCashOut.系统操作_应付预付金, ClientCashInCashOut.系统操作_追加保证金 };
|
||||
var accumulated = db.ClientCashInCashOut.AsNoTracking()
|
||||
.Where(x => x.TradeId != null && spanTradeIds.Contains(x.TradeId ?? 0)
|
||||
&& marginActions.Contains(x.Action)
|
||||
&& x.HappenDate <= valueDate
|
||||
&& x.ValidState != ConsGlobal.InValid
|
||||
&& (x.State == ClientCashInCashOut.已确认 || x.State == ClientCashInCashOut.已结算)
|
||||
&& x.Money != null)
|
||||
.GroupBy(x => x.TradeId)
|
||||
.Select(g => new { TradeId = g.Key ?? 0, Sum = -g.Sum(x => x.Money ?? 0d) })
|
||||
.ToDictionary(x => x.TradeId, x => x.Sum);
|
||||
|
||||
//追加保证金的授信占用净额(amount 占用记正 → 直接求和(BUG-01 修正口径);remark 前缀标识)
|
||||
var addCreditOccupied = db.client_credit_inout.AsNoTracking()
|
||||
.Where(x => x.trade_id != null && spanTradeIds.Contains(x.trade_id ?? 0)
|
||||
&& x.remark.StartsWith(ClientCreditInoutService.AdditionalMarginRemark))
|
||||
.GroupBy(x => x.trade_id)
|
||||
.Select(g => new { TradeId = g.Key ?? 0, Sum = g.Sum(x => x.amount) })
|
||||
.ToDictionary(x => x.TradeId, x => x.Sum);
|
||||
|
||||
foreach (var group in maintenance.GroupBy(x => x.ClientId ?? 0))
|
||||
{
|
||||
var total = group.Sum(x => x.Spv
|
||||
- (accumulated.TryGetValue(x.TradeId, out var acc) ? acc : 0d)
|
||||
- (addCreditOccupied.TryGetValue(x.TradeId, out var occupied) ? occupied : 0d));
|
||||
result[group.Key] = total;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Margin
|
||||
{
|
||||
/// <summary>
|
||||
/// R2 区间追保结构(规则15)维持保证金纯函数计算(实现方案阶段三 §3.1)。
|
||||
/// 数据来源约定(与阶段一 SpanConfig 结构、docx 确认书模板一致):
|
||||
/// - 多头(客户看多):第1层 [Lower, +∞)(上不封顶),第n层 [Lower, Upper);各层 Lower 逐层严格递减;
|
||||
/// - 空头(客户看空):第1层 (−∞, Upper](下不设限),第n层 (Lower, Upper];各层 Upper 逐层严格递增;
|
||||
/// - 区间边界为"×期初净价的百分比"小数(0.95=95%),价格口径:债券用净价,指数/ETF 用收盘价;
|
||||
/// - AmountRate 为累计到该层的追保金额比例(与确认书追保表每行"合计追保金额"同口径,直取不求和),
|
||||
/// 追保金额 = AmountRate × 期初全价 × 券面总额(债券)/ × 期初价格 × 名义份额(指数/ETF);
|
||||
/// - 维持保证金 = (初始保证金 + 总追加保证金) × 方向(我方净收取 +1 / 净支付 −1);
|
||||
/// - 价格跌破最深一层(平仓线外)按最深层计(追保金额不再上升)。
|
||||
/// 公式参数化(EQD-6948 待业务校验),所有口径集中在本类便于校验后调整。
|
||||
/// </summary>
|
||||
public static class SwapSpanMarginCalc
|
||||
{
|
||||
/// <summary>
|
||||
/// 判断 SpanConfig 是否已按方案B录入新区间结构(任一方向有边界/比例或预警/平仓线)。
|
||||
/// 全空视为存量 x/y 配置,引擎回落旧 名义×y 公式。
|
||||
/// </summary>
|
||||
public static bool HasSpanConfig(SpanConfig cfg)
|
||||
{
|
||||
if (cfg == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return cfg.WarnLine.HasValue || cfg.CloseLine.HasValue
|
||||
|| HasTierValue(cfg.LongSpans) || HasTierValue(cfg.ShortSpans);
|
||||
}
|
||||
|
||||
private static bool HasTierValue(List<SpanTierConfig> tiers)
|
||||
{
|
||||
return tiers != null && tiers.Any(t => t != null && (t.Lower.HasValue || t.Upper.HasValue || t.AmountRate.HasValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 客户方向判定(与确认书 IsCustomerLong 同规则):
|
||||
/// 我方方向 = 收取端(PosiDirection=收取)与 PositionType 同向、支付端反向;客户方向取反。
|
||||
/// </summary>
|
||||
public static bool IsCustomerLong(int posiDirection, int positionType)
|
||||
{
|
||||
var isOurLong = posiDirection == (int)SwapDirectionEnum.支付
|
||||
? positionType == (int)PositionTypeFlag.Short
|
||||
: positionType == (int)PositionTypeFlag.Long;
|
||||
return !isOurLong;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按当前价格相对期初价的比例落档。返回命中的层;价格在所有层区间之外(平仓线外)返回最深一层,无可用层返回 null。
|
||||
/// 多头:priceRatio ∈ [Lower, Upper)(第1层无上界);空头:priceRatio ∈ (Lower, Upper](第1层无下界)。
|
||||
/// </summary>
|
||||
public static SpanTierConfig MatchTier(List<SpanTierConfig> tiers, bool isCustomerLong, double priceRatio)
|
||||
{
|
||||
if (tiers == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var valid = tiers.Where(t => t != null && (t.Lower.HasValue || t.Upper.HasValue)).ToList();
|
||||
if (valid.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
foreach (var tier in valid)
|
||||
{
|
||||
var inLower = !tier.Lower.HasValue || (isCustomerLong ? priceRatio >= tier.Lower.Value : priceRatio > tier.Lower.Value);
|
||||
var inUpper = !tier.Upper.HasValue || (isCustomerLong ? priceRatio < tier.Upper.Value : priceRatio <= tier.Upper.Value);
|
||||
if (inLower && inUpper)
|
||||
{
|
||||
return tier;
|
||||
}
|
||||
}
|
||||
//未落任何层:价格已穿出最深一层边界(低于多头最深层下界/高于空头最深层上界),按最深层计;
|
||||
//最深层按边界值取(多头=最小下界、空头=最大上界),不依赖配置数组顺序(BUG-25 引擎侧防御)
|
||||
return isCustomerLong
|
||||
? valid.OrderBy(t => t.Lower ?? double.MinValue).First()
|
||||
: valid.OrderByDescending(t => t.Upper ?? double.MaxValue).First();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 总追加保证金 = 命中层 AmountRate × 期初全价 × 券面总额(债券)/ × 期初价格 × 名义份额(指数/ETF)。
|
||||
/// 数据上两者同为 期初价(PosiGrossPrice) × 数量(PosiQuantity),口径差异由调用方注释说明。
|
||||
/// </summary>
|
||||
public static double CalcAdditionalMargin(SpanTierConfig tier, double initPrice, double quantity)
|
||||
{
|
||||
if (tier == null || !tier.AmountRate.HasValue)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return tier.AmountRate.Value * initPrice * quantity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 维持保证金 = (初始保证金 + 总追加保证金) × 方向(我方净收取 +1 / 净支付 −1)。
|
||||
/// </summary>
|
||||
public static double CalcMaintenanceMargin(double initialMargin, double additionalMargin, double direction)
|
||||
{
|
||||
return (initialMargin + additionalMargin) * direction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 阶段三 §3.1 单笔交易引擎计算(纯函数,收盘价由调用方解析后传入——债券取中债估值净价、指数/ETF取收盘价)。
|
||||
/// closePrice<=0 视为未取到收盘价:追加保证金按 0、维持保证金=初始保证金(不抛错,由调用方记日志)。
|
||||
/// isInitialCalc=true(试算初始):只出初始项,追加保证金为收盘后口径不参与。
|
||||
/// 返回 null 表示缺有效标的腿(期初价),调用方跳过该交易不产出 trade_span。
|
||||
/// </summary>
|
||||
public static double? CalcTradeMaintenanceMargin(double? tradeInitialMargin, SpanConfig spanCfg,
|
||||
List<swap_position> legs, bool isInitialCalc, double closePrice)
|
||||
{
|
||||
//标的腿(多空):期初价格、数量、客户方向
|
||||
var underlyingLeg = legs?.FirstOrDefault(x => x.PositionType == (int)PositionTypeFlag.Long || x.PositionType == (int)PositionTypeFlag.Short);
|
||||
if (underlyingLeg == null || underlyingLeg.PosiGrossPrice <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var isCustomerLong = IsCustomerLong(underlyingLeg.PosiDirection, underlyingLeg.PositionType);
|
||||
|
||||
//初始保证金与方向:初始预付金腿(InterestMode=5)收付净额(多腿按净收取定方向);无腿时回落交易录入值(客户应付常态)
|
||||
var initMarginLegs = legs.Where(x => x.InterestMode == (int)InterestModeEnum.初始预付金).ToList();
|
||||
double initialMargin;
|
||||
double direction;
|
||||
if (initMarginLegs.Any())
|
||||
{
|
||||
var netReceive = initMarginLegs.Sum(x => x.InterestDirection == (int)SwapDirectionEnum.收取 ? x.InterestPrincipalFix : -x.InterestPrincipalFix);
|
||||
initialMargin = Math.Abs((double)netReceive);
|
||||
direction = netReceive >= 0 ? 1 : -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
initialMargin = tradeInitialMargin ?? 0;
|
||||
direction = 1;
|
||||
}
|
||||
|
||||
if (isInitialCalc)
|
||||
{
|
||||
return CalcMaintenanceMargin(initialMargin, 0, direction);
|
||||
}
|
||||
|
||||
var additional = 0.0;
|
||||
//期初价比基:债券/指数/ETF统一为期初净价(PosiNetNoFeePrice,确认书"参考标的期初净价"同源),缺省回落期初全价
|
||||
var initNetPrice = (double)(underlyingLeg.PosiNetNoFeePrice ?? 0m);
|
||||
if (initNetPrice <= 0)
|
||||
{
|
||||
initNetPrice = (double)underlyingLeg.PosiGrossPrice;
|
||||
}
|
||||
if (closePrice > 0 && initNetPrice > 0)
|
||||
{
|
||||
var tiers = isCustomerLong ? spanCfg.LongSpans : spanCfg.ShortSpans;
|
||||
var tier = MatchTier(tiers, isCustomerLong, closePrice / initNetPrice);
|
||||
//追保金额基数:期初全价×券面总额(债券)/ 期初价格×名义份额(指数/ETF),同为 期初价×数量
|
||||
additional = CalcAdditionalMargin(tier, (double)underlyingLeg.PosiGrossPrice, (double)underlyingLeg.PosiQuantity);
|
||||
}
|
||||
|
||||
return CalcMaintenanceMargin(initialMargin, additional, direction);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
/// <summary>
|
||||
/// 剩余可用授信 = 有效授信 − 已使用授信。
|
||||
/// 已使用授信 = 授信出入表 Σ(amount)(入金负/出金正)——入金使可用授信上升、出金使其收缩(业务口径)。
|
||||
/// 已使用授信 = 授信出入表 Σ(amount)(占用正/释放负,BUG-01 修正口径)——占用使可用授信收缩、释放使其回升。
|
||||
/// </summary>
|
||||
public double GetAvailableCredit(int clientId, DateTime valueDate)
|
||||
{
|
||||
@@ -54,16 +54,16 @@ namespace YLErp.Modules.SwapModule
|
||||
/// 簿记确认时对预付金腿定稿资金标签并产生资金记录(§2.3 四种情形,逐腿)。
|
||||
/// fund_tag 单列:录入时存用户选择(Credit/Cash/NULL),本方法读取选择后在同列定稿——
|
||||
/// 特批全现金;选授信按剩余额度分配(跨界腿拆单为 授信+现金 两条),未选/现金直接现金。
|
||||
/// 授信腿只写授信出入表占用(入金方向记负数,绑定腿 position_id,冗余 trade_id),不产生资金流水;
|
||||
/// 授信腿只写授信出入表占用(占用记正数,绑定腿 position_id,冗余 trade_id),不产生资金流水;
|
||||
/// 现金腿走 SaveSwapTradeClientCash 幂等 upsert 产生 应付预付金 记录。
|
||||
/// marginLegs 需为已过滤(IsDeductPrincipal 等)的预付金腿(InterestMode=5/6)。
|
||||
/// </summary>
|
||||
public void ApplyMarginFundTags(trade td, List<swap_position> marginLegs, ClientCashInCashOutService cashService, bool ignoreMoneyCheck)
|
||||
{
|
||||
var creditService = new ClientCreditInoutService(this);
|
||||
//重确认/重补场景自愈:清掉本交易旧占用记录后按腿上当前选择与最新额度重写
|
||||
//(阶段四追加保证金占用落地后需区分保留追加部分)
|
||||
creditService.RemoveByTrade(td.id);
|
||||
//重确认/重补场景自愈:清掉本交易旧占用记录后按腿上当前选择与最新额度重写;
|
||||
//追加保证金占用(阶段四 EOD 写入)不随重写清除——其资金记录不在本方法删除范围,由 EOD 幂等维护
|
||||
creditService.RemoveByTrade(td.id, keepAdditionalMargin: true);
|
||||
|
||||
var valueDate = td.TradeDate ?? DateTime.Now;
|
||||
var creditAvailable = GetAvailableCredit(td.ClientId, valueDate);
|
||||
@@ -111,8 +111,8 @@ namespace YLErp.Modules.SwapModule
|
||||
if (plan != null && plan.CreditAmount > 0)
|
||||
{
|
||||
//整腿授信 或 拆单后的授信部分:不产生资金流水,只写占用(拆单绑新拆出的授信腿)。
|
||||
//入金方向记负数(业务口径:出入表金额与资金流水同号,入金负/出金正;入金使可用授信上升)
|
||||
creditService.Occupy(td.ClientId, plan.CreditLeg?.id ?? leg.id, td.id, -plan.CreditAmount, happenDate,
|
||||
//占用记正数(BUG-01 修正:已使用授信=Σ(amount) 占用上升;2026-08-20"与资金流水同号入金负"口径已废弃)
|
||||
creditService.Occupy(td.ClientId, plan.CreditLeg?.id ?? leg.id, td.id, plan.CreditAmount, happenDate,
|
||||
plan.NeedSplit ? "簿记拆单授信部分" : "簿记授信占用");
|
||||
}
|
||||
//资金记录沿用既有符号口径(客户付钱为负 = -应付额):授信部分不产生流水,现金部分按差额产生
|
||||
@@ -136,13 +136,15 @@ namespace YLErp.Modules.SwapModule
|
||||
var leg = plan.Leg;
|
||||
//应付额 = fix × (dir==1 ? 1 : -1),反推 fix 用同一比例(±1 自反)
|
||||
var payableRatio = leg.InterestDirection == 1 ? 1 : -1;
|
||||
var originalFix = leg.InterestPrincipalFix;
|
||||
leg.InterestPrincipalFix = Math.Round(Convert.ToDecimal(plan.CashAmount) * payableRatio, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
leg.FundTag = ConsFundTag.Cash;
|
||||
|
||||
var creditLeg = leg.Clone();
|
||||
creditLeg.id = 0;
|
||||
creditLeg.PositionId = 0;
|
||||
creditLeg.InterestPrincipalFix = Math.Round(Convert.ToDecimal(plan.CreditAmount) * payableRatio, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
//授信腿倒挤 = 原 fix − 现金 fix(BUG-20:两腿分别独立舍入会有分位尾差,倒挤保证两腿合计与原 fix 守恒)
|
||||
creditLeg.InterestPrincipalFix = originalFix - leg.InterestPrincipalFix;
|
||||
creditLeg.FundTag = ConsFundTag.Credit;
|
||||
creditLeg.OptId = UserId;
|
||||
creditLeg.OptName = UserName;
|
||||
|
||||
@@ -1514,8 +1514,9 @@ namespace YLErp.Modules.SwapModule
|
||||
var eodSwapPositions = DbContext.eod_swap_position.Where(x => x.SwapTradeId == tradeId);
|
||||
var eodSwaps = DbContext.eod_swap.Where(x => x.SwapTradeId == tradeId);
|
||||
var clientcashinouts = DbContext.ClientCashInCashOut.Where(x => x.TradeId == tradeId && x.Action == ClientCashInCashOut.系统操作_应付预付金);
|
||||
// R4:修改清除资金记录时同步清理授信出入记录,重确认时按最新标签重写
|
||||
new ClientCreditInoutService(this).RemoveByTrade(tradeId);
|
||||
// R4:修改清除 应付预付金 资金记录时同步清理对应授信出入记录,重确认时按最新标签重写;
|
||||
// 追加保证金资金记录(阶段四 EOD 产生)不在本方法删除范围,其授信占用一并保留
|
||||
new ClientCreditInoutService(this).RemoveByTrade(tradeId, keepAdditionalMargin: true);
|
||||
swapFlowEvents.ForEach(x =>
|
||||
{
|
||||
x.DataState = (int)SwapFlowDateStateEnum.废弃;
|
||||
|
||||
@@ -227,10 +227,12 @@ namespace YLErp.Modules.TradeModule
|
||||
|
||||
/// <summary>
|
||||
/// 新版互换 期权费
|
||||
/// matchDate=true 时 upsert 键追加 HappenDate(按结算日幂等):用于追加保证金等逐日增量记录,
|
||||
/// 每个结算日一条独立记录,避免单条累计值覆盖 + HappenDate 前移导致跨日差分窗口全额重复计入(BUG-03)。
|
||||
/// </summary>
|
||||
/// <param name="swapTrade"></param>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public void SaveSwapTradeClientCash(trade swapTrade,double money, DateTime HappenDate,long positionId,string action= ClientCashInCashOut.系统操作_期权费)
|
||||
public void SaveSwapTradeClientCash(trade swapTrade,double money, DateTime HappenDate,long positionId,string action= ClientCashInCashOut.系统操作_期权费, bool matchDate = false)
|
||||
{
|
||||
if (swapTrade is null)
|
||||
{
|
||||
@@ -242,7 +244,8 @@ namespace YLErp.Modules.TradeModule
|
||||
}
|
||||
var valueDate = valuedateBLL.ValueDate;
|
||||
var client = DataCacheProvider.GetClientDataSource().GetData(swapTrade.ClientId);
|
||||
var cic = DbContext.ClientCashInCashOut.FirstOrDefault(t => t.TradeId == swapTrade.id && t.ValidState != ConsGlobal.InValid && t.Action == action &&t.Deal==positionId);
|
||||
var cic = DbContext.ClientCashInCashOut.FirstOrDefault(t => t.TradeId == swapTrade.id && t.ValidState != ConsGlobal.InValid && t.Action == action &&t.Deal==positionId
|
||||
&& (!matchDate || t.HappenDate == HappenDate));
|
||||
bool addClientCash = false;
|
||||
if (cic == null)
|
||||
{
|
||||
|
||||
@@ -15,8 +15,10 @@
|
||||
{Name:"互换开平仓事件流水",Rights:["交易管理-互换开平仓事件流水"],Url:"swapTrade2/EventList"},
|
||||
{Name:"交易审批",Rights:["交易管理-交易审批"],Url:"trade/TradeApproval"},
|
||||
{Name:"凭证审批",Rights:["交易管理-凭证审批"],Url:"v3/product/approve"},
|
||||
{Name:"标的交易",Rights:["交易管理-标的交易"],Url:"exchangetrade/tradelist"},
|
||||
{Name:"变更记录",Rights:["交易管理-变更记录"],Url:"trade/TradeChange"},
|
||||
{Name:"标的交易",Rights:["交易管理-标的交易"],Url:"exchangetrade/tradelist"},
|
||||
{Name:"变更记录",Rights:["交易管理-变更记录"],Url:"trade/TradeChange"},
|
||||
//R1 三层级(交易绑定→客户默认→全局默认,2026-08-24):交易维度逐笔查看/调整模板绑定的入口
|
||||
{Name:"交易预付金配置",Rights:["交易管理-交易预付金配置"],Url:"trade/tradeMarginTemplateList"},
|
||||
]
|
||||
},
|
||||
{Name:"风险管理",Rights:["风险控制"],Icon:"menu-icon iconseven"
|
||||
@@ -66,9 +68,10 @@
|
||||
{Name:"黑名单审批",Rights:["客户管理-黑名单审批"],Url:"clientblack/clientblackApproval"},
|
||||
{Name:"授信管理",Rights:["客户管理-授信管理"],Url:"credit/creditList"},
|
||||
{Name:"资信评级",Rights:["客户管理-资信评级"],Url:"client_rating/List"},
|
||||
{Name:"机构账号设置",Rights:["客户管理-机构账号设置"],Url:"v3/client/account"}
|
||||
//保证金模板在交易页直接选择,客户预付金配置入口不展示(页面代码保留)
|
||||
//{Name:"客户预付金配置",Rights:["客户管理-客户预付金配置"],Url:"client_margin_template/client_margin_templateList"},
|
||||
{Name:"机构账号设置",Rights:["客户管理-机构账号设置"],Url:"v3/client/account"},
|
||||
//R1 三层级(2026-08-24):客户默认模板绑定入口恢复——二级(client_margin_template)生效的必需配置页;
|
||||
//原下线理由"保证金模板在交易页直接选择"只覆盖交易级,客户/全局兜底落地后不再成立
|
||||
{Name:"客户预付金配置",Rights:["客户管理-客户预付金配置"],Url:"client_margin_template/client_margin_templateList"},
|
||||
]
|
||||
},
|
||||
{Name:"互换簿记预设",Rights:["互换簿记预设"],Icon:"menu-icon iconfour"
|
||||
|
||||
@@ -80,7 +80,7 @@ namespace YLErp.Web.Controllers
|
||||
{
|
||||
c.VarietyName = GetWhiteListVarietyNames(c);
|
||||
}
|
||||
// R4:已使用授信(授信出入表 SUM(占用)−SUM(释放)+SUM(调整),按客户汇总)
|
||||
// R4:已使用授信(授信出入表 Σ(amount),占用记正/释放记负,按客户汇总)
|
||||
var usedCredits = Modules.SwapModule.ClientCreditInoutService.GetUsedCreditByClients(
|
||||
sList.rows.Where(x => x.ClientId > 0).Select(x => x.ClientId ?? 0).Distinct().ToList(), yldb);
|
||||
foreach (var c in sList.rows)
|
||||
|
||||
@@ -317,6 +317,19 @@ namespace YLErp.Web.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
//BUG-13:期限档值须为合法四档(页面为固定区块正常到不了这里,接口直传非法档值会成取数侧永远匹配不到的死配置)
|
||||
if (marginTemplate.RuleType == (int)MarginRuleTypeEnum.区间追保结构)
|
||||
{
|
||||
foreach (var detail in marginTemplate.Details)
|
||||
{
|
||||
var bondTerm = detail.SpanConfig?.BondTerm;
|
||||
if (!string.IsNullOrEmpty(bondTerm) && !ConsMarginTerm.IsValidTerm(bondTerm))
|
||||
{
|
||||
throw new Exception("非法的利率债期限档:" + bondTerm + "(允许值:" + string.Join(" / ", ConsMarginTerm.AllowedTerms) + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//区间追保结构:客户原型全量手工录入的预警线/平仓线与多空各4层追保区间校验。
|
||||
//预警线/平仓线/两层区间全部为空视为未配置(兼容存量 x/y 过渡),任一值存在即按规则校验:
|
||||
//数值非负;同层上下边界都有时上边界>下边界;单层部分填写报错提示完整填写
|
||||
@@ -359,13 +372,29 @@ namespace YLErp.Web.Controllers
|
||||
{
|
||||
if (marginTemplate.RuleType == (int)MarginRuleTypeEnum.区间追保结构)
|
||||
{
|
||||
//区间追保结构:按 (利率债期限档, ETF细分) 元组分组校验(EtfKind 为预留扩展键,本期无 UI 写入,等价于仅按期限档分组),
|
||||
//同一分组内标的类型不允许重复,不同分组允许相同标的类型
|
||||
//区间追保结构:按 (利率债期限档, ETF细分) 元组分组校验(EtfKind 为预留扩展键,本期无 UI 写入,等价于仅期限档分组),
|
||||
//同一分组内标的类型不允许重复,不同分组允许相同标的类型;
|
||||
//通配行(空/全部,UnderlyingType=None/All)两两之间位与恒为 0 检不出,须单独拦截(BUG-09)——
|
||||
//否则取数侧通配兜底 matched.First() 命中不确定
|
||||
foreach (var termGroup in marginTemplate.Details.GroupBy(x => (x.SpanConfig?.BondTerm ?? "", x.SpanConfig?.EtfKind ?? "")))
|
||||
{
|
||||
var termUnderlyingType = UnderlyingTypeEnum.None;
|
||||
var wildcardSeen = false;
|
||||
foreach (var detail in termGroup)
|
||||
{
|
||||
var isWildcard = detail.UnderlyingType == UnderlyingTypeEnum.None || detail.UnderlyingType == UnderlyingTypeEnum.All;
|
||||
if (isWildcard && wildcardSeen)
|
||||
{
|
||||
if (marginTemplate.UnderlyingSeperateType == (int)UnderlyingSeperateTypeEnum.None)
|
||||
{
|
||||
throw new Exception("同一利率债期限档下存在重复的参数组");
|
||||
}
|
||||
throw new Exception("同一分组下存在重复的兜底参数组(标的类型为空或全部)");
|
||||
}
|
||||
if (isWildcard)
|
||||
{
|
||||
wildcardSeen = true;
|
||||
}
|
||||
if ((detail.UnderlyingType & termUnderlyingType) > 0)
|
||||
{
|
||||
if (marginTemplate.UnderlyingSeperateType == (int)UnderlyingSeperateTypeEnum.None)
|
||||
@@ -444,6 +473,15 @@ namespace YLErp.Web.Controllers
|
||||
|
||||
yldb.SaveChanges();
|
||||
|
||||
//BUG-14:明细生效日期回写——前端新增行只带占位日期(2020-01-01 前后),此前不回写使新行(含模板拷贝)
|
||||
//落库 0001-01-01,被取数侧 Max(ValueDate) 分组静默丢弃;占位日期对齐本次提交行的最新生效日期,无有效行时用模板生效日期
|
||||
var placeholderDate = new DateTime(2020, 1, 1);
|
||||
var effectiveValueDate = marginTemplate.Details
|
||||
.Where(d => d.ValueDate > placeholderDate)
|
||||
.Select(d => d.ValueDate)
|
||||
.DefaultIfEmpty(marginTemplate.ValueDate > placeholderDate ? marginTemplate.ValueDate : placeholderDate)
|
||||
.Max();
|
||||
|
||||
marginTemplate.Details.ForEach(x =>
|
||||
{
|
||||
var detail = yldb.margin_template_detail.Find(x.id);
|
||||
@@ -453,6 +491,7 @@ namespace YLErp.Web.Controllers
|
||||
detail = new margin_template_detail();
|
||||
yldb.margin_template_detail.Add(detail);
|
||||
}
|
||||
detail.ValueDate = x.ValueDate > placeholderDate ? x.ValueDate : effectiveValueDate;
|
||||
detail.MarginTemplateId = marginTemplate.id;
|
||||
detail.GroupRatio = x.GroupRatio;
|
||||
detail.MarginRatio1 = x.MarginRatio1;
|
||||
|
||||
@@ -7972,7 +7972,9 @@ namespace YLErp.Web.Controllers
|
||||
public ActionResult tradeMarginTemplateEdit(int tradeId)
|
||||
{
|
||||
var trade = yldb.trade.Find(tradeId);
|
||||
var marginTemplates = yldb.margin_template_v2.Where(x => x.TradeTypes.Contains(trade.TradeType) && !x.IsDefault).ToList();
|
||||
//R1 三层级(2026-08-24 配合修改):候选只列有效模板——已作废模板一旦绑定,
|
||||
//解析一级"找到即停"会让该交易直接视为无预付金(仅留日志),必须从源头挡住
|
||||
var marginTemplates = yldb.margin_template_v2.Where(x => x.IsValid && x.TradeTypes.Contains(trade.TradeType) && !x.IsDefault).ToList();
|
||||
marginTemplates.ForEach(x => x.Details = yldb.margin_template_detail.Where(y => y.MarginTemplateId == x.id).ToList());
|
||||
|
||||
var latestTradeMarginTemplate = yldb.trade_margin_template.Where(x => x.TradeId == tradeId).OrderByDescending(x => x.ValueDate).FirstOrDefault();
|
||||
@@ -8005,6 +8007,9 @@ namespace YLErp.Web.Controllers
|
||||
}
|
||||
|
||||
|
||||
//BUG-05 修正:接口补权限注解(与同组 tradeMarginTemplateList/Edit 一致),任何登录用户不可直改交易模板绑定
|
||||
[MyAuthorize("交易管理-交易预付金配置")]
|
||||
[HttpPost]
|
||||
public JsonResult saveTradeMarginTemplate(trade_margin_template tradeMarginTemplate)
|
||||
{
|
||||
try
|
||||
@@ -8030,14 +8035,20 @@ namespace YLErp.Web.Controllers
|
||||
#region 重置初始预付金
|
||||
|
||||
var trade = yldb.trade.Find(tradeMarginTemplate.TradeId);
|
||||
trade.InitialMargin = MarginDefault.GetInitialMarginWithPreHandle(trade);
|
||||
yldb.SaveChanges();
|
||||
|
||||
if (trade.IsGroup == 2)
|
||||
//R1 三层级(2026-08-24 配合修改):收益互换跳过系统重算——互换初始预付金为手工录入/预付金腿承载
|
||||
//(方案B 引擎回落链消费 trade.InitialMargin),且试算链路(GetInitialMarginWithPreHandle 克隆 id=0)
|
||||
//对互换取不到模板绑定恒得 0,无条件重置会把手工录入值清零;与 TradeSaveService 保存守卫语义对齐
|
||||
if (trade.TradeType != "收益互换")
|
||||
{
|
||||
var parentTrade = yldb.trade.Find(trade.ParentTradeId);
|
||||
parentTrade.InitialMargin = yldb.trade.Where(x => x.ValidState != "InValid" && x.ParentTradeId == trade.ParentTradeId).Sum(y => y.InitialMargin);
|
||||
trade.InitialMargin = MarginDefault.GetInitialMarginWithPreHandle(trade);
|
||||
yldb.SaveChanges();
|
||||
|
||||
if (trade.IsGroup == 2)
|
||||
{
|
||||
var parentTrade = yldb.trade.Find(trade.ParentTradeId);
|
||||
parentTrade.InitialMargin = yldb.trade.Where(x => x.ValidState != "InValid" && x.ParentTradeId == trade.ParentTradeId).Sum(y => y.InitialMargin);
|
||||
yldb.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -8049,6 +8060,8 @@ namespace YLErp.Web.Controllers
|
||||
return JsonError(ex.Message);
|
||||
}
|
||||
}
|
||||
//BUG-05 修正(同组接口一并收紧):批量重设初始预付金同样要求预付金配置权限
|
||||
[MyAuthorize("交易管理-交易预付金配置")]
|
||||
[HttpPost]
|
||||
public JsonResult batchSetInitialMargin(TradeMarginTemplateReq req)
|
||||
{
|
||||
@@ -8087,6 +8100,8 @@ namespace YLErp.Web.Controllers
|
||||
/// <summary>
|
||||
/// 获取交易预付金规则列表
|
||||
/// </summary>
|
||||
//BUG-05 修正:接口补权限注解(与同组 tradeMarginTemplateList/Edit 一致)
|
||||
[MyAuthorize("交易管理-交易预付金配置")]
|
||||
[HttpPost]
|
||||
public JsonResult tradeMarginTemplateQuery(TradeMarginTemplateReq req)
|
||||
{
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
<td>期初结存</td>
|
||||
<td id="LastDayRemainFund"></td>
|
||||
<td>初始保证金金额</td>
|
||||
<td id="MySideMargin"></td>
|
||||
<td id="SwapInitMargin"></td>
|
||||
<td>已实现盈亏</td>
|
||||
<td id="WinLoss"></td>
|
||||
</tr>
|
||||
@@ -103,16 +103,16 @@
|
||||
<td id="NetFundAll"></td>
|
||||
<td>盯市金额</td>
|
||||
<td id="SwapMarketAmount"></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>授信额度</td>
|
||||
<td id="TotalCredit"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>追保账户</td>
|
||||
<td id="VmFundSum"></td>
|
||||
<td>可用资金</td>
|
||||
<td id="AvailableFund"></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>已使用授信</td>
|
||||
<td id="UsedCredit"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>其他收支</td>
|
||||
|
||||
@@ -322,7 +322,7 @@
|
||||
else if (cellValue == "出金" || cellValue == "其他支出") {
|
||||
return "支出";
|
||||
}
|
||||
else if (rowObject.Action == "系统操作-期权费" || rowObject.Action == "系统操作-平仓费" || rowObject.Action == "系统操作-行权费" || rowObject.Action == "系统操作-票息" || rowObject.Action == "系统操作-互换" || rowObject.Action == "系统操作-应付预付金" || rowObject.Action == "系统操作-预付金返息") {
|
||||
else if (rowObject.Action == "系统操作-期权费" || rowObject.Action == "系统操作-平仓费" || rowObject.Action == "系统操作-行权费" || rowObject.Action == "系统操作-票息" || rowObject.Action == "系统操作-互换" || rowObject.Action == "系统操作-应付预付金" || rowObject.Action == "系统操作-预付金返息" || rowObject.Action == "系统操作-追加保证金") {
|
||||
if (rowObject.Money < 0) {
|
||||
return "支出";
|
||||
}
|
||||
|
||||
@@ -384,6 +384,10 @@
|
||||
else if (rowObject.Action == "系统操作-预付金返息") {
|
||||
action = "预付金返息";
|
||||
}
|
||||
else if (rowObject.Action == "系统操作-追加保证金") {
|
||||
//阶段四 §4.1 合约维度 EOD 结算产生的追保入金
|
||||
action = "追加保证金";
|
||||
}
|
||||
if (action) {
|
||||
return `${action}${parseFloat(rowObject.Money) > 0 ? "收入" : "支出"}`;
|
||||
}
|
||||
|
||||
@@ -253,7 +253,7 @@
|
||||
{{spanText(sec.detail).priceCurShort1}} ≤ <vue-number-input v-model="tier.Upper" v-bind:format="inputFormatPercent"></vue-number-input> × {{spanText(sec.detail).priceInit}}
|
||||
</template>
|
||||
<template v-else>
|
||||
<vue-number-input v-model="tier.Lower" v-bind:format="inputFormatPercent"></vue-number-input> × {{spanText(sec.detail).priceInit}} < {{spanText(sec.detail).priceCur}} ≤ <vue-number-input v-model="tier.Upper" v-bind:format="inputFormatPercent"></vue-number-input> × {{spanText(sec.detail).priceInit}}
|
||||
<vue-number-input v-model="tier.Lower" v-bind:format="inputFormatPercent"></vue-number-input> × {{spanText(sec.detail).priceInit}} < {{spanText(sec.detail).priceCurShort}} ≤ <vue-number-input v-model="tier.Upper" v-bind:format="inputFormatPercent"></vue-number-input> × {{spanText(sec.detail).priceInit}}
|
||||
</template>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -254,7 +254,7 @@
|
||||
{{spanText(sec.detail).priceCurShort1}} ≤ <vue-number-input v-model="tier.Upper" v-bind:format="inputFormatPercent"></vue-number-input> × {{spanText(sec.detail).priceInit}}
|
||||
</template>
|
||||
<template v-else>
|
||||
<vue-number-input v-model="tier.Lower" v-bind:format="inputFormatPercent"></vue-number-input> × {{spanText(sec.detail).priceInit}} < {{spanText(sec.detail).priceCur}} ≤ <vue-number-input v-model="tier.Upper" v-bind:format="inputFormatPercent"></vue-number-input> × {{spanText(sec.detail).priceInit}}
|
||||
<vue-number-input v-model="tier.Lower" v-bind:format="inputFormatPercent"></vue-number-input> × {{spanText(sec.detail).priceInit}} < {{spanText(sec.detail).priceCurShort}} ≤ <vue-number-input v-model="tier.Upper" v-bind:format="inputFormatPercent"></vue-number-input> × {{spanText(sec.detail).priceInit}}
|
||||
</template>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -230,7 +230,7 @@
|
||||
{{spanText(sec.detail).priceCurShort1}} ≤ <vue-number-input v-model="tier.Upper" v-bind:format="inputFormatPercent"></vue-number-input> × {{spanText(sec.detail).priceInit}}
|
||||
</template>
|
||||
<template v-else>
|
||||
<vue-number-input v-model="tier.Lower" v-bind:format="inputFormatPercent"></vue-number-input> × {{spanText(sec.detail).priceInit}} < {{spanText(sec.detail).priceCur}} ≤ <vue-number-input v-model="tier.Upper" v-bind:format="inputFormatPercent"></vue-number-input> × {{spanText(sec.detail).priceInit}}
|
||||
<vue-number-input v-model="tier.Lower" v-bind:format="inputFormatPercent"></vue-number-input> × {{spanText(sec.detail).priceInit}} < {{spanText(sec.detail).priceCurShort}} ≤ <vue-number-input v-model="tier.Upper" v-bind:format="inputFormatPercent"></vue-number-input> × {{spanText(sec.detail).priceInit}}
|
||||
</template>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -290,11 +290,13 @@ function SearchClientBalance() {
|
||||
|
||||
main.post("/trade_span/GetClientLatestBalance", { clientId: $("#ClientId").val(), ValueDateFrom: param.ValueDateStart, ValueDateTo: param.ValueDateEnd, IsClientBalanceGap: param.IsClientBalanceGap, IsGetOuterMarginGap: param.IsGetOuterMarginGap, ParentFlag: param.ParentFlag }).done(function (data) {
|
||||
$("#LastDayRemainFund").text(numFormart(data.LastDayRemainFund));
|
||||
$("#MySideMargin").text(numFormart(data.MySideMargin));
|
||||
//R2 阶段三 §3.2 口径(BUG-07 修正,与邮件/Excel 报告 SettlementReportService 同源):
|
||||
//初始保证金=SwapInitMargin(应付预付金净额)、维持保证金=−MySideMargin(client_span 维持保证金反号聚合)
|
||||
$("#SwapInitMargin").text(numFormart(data.SwapInitMargin));
|
||||
$("#WinLoss").text(numFormart(data.WinLoss));
|
||||
$("#CashInCashOutChange").text(numFormart(data.NetFundAll));
|
||||
$("#PositionPnl").text(numFormart(isPvRounded ? data.RoundedPositionPnl : data.PositionPnl));
|
||||
$("#MaintenanceMargin").text(numFormart(data.MaintenanceMargin));
|
||||
$("#MaintenanceMargin").text(numFormart(-data.MySideMargin));
|
||||
$("#NetFundAll").text(numFormart(data.NetFund));
|
||||
$("#SwapMarketAmount").text(numFormart(data.SwapMarketAmount));
|
||||
$("#VmFundSum").text(numFormart(data.VmFundSum));
|
||||
@@ -303,6 +305,8 @@ function SearchClientBalance() {
|
||||
$("#MarginByPayableMargin").text(numFormart(data.MarginByPayableMarginTotal));
|
||||
$("#ToDayRemainFund").text(numFormart(data.AmountFund));
|
||||
$("#DesirableFund").text(numFormart(data.DesirableFundTotal));
|
||||
$("#TotalCredit").text(numFormart(data.TotalCredit));
|
||||
$("#UsedCredit").text(numFormart(data.UsedCredit));
|
||||
});
|
||||
|
||||
var data = { clientId: $("#ClientId").val(), ValueDateFrom: $("#ValueDateFrom").val(), ValueDateTo: $("#ValueDate").val() };
|
||||
|
||||
@@ -462,7 +462,7 @@ const vue = new Vue({
|
||||
},
|
||||
//规则15录入区块文案:按 detail 资产类型切换计价口径。
|
||||
//债券类(利率债16/信用债32/其它债券64)及其他类型(默认债券口径):期初净价/当前净价,金额=×期初全价×券面总额;
|
||||
//基金及基金专户(32768)/债券指数(536870912):参考标的期初净价/参考标的当前净价(空头第1层用"参考标的当前收盘价"),
|
||||
//基金及基金专户(32768)/债券指数(536870912):参考标的期初净价/参考标的当前收盘价(空头各层统一收盘价,BUG-11 修正),
|
||||
//金额=×参考标的期初价格×参考标的名义份额。
|
||||
spanText(detail) {
|
||||
var ut = (detail && detail.UnderlyingType) || 0;
|
||||
@@ -471,6 +471,7 @@ const vue = new Vue({
|
||||
priceInit: '参考标的期初净价',
|
||||
priceCur: '参考标的当前净价',
|
||||
priceCurShort1: '参考标的当前收盘价',
|
||||
priceCurShort: '参考标的当前收盘价',
|
||||
amountBase: '参考标的期初价格 × 参考标的名义份额'
|
||||
};
|
||||
}
|
||||
@@ -478,6 +479,7 @@ const vue = new Vue({
|
||||
priceInit: '期初净价',
|
||||
priceCur: '当前净价',
|
||||
priceCurShort1: '当前净价',
|
||||
priceCurShort: '当前净价',
|
||||
amountBase: '期初全价 × 券面总额'
|
||||
};
|
||||
},
|
||||
|
||||
@@ -226,7 +226,7 @@ const vue = new Vue({
|
||||
needDetailRules.push(MarginRuleTypeEnum.买入自动赎回规则1);
|
||||
needDetailRules.push(MarginRuleTypeEnum.卖出自动赎回规则1);
|
||||
needDetailRules.push(MarginRuleTypeEnum.区间追保结构);
|
||||
needDetailRules.push(MarginRuleTypeEnum.单独计算规则1);
|
||||
needDetailRules.push(MarginRuleTypeEnum.单独计算规则);
|
||||
if (needDetailRules.indexOf(that.marginTemplate.RuleType.toString()) >= 0) {
|
||||
new Promise((reslove) => {
|
||||
//规则15:新增的是"资产类型区块"(含区间录入结构与独立区块键);其他规则维持原样
|
||||
@@ -473,7 +473,7 @@ const vue = new Vue({
|
||||
},
|
||||
//规则15录入区块文案:按 detail 资产类型切换计价口径。
|
||||
//债券类(利率债16/信用债32/其它债券64)及其他类型(默认债券口径):期初净价/当前净价,金额=×期初全价×券面总额;
|
||||
//基金及基金专户(32768)/债券指数(536870912):参考标的期初净价/参考标的当前净价(空头第1层用"参考标的当前收盘价"),
|
||||
//基金及基金专户(32768)/债券指数(536870912):参考标的期初净价/参考标的当前收盘价(空头各层统一收盘价,BUG-11 修正),
|
||||
//金额=×参考标的期初价格×参考标的名义份额。
|
||||
spanText(detail) {
|
||||
var ut = (detail && detail.UnderlyingType) || 0;
|
||||
@@ -482,6 +482,7 @@ const vue = new Vue({
|
||||
priceInit: '参考标的期初净价',
|
||||
priceCur: '参考标的当前净价',
|
||||
priceCurShort1: '参考标的当前收盘价',
|
||||
priceCurShort: '参考标的当前收盘价',
|
||||
amountBase: '参考标的期初价格 × 参考标的名义份额'
|
||||
};
|
||||
}
|
||||
@@ -489,6 +490,7 @@ const vue = new Vue({
|
||||
priceInit: '期初净价',
|
||||
priceCur: '当前净价',
|
||||
priceCurShort1: '当前净价',
|
||||
priceCurShort: '当前净价',
|
||||
amountBase: '期初全价 × 券面总额'
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
var g_grid = {};
|
||||
var g_grid = {};
|
||||
var query_data = {};
|
||||
|
||||
$(function () {
|
||||
@@ -337,13 +337,17 @@ function confirmAllSelect() {
|
||||
main.message("请选择需确认的交易");
|
||||
return;
|
||||
}
|
||||
var confirmFunc = function (additionalProcessing) {
|
||||
var confirmFunc = function (additionalProcessing, id, useRecall) {
|
||||
var ggrid = jQuery('#listGrid');
|
||||
var rowIds = ggrid.jqGrid('getGridParam', 'selarrrow');
|
||||
var ors = [];
|
||||
for (var i = 0; i < rowIds.length; i++) {
|
||||
var or = ggrid.getRowData(rowIds[i]).id;
|
||||
ors.push(or);
|
||||
if (id) {
|
||||
ors.push(id);
|
||||
} else {
|
||||
for (var i = 0; i < rowIds.length; i++) {
|
||||
var or = ggrid.getRowData(rowIds[i]).id;
|
||||
ors.push(or);
|
||||
}
|
||||
}
|
||||
var pData = { tradeids: ors.join(",") };
|
||||
// R4:不再硬编码 IsSkipCheck=true 静默跳过资金校验——普通确认走校验,
|
||||
@@ -357,6 +361,72 @@ function confirmAllSelect() {
|
||||
main.message(data.msg);
|
||||
return;
|
||||
}
|
||||
//R4(BUG-04 修正):资金不足时后端返回 success=true + proccessType="AdditionalProcessing"(交易实际未确认),
|
||||
//必须弹额度试算特批窗口并按弹窗结果重提(复用确认列表页 swapTradeConfirmList.js 逻辑),不能静默刷新
|
||||
if (data.obj && data.obj.proccessType === "AdditionalProcessing") {
|
||||
if (data.obj.TrialDataId) {
|
||||
var layerSetting = {
|
||||
type: 2,
|
||||
title: "提示",
|
||||
shadeClose: false,
|
||||
shade: 0.4,
|
||||
area: ['800px', '500px'],
|
||||
content: "/trade/showQuotaTrial?id=" + data.obj.TrialDataId,
|
||||
yes: function (index) {
|
||||
var obj = window["layui-layer-iframe" + index].page;
|
||||
if (obj.IsTF) {
|
||||
if (obj.Data.Remark && obj.Data.Remark.length > 0) {
|
||||
main.post("/trade/SaveQuotaTrial", obj.Data, { async: false }).done(function (res) {
|
||||
if (!res || !res.success) {
|
||||
main.message(res.msg);
|
||||
}
|
||||
});
|
||||
confirmFunc("LackOfMoney", obj.Data.TradeId, true);
|
||||
} else {
|
||||
main.message("必须填写说明内容,才可以录入交易");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (obj.Data.Remark && obj.Data.Remark.length > 0) {
|
||||
main.post("/trade/SaveQuotaTrial", obj.Data, { async: false }).done(function (res) {
|
||||
if (!res || !res.success) {
|
||||
main.message(res.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
confirmFunc("LackOfMoney", obj.Data.TradeId, true);
|
||||
}
|
||||
(parent || window).layer.closeAll();
|
||||
},
|
||||
cancel: function () {
|
||||
if (window.parent && window.parent.reloadtrade) {
|
||||
window.parent.reloadtrade();
|
||||
}
|
||||
(parent || window).layer.closeAll();
|
||||
}
|
||||
};
|
||||
if (data.obj.type === "LackOfMoney") {
|
||||
layerSetting.btn = ["交易特批", '取消'];
|
||||
}
|
||||
// 只展示"资金状况"的情况
|
||||
if (data.obj.typecode === "FundStatus") {
|
||||
layerSetting.btn = ["确认", '取消'];
|
||||
}
|
||||
layer.open(layerSetting);
|
||||
}
|
||||
else {
|
||||
if (data.obj.message) {
|
||||
main.message(data.obj.message);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (useRecall) {
|
||||
//特批单笔已通过,重跑本批剩余交易
|
||||
confirmFunc();
|
||||
return;
|
||||
}
|
||||
main.message(data.msg);
|
||||
SearchClick(true);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
var g_grid = {};
|
||||
var g_grid = {};
|
||||
var query_data = {};
|
||||
|
||||
$(function () {
|
||||
@@ -1048,13 +1048,17 @@ function confirmAllSelect() {
|
||||
main.message("请选择需确认的交易");
|
||||
return;
|
||||
}
|
||||
var confirmFunc = function (additionalProcessing) {
|
||||
var confirmFunc = function (additionalProcessing, id, useRecall) {
|
||||
var ggrid = jQuery('#listGrid');
|
||||
var rowIds = ggrid.jqGrid('getGridParam', 'selarrrow');
|
||||
var ors = [];
|
||||
for (var i = 0; i < rowIds.length; i++) {
|
||||
var or = ggrid.getRowData(rowIds[i]).id;
|
||||
ors.push(or);
|
||||
if (id) {
|
||||
ors.push(id);
|
||||
} else {
|
||||
for (var i = 0; i < rowIds.length; i++) {
|
||||
var or = ggrid.getRowData(rowIds[i]).id;
|
||||
ors.push(or);
|
||||
}
|
||||
}
|
||||
var pData = { tradeids: ors.join(",") };
|
||||
// R4:不再硬编码 IsSkipCheck=true 静默跳过资金校验——普通确认走校验,
|
||||
@@ -1069,6 +1073,72 @@ function confirmAllSelect() {
|
||||
main.message(data.msg);
|
||||
return;
|
||||
}
|
||||
//R4(BUG-04 修正):资金不足时后端返回 success=true + proccessType="AdditionalProcessing"(交易实际未确认),
|
||||
//必须弹额度试算特批窗口并按弹窗结果重提(复用确认列表页 swapTradeConfirmList.js 逻辑),不能静默刷新
|
||||
if (data.obj && data.obj.proccessType === "AdditionalProcessing") {
|
||||
if (data.obj.TrialDataId) {
|
||||
var layerSetting = {
|
||||
type: 2,
|
||||
title: "提示",
|
||||
shadeClose: false,
|
||||
shade: 0.4,
|
||||
area: ['800px', '500px'],
|
||||
content: "/trade/showQuotaTrial?id=" + data.obj.TrialDataId,
|
||||
yes: function (index) {
|
||||
var obj = window["layui-layer-iframe" + index].page;
|
||||
if (obj.IsTF) {
|
||||
if (obj.Data.Remark && obj.Data.Remark.length > 0) {
|
||||
main.post("/trade/SaveQuotaTrial", obj.Data, { async: false }).done(function (res) {
|
||||
if (!res || !res.success) {
|
||||
main.message(res.msg);
|
||||
}
|
||||
});
|
||||
confirmFunc("LackOfMoney", obj.Data.TradeId, true);
|
||||
} else {
|
||||
main.message("必须填写说明内容,才可以录入交易");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (obj.Data.Remark && obj.Data.Remark.length > 0) {
|
||||
main.post("/trade/SaveQuotaTrial", obj.Data, { async: false }).done(function (res) {
|
||||
if (!res || !res.success) {
|
||||
main.message(res.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
confirmFunc("LackOfMoney", obj.Data.TradeId, true);
|
||||
}
|
||||
(parent || window).layer.closeAll();
|
||||
},
|
||||
cancel: function () {
|
||||
if (window.parent && window.parent.reloadtrade) {
|
||||
window.parent.reloadtrade();
|
||||
}
|
||||
(parent || window).layer.closeAll();
|
||||
}
|
||||
};
|
||||
if (data.obj.type === "LackOfMoney") {
|
||||
layerSetting.btn = ["交易特批", '取消'];
|
||||
}
|
||||
// 只展示"资金状况"的情况
|
||||
if (data.obj.typecode === "FundStatus") {
|
||||
layerSetting.btn = ["确认", '取消'];
|
||||
}
|
||||
layer.open(layerSetting);
|
||||
}
|
||||
else {
|
||||
if (data.obj.message) {
|
||||
main.message(data.obj.message);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (useRecall) {
|
||||
//特批单笔已通过,重跑本批剩余交易
|
||||
confirmFunc();
|
||||
return;
|
||||
}
|
||||
main.message(data.msg);
|
||||
SearchClick(true);
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user