#EQD-6948 国联民生-实现保证金规则(2)追保金额的产生与收盘计算 授信和现金tag

This commit is contained in:
锦麟 王
2026-08-21 10:33:29 +08:00
parent febd0ce02e
commit 7b1f69a410
27 changed files with 874 additions and 69 deletions
@@ -0,0 +1,81 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace YLErp.DBModels
{
/// <summary>
/// 客户授信出入表(R4 决策④):记录授信占用/释放/调整的全量变化。
/// 金额符号与资金流水同号(2026-08-20 业务确认):入金为负、出金为正;
/// 已使用授信 = SUM(amount) 直接求和,可用授信 = 有效授信 − 已使用授信(入金使可用授信上升、出金使其收缩)。
/// change_type(占用/释放/调整)仅作分类审计;业务字段名一律小写下划线(兼容 PostgreSQL)。
/// </summary>
[Table("client_credit_inout")]
public class client_credit_inout : DBModelBaseV2
{
/// <summary>
/// 变更类型:占用(簿记授信部分)
/// </summary>
public const int ChangeTypeOccupy = 1;
/// <summary>
/// 变更类型:释放(平仓/到期按原标签返还)
/// </summary>
public const int ChangeTypeRelease = 2;
/// <summary>
/// 变更类型:人工调整(授信审批额度变化/特批后补记)
/// </summary>
public const int ChangeTypeAdjust = 3;
/// <summary>
/// 客户
/// </summary>
public int client_id { get; set; }
/// <summary>
/// 关联预付金腿(swap_position.id,授信占用/释放绑定到那条腿;交易级占用/释放可为空)
/// </summary>
public long? position_id { get; set; }
/// <summary>
/// 关联交易(与 position_id 并存,冗余便于按交易查询;授信调整类可为空)
/// </summary>
public int? trade_id { get; set; }
/// <summary>
/// 变更类型 1=占用 2=释放 3=人工调整
/// </summary>
public int change_type { get; set; }
/// <summary>
/// 变更金额:与资金流水同号(入金为负、出金为正);入金使可用授信上升、出金使其收缩
/// </summary>
public double amount { get; set; }
/// <summary>
/// 变更后已使用授信(冗余快照,便于核对与报表)
/// </summary>
public double used_after { get; set; }
/// <summary>
/// 发生日期
/// </summary>
public DateTime happen_date { get; set; }
/// <summary>
/// 备注(如"簿记拆单授信部分""平仓释放"
/// </summary>
public string remark { get; set; }
// 基类操作人列为 PascalCase(其他存量表共用),本表全列小写下划线——override 并映射小写列名。
// 新建表约定:所有列一律小写下划线,基类继承列按此方式覆写。
/// <summary>操作人ID(列 opt_id</summary>
[Column("opt_id")]
public override int OptId { get; set; }
/// <summary>操作人名称(列 opt_name</summary>
[Column("opt_name")]
public override string OptName { get; set; }
/// <summary>操作时间(列 opt_time</summary>
[Column("opt_time")]
public override DateTime OptTime { get; set; }
}
}
@@ -0,0 +1,28 @@
namespace YLErp.DBModels
{
/// <summary>
/// 预付金腿资金标签常量(R4 授信/现金标签体系)。
/// 授信不进资金:授信占用/释放通过 swap_position.FundTag + 授信出入表(client_credit_inout)体现,
/// 资金流水(ClientCashInCashOut)只记录现金部分,不加标签。
/// </summary>
public static class ConsFundTag
{
/// <summary>
/// 授信(该腿预付金占用授信额度,不产生资金流水,变化记入授信出入表)
/// </summary>
public const string Credit = "Credit";
/// <summary>
/// 现金(该腿预付金正常产生资金流水)
/// </summary>
public const string Cash = "Cash";
/// <summary>
/// 存量腿无标签时视同现金
/// </summary>
public static string EffectiveTag(string fundTag)
{
return fundTag == Credit ? Credit : Cash;
}
}
}
+11 -2
View File
@@ -26,17 +26,26 @@ namespace YLErp.DBModels
public double? Credit { get; set; }
/// <summary>
/// 原始授信值
/// 原始授信值(数据库列 original_credit,小写)
/// </summary>
[DisplayName("原始授信值")]
[Column("original_credit")]
public double? OriginalCredit { get; set; }
/// <summary>
/// 最大授信可用比例(0-1,NULL按1)
/// 最大授信可用比例(0-1,NULL按1)(数据库列 max_credit_use_ratio,小写)
/// </summary>
[DisplayName("最大授信可用比例")]
[Column("max_credit_use_ratio")]
public double? MaxCreditUseRatio { get; set; }
/// <summary>
/// 已使用授信(R4,非持久化:授信出入表 SUM(占用)−SUM(释放)+SUM(调整),列表展示用)
/// </summary>
[DisplayName("已使用授信")]
[NotMapped]
public double? UsedCredit { get; set; }
/// <summary>
/// PFE授信
/// </summary>
@@ -255,6 +255,15 @@ namespace YLErp.DBModels
/// </summary>
public string category_tag { get; set; }
/// <summary>
/// 资金标签(R4,单列,数据库列名 fund_tag):Credit=授信 / Cash=现金 / NULL=未选(默认现金,存量视同现金)。
/// 簿记录入时存用户逐腿选择;确认成交时系统在同列定稿——整腿授信→Credit、整腿现金→Cash、
/// 授信额度不足跨界时拆单(原腿 Cash + 新拆授信腿 Credit);特批全部定稿为 Cash。
/// 授信腿不产生资金流水,变化记入授信出入表(client_credit_inout)。
/// </summary>
[DisplayName("资金标签")]
[Column("fund_tag")]
public string FundTag { get; set; }
/// <summary>
/// 互换观察日集合
/// </summary>
[NotMapped]
+38 -6
View File
@@ -1,4 +1,4 @@
using CsvHelper;
using CsvHelper;
using DocumentFormat.OpenXml.Bibliography;
using DocumentFormat.OpenXml.Drawing;
using DocumentFormat.OpenXml.Drawing.Charts;
@@ -2327,12 +2327,44 @@ namespace YLErp.BLL.Eod
}
if (trade.ExerciseDate.Value.Date >= valuedateBLL.ValueDate.Date)
{
var margin = trade.InitialMargin ?? 0;
var endMoney = margin + tradePrice;
AvailableAmount = Math.Max(clientBalance.AvailableAmount + clientBalance.TotalCredit, 0);
if (AvailableAmount < margin + tradePrice)
// R4 簿记资金校验口径(2026-08-21 业务强调"走了资金的就不能占用授信"):
// 按腿的资金走向分流——走现金的部分(未选/选现金腿 + 成交金额)只认现金结存;
// 选授信的腿认 剩余可用授信(有效授信−已使用授信,授信出入表 Σ(amount)),
// 授信不够覆盖的部分回落现金,同样只认现金结存。杜绝"现金腿拿授信垫付校验→现金透支"。
var marginModes = new[] { (int)InterestModeEnum., (int)InterestModeEnum. };
var legs = trade.swap_positions?.Where(x => marginModes.Contains(x.InterestMode)).ToList();
if (legs == null || legs.Count == 0)
{
errorMsg = $"当前交易应付预付金:{trade.InitialMargin ?? 0:#,##0.000},应付成交金额:{tradePrice:#,##0.000},总应付资金:{endMoney:#,##0.000}。当前剩余资金:{clientBalance.AmountFund:F3},冻结资金:{clientBalance.AllFreezeBalance():F3},抵押品价值:{clientBalance.GuaranteesTotalAmount:F3},授信额度:{clientBalance.TotalCredit:F3},可用总额度:{AvailableAmount:F3}。不足以支付上述金额,交易费用:{tradePrice ?? 0:F3}";
legs = db.swap_position.Where(x => x.SwapTradeId == trade.id && x.IsInitial && !x.Invalid
&& marginModes.Contains(x.InterestMode)).ToList();
}
//客户应付为正:收取方向(dir=1)腿 fix 为正应付额;支付方向为客户收钱不参与
double creditPayable = 0, cashPayable = 0;
foreach (var leg in legs)
{
var payable = Convert.ToDouble(leg.InterestPrincipalFix) * (leg.InterestDirection == 1 ? 1 : -1);
if (payable <= 0)
{
continue;
}
if (leg.FundTag == YLErp.DBModels.ConsFundTag.Credit)
{
creditPayable += payable;
}
else
{
cashPayable += payable;
}
}
var usedCredit = Modules.SwapModule.ClientCreditInoutService.GetUsedCredit(clientId, db);
var creditCap = Math.Max(clientBalance.TotalCredit - usedCredit, 0);
var creditCovered = Math.Min(creditPayable, creditCap);
//授信覆盖不足的回落现金部分 + 走现金部分 + 成交金额,合计必须 ≤ 现金结存
var cashNeed = tradePrice + cashPayable + (creditPayable - creditCovered);
if (cashNeed > clientBalance.AmountFund)
{
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}(授信仅覆盖选授信部分,不足回落现金)。现金不足以覆盖应付的现金部分。";
return false;
}
}
+1
View File
@@ -346,6 +346,7 @@ namespace YLErp.BLL
public DbSet<swap_flow_merge> swap_flow_merge { get; set; }
public DbSet<swap_position> swap_position { get; set; }
public DbSet<client_credit_inout> client_credit_inout { get; set; }
public DbSet<trade_extend> trade_extend { get; set; }
public DbSet<trade_initial_margin> trade_initial_margin { get; set; }
public DbSet<swap_flow_event> swap_flow_event { get; set; }
@@ -795,7 +795,12 @@ namespace YLErp.Modules.EodModule.SettlementModule
var clientSpan = client.ClientSpan;
if (clientSpan != null)
{
PayableMargin = clientSpan.WorstCastClientPayable ?? 0d;
// 合约维度盯市(MarginWatchRule==0)客户成交时已产生出入金,保证金不再计入占用(PayableMargin),避免双重体现;
// 客户维度盯市(null/1)维持现状,计入占用。存量客户为 null,行为不变。
if (client.MarginWatchRule != 0)
{
PayableMargin = clientSpan.WorstCastClientPayable ?? 0d;
}
DeltaMargin = clientSpan.DeltaMargin ?? 0d;
SwapPayableMargin = clientSpan.SwapWorstCastClientPayable ?? 0d;
TwoSideMargin = clientSpan.TwoSideMargin ?? 0d;
@@ -150,10 +150,10 @@ namespace YLErp.Modules.MarginModule
}
/// <summary>
/// 标的细分分类判定钩子(转债ETF/科创债ETF/中债指数等)。
/// 标的细分分类判定钩子(转债ETF/科创债ETF/中债指数等)public 供单元测试回归默认行为
/// 本期默认返回 null → 走通配行兜底,行为与现状一致;后续需求按业务给的判定规则(代码段/标的维护字段)实现。
/// </summary>
private static string GetUnderlyingCategory(string underlyingCode, string underlyingInstrumentType)
public static string GetUnderlyingCategory(string underlyingCode, string underlyingInstrumentType)
{
return null;
}
@@ -0,0 +1,123 @@
using YLErp.BLL;
using YLErp.DBModels;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// 客户授信出入服务(R4 授信/现金标签体系,决策④)。
/// 授信出入表的写入口集中在 标签赋值(占用)与 平仓/到期返还(释放)两个链路内,禁止散落调用。
/// 金额符号口径(2026-08-20 业务确认):与资金流水同号——入金为负、出金为正;
/// 已使用授信 = SUM(amount) 直接求和(入金使已使用授信下降、可用授信=有效授信−已使用授信 上升;出金反之)。
/// change_type(占用/释放/调整)仅作分类审计,不参与求和方向。
/// </summary>
public class ClientCreditInoutService : YLBaseService
{
public ClientCreditInoutService(OptUserInfo userInfo) : base(userInfo)
{
}
public ClientCreditInoutService(YLBaseService baseService) : base(baseService)
{
}
/// <summary>
/// 客户已使用授信 = SUM(amount)(金额与资金流水同号:入金负、出金正)。
/// 可用授信 = 有效授信 − 本值;入金使可用授信增长、出金使其收缩。
/// 供交易确认校验(RealtimePnlCalc.TradeCanBeConfirm)等静态上下文直接调用。
/// </summary>
public static double GetUsedCredit(int clientId, YLContext db)
{
var records = db.client_credit_inout.Where(x => x.client_id == clientId).ToList();
return Math.Round(records.Sum(x => x.amount), 2, MidpointRounding.AwayFromZero);
}
/// <summary>
/// 客户当前已使用授信(实例方法,走服务 DbContext)
/// </summary>
public double GetUsedCredit(int clientId)
{
return GetUsedCredit(clientId, DbContext);
}
/// <summary>
/// 写入一条授信变化记录(占用/释放/调整统一入口),并冗余记录变更后已使用授信。
/// amount 带符号:入金(客户付)为负、出金(客户收)为正,与资金流水 Money 同号。
/// </summary>
public client_credit_inout Record(int clientId, long? positionId, int? tradeId, int changeType,
double amount, DateTime happenDate, string remark)
{
if (amount == 0)
{
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
{
client_id = clientId,
position_id = positionId,
trade_id = tradeId,
change_type = changeType,
amount = amountRounded,
used_after = usedAfter,
happen_date = happenDate,
remark = remark,
OptId = UserId,
OptName = UserName,
OptTime = DateTime.Now
};
DbContext.client_credit_inout.Add(record);
DbContext.SaveChanges();
return record;
}
/// <summary>
/// 占用:预付金腿标 Credit 的簿记入金(含拆单的授信部分;交易级占用 positionId 为空)。
/// 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);
}
/// <summary>
/// 释放:平仓/到期按原标签返还授信部分(按腿的 position_id 匹配原占用记录)。
/// 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);
}
/// <summary>
/// 删除某交易的全部授信出入记录:与资金记录同生命周期——
/// 交易回退到开仓(DeleteTradeCashInCashOut)、修改清除(ClearSwapPositions)、删除交易时同步清理,
/// 重新确认/重补时按最新标签重写,避免占用悬挂。
/// 注意:阶段四追加保证金占用落地后,此处需区分保留追加部分。
/// </summary>
public void RemoveByTrade(int tradeId)
{
var records = DbContext.client_credit_inout.Where(x => x.trade_id == tradeId).ToList();
DbContext.client_credit_inout.RemoveRange(records);
}
/// <summary>
/// 按客户批量查询已使用授信(客户列表/详情"已使用授信"展示,数据源即本表)。
/// </summary>
public static Dictionary<int, double> GetUsedCreditByClients(List<int> clientIds, YLContext db)
{
var result = new Dictionary<int, double>();
if (clientIds == null || clientIds.Count == 0)
{
return result;
}
var records = db.client_credit_inout.Where(x => clientIds.Contains(x.client_id)).ToList();
foreach (var group in records.GroupBy(x => x.client_id))
{
result[group.Key] = Math.Round(group.Sum(x => x.amount), 2, MidpointRounding.AwayFromZero);
}
return result;
}
}
}
@@ -0,0 +1,176 @@
using YLErp.DBModels;
namespace YLErp.Modules.SwapModule.Margin;
/// <summary>
/// 授信/现金资金标签分配的纯函数(从服务层剥离,便于单元测试)。
/// 对应实现方案阶段二 §2.3 标签赋值四种情形 与 §2.4 平仓按原标签返还。
/// 资金标签为预付金腿上的单列(swap_position.fund_tag,逐腿录入选择、确认时定稿),非交易级。
/// </summary>
public static class FundTagCalc
{
/// <summary>
/// 按腿分配授信额度(§2.3 四种情形的逐腿版本):
/// 选授信的腿按顺序消耗剩余额度(有效授信−已使用授信),额度耗尽的授信腿拆单(剩余授信+现金差额),
/// 之后的授信腿全额现金;未选/选现金的腿直接现金;特批(情形4)全部现金不占授信;
/// 客户净收取(负金额)的腿不参与授信分配。
/// legs 需按预期占用顺序传入(HappenDate、id)。
/// </summary>
public static List<LegFundPlan> AllocateByLegPreference(List<LegAmount> legs, double creditAvailable, bool ignoreMoneyCheck)
{
var plans = new List<LegFundPlan>();
var remaining = Math.Round(Math.Max(creditAvailable, 0), 2, MidpointRounding.AwayFromZero);
foreach (var leg in legs)
{
var amount = Math.Round(leg.Amount, 2, MidpointRounding.AwayFromZero);
var credit = 0.0;
if (!ignoreMoneyCheck && leg.PreferCredit && amount > 0)
{
credit = Math.Min(amount, remaining);
remaining = Math.Round(remaining - credit, 2, MidpointRounding.AwayFromZero);
}
plans.Add(new LegFundPlan
{
Leg = leg.Leg,
Amount = amount,
CreditAmount = credit,
CashAmount = Math.Round(amount - credit, 2, MidpointRounding.AwayFromZero)
});
}
return plans;
}
/// <summary>
/// 平仓/到期返还金额按被平仓腿的 FundTag 分流(§2.4):
/// Credit 腿的返还本金与返息不产生资金流水(本金写授信出入表"释放",出金方向记正数),Cash/无标签(存量)走现金。
/// 返回 现金部分返还本金、现金部分返息、以及按腿的授信释放明细。
/// </summary>
public static UnwindTagSplit SplitUnwindByTag(List<MarginLegSettlement> settlements)
{
var result = new UnwindTagSplit();
foreach (var s in settlements)
{
var margin = Convert.ToDouble(s.MarginAmount);
var rebate = Convert.ToDouble(s.RebateAmount);
if (s.Tag == ConsFundTag.Credit)
{
result.CreditMargin += margin;
result.CreditRebate += rebate;
if (s.MarginAmount != 0)
{
result.Releases.Add(new TagRelease
{
PositionId = s.PositionId,
Amount = Math.Round(margin, 2, MidpointRounding.AwayFromZero)
});
}
}
else
{
result.CashMargin += margin;
result.CashRebate += rebate;
}
}
result.CashMargin = Math.Round(result.CashMargin, 2, MidpointRounding.AwayFromZero);
result.CreditMargin = Math.Round(result.CreditMargin, 2, MidpointRounding.AwayFromZero);
result.CashRebate = Math.Round(result.CashRebate, 2, MidpointRounding.AwayFromZero);
result.CreditRebate = Math.Round(result.CreditRebate, 2, MidpointRounding.AwayFromZero);
return result;
}
}
/// <summary>
/// 参与标签分配的预付金腿及其簿记金额(客户应付为正)、资金来源选择
/// </summary>
public class LegAmount
{
public swap_position Leg { get; set; }
public double Amount { get; set; }
/// <summary>腿上是否选了授信(swap_position.fund_tag=='Credit'</summary>
public bool PreferCredit { get; set; }
}
/// <summary>
/// 单腿分配结果:CreditAmount 与 CashAmount 皆大于 0 时该腿需拆为两条(拆单)
/// </summary>
public class LegFundPlan
{
public swap_position Leg { get; set; }
/// <summary>腿原簿记金额</summary>
public double Amount { get; set; }
/// <summary>授信部分金额</summary>
public double CreditAmount { get; set; }
/// <summary>现金部分金额</summary>
public double CashAmount { get; set; }
/// <summary>拆单时新拆出的授信腿(占用记录绑定到它)</summary>
public swap_position CreditLeg { get; set; }
public bool NeedSplit => CreditAmount > 0 && CashAmount > 0;
}
/// <summary>
/// 平仓/到期结算中的单条保证金腿结算额(客户应收返还为正)
/// </summary>
public class MarginLegSettlement
{
public long PositionId { get; set; }
/// <summary>该腿资金标签(EffectiveTag 后:Credit 或 Cash</summary>
public string Tag { get; set; }
/// <summary>返还本金(swap_flow_event.InterestPrincipal × 方向比)</summary>
public decimal MarginAmount { get; set; }
/// <summary>预付金返息(InterestClosePnL</summary>
public decimal RebateAmount { get; set; }
}
/// <summary>
/// 平仓利息事件逐腿构造结算额(纯函数,便于单元测试):
/// 预付金腿(InterestMode=5/6、无标的代码)按标签表查标签(无标签存量按现金),
/// 返还本金=InterestPrincipal×方向比(InterestDirection==1 取 -1,与 MarginCalc.AccumulateSettlement 一致),返息=InterestClosePnL。
/// </summary>
public static class MarginSettlementBuilder
{
private static readonly int[] MarginModes = { (int)InterestModeEnum., (int)InterestModeEnum. };
public static List<MarginLegSettlement> Build(IDictionary<long, string> positionTags, IEnumerable<swap_flow_event> interestEvents)
{
var settlements = new List<MarginLegSettlement>();
foreach (var x in interestEvents ?? Array.Empty<swap_flow_event>())
{
if (!string.IsNullOrEmpty(x.UnderlyingCode) || !MarginModes.Contains(x.InterestMode))
{
continue;
}
var interestRatio = x.InterestDirection == 1 ? -1m : 1m;
settlements.Add(new MarginLegSettlement
{
PositionId = x.PositionId,
Tag = positionTags != null && positionTags.TryGetValue(x.PositionId, out var tag) ? tag : ConsFundTag.Cash,
MarginAmount = x.InterestPrincipal * interestRatio,
RebateAmount = x.InterestClosePnL
});
}
return settlements;
}
}
public class TagRelease
{
public long PositionId { get; set; }
public double Amount { get; set; }
}
/// <summary>
/// 按标签分流后的平仓结算金额
/// </summary>
public class UnwindTagSplit
{
/// <summary>现金部分返还本金(产生 应付预付金 资金流水)</summary>
public double CashMargin { get; set; }
/// <summary>授信部分返还本金(写授信出入表"释放",不产生资金流水)</summary>
public double CreditMargin { get; set; }
/// <summary>现金部分预付金返息(产生 预付金返息 资金流水)</summary>
public double CashRebate { get; set; }
/// <summary>授信部分预付金返息(授信不进资金,不产生资金流水)</summary>
public double CreditRebate { get; set; }
/// <summary>按腿的授信释放明细(position_id 匹配原占用记录)</summary>
public List<TagRelease> Releases { get; set; } = new List<TagRelease>();
}
+32 -19
View File
@@ -1095,26 +1095,39 @@ namespace YLErp.Modules.SwapModule
}
/// <summary>
/// 写入保证金的资金记录:应付预付金(SwapMarginAmount)和预付金返息(SwapMarginRebatePnl)。
/// 依赖实例方法 AddClientCash/AddClientCashInCashOut,暂留此处。
/// </summary>
private void RecordMarginCashFlow(trade td, UnwindData unwindData)
=> RecordMarginCashFlow(td, unwindData.ValueDate,
unwindData.SwapMarginAmount, unwindData.SwapMarginRebatePnl,
AddClientCashInCashOut);
/// <summary>
/// 写入保证金资金记录的通用重载,接受资金写入委托。
/// AddClientCash(virtual,测试可stub) 和 AddClientCashInCashOut(非virtual,直接写库)
/// 都可通过此重载统一。
/// R4 §2.4 平仓/到期按被平仓腿 FundTag 原路返还:
/// Credit 腿的返还本金与返息不产生资金流水(本金写授信出入表"释放",按 position_id 匹配原占用),
/// Cash/无标签(存量)部分正常产生资金流水。
/// </summary>
private void RecordMarginCashFlow(trade td, DateTime valueDate,
List<swap_flow_event> interestEvents,
decimal marginAmount, decimal marginRebate,
Func<trade, double, string, DateTime, int> writeCash)
{
if (marginAmount != 0)
writeCash(td, Convert.ToDouble(marginAmount), ClientCashInCashOut._应付预付金, valueDate);
if (marginRebate != 0)
writeCash(td, Convert.ToDouble(-marginRebate), ClientCashInCashOut._预付金返息, valueDate);
var split = interestEvents != null && interestEvents.Any(x => string.IsNullOrEmpty(x.UnderlyingCode))
? ReleaseMarginByFundTag(td, valueDate, interestEvents, marginAmount, marginRebate)
//无预付金腿结算事件(如金额手工归一化/无腿场景)——退化原逻辑,全额现金
: new UnwindTagSplit { CashMargin = Convert.ToDouble(marginAmount), CashRebate = Convert.ToDouble(marginRebate) };
if (split.CashMargin != 0)
writeCash(td, split.CashMargin, ClientCashInCashOut._应付预付金, valueDate);
if (split.CashRebate != 0)
writeCash(td, -split.CashRebate, ClientCashInCashOut._预付金返息, valueDate);
}
/// <summary>
/// 按标签分流并写授信释放记录(virtual,测试可 stub 为全现金,见 TestableSwapDealService)。
/// </summary>
protected virtual UnwindTagSplit ReleaseMarginByFundTag(trade td, DateTime valueDate, List<swap_flow_event> interestEvents,
decimal marginAmount, decimal marginRebate)
{
var settlements = new SwapFundTagService(this).GetSettlements(interestEvents);
var split = new SwapFundTagService(this).ReleaseMarginByTag(td, valueDate, settlements);
//结算事件缺失(异常数据)时保底按传入总额走现金,不丢资金记录
if (settlements.Count == 0)
{
return new UnwindTagSplit { CashMargin = Convert.ToDouble(marginAmount), CashRebate = Convert.ToDouble(marginRebate) };
}
return split;
}
/// <summary>
@@ -1528,7 +1541,7 @@ namespace YLErp.Modules.SwapModule
ExecuteInTransaction(() =>
{
int clientCashId = AddClientCash(td, Convert.ToDouble(-unwindData.SwapRealizedPnL), ClientCashInCashOut._平仓费, unwindData.ValueDate);
RecordMarginCashFlow(td, unwindData.ValueDate, unwindData.SwapMarginAmount, 0m, AddClientCash);
RecordMarginCashFlow(td, unwindData.ValueDate, unwindData.FlowEvents, unwindData.SwapMarginAmount, 0m, AddClientCash);
DealFloatPosition(unwindData);
var flowList = new List<swap_flow_event>(unwindData.FlowEvents);
var eventId = SaveSwapDeal(unwindData, (int)SwapEventTypeEnum., clientCashId, "系统操作_平仓");
@@ -1697,7 +1710,7 @@ namespace YLErp.Modules.SwapModule
private void DealUnwind(UnwindData unwindData, trade td, string actionMsg = "系统操作_自动平仓")
{
int clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapRealizedPnL), ClientCashInCashOut._平仓费, unwindData.ValueDate);
RecordMarginCashFlow(td, unwindData.ValueDate, unwindData.SwapMarginAmount, 0m, AddClientCashInCashOut);
RecordMarginCashFlow(td, unwindData.ValueDate, unwindData.FlowEvents, unwindData.SwapMarginAmount, 0m, AddClientCashInCashOut);
var flowList = new List<swap_flow_event>(unwindData.FlowEvents);
var eventId = SaveSwapDeal(unwindData, (int)SwapEventTypeEnum., clientCashId, actionMsg);
if (unwindData.CloseMethod == (int)CloseMethodEnum.)
@@ -1768,7 +1781,7 @@ namespace YLErp.Modules.SwapModule
ExecuteInTransaction(() =>
{
int clientCashId = AddClientCash(td, Convert.ToDouble(-unwindData.SwapRealizedPnL), ClientCashInCashOut._互换, unwindData.ValueDate);
RecordMarginCashFlow(td, unwindData.ValueDate, 0m, unwindData.SwapMarginRebatePnl, AddClientCash);
RecordMarginCashFlow(td, unwindData.ValueDate, unwindData.FlowEvents, 0m, unwindData.SwapMarginRebatePnl, AddClientCash);
foreach (var item in unwindData.FlowEvents)
{
item.OptLog = "手工操作";
@@ -1837,7 +1850,7 @@ namespace YLErp.Modules.SwapModule
int clientCashId = AddClientCash(td, Convert.ToDouble(-swapEvent.unwindData.SwapRealizedPnL), action, swapEvent.unwindData.ValueDate);
if (eventType == (int)SwapEventTypeEnum.)
{
RecordMarginCashFlow(td, swapEvent.unwindData.ValueDate, swapEvent.unwindData.SwapMarginAmount, 0m, AddClientCash);
RecordMarginCashFlow(td, swapEvent.unwindData.ValueDate, swapEvent.unwindData.FlowEvents, swapEvent.unwindData.SwapMarginAmount, 0m, AddClientCash);
}
swapEvent.ClientCashId = clientCashId;
td.UnWindDate = swapEvent.unwindData.UnwindDate;
@@ -741,10 +741,16 @@ namespace YLErp.Modules.SwapModule
}
// 预付金腿:单独插入一条资金记录(系统操作_预付金返息)
// R4 §2.4:返息按腿 FundTag 分流——授信部分不进资金(授信不产生流水),
// 只对现金部分(含无标签存量)产生返息资金记录
if (unwindData.SwapMarginRebatePnl != 0)
{
clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapMarginRebatePnl), ClientCashInCashOut._预付金返息, unwindData.ValueDate);
clientCashIds.Add(clientCashId);
var cashRebate = GetAutoSwapCashRebate(td, flowEvents, unwindData.SwapMarginRebatePnl);
if (cashRebate != 0)
{
clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(-cashRebate), ClientCashInCashOut._预付金返息, unwindData.ValueDate);
clientCashIds.Add(clientCashId);
}
}
unwindData.SwapCloseAmount = unwindData.SwapRealizedPnL;//需要算上预付金利息 和 分红; 只是不算预付金返还
// 分红:使用派息支付日偏移记录资金记录
@@ -784,6 +790,36 @@ namespace YLErp.Modules.SwapModule
}
return swapEvent.id;
}
/// <summary>
/// R4 §2.4:自动互换预付金返息按腿 FundTag 分流,返回现金部分返息。
/// 授信腿返息不进资金(授信不产生流水);无标签存量/无预付金腿事件时全额现金。
/// virtual 供纯内存测试 stub 为全额现金(见 TestableSwapEodPositionService)。
/// </summary>
protected virtual decimal GetAutoSwapCashRebate(trade td, List<swap_flow_event> flowEvents, decimal totalRebate)
{
if (flowEvents == null || flowEvents.Count == 0)
{
return totalRebate;
}
var premiumModes = MarginModes.ForLinq;
var legs = flowEvents.Where(x => string.IsNullOrEmpty(x.UnderlyingCode) && premiumModes.Contains(x.InterestMode)).ToList();
if (legs.Count == 0)
{
return totalRebate;
}
//GetSettlements 与 legs 同谓词同序过滤,settlements[i] 与 legs[i] 一一对应
var settlements = new SwapFundTagService(this).GetSettlements(legs);
for (var i = 0; i < legs.Count; i++)
{
//对齐 DealAutoInterests 返息符号口径:InterestClosePnL × ReceivePay(方向)
//自动互换只结返息,保证金本金不在此返还(不写释放记录)
settlements[i].MarginAmount = 0m;
settlements[i].RebateAmount = legs[i].InterestClosePnL * -DirectionRatio.ReceivePay(legs[i].InterestDirection);
}
var split = FundTagCalc.SplitUnwindByTag(settlements);
return Math.Round(totalRebate - Convert.ToDecimal(split.CreditRebate), 2, MidpointRounding.AwayFromZero);
}
/// <summary>
/// 互换更新实时持仓信息
/// </summary>
@@ -0,0 +1,191 @@
using YLErp.BLL;
using YLErp.DBModels;
using YLErp.Modules.SwapModule.Margin;
using YLErp.Modules.TradeModule;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// 互换预付金 授信/现金 标签服务(实现方案阶段二 §2.3/§2.4)。
/// 标签赋值与返还两个写入口集中在本服务,授信出入表(ClientCreditInoutService)的占用/释放由此统一触发。
/// 口径:授信值取 credit.Credit 合计(已审批+日期有效+含母公司,阶段一已折算),已使用授信取授信出入表;
/// 授信不进资金——授信部分不产生资金流水。
/// 资金标签是预付金腿上的单列(swap_position.fund_tag,逐腿):录入时存用户选择(授信/现金/未选默认现金),确认成交时系统在同列定稿。
/// </summary>
public class SwapFundTagService : YLBaseService
{
public SwapFundTagService(OptUserInfo userInfo) : base(userInfo)
{
}
public SwapFundTagService(YLBaseService baseService) : base(baseService)
{
}
/// <summary>
/// 有效授信合计:客户本人+母公司、已审批、日期有效(口径与 RealtimePnlCalc 取授信一致)。
/// Credit 列在保存时已折算(OriginalCredit×MaxCreditUseRatio),此处直接取用。
/// </summary>
public double GetEffectiveCredit(int clientId, DateTime valueDate)
{
var client = DataCacheProvider.GetClientDataSource().GetData(clientId);
var clientIds = new List<int> { clientId };
if (client != null && client.ParentId > 0)
{
clientIds.Add(client.ParentId);
}
var credits = DbContext.credit.Where(t =>
clientIds.Contains(t.ClientId ?? 0) && t.ProcessStatus == "已审批"
&& (!t.CreditDeadLine.HasValue || t.CreditDeadLine >= valueDate)
&& (!t.CreditStartDate.HasValue || t.CreditStartDate <= valueDate)).ToList();
return credits.Sum(t => t.Credit ?? 0);
}
/// <summary>
/// 剩余可用授信 = 有效授信 − 已使用授信。
/// 已使用授信 = 授信出入表 Σ(amount)(入金负/出金正)——入金使可用授信上升、出金使其收缩(业务口径)。
/// </summary>
public double GetAvailableCredit(int clientId, DateTime valueDate)
{
return GetEffectiveCredit(clientId, valueDate) - ClientCreditInoutService.GetUsedCredit(clientId, DbContext);
}
/// <summary>
/// 簿记确认时对预付金腿定稿资金标签并产生资金记录(§2.3 四种情形,逐腿)。
/// fund_tag 单列:录入时存用户选择(Credit/Cash/NULL),本方法读取选择后在同列定稿——
/// 特批全现金;选授信按剩余额度分配(跨界腿拆单为 授信+现金 两条),未选/现金直接现金。
/// 授信腿只写授信出入表占用(入金方向记负数,绑定腿 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);
var valueDate = td.TradeDate ?? DateTime.Now;
var creditAvailable = GetAvailableCredit(td.ClientId, valueDate);
//客户应付为正:资金记录符号口径为 Money<0=客户付钱,即 收取方向(dir=1)腿 fix 为正应付额——
//正是占用授信的场景;支付方向(dir=2)为客户收钱,不占用授信,直接现金。
var allocateLegs = marginLegs
.Select(x => new LegAmount
{
Leg = x,
Amount = Convert.ToDouble(x.InterestPrincipalFix * (x.InterestDirection == 1 ? 1 : -1)),
PreferCredit = x.FundTag == ConsFundTag.Credit
})
.Where(x => x.Amount > 0)
.OrderBy(x => x.Leg.HappenDate ?? DateTime.MaxValue)
.ThenBy(x => x.Leg.id)
.ToList();
var plans = FundTagCalc.AllocateByLegPreference(allocateLegs, creditAvailable, ignoreMoneyCheck);
//先落库拆分的新腿(需要 id 才能绑定占用记录)
foreach (var plan in plans.Where(p => p.NeedSplit))
{
plan.CreditLeg = SplitLeg(td, plan);
}
//标签定稿(覆盖录入选择):拆单的两条腿在 SplitLeg 内已分别标 Cash/Credit
//整腿授信→Credit、整腿现金/负应付(客户净收取)腿→Cash
foreach (var leg in marginLegs)
{
var plan = plans.FirstOrDefault(p => p.Leg.id == leg.id);
if (plan == null || plan.NeedSplit)
{
if (plan == null)
{
leg.FundTag = ConsFundTag.Cash;
}
continue;
}
leg.FundTag = plan.CreditAmount > 0 ? ConsFundTag.Credit : ConsFundTag.Cash;
}
DbContext.SaveChanges();
foreach (var leg in marginLegs)
{
var plan = plans.FirstOrDefault(p => p.Leg.id == leg.id);
var happenDate = leg.HappenDate ?? td.TradeDate ?? DateTime.Now;
if (plan != null && plan.CreditAmount > 0)
{
//整腿授信 或 拆单后的授信部分:不产生资金流水,只写占用(拆单绑新拆出的授信腿)。
//入金方向记负数(业务口径:出入表金额与资金流水同号,入金负/出金正;入金使可用授信上升)
creditService.Occupy(td.ClientId, plan.CreditLeg?.id ?? leg.id, td.id, -plan.CreditAmount, happenDate,
plan.NeedSplit ? "簿记拆单授信部分" : "簿记授信占用");
}
//资金记录沿用既有符号口径(客户付钱为负 = -应付额):授信部分不产生流水,现金部分按差额产生
var recordAmount = plan != null
? -plan.CashAmount
: Convert.ToDouble(leg.InterestPrincipalFix * (leg.InterestDirection == 1 ? -1 : 1));
if (recordAmount != 0)
{
cashService.SaveSwapTradeClientCash(td, recordAmount, happenDate, leg.id, ClientCashInCashOut._应付预付金);
}
}
}
/// <summary>
/// 拆单:把跨界腿拆为 授信+现金 两条。原腿保留现金部分并标 Cash(资金来源同步改现金,与最终标签一致),
/// 克隆一条授信腿(InterestPrincipalFix 按授信金额折算)标 Credit,返回新腿。
/// 拆出的腿为普通初始腿,后续编辑/回退/平仓链路按既有腿处理。
/// </summary>
private swap_position SplitLeg(trade td, LegFundPlan plan)
{
var leg = plan.Leg;
//应付额 = fix × (dir==1 ? 1 : -1),反推 fix 用同一比例(±1 自反)
var payableRatio = leg.InterestDirection == 1 ? 1 : -1;
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);
creditLeg.FundTag = ConsFundTag.Credit;
creditLeg.OptId = UserId;
creditLeg.OptName = UserName;
creditLeg.OptTime = DateTime.Now;
DbContext.swap_position.Add(creditLeg);
DbContext.SaveChanges();
creditLeg.PosiNumber = $"{td.TradeNumber}-{creditLeg.id}";
return creditLeg;
}
/// <summary>
/// 平仓/到期返还按被平仓腿 FundTag 分流(§2.4):
/// Credit 腿返还本金写授信出入表"释放"(按 position_id 匹配原占用)、本金与返息均不产生资金流水;
/// Cash/无标签(存量)部分由调用方按返回的现金金额正常产生返还流水。
/// settlements 由平仓利息事件(swap_flow_event)逐腿构造。
/// </summary>
public UnwindTagSplit ReleaseMarginByTag(trade td, DateTime valueDate, List<MarginLegSettlement> settlements)
{
var split = FundTagCalc.SplitUnwindByTag(settlements);
if (split.Releases.Count > 0)
{
var creditService = new ClientCreditInoutService(this);
foreach (var release in split.Releases)
{
creditService.Release(td.ClientId, release.PositionId, td.id, release.Amount, valueDate, "平仓/到期释放");
}
}
return split;
}
/// <summary>
/// 由平仓利息事件构造逐腿结算额(供 ReleaseMarginByTag 消费):
/// 预付金腿按 PositionId 查腿标签(无标签存量按现金),纯函数构造见 MarginSettlementBuilder.Build。
/// </summary>
public List<MarginLegSettlement> GetSettlements(IEnumerable<swap_flow_event> interestEvents)
{
var marginModes = new[] { (int)InterestModeEnum., (int)InterestModeEnum. };
var events = (interestEvents ?? Enumerable.Empty<swap_flow_event>())
.Where(x => string.IsNullOrEmpty(x.UnderlyingCode) && marginModes.Contains(x.InterestMode)).ToList();
var positionIds = events.Select(x => x.PositionId).Where(x => x > 0).Distinct().ToList();
var tags = DbContext.swap_position.Where(p => positionIds.Contains(p.id))
.ToDictionary(p => p.id, p => ConsFundTag.EffectiveTag(p.FundTag));
return MarginSettlementBuilder.Build(tags, events);
}
}
}
@@ -16,6 +16,7 @@ using System.Data;
using System.Linq.Expressions;
using System.Text;
using YLErp.BLL;
using YLErp.BLL.Eod;
using YLErp.Configuration;
using YLErp.Configuration.Enums;
using YLErp.CustomizedBizLogic;
@@ -86,8 +87,16 @@ namespace YLErp.Modules.SwapModule
{
var um = checkUnderlying(req);
trade dbTrade = new trade();
//交易保存处理
//交易保存处理PrepareInitialMargin 在此把 trade_Initial_Margin 折算进 req.InitialMargin
//资金校验须在其后取值,否则互换表单不填平铺 InitialMargin 时校验会按 0 放行)
var tradeNumberGenerated = PrepareTrade(req, TradeSourceEnum., um);
// R4 簿记资金校验:可用资金(现金结存+授信−已使用授信,口径见 RealtimePnlCalc.TradeCanBeConfirm
// 需覆盖 应付预付金+成交金额,不足拦截抛错;特批放行发生在确认成交环节(ignoreMoneyCheck)。
// ExerciseDate 为空(异常数据)时跳过该校验,避免 TradeCanBeConfirm 内部解引用抛错。
if (req.ExerciseDate.HasValue && !RealtimePnlCalc.TradeCanBeConfirm(req.ClientId, req, out var fundErrorMsg))
{
throw new ServiceException(fundErrorMsg);
}
var trans = DbContext.Database.BeginTransaction();
try
@@ -215,6 +224,9 @@ namespace YLErp.Modules.SwapModule
var delCashInCashOutArr = DbContext.ClientCashInCashOut.Where(predicate_cashIncashOut);
DbContext.ClientCashInCashOut.RemoveRange(delCashInCashOutArr);
// R4:授信出入记录与资金记录同生命周期,随资金记录一并清理(回退到开仓/删除交易),
// 重新确认时按最新标签与额度重写,避免授信占用悬挂
new ClientCreditInoutService(this).RemoveByTrade(tradeId);
}
/// <summary>
/// 单标的初始化实时持仓
@@ -1462,6 +1474,8 @@ namespace YLErp.Modules.SwapModule
position.interest_rest_days = swap.interest_rest_days;
position.interest_rule = swap.interest_rule;
position.category_tag = string.IsNullOrEmpty(swap.category_tag) ? "互换利率" : swap.category_tag;
// R4:资金标签(fund_tag 单列)随录入保存用户逐腿选择,确认成交时系统在同列定稿
position.FundTag = string.IsNullOrWhiteSpace(swap.FundTag) ? null : swap.FundTag;
position.InitYtm = swap.InitYtm;
if (position.InitYtm != null && position.InitYtm > 0)
{
@@ -1496,6 +1510,8 @@ 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);
swapFlowEvents.ForEach(x =>
{
x.DataState = (int)SwapFlowDateStateEnum.;
@@ -1629,14 +1645,19 @@ namespace YLErp.Modules.SwapModule
}
/// <summary>
/// 重新补录预付金记录
/// 重新补录预付金记录(回退到开仓后重补,与 SwapTradeConfirm 同口径)
/// </summary>
/// <param name="td"></param>
/// <param name="happenDate"></param>
/// <param name="swapPositions"></param>
private void ResetMarginAmount(trade td, DateTime happenDate, List<swap_position> swapPositions)
{
new ClientCashInCashOutService(this).SaveSwapTradeClientCash(td, td.TradePrice ?? 0, happenDate, 0);
var cashSvc = new ClientCashInCashOutService(this);
cashSvc.SaveSwapTradeClientCash(td, td.TradePrice ?? 0, happenDate, 0);
// R4:重补同样走标签分配(授信占用/拆单/现金流水);
// 特批标志在回退重补场景不可得,按当前剩余额度重新分配(回退即重新簿记)
var fundTagSvc = new SwapFundTagService(this);
var generateMarginLegs = new List<swap_position>();
foreach (var marginPositions in swapPositions.Where(x => x.HappenDate != null && x.IsInitial && x.InterestMode != (int)InterestModeEnum.).GroupBy(g => g.HappenDate))
{
var marginHappenDate = marginPositions.Key;
@@ -1653,18 +1674,21 @@ namespace YLErp.Modules.SwapModule
}
if (generateMargin)
{
var marginAmount = marginPosition.InterestPrincipalFix * (marginPosition.InterestDirection == 1 ? -1 : 1);
new ClientCashInCashOutService(this).SaveSwapTradeClientCash(td, Convert.ToDouble(marginAmount), marginHappenDate.HasValue ? marginHappenDate.Value : td.TradeDate.Value, marginPosition.id, ClientCashInCashOut._应付预付金);
generateMarginLegs.Add(marginPosition);
}
}
}
fundTagSvc.ApplyMarginFundTags(td, generateMarginLegs, cashSvc, false);
//标签定稿(含可能的拆单)后重克隆实时持仓:TradeBack 的克隆先于定稿生成,
//重克隆使实时腿继承定稿标签、新拆出的授信腿也获得克隆(平仓返还分流查的是实时腿标签)
InitialPosition(td);
// 合约维度盯市+无预付金腿:重建交易级(positionId=0)初始预付金记录(与 SwapTradeConfirm 一致,回退重补场景)。
// 有预付金腿的互换由上面 foreach 按腿重建,不在此重复生成。
// 有预付金腿的互换由上面按腿重建,不在此重复生成。
// R4:资金来源是预付金腿上的录入项,无腿即无从选择授信,本交易级记录恒为现金。
var resetWatchClient = DataCacheProvider.GetClientDataSource().GetData(td.ClientId);
var hasPrepayLeg = swapPositions != null && swapPositions.Any(x => x.InterestMode == (int)InterestModeEnum. || x.InterestMode == (int)InterestModeEnum.);
if (resetWatchClient != null && resetWatchClient.MarginWatchRule == 0 && !hasPrepayLeg)
{
var cashSvc = new ClientCashInCashOutService(this);
var initMargin = cashSvc.GetInitialMarginAmount(td);
if (initMargin > 0)
{
@@ -1706,6 +1730,8 @@ namespace YLErp.Modules.SwapModule
interest.id = 0;
interest.PositionId = item.id;
interest.IsInitial = false;
// R4:回到开仓保留 fund_tag(用户录入的选择),重新确认时按当前额度在同列定稿;
// 授信出入记录已随资金记录清理,重确认时重写
td.TradeAmount = td.TradeAmount + Convert.ToDouble(interest.PosiQuantity);
DbContext.swap_position.Add(interest);
}
@@ -189,6 +189,7 @@ namespace YLErp.Modules.TradeModule
// 追保规则-合约维度盯市:非互换交易(期权等)成交确认时,按计算保证金生成初始预付金资金记录(入金)。
// 客户维度盯市(null/1)不生成出入金,保证金只进 clientbalancedaily.PayableMargin(占用),维持现状。
// R4:资金来源是互换预付金腿上的录入项,期权等无腿交易无从选择授信,恒为现金(行为不变)。
var marginClient = DataCacheProvider.GetClientDataSource().GetData(newtrade.ClientId);
if (marginClient != null && marginClient.MarginWatchRule == 0)
{
@@ -280,7 +280,7 @@ namespace YLErp.Modules.TradeModule.DealModule
DbContext.processtradelog.Add(p);
new SalesCommissionDataService(OptUser).SetCommissionVaild(td.id);
EditReportStatus(td.id);
SwapTradeConfirm(td, history,false,optlog:"手工操作");
SwapTradeConfirm(td, history,false,optlog:"手工操作", ignoreMoneyCheck: ignoreMoneyCheck);
}
else if (passStatus == -3)
{
@@ -313,7 +313,7 @@ namespace YLErp.Modules.TradeModule.DealModule
DbContext.processtradelog.Add(p);
new SalesCommissionDataService(OptUser).SetCommissionVaild(td.id);
EditReportStatus(td.id);
SwapTradeConfirm(td, history, false, optlog: "手工操作");
SwapTradeConfirm(td, history, false, optlog: "手工操作", ignoreMoneyCheck: ignoreMoneyCheck);
}
new ClientCashInCashOutService(this).SaveClientCashInCashOut(td);
@@ -352,7 +352,8 @@ namespace YLErp.Modules.TradeModule.DealModule
/// 新版互换确认
/// </summary>
/// <param name="td"></param>
public void SwapTradeConfirm(trade td,string history,bool addlog,DateTime? openDate=null,string optlog="")
/// <param name="ignoreMoneyCheck">特批(忽略资金检查)推进:预付金腿全部标现金,不占授信(R4 情形4)</param>
public void SwapTradeConfirm(trade td,string history,bool addlog,DateTime? openDate=null,string optlog="", bool ignoreMoneyCheck = false)
{
if (td.TradeType != "收益互换")
{
@@ -363,12 +364,9 @@ namespace YLErp.Modules.TradeModule.DealModule
var marginModes = new int[]{ (int)InterestModeEnum., (int)InterestModeEnum. };
var positions= DbContext.swap_position.Where(x => x.SwapTradeId == td.id && x.IsInitial && !x.Invalid ).ToList();
td.swap_positions = positions.Where(x =>marginModes.Contains(x.InterestMode)).ToList();
var posiPositions= positions.Where(x => x.PosiDirection>0).ToList();
var posiPositions= positions.Where(x=>x.PosiDirection>0).ToList();
SwapTradeService swapTradeService = new SwapTradeService(this);
var swapEventService = new SwapEventService(this);
swapTradeService.InitialPosition(td);
swapTradeService.AddPositionEvent(td, optlog);
//new SwapEodPositionService(this).InitSaveEodSwapPosition(td);
var happenDate = td.TradeDate.Value;
if (td.trade_extend != null && !td.trade_extend.ExtendObj.NeedOpenFee)
{
@@ -376,26 +374,43 @@ namespace YLErp.Modules.TradeModule.DealModule
}
var cashService = new ClientCashInCashOutService(this);
cashService.SaveSwapTradeClientCash(td, td.TradePrice ?? 0, happenDate,0);
foreach (var marginPositions in td.swap_positions.GroupBy(g=>g.HappenDate))
// R4 授信/现金标签:预付金腿定稿资金标签(选授信按剩余授信分配,不足跨界腿拆单),
// 授信部分不产生资金流水(只写授信出入表占用),现金部分产生 应付预付金 记录;特批全现金。
// 必须在 InitialPosition/AddPositionEvent 之前执行:实时持仓克隆与初始事件要继承"定稿后"的标签,
// 拆单新拆出的授信腿也要被克隆、建事件(否则平仓返还分流会查到克隆腿上的旧标签/漏腿)。
var generateMarginLegs = new List<swap_position>();
foreach (var marginPosition in td.swap_positions)
{
var marginHappenDate = marginPositions.Key;
foreach (var marginPosition in marginPositions)
marginPosition.Obervation = DbContext.trade_obervation.FirstOrDefault(x => x.PositionId == marginPosition.id);
bool generateMargin = true;
if (marginPosition.Obervation != null)
{
marginPosition.Obervation = DbContext.trade_obervation.FirstOrDefault(x => x.PositionId == marginPosition.id);
bool generateMargin = true;
if (marginPosition.Obervation != null)
if (!marginPosition.Obervation.IsDeductPrincipal)
{
if (!marginPosition.Obervation.IsDeductPrincipal)
{
generateMargin = false;
}
}
if (generateMargin)
{
var marginAmount = marginPosition.InterestPrincipalFix * (marginPosition.InterestDirection == 1 ? -1 : 1);
cashService.SaveSwapTradeClientCash(td,Convert.ToDouble( marginAmount), marginHappenDate.HasValue ? marginHappenDate.Value : td.TradeDate.Value, marginPosition.id, ClientCashInCashOut._应付预付金);
generateMargin = false;
}
}
if (generateMargin)
{
generateMarginLegs.Add(marginPosition);
}
}
new SwapFundTagService(this).ApplyMarginFundTags(td, generateMarginLegs, cashService, ignoreMoneyCheck);
//标签定稿(含拆单)完成后,再克隆实时持仓、生成初始事件——克隆继承定稿标签
swapTradeService.InitialPosition(td);
swapTradeService.AddPositionEvent(td, optlog);
// 追保规则-合约维度盯市:互换无预付金腿时(td.swap_positions 在上方已过滤为只剩5/6预付金腿,为空即无预付金腿),
// 按计算保证金补一条交易级(positionId=0)初始预付金资金记录(入金),与按腿生成的记录去重。
// 客户维度盯市(null/1)不生成出入金,维持现状;有预付金腿的互换沿用上方既有记录,不在此重复生成。
// R4:资金来源是预付金腿上的录入项,无腿即无从选择授信,本交易级记录恒为现金。
var watchClient = DataCacheProvider.GetClientDataSource().GetData(td.ClientId);
if (watchClient != null && watchClient.MarginWatchRule == 0 && !td.swap_positions.Any())
{
var initMargin = cashService.GetInitialMarginAmount(td);
if (initMargin > 0)
{
cashService.SaveSwapTradeClientCash(td, initMargin, td.TradeDate.Value, 0, ClientCashInCashOut._应付预付金);
}
}
var swap_event= swapEventService.UpdateSwapEvent();
swapEventService.AddSwapEventDate(openDate.Value, td.id, (int)SwapEventTypeEnum., "", 0, true, history);
@@ -542,7 +542,7 @@ namespace YLErp.Modules.TradeModule.DealModule
{
SwapTradeService swapTradeService = new SwapTradeService(this);
new SwapEventService(UserInfo).AddSwapEventDate(DateTime.Now.Date, td.id, (int)SwapEventTypeEnum., "", 0, true, req.comments);
new TradeConfirmService(UserInfo).SwapTradeConfirm(td, "确认交易",false, optlog: "手工操作");
new TradeConfirmService(UserInfo).SwapTradeConfirm(td, "确认交易",false, optlog: "手工操作", ignoreMoneyCheck: req.ignoreMoneyCheck);
}
//组合交易(非互换)
if (td.IsGroup == 1)
+5 -2
View File
@@ -16,7 +16,7 @@ const editConfig =
{ name: "Name", label: "客户名称", type: "text", required: true },
{ name: "MainProtocolCode", label: "主协议编号", type: "text" },
{ name: "CustomerManagerId", label: "对冲交易询价对象", type: "select-m" },
{ name: "MarginWatchRule", label: "追保规则", required: true, type: "select" },
{ type: "new-col" },
{ name: "Abbreviation", label: "客户简称", type: "text" },
@@ -62,11 +62,13 @@ const editConfig =
IsAssessmentResultChange: [{ text: "否", value: 0 }, { text: "是", value: 1 }],
IsEvaluate: [{ text: "否", value: 0 }, { text: "是", value: 1 }],
EvaluateOfValidity: [{ text: "----", value: "" }, "3个月", "6个月", "12个月", "长期"],
MarginWatchRule: [{ text: "合约维度盯市", value: 0 }, { text: "客户维度盯市", value: 1 }],
},
defaults: {
ClientRight: "4",
SwapTradeType: 0,
DerivativesInvestmentVarieties: "2"
DerivativesInvestmentVarieties: "2",
AccessRule: "2", MarginOptionType: "0", MarginWatchRule: "1",
},
openList: [
{ name: 'id', label: '', hidden: true, optionHide: true, sortable: false, align: '', width: '', },
@@ -97,5 +99,6 @@ const editConfig =
{ name: 'SupProtocolCode', label: '补充协议编号', hidden: false, sortable: false, align: 'left', width: '150' },
{ name: 'ProcessStatus', label: '开户状态', hidden: false, sortable: false, align: 'left', width: '90' },
{ name: "Manager", label: "管理人名称", hidden: false, sortable: false, align: 'left', width: '100' },
{ name: 'MarginWatchRule', label: '追保规则', hidden: false, sortable: false, align: 'left', width: '90' },
]
}
+12
View File
@@ -80,6 +80,13 @@ namespace YLErp.Web.Controllers
{
c.VarietyName = GetWhiteListVarietyNames(c);
}
// R4:已使用授信(授信出入表 SUM(占用)−SUM(释放)+SUM(调整),按客户汇总)
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)
{
c.UsedCredit = usedCredits.TryGetValue(c.ClientId ?? 0, out var used) ? used : 0;
}
return Json(sList);
}
@@ -94,6 +101,11 @@ namespace YLErp.Web.Controllers
{
var intid = DataProtectHelper.DecryptInt(enid);
var r = yldb.credit.Find(intid);
// R4:已使用授信(授信出入表)
if (r != null && r.ClientId > 0)
{
r.UsedCredit = Modules.SwapModule.ClientCreditInoutService.GetUsedCredit(r.ClientId ?? 0, yldb);
}
return View(r);
}
@@ -302,6 +302,7 @@
<tr>
<th>收支方向</th>
<th>资金类别</th>
<th>资金来源</th>
<th>发生日期</th>
<th>金额</th>
<th>币种</th>
@@ -323,6 +324,13 @@
<option value=6>追加预付金</option>
</select>
</td>
<td>
<select v-model="item.FundTag" style="width:86px;" title="资金标签:确认成交时按此选择定稿——授信检查剩余额度,不足自动拆分为授信+现金两条;未选默认现金">
<option value="">默认(现金)</option>
<option value="Cash">现金</option>
<option value="Credit">授信</option>
</select>
</td>
<td>
<vue-datepicker :mindate="trade.TradeDate" :maxdate="trade.ExerciseDate" name="HappenDate" :holiday="1" v-model="item.HappenDate" style="width:100px;" ref="happenDateRefs" />
</td>
@@ -271,6 +271,7 @@
<tr>
<th>收支方向</th>
<th>资金类别</th>
<th>资金标签</th>
<th>发生日期</th>
<th>金额</th>
<th>币种</th>
@@ -286,6 +287,10 @@
<tr class="color-bule">
<td class="@bgclass" style="width:116px !important;">@((SwapDirectionEnum)item.InterestDirection)</td>
<td>@((InterestModeEnum)item.InterestMode)</td>
<td>
@*R4 资金标签(fund_tag 单列):录入时为用户选择,确认成交后为系统定稿(授信/现金)*@
@(item.FundTag == ConsFundTag.Credit ? "授信" : "现金")
</td>
<td>@item.HappenDate.OtcFormatDate()</td>
<td><span class="js-swap-common" data-value="@SwapCommonData(item.InterestPrincipalFix)" data-kind="amount"></span></td>
<td>@item.Currency</td>
+18 -3
View File
@@ -136,9 +136,16 @@
function checkSubmitData() {
var pass = $('#creditEditForm').valid();
if (page.UseClientStockEqvNotional) {
if ((!$("#Credit").val() && !$("#StockEqvNotional").val())) {
main.alert("名义本金规模和授信额度应至少填一项");
if (pass) {
if (page.UseClientStockEqvNotional) {
if ((!$("#Credit").val() && !$("#StockEqvNotional").val())) {
main.alert("名义本金规模和授信额度应至少填一项");
return false;
}
}
var ratio = $("#MaxCreditUseRatio").val();
if (ratio && (Number(ratio) < 0 || Number(ratio) > 1)) {
main.alert("最大授信可用比例取值范围为0-1");
return false;
}
}
@@ -346,6 +353,11 @@
$("#MarginLimit").val(moneyStr);
}
function moneyOnFocusOriginalCredit() {
var moneyStr = $("#OriginalCredit").val().replace(/,/g, "");
$("#OriginalCredit").val(moneyStr);
}
//转换为大写金额
function toAmountMoney(n) {
@@ -406,6 +418,9 @@
@Html.MyDecimalFor(model => model.Credit, new { onfocus = "moneyOnFocus()", onblur = "this.value=cc(this.value);" }, !pageObj.UseClientStockEqvNotional)
<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.MaxCreditUseRatio, required: false)
<span style="color:red">注:填写原始授信值后,授信额度将按"原始授信值×最大授信可用比例(空白按1)"自动计算.</span>
@if (PS.Config.Company == CompanyEnum.中金)
{
@Html.MyDecimalFor(model => model.PFECredit, new { onkeyup = "changePFECredit()", onfocus = "pfeCreditOnFocus()", onblur = "this.value=cc(this.value);" }, true)
+3
View File
@@ -78,6 +78,9 @@
<tr>@Html.MyDisplayFor(m => m.AuditStockEqvNotional)</tr>
}
<tr>@Html.MyDisplayFor(m => m.Credit)</tr>
<tr>@Html.MyDisplayFor(m => m.OriginalCredit)</tr>
<tr>@Html.MyDisplayFor(m => m.MaxCreditUseRatio)</tr>
<tr>@Html.MyDisplayFor(m => m.UsedCredit)</tr>
if (PS.Config.Company == CompanyEnum.中金)
{
<tr>@Html.MyDisplayFor(m => m.PFECredit)</tr>
@@ -108,6 +108,12 @@ const jqgrid1Mgr = (new function () {
}
colModelGrid = colModelGrid.concat([{
name: 'Credit', label: '授信额度', index: 'Credit', width: 110, align: 'right', formatter: 'number'
}, {
name: 'OriginalCredit', label: '原始授信值', index: 'OriginalCredit', width: 110, align: 'right', formatter: 'number'
}, {
name: 'MaxCreditUseRatio', label: '最大授信可用比例', index: 'MaxCreditUseRatio', width: 110, align: 'right', formatter: 'number'
}, {
name: 'UsedCredit', label: '已使用授信', index: 'UsedCredit', width: 110, align: 'right', formatter: 'number'
}, {
name: 'AuditCredit', label: '授信审批规模', index: 'AuditCredit', width: 110, align: 'right', formatter: 'number'
}, {
@@ -145,6 +151,10 @@ const jqgrid1Mgr = (new function () {
name: 'AuditStockEqvNotional', label: '名义本金审批规模', index: 'AuditStockEqvNotional', width: 110, align: 'right', formatter: 'number'
}, {
name: 'Credit', label: '授信额度', index: 'Credit', width: 110, align: 'right', formatter: 'number'
}, {
name: 'OriginalCredit', label: '原始授信值', index: 'OriginalCredit', width: 110, align: 'right', formatter: 'number'
}, {
name: 'MaxCreditUseRatio', label: '最大授信可用比例', index: 'MaxCreditUseRatio', width: 110, align: 'right', formatter: 'number'
}, {
name: 'PFECredit', label: 'PFE授信', index: 'PFECredit', width: 110, align: 'right', formatter: 'number'
}, {
@@ -1741,6 +1741,7 @@ const vue = new Vue({
IsAnnualized: true,//是否年化,
HappenDate: thisObj.trade.TradeDate,//发生日期,
Currency: 'CNY',//币种
FundTag: '',//资金标签(R4):空=默认现金,可选授信,录入存选择、确认时系统定稿
interest_rest_days: 7,//重置频率
interest_rule: null//利率准则
}
@@ -357,7 +357,8 @@ function confirmAllSelect() {
ors.push(or);
}
var pData = { tradeids: ors.join(",") };
pData.IsSkipCheck = true;
// R4:不再硬编码 IsSkipCheck=true 静默跳过资金校验——普通确认走校验,
// 资金不足时由 LackOfMoney 弹窗走显式特批(additionalProcessing)链路
if (!main.isEmpty(additionalProcessing)) {
pData.additionalProcessing = additionalProcessing;
}
@@ -1057,7 +1057,8 @@ function confirmAllSelect() {
ors.push(or);
}
var pData = { tradeids: ors.join(",") };
pData.IsSkipCheck = true;
// R4:不再硬编码 IsSkipCheck=true 静默跳过资金校验——普通确认走校验,
// 资金不足时由 LackOfMoney 弹窗走显式特批(additionalProcessing)链路
if (!main.isEmpty(additionalProcessing)) {
pData.additionalProcessing = additionalProcessing;
}