#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,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);
}