Merge remote-tracking branch 'origin/glms/feature/1.4.2' into glms/feature/1.4.2

This commit is contained in:
张名锐
2026-08-28 14:37:12 +08:00
29 changed files with 660 additions and 119 deletions
@@ -74,11 +74,6 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator
var underlying = Context.GetTradeUnderlying(flowEventGroup.UnderlyingCode); var underlying = Context.GetTradeUnderlying(flowEventGroup.UnderlyingCode);
var closeNotionalValue = unwindData?.CloseNotionalValue var closeNotionalValue = unwindData?.CloseNotionalValue
?? flowEventGroup.Quantity * flowEventGroup.ContractSize * posi.PosiGrossPrice; ?? flowEventGroup.Quantity * flowEventGroup.ContractSize * posi.PosiGrossPrice;
var settlementDate = flowEventGroup.UnwindDate
?? throw new ServiceException($"平仓事件{flowEventGroup.id}缺少结算日");
var currentDayFloatingDividend = Context.GetEodPositions(tradeId, settlementDate)
.FirstOrDefault(x => x.PositionId == flowEventGroup.PositionId)
?.TdPosiDividend ?? 0m;
// 行构造器统一处理客户视角、结算公式、品种差异和模板展示精度。 // 行构造器统一处理客户视角、结算公式、品种差异和模板展示精度。
var row = SwapSettlementBillRowBuilder.Build(new SwapSettlementBillRowInput var row = SwapSettlementBillRowBuilder.Build(new SwapSettlementBillRowInput
@@ -91,7 +86,6 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator
Positions = positions, Positions = positions,
UnderlyingInstrumentType = underlying?.UnderlyingInstrumentType, UnderlyingInstrumentType = underlying?.UnderlyingInstrumentType,
CloseNotionalValue = closeNotionalValue, CloseNotionalValue = closeNotionalValue,
CurrentDayFloatingDividend = currentDayFloatingDividend,
// 与提前终止详情页保持同一来源:读取 swap_flow_event 中的平仓浮动腿记录。 // 与提前终止详情页保持同一来源:读取 swap_flow_event 中的平仓浮动腿记录。
ExitYtm = flowEventGroup.ExitYtm, ExitYtm = flowEventGroup.ExitYtm,
IncludePeriodPaymentInNetting = (tradeExtend?.ExtendObj?.DividendPayDate ?? 1) == 0 IncludePeriodPaymentInNetting = (tradeExtend?.ExtendObj?.DividendPayDate ?? 1) == 0
@@ -3,9 +3,12 @@ using YLErp.Modules.SwapModule.Margin;
namespace YLErp.Modules.SwapModule namespace YLErp.Modules.SwapModule
{ {
/// <summary> /// <summary>
/// R2 阶段三 §3.2 可用资金公式测试(需求拆分 R2 口径,实时/EOD/报告三处共用 SwapSpanBalanceCalc): /// R2 阶段三 §3.2 可用资金/追保/可取资金公式测试(定稿 2026-08-28 口径,实时/EOD/报告三处共用 SwapSpanBalanceCalc):
/// 客户维度 = Max(现金结存 + 授信 − 已使用授信 + 初始保证金 − 维持保证金, 0); /// 客户维度可用 = Max(现金结存 + 授信 − 已使用授信 + 初始保证金 − 维持保证金, 0);
/// 合约维度 = Max(现金结存 + 授信 − 已使用授信 交易维度追加保证金合计, 0)。 /// 合约维度可用 = Max(现金结存 + 授信 − 已使用授信, 0)(定稿删除"−交易维度追加保证金",避免与现金结存/已使用授信双重扣减);
/// 追保金额两维度均 Max(...,0) 截断、恒 ≥ 0(只追不退);合约维度(2026-08-28 调整)= 当日追加保证金现金部分合计
/// (不与现金结存轧差、闲置现金不冲抵、授信不追),客户维度 = Max((维持−初始) − (现金+授信−已使用), 0);
/// 可取资金只算现金部分:合约维度 = Max(现金结存 + min(持仓盈亏,0), 0),客户维度 = Max(现金结存 + 初始 − 维持 + min(持仓盈亏,0), 0)。
/// 授信额度为 credit.Credit 合计(保存时已折算比例),现金结存=期末结存(阶段二起授信不进资金)。 /// 授信额度为 credit.Credit 合计(保存时已折算比例),现金结存=期末结存(阶段二起授信不进资金)。
/// </summary> /// </summary>
[TestClass] [TestClass]
@@ -35,40 +38,27 @@ namespace YLErp.Modules.SwapModule
cashBalance: 20, totalCredit: 100, usedCredit: 20, initialMargin: 100, maintenanceMargin: 130), 1e-6); cashBalance: 20, totalCredit: 100, usedCredit: 20, initialMargin: 100, maintenanceMargin: 130), 1e-6);
} }
/// <summary>
/// 合约维度可用资金(定稿 2026-08-28):Max(现金结存 + 授信 − 已使用授信, 0),不再减交易维度追加保证金。
/// </summary>
[TestMethod] [TestMethod]
public void SB_004_合约维度_按交易追加合计扣减() public void SB_004_合约维度_可用资金()
{ {
//现金20 + 授信100 已使用20 追加合计30 = 70 //现金20 + 授信100 已使用20 = 100
Assert.AreEqual(70, SwapSpanBalanceCalc.CalcContractDimensionAvailable( Assert.AreEqual(100, SwapSpanBalanceCalc.CalcContractDimensionAvailable(
cashBalance: 20, totalCredit: 100, usedCredit: 20, tradeAdditionalMarginSum: 30), 1e-6); cashBalance: 20, totalCredit: 100, usedCredit: 20), 1e-6);
} }
[TestMethod] [TestMethod]
public void SB_005_合约维度_扣尽截断为0() public void SB_005_合约维度_扣尽截断为0()
{ {
//现金20 + 授信0 已使用30 = 10 → 0
Assert.AreEqual(0, SwapSpanBalanceCalc.CalcContractDimensionAvailable( Assert.AreEqual(0, SwapSpanBalanceCalc.CalcContractDimensionAvailable(
cashBalance: 20, totalCredit: 0, usedCredit: 0, tradeAdditionalMarginSum: 30), 1e-6); cashBalance: 20, totalCredit: 0, usedCredit: 30), 1e-6);
} }
/// <summary> /// <summary>
/// 阶段三口径一致性:无追加保证金流水时(阶段四前),两维度公式数值一致—— /// 客户维度追保金额(定稿 2026-08-28Max(...,0) 截断,恒 ≥ 0,只追不退)。
/// 交易追加合计 = Σ(维持−累计) = 维持 − 初始,与客户维度的 (初始−维持) 项互相抵消。
/// </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> /// </summary>
[TestMethod] [TestMethod]
public void SB_010_客户维度追保金额_正数需追保() public void SB_010_客户维度追保金额_正数需追保()
@@ -79,15 +69,15 @@ namespace YLErp.Modules.SwapModule
} }
[TestMethod] [TestMethod]
public void SB_011_客户维度追保金额_负数可返还() public void SB_011_客户维度追保金额_盈余截断为0()
{ {
//差额 (维持130−初始100)=30,资金 20+10020=100 → 追保 = 30 100 = 70(可返还,不截断为 0 //差额 (维持130−初始100)=30,资金 20+10020=100 → 30 100 = 70 → 截断为 0(资金富余不展示负数
Assert.AreEqual(-70, SwapSpanBalanceCalc.CalcClientDimensionCallMargin( Assert.AreEqual(0, SwapSpanBalanceCalc.CalcClientDimensionCallMargin(
cashBalance: 20, totalCredit: 100, usedCredit: 20, initialMargin: 100, maintenanceMargin: 130), 1e-6); cashBalance: 20, totalCredit: 100, usedCredit: 20, initialMargin: 100, maintenanceMargin: 130), 1e-6);
} }
/// <summary> /// <summary>
/// 客户维度追保金额与可用资金公式互为反向( Max 截断):追保 = −(未截断可用资金)。 /// 客户维度追保金额与可用资金公式互为反向(各加 Max 截断):追保 = Max((未截断可用资金), 0)
/// </summary> /// </summary>
[TestMethod] [TestMethod]
public void SB_012_客户维度追保与可用资金反向一致() public void SB_012_客户维度追保与可用资金反向一致()
@@ -100,7 +90,7 @@ namespace YLErp.Modules.SwapModule
var available = SwapSpanBalanceCalc.CalcClientDimensionAvailable(cash, credit, used, initial, maintenance); var available = SwapSpanBalanceCalc.CalcClientDimensionAvailable(cash, credit, used, initial, maintenance);
var callMargin = SwapSpanBalanceCalc.CalcClientDimensionCallMargin(cash, credit, used, initial, maintenance); var callMargin = SwapSpanBalanceCalc.CalcClientDimensionCallMargin(cash, credit, used, initial, maintenance);
var availableUnfloored = cash + credit - used + initial - maintenance; var availableUnfloored = cash + credit - used + initial - maintenance;
Assert.AreEqual(-availableUnfloored, callMargin, 1e-9); Assert.AreEqual(Math.Max(-availableUnfloored, 0), callMargin, 1e-9);
//可用资金被 0 截断时追保为正(需追保),两者不矛盾 //可用资金被 0 截断时追保为正(需追保),两者不矛盾
if (availableUnfloored < 0) if (availableUnfloored < 0)
{ {
@@ -110,22 +100,60 @@ namespace YLErp.Modules.SwapModule
} }
/// <summary> /// <summary>
/// 合约维度追保金额(Max((现金+授信−已使用)×−1, 0)):账户透支为正=应补足,盈余截断为 0。 /// 合约维度追保金额(2026-08-28 新口径):当日产生的追加保证金现金部分合计,不与现金结存轧差、
/// 闲置现金不冲抵、授信部分不追;Max(...,0) 兜底截断(正常无负记录,追保回落不返还)。
/// </summary> /// </summary>
[TestMethod] [TestMethod]
public void SB_013_合约维度追保金额_透支为正() public void SB_013_合约维度追保金额_当日现金追加全额()
{ {
//现金−80 + 授信100 − 已使用30 = −10 → 追保 = 10(应补足) //场景:入金60万、现金初保50万(闲置10万),当日需追加40万(现金)——
Assert.AreEqual(10, SwapSpanBalanceCalc.CalcContractDimensionCallMargin( //旧口径缺口法追 30万(闲置现金被冲抵);新口径追当日现金追加全额 40万
cashBalance: -80, totalCredit: 100, usedCredit: 30), 1e-6); Assert.AreEqual(400000, SwapSpanBalanceCalc.CalcContractDimensionCallMargin(400000), 1e-6);
} }
[TestMethod] [TestMethod]
public void SB_014_合约维度追保金额_盈余截断为0() public void SB_014_合约维度追保金额_无当日追加为0()
{ {
//现金50 + 授信100 已使用20 = 130 → 盈余,Max(...,0) 截断 → 追保 = 0 //当日追加全走授信(无现金流水)/当日无追保 → 0;负值兜底截断为 0
Assert.AreEqual(0, SwapSpanBalanceCalc.CalcContractDimensionCallMargin( Assert.AreEqual(0, SwapSpanBalanceCalc.CalcContractDimensionCallMargin(0), 1e-6);
cashBalance: 50, totalCredit: 100, usedCredit: 20), 1e-6); Assert.AreEqual(0, SwapSpanBalanceCalc.CalcContractDimensionCallMargin(-100), 1e-6);
}
/// <summary>
/// 可取资金(定稿 2026-08-28):只算现金部分(授信不可取现);min(持仓盈亏,0) 只扣浮亏、浮盈不放行。
/// </summary>
[TestMethod]
public void SB_015_客户维度可取资金_浮亏扣减()
{
//现金200 + 初始100 维持150 + min(30,0) = 120
Assert.AreEqual(120, SwapSpanBalanceCalc.CalcClientDimensionDesirableFund(
cashBalance: 200, initialMargin: 100, maintenanceMargin: 150, positionPnl: -30), 1e-6);
}
[TestMethod]
public void SB_016_客户维度可取资金_浮盈不放行()
{
//现金200 + 初始100 维持150 + min(50,0)=0 → 150
Assert.AreEqual(150, SwapSpanBalanceCalc.CalcClientDimensionDesirableFund(
cashBalance: 200, initialMargin: 100, maintenanceMargin: 150, positionPnl: 50), 1e-6);
}
[TestMethod]
public void SB_017_客户维度可取资金_截断为0()
{
//现金20 + 初始100 维持150 + min(40,0) = 70 → 0
Assert.AreEqual(0, SwapSpanBalanceCalc.CalcClientDimensionDesirableFund(
cashBalance: 20, initialMargin: 100, maintenanceMargin: 150, positionPnl: -40), 1e-6);
}
[TestMethod]
public void SB_018_合约维度可取资金()
{
//Max(100 + min(30,0), 0) = 70;浮亏超过现金时截断为 0
Assert.AreEqual(70, SwapSpanBalanceCalc.CalcContractDimensionDesirableFund(cashBalance: 100, positionPnl: -30), 1e-6);
Assert.AreEqual(0, SwapSpanBalanceCalc.CalcContractDimensionDesirableFund(cashBalance: 20, positionPnl: -50), 1e-6);
//浮盈不放行:Max(100 + min(80,0), 0) = 100
Assert.AreEqual(100, SwapSpanBalanceCalc.CalcContractDimensionDesirableFund(cashBalance: 100, positionPnl: 80), 1e-6);
} }
} }
} }
@@ -0,0 +1,46 @@
using Dapper;
using YLErp.Modules.UnderlyingModule;
namespace YLErp.UnitTestProject.Modules.UnderlyingModule
{
/// <summary>
/// 基金管理人取数(bigdata 数据源)连通性与库名插值验证(连 96 测试库):
/// - DbSchema.Of 从连接串解析物理库名(glms_bigdata),SQL 不再硬编码库名、跨环境库名不同也能命中;
/// - Lookup 全链路(连接 96 → 插值 SQL 执行 → 结果归并)不返回 Unavailable 即为连通且 SQL 有效。
/// </summary>
[TestClass]
public class FundManagerLookupServiceTest
{
[TestMethod]
public void DbSchema_从连接串解析物理库名()
{
Assert.AreEqual("`glms_bigdata`", DbSchema.Of("bigdata"));
Assert.AreEqual("`glms_yltrs_ylcms`", DbSchema.Of("ylcms"));
}
[TestMethod]
public void Lookup_连接96大数据库_非降级()
{
var service = new FundManagerLookupService();
//96 库现有测试数据 161210/630006/511160SECUCODE 无后缀,与去后缀匹配逻辑一致),用库存代码验证全链路命中
foreach (var code in new[] { "511160.SH", "161210.SZ", "630006.SH" })
{
var result = service.Lookup(code);
Console.WriteLine($"code={code} → Status={result.Status}, InvestAdvisorName={result.InvestAdvisorName}");
Assert.AreNotEqual(FundManagerLookupStatus.Unavailable, result.Status,
$"code={code} 返回 Unavailable96 bigdata 库不可达或插值 SQL 执行失败");
}
}
[TestMethod]
public void Diag_查看库存secucode样例与行数()
{
using var connection = new MySqlConnector.MySqlConnection(AppManager.GetConnectionString("bigdata"));
var count = connection.ExecuteScalar<long>($"SELECT COUNT(*) FROM {DbSchema.Of("bigdata")}.mf_fundarchives");
var samples = connection.Query<string>($"SELECT SECUCODE FROM {DbSchema.Of("bigdata")}.mf_fundarchives LIMIT 8");
Console.WriteLine($"mf_fundarchives 行数={count}, SECUCODE样例=[{string.Join(",", samples)}]");
var advCount = connection.ExecuteScalar<long>($"SELECT COUNT(*) FROM {DbSchema.Of("bigdata")}.mf_investadvisoroutline");
Console.WriteLine($"mf_investadvisoroutline 行数={advCount}");
}
}
}
+2 -1
View File
@@ -3,7 +3,8 @@
"ylcms": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_ylcms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;", "ylcms": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_ylcms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;",
"yladmin": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_admin;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;", "yladmin": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_admin;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;",
"ylclient": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_client;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;", "ylclient": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_client;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;",
"bondoms": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=zszq_bond_oms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;" "bondoms": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=zszq_bond_oms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;",
"bigdata": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_bigdata;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;"
}, },
"LibreOffice": { "LibreOffice": {
"ExePath": "", "ExePath": "",
+1 -1
View File
@@ -171,7 +171,7 @@ namespace YLErp
"yladmin" => _configuration.GetConnectionString("yladmin"), "yladmin" => _configuration.GetConnectionString("yladmin"),
"ylclient" => _configuration.GetConnectionString("ylclient"), "ylclient" => _configuration.GetConnectionString("ylclient"),
"bondoms" => _configuration.GetConnectionString("bondoms"), "bondoms" => _configuration.GetConnectionString("bondoms"),
"glms_bigdata" => _configuration.GetConnectionString("glms_bigdata"), "bigdata" => _configuration.GetConnectionString("bigdata"),
"apex_oracle"=> _configuration.GetConnectionString("apex_oracle"), "apex_oracle"=> _configuration.GetConnectionString("apex_oracle"),
_ => string.Empty, _ => string.Empty,
}; };
@@ -21,6 +21,8 @@ namespace YLErp.BLL.EodSettlement
/// </summary> /// </summary>
public class ClientBalanceUtility public class ClientBalanceUtility
{ {
private static readonly IYcLogger logger = LogFactory.GetLogger("客户资金结算");
/// <summary> /// <summary>
/// 获取startDate至endDate 所有客户结算信息统计 /// 获取startDate至endDate 所有客户结算信息统计
/// </summary> /// </summary>
@@ -303,6 +305,8 @@ namespace YLErp.BLL.EodSettlement
var usedCreditDic = ClientCreditInoutService.GetUsedCreditByClients(clientIdS, db); var usedCreditDic = ClientCreditInoutService.GetUsedCreditByClients(clientIdS, db);
var swapInitMarginDic = SwapSpanBalanceQueryService.GetSwapInitMarginByClients(clientIdS, lastDate, db); var swapInitMarginDic = SwapSpanBalanceQueryService.GetSwapInitMarginByClients(clientIdS, lastDate, db);
var swapAdditionalDic = SwapSpanBalanceQueryService.GetTradeAdditionalMarginByClients(clientIdS, lastDate, db); var swapAdditionalDic = SwapSpanBalanceQueryService.GetTradeAdditionalMarginByClients(clientIdS, lastDate, db);
//合约维度追保金额新口径(2026-08-28):当日追加保证金现金部分合计
var todayCashAddDic = SwapSpanBalanceQueryService.GetTodayCashAdditionalMarginByClients(clientIdS, lastDate, db);
//原始授信额度(展示用):与 EOD 写入 clientbalancedaily.Credit 同批过滤条件(EodClientBalanceCalc :68), //原始授信额度(展示用):与 EOD 写入 clientbalancedaily.Credit 同批过滤条件(EodClientBalanceCalc :68),
//取 Σ(OriginalCredit ?? Credit) 不经比例折算;TotalCredit 仍为折算后值供公式使用 //取 Σ(OriginalCredit ?? Credit) 不经比例折算;TotalCredit 仍为折算后值供公式使用
var originalCreditDic = db.credit.AsNoTracking() var originalCreditDic = db.credit.AsNoTracking()
@@ -387,8 +391,9 @@ namespace YLErp.BLL.EodSettlement
// 未配置(MarginWatchRule=NULL,存量客户)维持旧口径:期末结存-追保账户余额 // 未配置(MarginWatchRule=NULL,存量客户)维持旧口径:期末结存-追保账户余额
if (client?.MarginWatchRule == 0) if (client?.MarginWatchRule == 0)
{ {
//定稿 2026-08-28Max(现金结存 + 授信额度 − 已使用授信, 0),不再减交易维度追加保证金(避免双重扣减)
balance.AvailableAmount = SwapSpanBalanceCalc.CalcContractDimensionAvailable( balance.AvailableAmount = SwapSpanBalanceCalc.CalcContractDimensionAvailable(
balance.AmountFund, balance.TotalCredit, balance.UsedCredit, balance.SwapAdditionalMarginTotal); balance.AmountFund, balance.TotalCredit, balance.UsedCredit);
} }
else if (client?.MarginWatchRule == 1) else if (client?.MarginWatchRule == 1)
{ {
@@ -401,9 +406,9 @@ namespace YLErp.BLL.EodSettlement
{ {
balance.AvailableAmount = balance.MarginBalance - (balance.VmInFundSum - balance.VmOutFundSum); balance.AvailableAmount = balance.MarginBalance - (balance.VmInFundSum - balance.VmOutFundSum);
} }
// 是否追保/追保金额(阶段四 §4.2 按维度分流,允许负值=双向,不以 0 截断): // 是否追保/追保金额(按维度分流,定稿 2026-08-28 起均 Max(...,0) 截断、恒 ≥ 0,只追不退):
// 客户维度(==1)= (维持−初始) − (现金+授信−已使用),负=可返还 // 客户维度(==1= Max((维持−初始) − (现金+授信−已使用), 0)
// 合约维度(==0= Max(−(现金+授信−已使用), 0)(需求原文公式,盈余截断为 0); // 合约维度(==0= 当日追加保证金现金部分合计(2026-08-28 新口径:不与现金结存轧差、闲置现金不冲抵、授信部分不追);
// 未配置(NULL 存量)维持旧口径:盯市低于维持时 = 初始保证金金额−盯市金额,否则 0 // 未配置(NULL 存量)维持旧口径:盯市低于维持时 = 初始保证金金额−盯市金额,否则 0
if (client?.MarginWatchRule == 1) if (client?.MarginWatchRule == 1)
{ {
@@ -414,7 +419,7 @@ namespace YLErp.BLL.EodSettlement
else if (client?.MarginWatchRule == 0) else if (client?.MarginWatchRule == 0)
{ {
balance.MarginByPayableMarginTotal = SwapSpanBalanceCalc.CalcContractDimensionCallMargin( balance.MarginByPayableMarginTotal = SwapSpanBalanceCalc.CalcContractDimensionCallMargin(
balance.AmountFund, balance.TotalCredit, balance.UsedCredit); todayCashAddDic.TryGetValue(data.ClientId, out var todayCashAdd) ? todayCashAdd : 0);
balance.NeedAddMargin = balance.MarginByPayableMarginTotal > 0; balance.NeedAddMargin = balance.MarginByPayableMarginTotal > 0;
} }
else else
@@ -422,8 +427,25 @@ namespace YLErp.BLL.EodSettlement
balance.NeedAddMargin = balance.SwapMarketAmount < balance.MaintenanceMargin; balance.NeedAddMargin = balance.SwapMarketAmount < balance.MaintenanceMargin;
balance.MarginByPayableMarginTotal = balance.NeedAddMargin ? (balance.MySideMargin - balance.SwapMarketAmount) : 0; balance.MarginByPayableMarginTotal = balance.NeedAddMargin ? (balance.MySideMargin - balance.SwapMarketAmount) : 0;
} }
// 可取资金=Math.Max(期末结存-min(持仓盈亏,0)-初始保证金,0) // 可取资金(定稿 2026-08-28 分维度口径,只算现金部分——授信不可取现;min(持仓盈亏,0) 只扣浮亏、浮盈不放行):
balance.DesirableFund = Math.Max(balance.MarginBalance + Math.Min(balance.RoundedPositionPnl, 0), 0); // 合约维度(==0= Max(现金结存 + min(持仓盈亏,0), 0)(追保已落账进现金结存,无需再叠加保证金约束);
// 客户维度(==1)= Max(现金结存 + 初始保证金 − 维持保证金 + min(持仓盈亏,0), 0)(追保不产现金流,显式叠加保证金约束);
// 未配置(NULL 存量)维持旧口径
if (client?.MarginWatchRule == 0)
{
balance.DesirableFund = SwapSpanBalanceCalc.CalcContractDimensionDesirableFund(
balance.AmountFund, balance.RoundedPositionPnl);
}
else if (client?.MarginWatchRule == 1)
{
balance.DesirableFund = SwapSpanBalanceCalc.CalcClientDimensionDesirableFund(
balance.AmountFund, balance.SwapInitMargin, -balance.MySideMargin, balance.RoundedPositionPnl);
}
else
{
balance.DesirableFund = Math.Max(balance.MarginBalance + Math.Min(balance.RoundedPositionPnl, 0), 0);
}
logger.Info($"客户资金结算:客户{client?.id}{client?.Name}{balance.ValueDate:yyyy-MM-dd} MarginWatchRule={client?.MarginWatchRule?.ToString() ?? "NULL"} 可用资金={balance.AvailableAmount:0.00} 追保金额={balance.MarginByPayableMarginTotal:0.00} 是否追保={balance.NeedAddMargin} 可取资金={balance.DesirableFund:0.00}(期末结存{balance.AmountFund:0.00} 授信{balance.TotalCredit:0.00} 已用授信{balance.UsedCredit:0.00} 互换初始{balance.SwapInitMargin:0.00} 维持{-balance.MySideMargin:0.00} 追加合计{balance.SwapAdditionalMarginTotal:0.00}");
} }
} }
@@ -30,6 +30,8 @@ namespace YLErp.BLL.Eod
/// </summary> /// </summary>
public class RealTimeClientBanlanceService : YLBaseService public class RealTimeClientBanlanceService : YLBaseService
{ {
private static readonly IYcLogger logger = LogFactory.GetLogger("实时客户资金");
readonly valuedate _systemDate; //系统参数 readonly valuedate _systemDate; //系统参数
readonly DateTime _valueDate; //系统交易日 readonly DateTime _valueDate; //系统交易日
readonly Dictionary<int, ClientBalanceEx> _clientBalanceDic; readonly Dictionary<int, ClientBalanceEx> _clientBalanceDic;
@@ -241,6 +243,8 @@ namespace YLErp.BLL.Eod
var usedCreditDic = Modules.SwapModule.ClientCreditInoutService.GetUsedCreditByClients(clientIdList, DbContext); var usedCreditDic = Modules.SwapModule.ClientCreditInoutService.GetUsedCreditByClients(clientIdList, DbContext);
var swapInitMarginDic = SwapSpanBalanceQueryService.GetSwapInitMarginByClients(clientIdList, calcDate.Value, DbContext); var swapInitMarginDic = SwapSpanBalanceQueryService.GetSwapInitMarginByClients(clientIdList, calcDate.Value, DbContext);
var swapAdditionalDic = SwapSpanBalanceQueryService.GetTradeAdditionalMarginByClients(clientIdList, calcDate.Value, DbContext); var swapAdditionalDic = SwapSpanBalanceQueryService.GetTradeAdditionalMarginByClients(clientIdList, calcDate.Value, DbContext);
//合约维度追保金额新口径(2026-08-28):当日追加保证金现金部分合计(日间 EOD 未跑时无当日记录 → 0,与通知书口径一致)
var todayCashAddDic = SwapSpanBalanceQueryService.GetTodayCashAdditionalMarginByClients(clientIdList, calcDate.Value, DbContext);
foreach (var item in _clientBalanceDic.Values) foreach (var item in _clientBalanceDic.Values)
{ {
@@ -286,8 +290,9 @@ namespace YLErp.BLL.Eod
var ruleClient = DataCacheProvider.GetClientDataSource().GetData(clientId); var ruleClient = DataCacheProvider.GetClientDataSource().GetData(clientId);
if (ruleClient?.MarginWatchRule == 0) if (ruleClient?.MarginWatchRule == 0)
{ {
//定稿 2026-08-28Max(现金结存 + 授信额度 − 已使用授信, 0),不再减交易维度追加保证金(避免双重扣减)
item.AvailableAmount = SwapSpanBalanceCalc.CalcContractDimensionAvailable( item.AvailableAmount = SwapSpanBalanceCalc.CalcContractDimensionAvailable(
item.AmountFund, item.TotalCredit, item.UsedCredit, item.SwapAdditionalMarginTotal); item.AmountFund, item.TotalCredit, item.UsedCredit);
} }
else if (ruleClient?.MarginWatchRule == 1) else if (ruleClient?.MarginWatchRule == 1)
{ {
@@ -300,9 +305,10 @@ namespace YLErp.BLL.Eod
{ {
item.AvailableAmount = item.MarginBalance - item.FrozenMarginMoney; item.AvailableAmount = item.MarginBalance - item.FrozenMarginMoney;
} }
// 是否追保/追保金额(按维度分流,与 ClientBalanceUtility 报告口径一致): // 是否追保/追保金额(按维度分流,与 ClientBalanceUtility 报告口径一致,定稿 2026-08-28 起均 Max(...,0) 截断):
// 客户维度(==1双向追保 = (维持−初始) − (现金+授信−已使用),允许负值(负=可返还) // 客户维度(==1= Max((维持−初始) − (现金+授信−已使用), 0)
// 合约维度(==0= Max(−(现金+授信−已使用), 0),盈余截断为 0;NULL 存量维持旧口径 // 合约维度(==0= 当日追加保证金现金部分合计(2026-08-28 新口径:不与现金结存轧差、闲置现金不冲抵、授信部分不追);
// NULL 存量维持旧口径
if (ruleClient?.MarginWatchRule == 1) if (ruleClient?.MarginWatchRule == 1)
{ {
item.MarginByPayableMarginTotal = SwapSpanBalanceCalc.CalcClientDimensionCallMargin( item.MarginByPayableMarginTotal = SwapSpanBalanceCalc.CalcClientDimensionCallMargin(
@@ -312,7 +318,7 @@ namespace YLErp.BLL.Eod
else if (ruleClient?.MarginWatchRule == 0) else if (ruleClient?.MarginWatchRule == 0)
{ {
item.MarginByPayableMarginTotal = SwapSpanBalanceCalc.CalcContractDimensionCallMargin( item.MarginByPayableMarginTotal = SwapSpanBalanceCalc.CalcContractDimensionCallMargin(
item.AmountFund, item.TotalCredit, item.UsedCredit); todayCashAddDic.TryGetValue(clientId, out var todayCashAdd) ? todayCashAdd : 0);
item.NeedAddMargin = item.MarginByPayableMarginTotal > 0; item.NeedAddMargin = item.MarginByPayableMarginTotal > 0;
} }
else else
@@ -322,8 +328,25 @@ namespace YLErp.BLL.Eod
// 追保金额=初始保证金金额-盯市金额 // 追保金额=初始保证金金额-盯市金额
item.MarginByPayableMarginTotal = item.NeedAddMargin ? (item.MySideMargin - item.SwapMarketAmount) : 0; item.MarginByPayableMarginTotal = item.NeedAddMargin ? (item.MySideMargin - item.SwapMarketAmount) : 0;
} }
// 可取资金=max(期末结存+min(持仓盈亏,0)-初始保证金,0) // 可取资金(定稿 2026-08-28 分维度口径,只算现金部分;min(持仓盈亏,0) 只扣浮亏、浮盈不放行):
item.DesirableFund =Math.Max( item.MarginBalance - item.FrozenMarginMoney + Math.Min(item.RoundedPositionPnl, 0),0); // 合约维度(==0= Max(现金结存 + min(持仓盈亏,0), 0)
// 客户维度(==1)= Max(现金结存 + 初始保证金 − 维持保证金 + min(持仓盈亏,0), 0)
// NULL 存量维持旧口径(含冻结保证金扣减)
if (ruleClient?.MarginWatchRule == 0)
{
item.DesirableFund = SwapSpanBalanceCalc.CalcContractDimensionDesirableFund(
item.AmountFund, item.RoundedPositionPnl);
}
else if (ruleClient?.MarginWatchRule == 1)
{
item.DesirableFund = SwapSpanBalanceCalc.CalcClientDimensionDesirableFund(
item.AmountFund, item.SwapInitMargin, -item.MySideMargin, item.RoundedPositionPnl);
}
else
{
item.DesirableFund = Math.Max(item.MarginBalance - item.FrozenMarginMoney + Math.Min(item.RoundedPositionPnl, 0), 0);
}
logger.Info($"实时客户资金:客户{clientId} MarginWatchRule={ruleClient?.MarginWatchRule?.ToString() ?? "NULL"} 可用资金={item.AvailableAmount:0.00} 追保金额={item.MarginByPayableMarginTotal:0.00} 是否追保={item.NeedAddMargin} 可取资金={item.DesirableFund:0.00}(期末结存{item.AmountFund:0.00} 授信{item.TotalCredit:0.00} 已用授信{item.UsedCredit:0.00} 互换初始{item.SwapInitMargin:0.00} 维持{-item.MySideMargin:0.00} 追加合计{item.SwapAdditionalMarginTotal:0.00}");
} }
return _clientBalanceDic.Values; return _clientBalanceDic.Values;
+28 -2
View File
@@ -2328,6 +2328,31 @@ namespace YLErp.BLL.Eod
var clientBalance = clientBalances[0]; var clientBalance = clientBalances[0];
using (var db = new YLContext()) using (var db = new YLContext())
{ {
//修改/复核已簿记交易(trade.id!=0)按增量口径校验:本笔自身已簿记的成交金额(期权费记录)
//与初始预付金流水已计入现金结存、初始授信占用已计入已使用授信,比较前先剔除——
//否则改备注等无关字段保存会被存量预付金二次拦截(例:入金60万、初始预付金已付50万、余额10万)。
//只剔除确认时会重写的记录;追保/票息/平仓等生命周期记录确认后仍保留,不剔除。
var ownCashBooked = 0d;
var ownCreditBooked = 0d;
if (trade.id != 0)
{
ownCashBooked = db.ClientCashInCashOut
.Where(x => x.TradeId == trade.id && x.ValidState != "InValid")
.ToList()
.Where(x => (x.State == ClientCashInCashOut. || x.State == ClientCashInCashOut.)
&& (x.Action == ClientCashInCashOut._期权费 || x.Action == ClientCashInCashOut._应付预付金))
.Sum(x => x.Money ?? 0);
ownCreditBooked = db.client_credit_inout
.Where(x => x.trade_id == trade.id)
.ToList()
.Where(x => !Modules.SwapModule.ClientCreditInoutService.IsAdditionalMarginRecord(x))
.Sum(x => x.amount);
clientBalance.AmountFund -= ownCashBooked;
clientBalance.AvailableAmount -= ownCashBooked;
}
var ownBookedNote = trade.id != 0
? $"(已剔除本笔已簿记资金{ownCashBooked:#,##0.000}与初始授信占用{ownCreditBooked:#,##0.000}"
: "";
var tradePrice = trade.TradePrice * (-TradeCalcHelper.GetSign(trade.BuySell)); var tradePrice = trade.TradePrice * (-TradeCalcHelper.GetSign(trade.BuySell));
var AvailableAmount = clientBalance.AvailablePremium(); var AvailableAmount = clientBalance.AvailablePremium();
@@ -2381,14 +2406,15 @@ namespace YLErp.BLL.Eod
} }
} }
var usedCredit = Modules.SwapModule.ClientCreditInoutService.GetUsedCredit(clientId, db); var usedCredit = Modules.SwapModule.ClientCreditInoutService.GetUsedCredit(clientId, db);
var creditCap = Math.Max(clientBalance.TotalCredit - usedCredit, 0); //授信上限剔除本笔自身初始占用(ownCreditBooked,增量口径,见上方净扣说明)
var creditCap = Math.Max(clientBalance.TotalCredit - (usedCredit - ownCreditBooked), 0);
var creditCovered = Math.Min(creditPayable, creditCap); var creditCovered = Math.Min(creditPayable, creditCap);
//授信覆盖不足的回落现金部分 + 走现金部分 + 成交金额,合计必须 ≤ 现金结存 //授信覆盖不足的回落现金部分 + 走现金部分 + 成交金额,合计必须 ≤ 现金结存
var cashNeed = tradePrice + cashPayable + (creditPayable - creditCovered); var cashNeed = tradePrice + cashPayable + (creditPayable - creditCovered);
if (cashNeed > clientBalance.AmountFund) if (cashNeed > clientBalance.AmountFund)
{ {
var totalPayable = tradePrice + cashPayable + creditPayable; var totalPayable = tradePrice + cashPayable + creditPayable;
errorMsg = $"当前交易应付总额:{totalPayable:#,##0.000}(走现金:{cashPayable + tradePrice:#,##0.000},选授信:{creditPayable:#,##0.000})。当前现金结存:{clientBalance.AmountFund:F3}(走现金部分只认现金结存),授信额度:{clientBalance.TotalCredit:F3},已使用授信:{usedCredit:F3},剩余授信:{creditCap:F3}(授信仅覆盖选授信部分,不足回落现金)。现金不足以覆盖应付的现金部分。"; errorMsg = $"当前交易应付总额:{totalPayable:#,##0.000}(走现金:{cashPayable + tradePrice:#,##0.000},选授信:{creditPayable:#,##0.000})。当前现金结存:{clientBalance.AmountFund:F3}(走现金部分只认现金结存),授信额度:{clientBalance.TotalCredit:F3},已使用授信:{usedCredit - ownCreditBooked:F3},剩余授信:{creditCap:F3}(授信仅覆盖选授信部分,不足回落现金)。现金不足以覆盖应付的现金部分。{ownBookedNote}";
return false; return false;
} }
} }
@@ -531,7 +531,12 @@ namespace YLErp.BLL.MarginCalculation
} }
} }
return SwapSpanMarginCalc.CalcTradeMaintenanceMargin(trade.InitialMargin, spanCfg, legs, isInitialCalc, closePrice); var margin = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(trade.InitialMargin, spanCfg, legs, isInitialCalc, closePrice);
if (margin.HasValue)
{
logger.Info($"规则15新引擎:交易{trade.id} 标的{trade.UnderlyingCode} 收盘价={closePrice:0.####}(试算初始={isInitialCalc})→ 维持保证金={margin.Value:0.00}");
}
return margin;
} }
} }
} }
+41
View File
@@ -0,0 +1,41 @@
using MySqlConnector;
using System.Collections.Concurrent;
using System.Text.RegularExpressions;
namespace YLErp.BLL
{
/// <summary>
/// 跨库 SQL 的物理库名解析器:代码里只写逻辑连接名(appsettings ConnectionStrings 的 key
/// 与 Java @Mapper 指定数据源名同语义),物理库名由各环境连接串的 database= 决定——
/// 同一逻辑库在不同环境库名不同(如 bigdata 在测试环境为 glms_bigdata),SQL 中硬编码库名会跨环境失败。
/// 用法:{@DbSchema.Of("bigdata")}.mf_fundarchives(返回带反引号的库名,可直接内插)。
/// 仅标识自家的 appsettings 连接串,值不来自用户输入;仍做 \w+ 白名单校验防御配置笔误。
/// </summary>
public static class DbSchema
{
private static readonly ConcurrentDictionary<string, string> Cache = new();
private static readonly Regex SafeIdentifier = new(@"^\w+$", RegexOptions.Compiled);
/// <summary>
/// 取逻辑连接名对应的物理库名(形如 `glms_bigdata`,含反引号)。配置缺失或库名非法立即抛错——
/// 跨库 SQL 拼错库名在运行期才暴露更难排查,配置错误应尽早失败。
/// </summary>
public static string Of(string connectionKey)
{
return Cache.GetOrAdd(connectionKey, key =>
{
var connectionString = AppManager.GetConnectionString(key);
if (string.IsNullOrWhiteSpace(connectionString))
{
throw new InvalidOperationException($"跨库SQL依赖的连接串未配置:{key}");
}
var database = new MySqlConnectionStringBuilder(connectionString).Database;
if (string.IsNullOrWhiteSpace(database) || !SafeIdentifier.IsMatch(database))
{
throw new InvalidOperationException($"连接串 {key} 缺少 database 或库名非法:{database}");
}
return "`" + database + "`";
});
}
}
}
@@ -110,12 +110,14 @@ namespace YLErp.Modules.MarginModule
//2.无预付金规则:率直接为 0 //2.无预付金规则:率直接为 0
if (template.RuleType == (int)MarginRuleTypeEnum.) if (template.RuleType == (int)MarginRuleTypeEnum.)
{ {
logger.Info($"预付金模板取数:模板{template.id}(规则=无预付金)标的{underlyingCode}x=y=0");
return new MarginRateResult { Template = template, InitRate = 0m, MaintainRate = 0m }; return new MarginRateResult { Template = template, InitRate = 0m, MaintainRate = 0m };
} }
if (template.RuleType != (int)MarginRuleTypeEnum.) if (template.RuleType != (int)MarginRuleTypeEnum.)
{ {
//其他规则不在本帮助类支持范围,显式返回 null //其他规则不在本帮助类支持范围,显式返回 null
logger.Info($"【警告】预付金模板取数:模板{template.id} 规则{template.RuleType}不在取数支持范围(仅 无预付金/区间追保结构),返回null由调用方兜底");
return null; return null;
} }
@@ -124,6 +126,7 @@ namespace YLErp.Modules.MarginModule
.Where(x => x.MarginTemplateId == template.id && x.ValueDate <= valueDate); .Where(x => x.MarginTemplateId == template.id && x.ValueDate <= valueDate);
if (!detailQuery.Any()) if (!detailQuery.Any())
{ {
logger.Info($"【警告】预付金模板取数:模板{template.id} 无生效明细行(ValueDate≤{valueDate:yyyy-MM-dd}),返回null由调用方兜底");
return null; return null;
} }
var latestValueDate = detailQuery.Max(x => x.ValueDate); var latestValueDate = detailQuery.Max(x => x.ValueDate);
@@ -158,6 +161,7 @@ namespace YLErp.Modules.MarginModule
} }
if (!matched.Any()) if (!matched.Any())
{ {
logger.Info($"【警告】预付金模板取数:模板{template.id}(生效日{latestValueDate:yyyy-MM-dd})标的{underlyingCode}(品种{underlyingInstrumentType},期限档{term})无匹配明细行,返回null由调用方兜底");
return null; return null;
} }
@@ -170,12 +174,15 @@ namespace YLErp.Modules.MarginModule
} }
var detail = matched.First(); var detail = matched.First();
var initRate = ToDecimalRate(detail.MarginRatio1);
var maintainRate = ToDecimalRate(detail.MarginRatio2);
logger.Info($"预付金模板取数:模板{template.id}(生效日{latestValueDate:yyyy-MM-dd})标的{underlyingCode}(品种{underlyingInstrumentType},期限档{term})命中明细行{detail.id}:初始x={(initRate?.ToString("0.####") ?? "")},维持y={(maintainRate?.ToString("0.####") ?? "")}");
return new MarginRateResult return new MarginRateResult
{ {
Template = template, Template = template,
Detail = detail, Detail = detail,
InitRate = ToDecimalRate(detail.MarginRatio1), InitRate = initRate,
MaintainRate = ToDecimalRate(detail.MarginRatio2) MaintainRate = maintainRate
}; };
} }
@@ -226,6 +233,7 @@ namespace YLErp.Modules.MarginModule
var bound = db.margin_template_v2.AsNoTracking().FirstOrDefault(x => x.id == bindingTemplateId.Value && x.IsValid); var bound = db.margin_template_v2.AsNoTracking().FirstOrDefault(x => x.id == bindingTemplateId.Value && x.IsValid);
if (bound != null && bound.IsApplicableToBook(tradeAssetId)) if (bound != null && bound.IsApplicableToBook(tradeAssetId))
{ {
logger.Info($"预付金模板取数:交易{tradeId}(客户{clientId})命中层级=交易绑定 → 模板{bound.id}");
return bound; return bound;
} }
if (bound != null) if (bound != null)
@@ -264,17 +272,23 @@ namespace YLErp.Modules.MarginModule
var ret = clientTemplate.FirstOrDefault(x => x.IsApplicableToBook(tradeAssetId)); var ret = clientTemplate.FirstOrDefault(x => x.IsApplicableToBook(tradeAssetId));
if (ret != null) if (ret != null)
{ {
logger.Info($"预付金模板取数:交易{tradeId}(客户{clientId},等级{levelName ?? ""})命中层级=客户默认 → 模板{ret.id}");
return ret; return ret;
} }
} }
//3.全局默认 //3.全局默认
return db.margin_template_v2.AsNoTracking() var globalDefault = db.margin_template_v2.AsNoTracking()
.Where(x => x.IsDefault && !x.IsForClient && x.IsValid && x.TradeTypes.Contains("收益互换") && x.ValueDate <= valueDate) .Where(x => x.IsDefault && !x.IsForClient && x.IsValid && x.TradeTypes.Contains("收益互换") && x.ValueDate <= valueDate)
.OrderByDescending(x => x.ValueDate) .OrderByDescending(x => x.ValueDate)
.ThenByDescending(x => x.id) .ThenByDescending(x => x.id)
.ToList() .ToList()
.FirstOrDefault(x => x.IsApplicableToBook(tradeAssetId)); .FirstOrDefault(x => x.IsApplicableToBook(tradeAssetId));
if (globalDefault != null)
{
logger.Info($"预付金模板取数:交易{tradeId}(客户{clientId})命中层级=全局默认 → 模板{globalDefault.id}");
}
return globalDefault;
} }
/// <summary> /// <summary>
@@ -373,6 +387,7 @@ namespace YLErp.Modules.MarginModule
if (boundTemplates.TryGetValue(templateId, out var bound) && bound.IsApplicableToBook(trade.AssetId)) if (boundTemplates.TryGetValue(templateId, out var bound) && bound.IsApplicableToBook(trade.AssetId))
{ {
result[trade.id] = bound; result[trade.id] = bound;
logger.Info($"预付金模板取数:交易{trade.id}(客户{trade.ClientId})命中层级=交易绑定 → 模板{bound.id}");
} }
else if (boundTemplates.ContainsKey(templateId)) else if (boundTemplates.ContainsKey(templateId))
{ {
@@ -391,12 +406,18 @@ namespace YLErp.Modules.MarginModule
if (clientTemplate != null) if (clientTemplate != null)
{ {
result[trade.id] = clientTemplate.Template; result[trade.id] = clientTemplate.Template;
logger.Info($"预付金模板取数:交易{trade.id}(客户{trade.ClientId},等级{levelName ?? ""})命中层级=客户默认 → 模板{clientTemplate.Template.id}");
continue; continue;
} }
var globalDefault = globalDefaults.FirstOrDefault(x => x.IsApplicableToBook(trade.AssetId)); var globalDefault = globalDefaults.FirstOrDefault(x => x.IsApplicableToBook(trade.AssetId));
if (globalDefault != null) if (globalDefault != null)
{ {
result[trade.id] = globalDefault; result[trade.id] = globalDefault;
logger.Info($"预付金模板取数:交易{trade.id}(客户{trade.ClientId})命中层级=全局默认 → 模板{globalDefault.id}");
}
else
{
logger.Info($"【警告】预付金模板取数:交易{trade.id}(客户{trade.ClientId})三层级(交易绑定/客户默认/全局默认)均未命中有效模板");
} }
} }
@@ -3,6 +3,7 @@ using YLErp.DBModels;
using YLErp.Enums; using YLErp.Enums;
using YLErp.Modules.MarginModule; using YLErp.Modules.MarginModule;
using YLErp.Modules.TradeModule; using YLErp.Modules.TradeModule;
using YLErp.Helpers;
namespace YLErp.Modules.SwapModule.Margin namespace YLErp.Modules.SwapModule.Margin
{ {
@@ -24,6 +25,8 @@ namespace YLErp.Modules.SwapModule.Margin
/// </summary> /// </summary>
public class SwapAdditionalMarginService : YLBaseService public class SwapAdditionalMarginService : YLBaseService
{ {
private static readonly IYcLogger logger = LogFactory.GetLogger("EOD追保");
/// <summary> /// <summary>
/// EOD 追保腿打标(OptName):与手工追加预付金腿(OptName=操作员实名)区分, /// EOD 追保腿打标(OptName):与手工追加预付金腿(OptName=操作员实名)区分,
/// 幂等清理、RemoveByTrade 保护与时间轴回退清理均以此识别。拆单现金腿落库时被打服务身份,簿记后回打本标识。 /// 幂等清理、RemoveByTrade 保护与时间轴回退清理均以此识别。拆单现金腿落库时被打服务身份,簿记后回打本标识。
@@ -79,8 +82,23 @@ namespace YLErp.Modules.SwapModule.Margin
/// <summary> /// <summary>
/// 结算日逐客户逐交易产生追加保证金(clientFilter 为部分结算的客户过滤,与 EOD 请求一致)。 /// 结算日逐客户逐交易产生追加保证金(clientFilter 为部分结算的客户过滤,与 EOD 请求一致)。
/// 顶层兜异常日志(Error 级)后原样抛出,避免异常栈被 EOD 框架层吞掉无从定位。
/// </summary> /// </summary>
public void SettleAdditionalMargin(DateTime settleDate, List<int> clientFilter = null) public void SettleAdditionalMargin(DateTime settleDate, List<int> clientFilter = null)
{
try
{
SettleAdditionalMarginCore(settleDate, clientFilter);
}
catch (Exception ex)
{
var filterDesc = clientFilter != null && clientFilter.Any() ? string.Join(",", clientFilter) : "全部";
logger.Error($"EOD追保:结算日{settleDate:yyyy-MM-dd}(客户范围:{filterDesc})追保腿生成异常", ex);
throw;
}
}
private void SettleAdditionalMarginCore(DateTime settleDate, List<int> clientFilter)
{ {
//合约维度盯市客户 //合约维度盯市客户
var watchClientIds = DbContextFactory.GetClientDbContext(OptUser).client.AsNoTracking() var watchClientIds = DbContextFactory.GetClientDbContext(OptUser).client.AsNoTracking()
@@ -93,8 +111,10 @@ namespace YLErp.Modules.SwapModule.Margin
} }
if (watchClientIds.Count == 0) if (watchClientIds.Count == 0)
{ {
logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 无合约维度盯市(MarginWatchRule=0)客户,结束");
return; return;
} }
logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 合约维度盯市客户{watchClientIds.Count}个({string.Join(",", watchClientIds)}");
//存续中的互换交易(状态口径与 eodSwapQuery 一致,含当日已了结) //存续中的互换交易(状态口径与 eodSwapQuery 一致,含当日已了结)
var tradeStatuses = ConsTrade.TradeStatusAfterConfirmed; var tradeStatuses = ConsTrade.TradeStatusAfterConfirmed;
@@ -107,17 +127,39 @@ namespace YLErp.Modules.SwapModule.Margin
.ToList(); .ToList();
if (trades.Count == 0) if (trades.Count == 0)
{ {
logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 盯市客户名下无存续收益互换交易,结束");
return; return;
} }
//开始日门槛:未到交易开始日(未起息)的不参与追保——维持保证金引擎从成交日就产出 trade_span,
//初始预付金流水却到开始日才入账,不过滤会在 成交日~开始日 之间把未到期的初始预付金
//误判成缺口、按维持全额追加(2026-08-28 交易2571实证:07-31成交/08-03起息,重刷07-31误追500000
var notStartedTrades = trades.Where(t => (t.StartDate ?? t.TradeDate) > settleDate).ToList();
if (notStartedTrades.Count > 0)
{
logger.Info($"EOD追保:{notStartedTrades.Count}笔交易未到开始日(id=[{string.Join(",", notStartedTrades.Select(t => t.id))}]),不参与追保结算");
trades = trades.Except(notStartedTrades).ToList();
if (trades.Count == 0)
{
logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 交易均未到开始日,结束");
return;
}
}
//规则15(区间追保结构)交易:R1 三层级解析(BUG-02 修正,与引擎/确认书同口径)—— //规则15(区间追保结构)交易:R1 三层级解析(BUG-02 修正,与引擎/确认书同口径)——
//交易绑定→客户默认→全局默认 找到即停,只配客户/全局默认模板的交易同样纳入追保结算 //交易绑定→客户默认→全局默认 找到即停,只配客户/全局默认模板的交易同样纳入追保结算
var candidateCount = trades.Count;
var templatesByTrade = MarginTemplateV2RateHelper.ResolveTieredTemplates(trades, settleDate, DbContext); var templatesByTrade = MarginTemplateV2RateHelper.ResolveTieredTemplates(trades, settleDate, DbContext);
trades = trades.Where(t => templatesByTrade.TryGetValue(t.id, out var tpl) trades = trades.Where(t => templatesByTrade.TryGetValue(t.id, out var tpl)
&& tpl.RuleType == (int)MarginRuleTypeEnum.).ToList(); && tpl.RuleType == (int)MarginRuleTypeEnum.).ToList();
if (candidateCount > trades.Count)
{
logger.Info($"EOD追保:{candidateCount - trades.Count}笔交易模板非区间追保结构(或三级未命中),不参与追保结算");
}
var tradeIds = trades.Select(t => t.id).ToList(); var tradeIds = trades.Select(t => t.id).ToList();
if (tradeIds.Count == 0) if (tradeIds.Count == 0)
{ {
logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 规则15交易为0,结束");
return; return;
} }
@@ -175,21 +217,28 @@ namespace YLErp.Modules.SwapModule.Margin
var eodPositionService = new SwapEodPositionService(this); var eodPositionService = new SwapEodPositionService(this);
var flowEventService = new SwapFlowEventService(this); var flowEventService = new SwapFlowEventService(this);
var settledTrades = 0;
var settledLegs = 0;
var totalIncrement = 0d;
foreach (var clientGroup in trades.GroupBy(t => t.ClientId).OrderBy(g => g.Key)) foreach (var clientGroup in trades.GroupBy(t => t.ClientId).OrderBy(g => g.Key))
{ {
foreach (var td in clientGroup.OrderBy(t => t.id)) foreach (var td in clientGroup.OrderBy(t => t.id))
{ {
if (!maintenanceByTrade.TryGetValue(td.id, out var maintenance) || maintenance <= 0) if (!maintenanceByTrade.TryGetValue(td.id, out var maintenance) || maintenance <= 0)
{ {
logger.Info($"EOD追保:交易{td.id}(客户{td.ClientId})跳过——当日无trade_span维持保证金或维持≤0maintenance={maintenance:0.00}");
continue; continue;
} }
//目标追加 = 维持保证金 − 已缴初始保证金净额(现金应付预付金净收额 + 授信初始占用净额, //目标追加 = 维持保证金 − 已缴初始保证金净额(现金应付预付金净收额 + 授信初始占用净额,
//授信垫付与现金垫付同等对待,杜绝授信初始预付金被重复追加) //授信垫付与现金垫付同等对待,杜绝授信初始预付金被重复追加)
var payableNet = (payableNetByTrade.TryGetValue(td.id, out var payable) ? payable : 0) var payable = payableNetByTrade.TryGetValue(td.id, out var p) ? p : 0;
+ (initCreditByTrade.TryGetValue(td.id, out var initCredit) ? initCredit : 0); var initCredit = initCreditByTrade.TryGetValue(td.id, out var ic) ? ic : 0;
var payableNet = payable + initCredit;
var target = SwapAdditionalMarginCalc.CalcTarget(maintenance, payableNet); var target = SwapAdditionalMarginCalc.CalcTarget(maintenance, payableNet);
if (target <= 0) if (target <= 0)
{ {
logger.Info($"EOD追保:交易{td.id}(客户{td.ClientId})跳过——目标追加≤0:维持={maintenance:0.00},已缴={payableNet:0.00}(应付净额{payable:0.00}+初始授信占用{initCredit:0.00})已覆盖");
continue; continue;
} }
var fundedCash = addRecordByTrade.TryGetValue(td.id, out var cash) ? cash : 0; var fundedCash = addRecordByTrade.TryGetValue(td.id, out var cash) ? cash : 0;
@@ -198,8 +247,10 @@ namespace YLErp.Modules.SwapModule.Margin
if (increment <= 0) if (increment <= 0)
{ {
//已补足;追保回落(目标下降)不返还——负缺口在可用资金公式(Σ维持−累计)体现 //已补足;追保回落(目标下降)不返还——负缺口在可用资金公式(Σ维持−累计)体现
logger.Info($"EOD追保:交易{td.id}(客户{td.ClientId})跳过——增量≤0:目标={target:0.00},已补足(现金{fundedCash:0.00}+授信{fundedCredit:0.00}),追保回落不返还");
continue; continue;
} }
logger.Info($"EOD追保:交易{td.id}(客户{td.ClientId})需追加:维持={maintenance:0.00},已缴={payableNet:0.00}(应付净额{payable:0.00}+初始授信占用{initCredit:0.00}),目标={target:0.00},已补足(现金{fundedCash:0.00}+授信{fundedCredit:0.00}),本次增量={increment:0.00}");
//幂等清理:先删本结算日起 EOD 旧追保腿及其簿记(腿/流水/占用/快照同生共死),再按最新增量重建; //幂等清理:先删本结算日起 EOD 旧追保腿及其簿记(腿/流水/占用/快照同生共死),再按最新增量重建;
//手工追加预付金腿(OptName≠EOD追保)不受影响 //手工追加预付金腿(OptName≠EOD追保)不受影响
@@ -228,6 +279,10 @@ namespace YLErp.Modules.SwapModule.Margin
.ToList(); .ToList();
newLegs.ForEach(x => x.OptName = EodOptName); newLegs.ForEach(x => x.OptName = EodOptName);
DbContext.SaveChanges(); DbContext.SaveChanges();
logger.Info($"EOD追保:交易{td.id} 追保腿落库完成——新腿{newLegs.Count}条(id=[{string.Join(",", newLegs.Select(x => x.id))}],含拆单现金腿)");
settledTrades++;
settledLegs += newLegs.Count;
totalIncrement += increment;
//实时持仓克隆 + 开仓事件(参照 TradeConfirmService 簿记后动作,但只针对本次新腿—— //实时持仓克隆 + 开仓事件(参照 TradeConfirmService 簿记后动作,但只针对本次新腿——
//整交易 InitialPosition 会把浮动腿实时持仓重置回开仓态、AddPositionEvent 会为全部腿重复建开仓事件,EOD 场景不可用); //整交易 InitialPosition 会把浮动腿实时持仓重置回开仓态、AddPositionEvent 会为全部腿重复建开仓事件,EOD 场景不可用);
@@ -252,6 +307,8 @@ namespace YLErp.Modules.SwapModule.Margin
} }
} }
} }
logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 完成——规则15交易{tradeIds.Count}笔,{settledTrades}笔产生追保(腿{settledLegs}条,合计增量{totalIncrement:0.00}");
} }
/// <summary> /// <summary>
@@ -289,6 +346,7 @@ namespace YLErp.Modules.SwapModule.Margin
var oldEodRows = DbContext.eod_swap_position.Where(x => x.SwapTradeId == td.id var oldEodRows = DbContext.eod_swap_position.Where(x => x.SwapTradeId == td.id
&& x.ValueDate >= settleDate && oldInitialIds.Contains(x.PositionId)) && x.ValueDate >= settleDate && oldInitialIds.Contains(x.PositionId))
.ToList(); .ToList();
logger.Info($"EOD追保:交易{td.id} 幂等清理(重跑)——删EOD追保腿{oldLegs.Count}条(id=[{string.Join(",", oldLegs.Select(x => x.id))}])及其簿记:现金流水{oldCashRecords.Count}条、授信占用{oldCreditRecords.Count}条、开仓事件{oldEvents.Count}条、eod快照{oldEodRows.Count}行");
DbContext.eod_swap_position.RemoveRange(oldEodRows); DbContext.eod_swap_position.RemoveRange(oldEodRows);
DbContext.swap_position.RemoveRange(oldLegs); DbContext.swap_position.RemoveRange(oldLegs);
DbContext.SaveChanges(); DbContext.SaveChanges();
@@ -5,7 +5,7 @@ namespace YLErp.Modules.SwapModule.Margin
/// 授信额度取 credit.Credit 合计(阶段一 §1.1 已在保存时折算 原始授信值×最大授信可用比例,消费方不再乘比例); /// 授信额度取 credit.Credit 合计(阶段一 §1.1 已在保存时折算 原始授信值×最大授信可用比例,消费方不再乘比例);
/// 现金结存 = 期末结存 AmountFund(阶段二起授信不进资金,流水天然不含授信部分,无需排除); /// 现金结存 = 期末结存 AmountFund(阶段二起授信不进资金,流水天然不含授信部分,无需排除);
/// 已使用授信 = 授信出入表 Σ(amount); /// 已使用授信 = 授信出入表 Σ(amount);
/// 初始保证金(净收取为正)= 应付预付金流水收付净额(平仓返还自动冲减); /// 初始保证金(净收取为正)= 初始预付金的和 = 应付预付金流水收付净额 + 初始预付金授信占用净额(2026-08-28 业务裁定:与页面"初始保证金金额"字段同口径);
/// 维持保证金(净收取为正)= MySideMarginclient_span 维持保证金写入 trade_span 后经 CalcClientMargin 反号聚合)。 /// 维持保证金(净收取为正)= MySideMarginclient_span 维持保证金写入 trade_span 后经 CalcClientMargin 反号聚合)。
/// 公式整体待业务校验(EQD-6948),参数化集中在此便于校验后调整。 /// 公式整体待业务校验(EQD-6948),参数化集中在此便于校验后调整。
/// </summary> /// </summary>
@@ -24,34 +24,61 @@ namespace YLErp.Modules.SwapModule.Margin
/// <summary> /// <summary>
/// 可用资金(合约维度,MarginWatchRule=0): /// 可用资金(合约维度,MarginWatchRule=0):
/// Max(现金结存 + 授信额度 − 已使用授信 交易维度追加保证金合计, 0) /// Max(现金结存 + 授信额度 − 已使用授信, 0)(定稿 2026-08-28:删除"交易维度追加保证金"——
/// 交易维度追加保证金合计 = Σ(维持保证金 − 累计保证金)(阶段三:累计=应付预付金净额;阶段四含追加保证金流水)。 /// 每日追保落账后现金部分已进现金结存、授信部分已进已使用授信,再减属双重扣减)。
/// </summary> /// </summary>
public static double CalcContractDimensionAvailable(double cashBalance, double totalCredit, double usedCredit, public static double CalcContractDimensionAvailable(double cashBalance, double totalCredit, double usedCredit)
double tradeAdditionalMarginSum)
{ {
return Math.Max(cashBalance + totalCredit - usedCredit - tradeAdditionalMarginSum, 0); return Math.Max(cashBalance + totalCredit - usedCredit, 0);
} }
/// <summary> /// <summary>
/// 追保金额(客户维度,MarginWatchRule=1阶段四 §4.2 双向——不以 0 截断): /// 追保金额(客户维度,MarginWatchRule=1定稿 2026-08-28Max(...,0) 截断,恒 ≥ 0,只追不退):
/// (维持保证金 − 初始保证金) − (现金结存 + 授信额度 − 已使用授信),正=需追保、负=可返还。 /// Max((维持保证金 − 初始保证金) − (现金结存 + 授信额度 − 已使用授信), 0),正=需追保
/// 即客户维度可用资金公式的反向值(去 Max 截断)——与需求原文"现金结存+授信额度−(维持−初始)"数值互为相反数, /// 资金富余时为 0(富余状态由可用资金/可取资金为正表达)。
/// 即客户维度可用资金公式反向值的 Max 截断——与需求原文"现金结存+授信额度−(维持−初始)"数值互为相反数,
/// 此处按 MarginByPayableMarginTotal 字段既有口径(正数=应追加,估值报告"应追加预付金X元")定向。 /// 此处按 MarginByPayableMarginTotal 字段既有口径(正数=应追加,估值报告"应追加预付金X元")定向。
/// </summary> /// </summary>
public static double CalcClientDimensionCallMargin(double cashBalance, double totalCredit, double usedCredit, public static double CalcClientDimensionCallMargin(double cashBalance, double totalCredit, double usedCredit,
double initialMargin, double maintenanceMargin) double initialMargin, double maintenanceMargin)
{ {
return Math.Round(maintenanceMargin - initialMargin - (cashBalance + totalCredit - usedCredit), 2, MidpointRounding.AwayFromZero); return Math.Max(Math.Round(maintenanceMargin - initialMargin - (cashBalance + totalCredit - usedCredit), 2, MidpointRounding.AwayFromZero), 0);
} }
/// <summary> /// <summary>
/// 追保金额(合约维度,MarginWatchRule=0):Max(−(现金结存 + 授信额度 − 已使用授信), 0)。 /// 追保金额(合约维度,MarginWatchRule=02026-08-28 新口径):
/// 即需求原文公式 Max((现金结存+授信额度−已使用授信)×−1, 0):账户透支(现金+授信不足)为正=应补足,盈余截断为 0(不展示负数)。 /// = 当日产生的追加保证金资金记录中"资金来源=现金"的金额合计(入参即该值,见
/// SwapSpanBalanceQueryService.GetTodayCashAdditionalMarginByClients),Max(...,0) 兜底截断。
/// 语义:当日该笔合约产生的现金追保额(不与现金结存轧差、闲置现金不冲抵;授信部分不追);
/// 恒 ≥ 0(只追不退,退还走平仓/出金)。通知书按当日现金追保全额展示,
/// 累计未补足欠款体现在账户余额(现金结存)中,不并入本字段。
/// </summary> /// </summary>
public static double CalcContractDimensionCallMargin(double cashBalance, double totalCredit, double usedCredit) public static double CalcContractDimensionCallMargin(double todayCashAdditionalMargin)
{ {
return Math.Max(Math.Round(-(cashBalance + totalCredit - usedCredit), 2, MidpointRounding.AwayFromZero), 0); return Math.Max(Math.Round(todayCashAdditionalMargin, 2, MidpointRounding.AwayFromZero), 0);
}
/// <summary>
/// 可取资金(客户维度,MarginWatchRule=1,定稿 2026-08-28 新增):
/// Max(现金结存 + 初始保证金 − 维持保证金 + min(持仓盈亏, 0), 0)。
/// 可用资金含授信但授信不可取现,可取资金只算现金部分;min(持仓盈亏,0) 只扣未实现亏损、浮盈不放行;
/// 客户维度追保不产生现金流,需显式叠加保证金约束(+初始−维持)。
/// initialMargin/maintenanceMargin 均为净收取为正。
/// </summary>
public static double CalcClientDimensionDesirableFund(double cashBalance, double initialMargin,
double maintenanceMargin, double positionPnl)
{
return Math.Max(cashBalance + initialMargin - maintenanceMargin + Math.Min(positionPnl, 0), 0);
}
/// <summary>
/// 可取资金(合约维度,MarginWatchRule=0,定稿 2026-08-28 新增):
/// Max(现金结存 + min(持仓盈亏, 0), 0)。
/// 合约维度追保已通过资金记录反映在现金结存中,无需再叠加保证金约束。
/// </summary>
public static double CalcContractDimensionDesirableFund(double cashBalance, double positionPnl)
{
return Math.Max(cashBalance + Math.Min(positionPnl, 0), 0);
} }
} }
} }
@@ -1,6 +1,7 @@
using YLErp.BLL; using YLErp.BLL;
using YLErp.DBModels; using YLErp.DBModels;
using YLErp.Enums; using YLErp.Enums;
using YLErp.Helpers;
using YLErp.Modules.MarginModule; using YLErp.Modules.MarginModule;
namespace YLErp.Modules.SwapModule.Margin namespace YLErp.Modules.SwapModule.Margin
@@ -18,6 +19,8 @@ namespace YLErp.Modules.SwapModule.Margin
/// </summary> /// </summary>
public static class SwapSpanBalanceQueryService public static class SwapSpanBalanceQueryService
{ {
private static readonly IYcLogger logger = LogFactory.GetLogger("预付金缺口查询");
/// <summary> /// <summary>
/// 客户维度输入:互换初始保证金(净收取为正,按客户汇总)。 /// 客户维度输入:互换初始保证金(净收取为正,按客户汇总)。
/// 初始保证金 = 应付预付金流水收付净额 + 初始预付金授信占用净额(非"追加保证金"前缀、关联交易)—— /// 初始保证金 = 应付预付金流水收付净额 + 初始预付金授信占用净额(非"追加保证金"前缀、关联交易)——
@@ -95,6 +98,11 @@ namespace YLErp.Modules.SwapModule.Margin
.Select(t => t.id) .Select(t => t.id)
.ToHashSet(); .ToHashSet();
maintenance = maintenance.Where(x => rule15TradeIds.Contains(x.TradeId)).ToList(); maintenance = maintenance.Where(x => rule15TradeIds.Contains(x.TradeId)).ToList();
var droppedCount = spanTradeIds.Count - rule15TradeIds.Count;
if (droppedCount > 0)
{
logger.Info($"预付金缺口查询:{valueDate:yyyy-MM-dd} {droppedCount}笔有span交易模板非规则15(或三级未命中),不计入交易维度追加合计");
}
if (maintenance.Count == 0) if (maintenance.Count == 0)
{ {
return result; return result;
@@ -142,7 +150,38 @@ namespace YLErp.Modules.SwapModule.Margin
result[group.Key] = total; result[group.Key] = total;
} }
logger.Info($"预付金缺口查询:{valueDate:yyyy-MM-dd} 规则15交易{rule15TradeIds.Count}笔,客户{result.Count}个,交易维度追加合计:{string.Join("", result.Select(kv => $"{kv.Key}={kv.Value:0.00}"))}");
return result; return result;
} }
/// <summary>
/// 合约维度追保金额输入(2026-08-28 新口径):当日产生的追加保证金资金记录中"资金来源=现金"的金额合计(按客户)。
/// 仅统计 Action=系统操作_追加保证金 且 HappenDate=当日 的现金流水(−Σ(Money),收取为正)——
/// 授信部分只写 client_credit_inout 不产生资金记录(授信不追、现金才追);
/// 腿化改造后现金流水逐日增量(每日新腿一条、HappenDate=结算日,同日重跑先清后建,幂等);
/// 该 Action 仅 EOD 追保会写(手工链路写应付预付金,语义天然隔离);追保回落不返还(无负记录),结果天然 ≥ 0。
/// 注意:EOD 未跑(日间实时)时当日无记录,结果为 0——与通知书口径一致。
/// </summary>
public static Dictionary<int, double> GetTodayCashAdditionalMarginByClients(List<int> clientIds, DateTime valueDate, YLContext db)
{
if (clientIds == null || clientIds.Count == 0)
{
return new Dictionary<int, double>();
}
var dayStart = valueDate.Date;
var dayEnd = dayStart.AddDays(1);
return db.ClientCashInCashOut.AsNoTracking()
.Where(x => clientIds.Contains(x.ClientId ?? 0)
&& x.Action == ClientCashInCashOut._追加保证金
&& x.HappenDate >= dayStart && x.HappenDate < dayEnd
&& x.ValidState != ConsGlobal.InValid
&& (x.State == ClientCashInCashOut. || x.State == ClientCashInCashOut.)
&& x.Money != null)
.GroupBy(x => x.ClientId ?? 0)
.Select(g => new { ClientId = g.Key, Sum = -g.Sum(x => x.Money ?? 0d) })
.ToDictionary(x => x.ClientId, x => x.Sum);
}
} }
} }
@@ -2,6 +2,7 @@ using YLErp.BLL;
using YLErp.DBModels; using YLErp.DBModels;
using YLErp.Modules.SwapModule.Margin; using YLErp.Modules.SwapModule.Margin;
using YLErp.Modules.TradeModule; using YLErp.Modules.TradeModule;
using YLErp.Helpers;
namespace YLErp.Modules.SwapModule namespace YLErp.Modules.SwapModule
{ {
@@ -15,6 +16,8 @@ namespace YLErp.Modules.SwapModule
/// </summary> /// </summary>
public class SwapFundTagService : YLBaseService public class SwapFundTagService : YLBaseService
{ {
private static readonly IYcLogger logger = LogFactory.GetLogger("预付金簿记");
public SwapFundTagService(OptUserInfo userInfo) : base(userInfo) public SwapFundTagService(OptUserInfo userInfo) : base(userInfo)
{ {
} }
@@ -167,6 +170,7 @@ namespace YLErp.Modules.SwapModule
.ThenBy(x => x.Leg.id) .ThenBy(x => x.Leg.id)
.ToList(); .ToList();
var plans = FundTagCalc.AllocateByLegPreference(allocateLegs, (decimal)creditAvailable, ignoreMoneyCheck); var plans = FundTagCalc.AllocateByLegPreference(allocateLegs, (decimal)creditAvailable, ignoreMoneyCheck);
logger.Info($"预付金簿记:交易{td.id}(客户{td.ClientId})簿记日{valueDate:yyyy-MM-dd}Action={cashAction})可用授信={creditAvailable:0.00}{marginLegs.Count}条预付金腿分配:{(plans.Any() ? string.Join("", plans.Select(p => $"{p.Leg.id}{p.Amount:0.00}{p.CreditAmount:0.00}/{p.CashAmount:0.00}{(p.NeedSplit ? "(拆单)" : "")}")) : "")}");
//先落库拆分的新现金腿(需要 id 才能绑定现金流水) //先落库拆分的新现金腿(需要 id 才能绑定现金流水)
foreach (var plan in plans.Where(p => p.NeedSplit)) foreach (var plan in plans.Where(p => p.NeedSplit))
@@ -1,6 +1,7 @@
using YLErp.DBModels; using YLErp.DBModels;
using YLErp.DBModels.Consts; using YLErp.DBModels.Consts;
using YLErp.DBModels.Enums; using YLErp.DBModels.Enums;
using YLErp.Helpers;
using YLErp.Models; using YLErp.Models;
using YLErp.Modules.SwapModule; using YLErp.Modules.SwapModule;
using YLErp.Modules.SwapModule.ReturnLegs; using YLErp.Modules.SwapModule.ReturnLegs;
@@ -9,7 +10,7 @@ namespace YLErp.Modules.TradeModule.DocGenerateModule;
/// <summary> /// <summary>
/// 构造普通收益互换结算单单行数据所需的业务输入。 /// 构造普通收益互换结算单单行数据所需的业务输入。
/// 数据来自平仓事件关联交易及结算日日终持仓 /// 数据来自平仓事件及其关联交易。
/// </summary> /// </summary>
public sealed class SwapSettlementBillRowInput public sealed class SwapSettlementBillRowInput
{ {
@@ -40,9 +41,6 @@ public sealed class SwapSettlementBillRowInput
/// <summary>平仓事件浮动腿记录的期末结算收益率(展示态数值)。</summary> /// <summary>平仓事件浮动腿记录的期末结算收益率(展示态数值)。</summary>
public decimal? ExitYtm { get; set; } public decimal? ExitYtm { get; set; }
/// <summary>结算日日终持仓中的当日浮动端分红,保留原始收付方向。</summary>
public decimal CurrentDayFloatingDividend { get; set; }
/// <summary>期间付息或分红是否计入本次净额结算。</summary> /// <summary>期间付息或分红是否计入本次净额结算。</summary>
public bool IncludePeriodPaymentInNetting { get; set; } public bool IncludePeriodPaymentInNetting { get; set; }
} }
@@ -89,6 +87,7 @@ public static class SwapSettlementBillRowBuilder
var floatingAmount = -input.CloseFlow.MarkClosePnl; var floatingAmount = -input.CloseFlow.MarkClosePnl;
var fee = -(input.CloseFlow.TradingFee + input.CloseFlow.TradingFeePending); var fee = -(input.CloseFlow.TradingFee + input.CloseFlow.TradingFeePending);
var marginInterest = -marginEvents.Sum(x => x.InterestClosePnL); var marginInterest = -marginEvents.Sum(x => x.InterestClosePnL);
var periodAmount = -input.CloseFlow.DividendPending;
var initialMargin = SumMargin(effectiveMargins, InterestModeEnum.); var initialMargin = SumMargin(effectiveMargins, InterestModeEnum.);
var additionalMarginPositions = positions var additionalMarginPositions = positions
@@ -106,6 +105,16 @@ public static class SwapSettlementBillRowBuilder
var isCashBond = ConsGlobal.InstrumentType.IsBond(input.UnderlyingInstrumentType); var isCashBond = ConsGlobal.InstrumentType.IsBond(input.UnderlyingInstrumentType);
var isEtf = ConsGlobal.InstrumentType.Fund.Equals(
input.UnderlyingInstrumentType,
StringComparison.OrdinalIgnoreCase);
var initialPrice = settlementPosition?.PosiGrossPrice ?? 0m;
var closePrice = input.CloseFlow.TradingAmountAvg;
if (isCashBond)
{
initialPrice = BondPriceConverter.ToDisplay(initialPrice);
closePrice = BondPriceConverter.ToDisplay(closePrice);
}
return new ExcelReportModel return new ExcelReportModel
@@ -121,16 +130,16 @@ public static class SwapSettlementBillRowBuilder
PosiNotionalValue = input.CloseNotionalValue.ToString("0.00"), PosiNotionalValue = input.CloseNotionalValue.ToString("0.00"),
Quantity = input.CloseFlow.Quantity.ToString("0.00"), Quantity = input.CloseFlow.Quantity.ToString("0.00"),
DividendIn = isCashBond DividendIn = isCashBond
? input.CurrentDayFloatingDividend.ToString("0.00") ? periodAmount.ToString("0.00")
: string.Empty, : string.Empty,
PeriodDividend = isCashBond PeriodDividend = isEtf
? string.Empty ? periodAmount.ToString("0.00")
: input.CurrentDayFloatingDividend.ToString("0.00"), : string.Empty,
PosiNetPrice = ((settlementPosition?.PosiGrossPrice ?? 0m) * 100m).ToString("0.00000000"), PosiNetPrice = initialPrice.ToString("0.00000000"),
InitYtm = isCashBond && input.Trade.InitYtm.HasValue InitYtm = isCashBond && input.Trade.InitYtm.HasValue
? input.Trade.InitYtm.Value.ToString("0.####%") ? input.Trade.InitYtm.Value.ToString("0.####%")
: string.Empty, : string.Empty,
ClosePrice = (input.CloseFlow.TradingAmountAvg * 100m).ToString("0.00000000"), ClosePrice = closePrice.ToString("0.00000000"),
ExitYtm = input.ExitYtm.HasValue ExitYtm = input.ExitYtm.HasValue
? input.ExitYtm.Value.ToString("0.0000") ? input.ExitYtm.Value.ToString("0.0000")
: string.Empty, : string.Empty,
+3 -1
View File
@@ -334,7 +334,9 @@ namespace YLErp.BLL
.FirstOrDefault(); .FirstOrDefault();
if (oa != null) if (oa != null)
{ {
tradeLinq.OaRemark = string.IsNullOrWhiteSpace(oa.oa_msg) tradeLinq.OaRemark = oa.status == "提交失败"
? oa.status
: string.IsNullOrWhiteSpace(oa.oa_msg)
? oa.status ? oa.status
: oa.status + "" + oa.oa_msg; : oa.status + "" + oa.oa_msg;
} }
@@ -30,16 +30,21 @@ namespace YLErp.Modules.UnderlyingModule
public string InvestAdvisorName { get; set; } public string InvestAdvisorName { get; set; }
} }
private const string LookupSql = @" //物理库名由连接串 database= 决定(DbSchema 解析),各环境库名不同(如测试环境 glms_bigdata),
//SQL 不硬编码库名;连接开在 bigdata 数据源上,将来跨库 join ERP 主库时用 DbSchema.Of("ylcms") 同法插值
private string BuildLookupSql()
{
return $@"
SELECT SELECT
ia.investadvisorcode AS InvestAdvisorCode, ia.investadvisorcode AS InvestAdvisorCode,
ia.investadvisorname AS InvestAdvisorName ia.investadvisorname AS InvestAdvisorName
FROM glms_bigdata.mf_fundarchives AS fa FROM {DbSchema.Of("bigdata")}.mf_fundarchives AS fa
INNER JOIN glms_bigdata.mf_investadvisoroutline AS ia INNER JOIN {DbSchema.Of("bigdata")}.mf_investadvisoroutline AS ia
ON CONVERT(fa.investadvisorcode USING utf8mb4) COLLATE utf8mb4_unicode_ci = ON CONVERT(fa.investadvisorcode USING utf8mb4) COLLATE utf8mb4_unicode_ci =
CONVERT(ia.investadvisorcode USING utf8mb4) COLLATE utf8mb4_unicode_ci CONVERT(ia.investadvisorcode USING utf8mb4) COLLATE utf8mb4_unicode_ci
WHERE CONVERT(fa.secucode USING utf8mb4) COLLATE utf8mb4_unicode_ci = WHERE CONVERT(fa.secucode USING utf8mb4) COLLATE utf8mb4_unicode_ci =
CONVERT(TRIM(SUBSTRING_INDEX(@UnderlyingCode, '.', 1)) USING utf8mb4) COLLATE utf8mb4_unicode_ci"; CONVERT(TRIM(SUBSTRING_INDEX(@UnderlyingCode, '.', 1)) USING utf8mb4) COLLATE utf8mb4_unicode_ci";
}
public FundManagerLookupResult Lookup(string underlyingCode) public FundManagerLookupResult Lookup(string underlyingCode)
{ {
@@ -49,7 +54,7 @@ WHERE CONVERT(fa.secucode USING utf8mb4) COLLATE utf8mb4_unicode_ci =
return new FundManagerLookupResult { Status = FundManagerLookupStatus.NotFound }; return new FundManagerLookupResult { Status = FundManagerLookupStatus.NotFound };
} }
var connectionString = AppManager.GetConnectionString("glms_bigdata"); var connectionString = AppManager.GetConnectionString("bigdata");
if (string.IsNullOrWhiteSpace(connectionString)) if (string.IsNullOrWhiteSpace(connectionString))
{ {
return new FundManagerLookupResult { Status = FundManagerLookupStatus.Unavailable }; return new FundManagerLookupResult { Status = FundManagerLookupStatus.Unavailable };
@@ -58,7 +63,7 @@ WHERE CONVERT(fa.secucode USING utf8mb4) COLLATE utf8mb4_unicode_ci =
try try
{ {
using var connection = new MySqlConnection(connectionString); using var connection = new MySqlConnection(connectionString);
var matches = connection.Query<FundManagerRow>(LookupSql, new { UnderlyingCode = normalizedCode }, commandTimeout: 10) var matches = connection.Query<FundManagerRow>(BuildLookupSql(), new { UnderlyingCode = normalizedCode }, commandTimeout: 10)
.Where(row => !string.IsNullOrWhiteSpace(row.InvestAdvisorName)) .Where(row => !string.IsNullOrWhiteSpace(row.InvestAdvisorName))
.GroupBy(row => (row.InvestAdvisorCode ?? string.Empty).Trim(), StringComparer.OrdinalIgnoreCase) .GroupBy(row => (row.InvestAdvisorCode ?? string.Empty).Trim(), StringComparer.OrdinalIgnoreCase)
.Select(group => group.Select(row => row.InvestAdvisorName.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToArray()) .Select(group => group.Select(row => row.InvestAdvisorName.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToArray())
@@ -33,6 +33,11 @@ namespace YLErp.Web.Controllers
else else
{ {
var marginTemplate = yldb.margin_template_v2.Find(template.MarginTemplateId); var marginTemplate = yldb.margin_template_v2.Find(template.MarginTemplateId);
//绑定模板不在上方下拉筛选范围内(如已失效)时,Find 得到的是未挂 Details 的新实例,右栏只读回显会空白
if (marginTemplate.Details.Count == 0)
{
marginTemplate.Details = yldb.margin_template_detail.Where(y => y.MarginTemplateId == marginTemplate.id).ToList();
}
ViewBag.MarginTemplate = marginTemplate; ViewBag.MarginTemplate = marginTemplate;
} }
return View(template); return View(template);
@@ -51,7 +51,7 @@
@if (pageObj.isAdd) @if (pageObj.isAdd)
{ {
@*新增:客户多选(类似适用簿记账户),保存时按选中客户逐条调用原有单条保存接口*@ @*新增:客户多选(类似适用簿记账户),保存时按选中客户逐条调用原有单条保存接口*@
<select id="ClientId" class="chosen-select" multiple data-placeholder="请选择客户" style="width:260px;" v-model="selectedClientIds"> <select id="ClientId" class="chosen-select" multiple data-placeholder="请选择客户" style="width:260px;">
<option v-for="item in clientNames" v-bind:value="item.Value">{{item.Text}}</option> <option v-for="item in clientNames" v-bind:value="item.Value">{{item.Text}}</option>
</select> </select>
} }
@@ -104,8 +104,69 @@
</div> </div>
<div class="col form-layout" style="height: 520px; overflow-y:auto;"> <div class="col form-layout" style="height: 520px; overflow-y:auto;">
@*区间追保结构(规则15)只读回显:参数组区块与 margin_template_v2Edit 同款样式(本页为绑定信息页,参数本身在预付金模板V2维护,故全部只读)*@
<template v-if="marginTemplate.RuleType == @((int)MarginRuleTypeEnum.区间追保结构)">
<div v-for="blk in ruleRangeBlocks" v-bind:key="'spanblk'+blk.key" class="border spanBlk" style="padding:6px 10px; margin-top:10px;">
<p style="margin:4px 0; font-weight:bold;">
参数组{{blk.no}}<span v-if="marginTemplate.UnderlyingSeperateType == @((int)UnderlyingSeperateTypeEnum.CustomInstrumentType)">资产类型:{{underlyingTypeNames(blk.ut)}}</span>
<span v-if="blk.etfKind"> ETF 子类:{{blk.etfKind}}</span>
<span v-if="blk.isTBond">(按期限分档:≤5y、(5y-10y]、(10y-30y]、&gt;30y,每个档位独立设置)</span>
</p>
<div v-for="sec in blk.sections" v-bind:key="sec.key">
<p v-if="blk.isTBond" style="margin:8px 0 2px; font-weight:bold;">期限档位:{{sec.termLabel}}</p>
<template v-if="sec.detail && sec.detail.SpanConfig">
<p style="margin:4px 0;">
预警线:<vue-number-input v-model="sec.detail.SpanConfig.WarnLine" v-bind:format="inputFormatPercent" disabled></vue-number-input>
 平仓线:<vue-number-input v-model="sec.detail.SpanConfig.CloseLine" v-bind:format="inputFormatPercent" disabled></vue-number-input>
</p>
<p style="margin:4px 0;">多头方向(客户看多)</p>
<table class="table table-bordered" style="margin-bottom:6px;">
<thead>
<tr><th>追保价格区间</th><th>追保金额</th></tr>
</thead>
<tbody>
<tr v-for="(tier, ti) in sec.detail.SpanConfig.LongSpans" v-bind:key="'long'+ti">
<td>
<template v-if="ti === 0">
<vue-number-input v-model="tier.Lower" v-bind:format="inputFormatPercent" disabled></vue-number-input> × {{spanText(sec.detail).priceInit}} ≤ {{spanText(sec.detail).priceCur}}
</template>
<template v-else>
<vue-number-input v-model="tier.Lower" v-bind:format="inputFormatPercent" disabled></vue-number-input> × {{spanText(sec.detail).priceInit}} ≤ {{spanText(sec.detail).priceCur}} &lt; <vue-number-input v-model="tier.Upper" v-bind:format="inputFormatPercent" disabled></vue-number-input> × {{spanText(sec.detail).priceInit}}
</template>
</td>
<td>
<vue-number-input v-model="tier.AmountRate" v-bind:format="inputFormatPercent" disabled></vue-number-input> × {{spanText(sec.detail).amountBase}}
</td>
</tr>
</tbody>
</table>
<p style="margin:4px 0;">空头方向(客户看空)</p>
<table class="table table-bordered" style="margin-bottom:0;">
<thead>
<tr><th>追保价格区间</th><th>追保金额</th></tr>
</thead>
<tbody>
<tr v-for="(tier, ti) in sec.detail.SpanConfig.ShortSpans" v-bind:key="'short'+ti">
<td>
<template v-if="ti === 0">
{{spanText(sec.detail).priceCurShort1}} ≤ <vue-number-input v-model="tier.Upper" v-bind:format="inputFormatPercent" disabled></vue-number-input> × {{spanText(sec.detail).priceInit}}
</template>
<template v-else>
<vue-number-input v-model="tier.Lower" v-bind:format="inputFormatPercent" disabled></vue-number-input> × {{spanText(sec.detail).priceInit}} &lt; {{spanText(sec.detail).priceCurShort}} ≤ <vue-number-input v-model="tier.Upper" v-bind:format="inputFormatPercent" disabled></vue-number-input> × {{spanText(sec.detail).priceInit}}
</template>
</td>
<td>
<vue-number-input v-model="tier.AmountRate" v-bind:format="inputFormatPercent" disabled></vue-number-input> × {{spanText(sec.detail).amountBase}}
</td>
</tr>
</tbody>
</table>
</template>
</div>
</div>
</template>
<template v-for="(detail,index) in marginTemplate.Details"> <template v-for="(detail,index) in marginTemplate.Details">
<div class="border detail"> <div class="border detail" v-show="marginTemplate.RuleType != @((int)MarginRuleTypeEnum.区间追保结构)">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col"> <div class="col">
<template v-if="marginTemplate.RuleType == @((int)MarginRuleTypeEnum.按固定利率) || marginTemplate.RuleType == @((int)MarginRuleTypeEnum.按浮动盈亏)"> <template v-if="marginTemplate.RuleType == @((int)MarginRuleTypeEnum.按固定利率) || marginTemplate.RuleType == @((int)MarginRuleTypeEnum.按浮动盈亏)">
@@ -1,6 +1,6 @@
@using YLErp.Enums; @using YLErp.Enums;
@{ @{
ViewBag.Title = "预付金模板V2"; ViewBag.Title = "预付金模板";
ViewBag.Menu = "数据管理-预付金模板V2"; ViewBag.Menu = "数据管理-预付金模板V2";
Layout = "~/Views/Shared/_MainLayout.cshtml"; Layout = "~/Views/Shared/_MainLayout.cshtml";
} }
@@ -70,7 +70,7 @@
function setClientMarginTemplate(id) { function setClientMarginTemplate(id) {
var editurl = "/client_margin_template/client_margin_templateEdit/?id=" + id; var editurl = "/client_margin_template/client_margin_templateEdit/?id=" + id;
main.infopage("设置客户预付金", editurl, { area: ['1480px', '800px'] }); main.infopage(id ? "编辑客户预付金绑定" : "新增客户预付金绑定", editurl, { area: ['1480px', '800px'] });
} }
function deleteClientMarginTemplate(id) { function deleteClientMarginTemplate(id) {
+23 -2
View File
@@ -60,6 +60,14 @@
changeFormCredit(Number(moneytemp).toFixed(2) + ""); changeFormCredit(Number(moneytemp).toFixed(2) + "");
} }
//授信可用比例默认填1;授信额度置灰,按 原始授信值×最大授信可用比例 联动计算
if (!$("#MaxCreditUseRatio").val()) {
$("#MaxCreditUseRatio").val("1");
}
$("#OriginalCredit").on("input change", calcCreditByRatio);
$("#MaxCreditUseRatio").on("input change", calcCreditByRatio);
calcCreditByRatio();
//$('#Credit').keyup(function () { //$('#Credit').keyup(function () {
// changePrice(); // changePrice();
//}); //});
@@ -134,6 +142,19 @@
} }
} }
//授信额度 = 原始授信值 × 最大授信可用比例(空白按1);原始授信值为空时不折算,保留原授信额度
function calcCreditByRatio() {
var original = $("#OriginalCredit").val();
if (original == null || original == "") {
return;
}
var ratio = $("#MaxCreditUseRatio").val();
var money = (Number(original.replace(/,/g, "")) * (ratio == "" ? 1 : Number(ratio))).toFixed(2);
$("#Credit").val(money);
$("#CreditVal").val(money);
changeFormCredit(money);
}
function checkSubmitData() { function checkSubmitData() {
var pass = $('#creditEditForm').valid(); var pass = $('#creditEditForm').valid();
if (pass) { if (pass) {
@@ -416,11 +437,11 @@
</div> </div>
} }
@Html.MyDecimalFor(model => model.Credit, new { onfocus = "moneyOnFocus()", onblur = "this.value=cc(this.value);" }, !pageObj.UseClientStockEqvNotional) @Html.MyDecimalFor(model => model.Credit, new { @readonly = "readonly", style = "background-color:#eee;" }, !pageObj.UseClientStockEqvNotional)
<input type="hidden" value="@Model.Credit" name="CreditVal" id="CreditVal" /> <input type="hidden" value="@Model.Credit" name="CreditVal" id="CreditVal" />
@Html.MyDecimalFor(model => model.OriginalCredit, new { onfocus = "moneyOnFocusOriginalCredit()", onblur = "this.value=cc(this.value);" }, required: false) @Html.MyDecimalFor(model => model.OriginalCredit, new { onfocus = "moneyOnFocusOriginalCredit()", onblur = "this.value=cc(this.value);" }, required: false)
@Html.MyDecimalFor(model => model.MaxCreditUseRatio, required: false) @Html.MyDecimalFor(model => model.MaxCreditUseRatio, required: false)
<span style="color:red">注:填写原始授信值后,授信额度按"原始授信值×最大授信可用比例(空白按1)"自动计算.</span> <span style="color:red">注:授信额度按"原始授信值×最大授信可用比例(默认1)"自动计算,无需填写;原始授信值为空时不折算,保留原授信额度.</span>
@if (PS.Config.Company == CompanyEnum.中金) @if (PS.Config.Company == CompanyEnum.中金)
{ {
@Html.MyDecimalFor(model => model.PFECredit, new { onkeyup = "changePFECredit()", onfocus = "pfeCreditOnFocus()", onblur = "this.value=cc(this.value);" }, true) @Html.MyDecimalFor(model => model.PFECredit, new { onkeyup = "changePFECredit()", onfocus = "pfeCreditOnFocus()", onblur = "this.value=cc(this.value);" }, true)
@@ -1,5 +1,5 @@
@{ @{
ViewBag.Title = "预付金模板V2"; ViewBag.Title = "预付金模板";
ViewBag.Menu = "数据管理-预付金模板V2"; ViewBag.Menu = "数据管理-预付金模板V2";
Layout = "~/Views/Shared/_MainLayout.cshtml"; Layout = "~/Views/Shared/_MainLayout.cshtml";
} }
@@ -119,13 +119,13 @@
if (id) { if (id) {
editurl = "/margin_template_v2/margin_template_v2ClientEdit/?enid=" + id; editurl = "/margin_template_v2/margin_template_v2ClientEdit/?enid=" + id;
} }
main.infopage("编辑默认预付金模板", editurl, { area: ['1480px', '800px'] }); main.infopage(id ? "编辑客户预付金模板" : "新增客户预付金模板", editurl, { area: ['1480px', '800px'] });
} }
function startCopymargin_template_v2(id) { function startCopymargin_template_v2(id) {
var editurl = "/margin_template_v2/margin_template_v2ClientCopy/?enid=" + id; var editurl = "/margin_template_v2/margin_template_v2ClientCopy/?enid=" + id;
main.infopage("复制默认预付金模板", editurl, { area: ['1480px', '800px'] }); main.infopage("复制客户预付金模板", editurl, { area: ['1480px', '800px'] });
} }
function deleteEditmargin_template_v2(id) { function deleteEditmargin_template_v2(id) {
@@ -1,5 +1,5 @@
@{ @{
ViewBag.Title = "预付金模板V2"; ViewBag.Title = "预付金模板";
ViewBag.Menu = "数据管理-预付金模板V2"; ViewBag.Menu = "数据管理-预付金模板V2";
Layout = "~/Views/Shared/_MainLayout.cshtml"; Layout = "~/Views/Shared/_MainLayout.cshtml";
} }
@@ -127,12 +127,12 @@
if (id) { if (id) {
editurl = "/margin_template_v2/margin_template_v2DefaultEdit/?enid=" + id; editurl = "/margin_template_v2/margin_template_v2DefaultEdit/?enid=" + id;
} }
main.infopage("编辑默认预付金模板", editurl, { area: ['1480px', '800px'] }); main.infopage(id ? "编辑全局预付金模板" : "新增全局预付金模板", editurl, { area: ['1480px', '800px'] });
} }
function startCopymargin_template_v2(id) { function startCopymargin_template_v2(id) {
editurl = "/margin_template_v2/margin_template_v2DefaultCopy/?enid=" + id; editurl = "/margin_template_v2/margin_template_v2DefaultCopy/?enid=" + id;
main.infopage("复制默认预付金模板", editurl, { area: ['1480px', '800px'] }); main.infopage("复制全局预付金模板", editurl, { area: ['1480px', '800px'] });
} }
function deleteEditmargin_template_v2(id) { function deleteEditmargin_template_v2(id) {
@@ -1,6 +1,6 @@
@using YLErp.Enums; @using YLErp.Enums;
@{ @{
ViewBag.Title = "预付金模板V2"; ViewBag.Title = "预付金模板";
ViewBag.Menu = "数据管理-预付金模板V2"; ViewBag.Menu = "数据管理-预付金模板V2";
Layout = "~/Views/Shared/_MainLayout.cshtml"; Layout = "~/Views/Shared/_MainLayout.cshtml";
} }
@@ -134,13 +134,13 @@
if (id) { if (id) {
editurl = "/margin_template_v2/margin_template_v2Edit/?enid=" + id; editurl = "/margin_template_v2/margin_template_v2Edit/?enid=" + id;
} }
main.infopage("编辑预付金模板", editurl, { area: ['1180px', '92%'] }); main.infopage(id ? "编辑自定义预付金模板" : "新增自定义预付金模板", editurl, { area: ['1180px', '92%'] });
} }
function startCopymargin_template_v2(id) { function startCopymargin_template_v2(id) {
editurl = "/margin_template_v2/margin_template_v2Copy/?enid=" + id; editurl = "/margin_template_v2/margin_template_v2Copy/?enid=" + id;
main.infopage("复制预付金模板", editurl, { area: ['1180px', '92%'] }); main.infopage("复制自定义预付金模板", editurl, { area: ['1180px', '92%'] });
} }
function startDeletemargin_template_v2(id) { function startDeletemargin_template_v2(id) {
+2 -1
View File
@@ -10,7 +10,8 @@
"ylcms": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_ylcms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;", "ylcms": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_ylcms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;",
"yladmin": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_admin;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;", "yladmin": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_admin;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;",
"ylclient": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_client;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;", "ylclient": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_client;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;",
"bondoms": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_bond_oms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;" "bondoms": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_bond_oms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;",
"bigdata": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_bigdata;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;"
}, },
"AppSettings": { "AppSettings": {
"VirtualPathRoot": "", "VirtualPathRoot": "",
@@ -3,6 +3,21 @@ const inputFormatPercent = Object.freeze({ precision: 2, append: '%' });
const inputFormatPercentNegative = Object.freeze({ precision: 2, append: '%', negative: true }); const inputFormatPercentNegative = Object.freeze({ precision: 2, append: '%', negative: true });
const inputFormatNegative = Object.freeze({ precision: 2, negative: true }); const inputFormatNegative = Object.freeze({ precision: 2, negative: true });
//区间追保结构(RuleType=15)只读回显用常量,与 marginTemplateV2Edit.js 保持一致
const RangeMarginRuleType = 15;
//允许配期限档(SpanConfig.BondTerm)的资产类型掩码,本期仅利率债
const TermTierEnabledUnderlyingMask = 16;
//利率债区块固定展开的4个期限档([BondTerm值, 展示文案]
const SpanBondTerms = [['<5y', '≤5y'], ['5y-10y', '(5y-10y]'], ['10y-30y', '(10y-30y]'], ['>30y', '>30y']];
//标的资产类型标志位 -> 名称(与 marginTemplateV2Edit 的录入区块标题一致)
const UnderlyingTypeNames = [
[16, "利率债"], [32, "信用债"], [64, "其它债券"], [128, "股票"], [256, "股指"], [512, "股指期货"],
[1024, "商品期货"], [2048, "商品现货"], [4096, "新三板挂牌股票"], [8192, "香港股票"], [16384, "香港股指"],
[32768, "基金及基金专户"], [65536, "黄金期货"], [131072, "国债期货"], [262144, "其他期货"], [524288, "黄金现货"],
[1048576, "其他现货"], [2097152, "境外期货"], [4194304, "境外现货"], [8388608, "境外股票"], [16777216, "境外股指"],
[33554432, "汇率"], [67108864, "Shibor"], [134217728, "银行间回购定盘"], [268435456, "利率收益率"], [536870912, "债券指数"]
];
const vue = new Vue({ const vue = new Vue({
el: '#marginTemplateV2Form', el: '#marginTemplateV2Form',
data: { data: {
@@ -10,14 +25,92 @@ const vue = new Vue({
marginTemplates: page.marginTemplates, marginTemplates: page.marginTemplates,
marginTemplate: page.marginTemplate, marginTemplate: page.marginTemplate,
clientNames: [], clientNames: [],
//新增模式的客户多选(编辑模式仍为单条绑定,走 clientMarginTemplate.ClientId
selectedClientIds: [],
isAdd: page.isAdd isAdd: page.isAdd
}, },
created: function () { created: function () {
this.clientMarginTemplate.ValueDate = new moment(this.clientMarginTemplate.ValueDate).format("YYYY-MM-DD"); this.clientMarginTemplate.ValueDate = new moment(this.clientMarginTemplate.ValueDate).format("YYYY-MM-DD");
}, },
computed: {
//区间追保结构只读参数组:连续利率债行归一个区块、按固定4档对位(缺档不补造、同档重复/异常档独立成段保留展示);
//其余行(含ETF子类行)每行独立成块——分组口径与 marginTemplateV2Edit.rebuildSpanBlocks 一致,但只回显不新增行
ruleRangeBlocks() {
if (this.marginTemplate.RuleType != RangeMarginRuleType) return [];
var details = this.marginTemplate.Details || [];
var that = this;
var blocks = [];
var i = 0;
while (i < details.length) {
if ((details[i].UnderlyingType || 0) === TermTierEnabledUnderlyingMask) {
var j = i;
while (j < details.length && (details[j].UnderlyingType || 0) === TermTierEnabledUnderlyingMask) j++;
var byTerm = {};
var extras = [];
for (var k = i; k < j; k++) {
var bt = (details[k].SpanConfig && details[k].SpanConfig.BondTerm) || '';
var canonical = SpanBondTerms.some(function (t) { return t[0] === bt; });
if (canonical && !byTerm[bt]) byTerm[bt] = details[k]; else extras.push(details[k]);
}
var sections = SpanBondTerms.map(function (t) {
return { key: 't' + t[0], termLabel: t[1], detail: byTerm[t[0]] || null };
});
extras.forEach(function (r, xi) {
sections.push({ key: 'x' + xi, termLabel: that.bondTermLabel(r), detail: r });
});
blocks.push({ key: 'i' + i, no: blocks.length + 1, ut: TermTierEnabledUnderlyingMask, isTBond: true, etfKind: '', sections: sections });
i = j;
} else {
var d = details[i];
blocks.push({
key: 'i' + i, no: blocks.length + 1, ut: d.UnderlyingType || 0, isTBond: false,
etfKind: (d.SpanConfig && d.SpanConfig.EtfKind) || '',
sections: [{ key: 'single', termLabel: '', detail: d }]
});
i++;
}
}
return blocks;
}
},
methods: { methods: {
//资产类型标志位掩码 -> 名称(0=全部),与 marginTemplateV2Edit.underlyingTypeNames 一致
underlyingTypeNames(mask) {
mask = parseInt(mask) || 0;
if (mask === 0) return "全部";
var names = [];
for (var i = 0; i < UnderlyingTypeNames.length; i++) {
if ((mask & UnderlyingTypeNames[i][0]) > 0) names.push(UnderlyingTypeNames[i][1]);
}
return names.length > 0 ? names.join("、") : "全部";
},
//期限档 BondTerm -> 展示文案(空=全部)
bondTermLabel(detail) {
var v = (detail.SpanConfig && detail.SpanConfig.BondTerm) || '';
var map = { '': '全部', '<5y': '≤5y', '5y-10y': '(5y-10y]', '10y-30y': '(10y-30y]', '>30y': '>30y' };
return map[v] !== undefined ? map[v] : v;
},
//区间计价口径文案:按 detail 资产类型切换,与取数侧对齐(债券类=中债估值净价/期初全价×券面总额,其余=收盘价口径),
//直接沿用 marginTemplateV2Edit.spanText 的判定
spanText(detail) {
var ut = (detail && detail.UnderlyingType) || 0;
var bondMask = 16 | 32 | 64;
var isBond = ut !== 0 && (ut & ~bondMask) === 0;
if (!isBond) {
return {
priceInit: '参考标的期初净价',
priceCur: '参考标的当前收盘价',
priceCurShort1: '参考标的当前收盘价',
priceCurShort: '参考标的当前收盘价',
amountBase: '参考标的期初价格 × 参考标的名义份额'
};
}
return {
priceInit: '期初净价',
priceCur: '当前净价',
priceCurShort1: '当前净价',
priceCurShort: '当前净价',
amountBase: '期初全价 × 券面总额'
};
},
changeMarginTemplate() { changeMarginTemplate() {
this.marginTemplates.forEach(x => { this.marginTemplates.forEach(x => {
if (this.clientMarginTemplate.MarginTemplateId === x.id) { if (this.clientMarginTemplate.MarginTemplateId === x.id) {
@@ -32,7 +125,12 @@ const vue = new Vue({
//当前内容上供修正重试(已成功的客户重试会因同天同结构互斥校验失败,不会重复入库) //当前内容上供修正重试(已成功的客户重试会因同天同结构互斥校验失败,不会重复入库)
saveMarginTemplateV2() { saveMarginTemplateV2() {
var thisObj = this; var thisObj = this;
var ids = this.isAdd ? (this.selectedClientIds || []) : [this.clientMarginTemplate.ClientId]; //取值直接读控件(与兄弟页面读 chosen 的 $("#tradeTypes").val() 一致,不依赖 v-model 同步):
//新增=多选控件 val()(数组);编辑=表单对象上的原绑定客户
var chosenVal = this.isAdd ? $("#ClientId").val() : this.clientMarginTemplate.ClientId;
var ids = this.isAdd
? (Array.isArray(chosenVal) ? chosenVal : (chosenVal ? [chosenVal] : []))
: [chosenVal];
if (!ids.length) { if (!ids.length) {
main.message("请至少选择一个客户"); main.message("请至少选择一个客户");
return; return;
@@ -1968,6 +1968,8 @@ function __init(vue) {
let clientId = page.Trade.ClientId; let clientId = page.Trade.ClientId;
let client = clientId ? consClients.find(x => x.id === clientId) : null; let client = clientId ? consClients.find(x => x.id === clientId) : null;
!client && page.isAdd && (client = consClients[0]); !client && page.isAdd && (client = consClients[0]);
//setData 只写控件显示值、不触发 onSelect 回写 v-model——新增时默认客户须显式回写,否则 trade.ClientId 为空、保存报"请设置交易对手方"
client && page.isAdd && vue.changeClient(client);
autoClient.setData(client); autoClient.setData(client);
_getMainProtocolCode(client); _getMainProtocolCode(client);
} }
@@ -1185,6 +1185,8 @@ function __init(vue) {
let clientId = page.Trade.ClientId; let clientId = page.Trade.ClientId;
let client = clientId ? consClients.find(x => x.id === clientId) : null; let client = clientId ? consClients.find(x => x.id === clientId) : null;
!client && page.IsNew && (client = consClients[0]); !client && page.IsNew && (client = consClients[0]);
//setData 只写控件显示值、不触发 onSelect 回写 v-model——新增时默认客户须显式回写,否则 trade.ClientId 为空、保存报"请设置交易对手方"
client && page.IsNew && (vue.trade.ClientId = client.id);
autoClient.setData(client); autoClient.setData(client);
_getMainProtocolCode(client); _getMainProtocolCode(client);
} }