授信拆单;保证金模板选择;客户层级资金来源
This commit is contained in:
@@ -40,6 +40,49 @@ public static class FundTagCalc
|
||||
return plans;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存前授信拆单(§2.3 保存前拆单,2026-08-26 业务确认):把确认成交阶段的物理拆分前移到录入保存——
|
||||
/// 对 NeedSplit 的腿:原腿保留授信部分(InterestPrincipalFix 按可用额度折算)标 Credit,
|
||||
/// 克隆一条现金差额腿(倒挤守恒)标 Cash 返回(Obervation 置空,防 SaveSwapPositions 重复插观察配置);
|
||||
/// 不拆的授信偏好腿同步定稿标签:全额授信→Credit、额度为0/耗尽全额现金→Cash;
|
||||
/// 现金/默认腿不动(最终定稿仍由确认成交 ApplyMarginFundTags 兜底重写)。
|
||||
/// legs 与 plans 须为 AllocateByLegPreference 的同序输入输出。占用/流水仍发生在确认成交。
|
||||
/// </summary>
|
||||
public static List<swap_position> ApplySaveTimeSplit(List<LegAmount> legs, List<LegFundPlan> plans)
|
||||
{
|
||||
var newLegs = new List<swap_position>();
|
||||
for (var i = 0; i < plans.Count; i++)
|
||||
{
|
||||
if (!legs[i].PreferCredit)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var plan = plans[i];
|
||||
if (plan.NeedSplit)
|
||||
{
|
||||
var position = plan.Leg;
|
||||
//应付额 = fix × (dir==1 ? 1 : -1),反推 fix 用同一比例(±1 自反)
|
||||
var payableRatio = position.InterestDirection == 1 ? 1 : -1;
|
||||
var originalFix = position.InterestPrincipalFix;
|
||||
position.InterestPrincipalFix = Math.Round(Convert.ToDecimal(plan.CreditAmount) * payableRatio, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
position.FundTag = ConsFundTag.Credit;
|
||||
var cashLeg = position.Clone();
|
||||
cashLeg.id = 0;
|
||||
cashLeg.PositionId = 0;
|
||||
cashLeg.Obervation = null;
|
||||
//现金腿倒挤 = 原 fix − 授信 fix(分别独立舍入会有分位尾差,倒挤保证两腿合计与原 fix 守恒)
|
||||
cashLeg.InterestPrincipalFix = originalFix - position.InterestPrincipalFix;
|
||||
cashLeg.FundTag = ConsFundTag.Cash;
|
||||
newLegs.Add(cashLeg);
|
||||
}
|
||||
else
|
||||
{
|
||||
plan.Leg.FundTag = plan.CreditAmount > 0 ? ConsFundTag.Credit : ConsFundTag.Cash;
|
||||
}
|
||||
}
|
||||
return newLegs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 平仓/到期返还金额按被平仓腿的 FundTag 分流(§2.4):
|
||||
/// Credit 腿的返还本金与返息不产生资金流水(本金写授信出入表"释放",出金方向记正数),Cash/无标签(存量)走现金。
|
||||
@@ -102,8 +145,8 @@ public class LegFundPlan
|
||||
public double CreditAmount { get; set; }
|
||||
/// <summary>现金部分金额</summary>
|
||||
public double CashAmount { get; set; }
|
||||
/// <summary>拆单时新拆出的授信腿(占用记录绑定到它)</summary>
|
||||
public swap_position CreditLeg { get; set; }
|
||||
/// <summary>拆单时新拆出的现金腿(授信不足的差额;占用记录绑原腿、现金流水绑它)</summary>
|
||||
public swap_position CashLeg { get; set; }
|
||||
public bool NeedSplit => CreditAmount > 0 && CashAmount > 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ namespace YLErp.Modules.SwapModule
|
||||
/// 标签赋值与返还两个写入口集中在本服务,授信出入表(ClientCreditInoutService)的占用/释放由此统一触发。
|
||||
/// 口径:授信值取 credit.Credit 合计(已审批+日期有效+含母公司,阶段一已折算),已使用授信取授信出入表;
|
||||
/// 授信不进资金——授信部分不产生资金流水。
|
||||
/// 资金标签是预付金腿上的单列(swap_position.fund_tag,逐腿):录入时存用户选择(授信/现金/未选默认现金),确认成交时系统在同列定稿。
|
||||
/// 资金标签是预付金腿上的单列(swap_position.fund_tag,逐腿):录入时存用户选择(授信/现金/未选回退交易级
|
||||
/// margin_fund_source,交易级也未设默认现金),确认成交时系统在同列定稿。
|
||||
/// </summary>
|
||||
public class SwapFundTagService : YLBaseService
|
||||
{
|
||||
@@ -50,10 +51,81 @@ namespace YLErp.Modules.SwapModule
|
||||
return GetEffectiveCredit(clientId, valueDate) - ClientCreditInoutService.GetUsedCredit(clientId, DbContext);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存前授信拆单(§2.3 保存前拆单,2026-08-26 业务确认:保存检查授信→不足拦截→UI 确认→拆完再保存)。
|
||||
/// 按当前剩余授信对偏好授信的预付金腿(腿选授信,或腿默认回退交易级资金来源=授信)做物理拆分:
|
||||
/// 原腿=可用额度 标授信、克隆现金差额腿(插回 td.swap_positions 随保存落库);额度为0/耗尽的授信腿整体定稿现金。
|
||||
/// 有授信不足且未带确认标记(allowSplit=false)时抛 TradeMarginCreditSplitException——
|
||||
/// controller 返回 AdditionalProcessing/MarginCreditSplit 由 UI 确认后带参重提。
|
||||
/// 本方法只拆腿不定簿记:授信占用/资金流水仍在确认成交 ApplyMarginFundTags。
|
||||
/// </summary>
|
||||
public void PreSplitMarginLegsByCredit(trade td, bool allowSplit)
|
||||
{
|
||||
var marginModes = new[] { (int)InterestModeEnum.追加预付金, (int)InterestModeEnum.初始预付金 };
|
||||
var preferLegs = (td.swap_positions ?? new List<swap_position>())
|
||||
.Where(x => marginModes.Contains(x.InterestMode)
|
||||
//不扣本金的腿不产生预付金簿记(与 SwapTradeConfirm 同口径),不参与拆分
|
||||
&& (x.Obervation == null || x.Obervation.IsDeductPrincipal)
|
||||
&& ConsFundTag.PreferCredit(x.FundTag, td.MarginFundSource))
|
||||
.ToList();
|
||||
if (preferLegs.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var valueDate = td.TradeDate ?? DateTime.Now;
|
||||
var creditAvailable = GetAvailableCredit(td.ClientId, valueDate);
|
||||
var allocateLegs = preferLegs
|
||||
.Select(x => new LegAmount
|
||||
{
|
||||
Leg = x,
|
||||
Amount = Convert.ToDouble(x.InterestPrincipalFix * (x.InterestDirection == 1 ? 1 : -1)),
|
||||
PreferCredit = true
|
||||
})
|
||||
.Where(x => x.Amount > 0)
|
||||
.OrderBy(x => x.Leg.HappenDate ?? DateTime.MaxValue)
|
||||
.ThenBy(x => x.Leg.id)
|
||||
.ToList();
|
||||
if (allocateLegs.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var plans = FundTagCalc.AllocateByLegPreference(allocateLegs, creditAvailable, ignoreMoneyCheck: false);
|
||||
//授信不足的腿 = 偏好授信但授信没覆盖全额(含额度为0/被前腿耗尽的整体转现金)
|
||||
var shortPlans = plans.Where(p => p.CreditAmount < p.Amount).ToList();
|
||||
if (shortPlans.Count == 0)
|
||||
{
|
||||
//额度充足:全额授信腿就法定稿授信(含"默认+交易级授信"回退解析),无拆分、无拦截
|
||||
FundTagCalc.ApplySaveTimeSplit(allocateLegs, plans);
|
||||
return;
|
||||
}
|
||||
if (!allowSplit)
|
||||
{
|
||||
var detail = string.Join(";", shortPlans.Select(p => p.NeedSplit
|
||||
? $"金额 {p.Amount:#,##0.00} → 授信 {p.CreditAmount:#,##0.00} + 现金 {p.CashAmount:#,##0.00}"
|
||||
: $"金额 {p.Amount:#,##0.00} → 全额现金(可用授信不足)"));
|
||||
throw new TradeMarginCreditSplitException(
|
||||
$"预付金授信额度不足,剩余可用授信 {Math.Max(creditAvailable, 0):#,##0.00}:{detail}。"
|
||||
+ "确认后将按上述拆分保存(授信部分确认成交时占用授信额度、不产生资金流水;现金部分产生应付预付金)。");
|
||||
}
|
||||
var newLegs = FundTagCalc.ApplySaveTimeSplit(allocateLegs, plans);
|
||||
//新现金腿插回原腿之后(列表相邻,随 SaveSwapPositions 落库并分配 PosiNumber)
|
||||
var splitPlans = plans.Where(p => p.NeedSplit).ToList();
|
||||
for (var i = 0; i < newLegs.Count; i++)
|
||||
{
|
||||
newLegs[i].OptId = UserId;
|
||||
newLegs[i].OptName = UserName;
|
||||
newLegs[i].OptTime = DateTime.Now;
|
||||
var original = splitPlans[i].Leg;
|
||||
var index = td.swap_positions.IndexOf(original);
|
||||
td.swap_positions.Insert(index < 0 ? td.swap_positions.Count : index + 1, newLegs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 簿记确认时对预付金腿定稿资金标签并产生资金记录(§2.3 四种情形,逐腿)。
|
||||
/// fund_tag 单列:录入时存用户选择(Credit/Cash/NULL),本方法读取选择后在同列定稿——
|
||||
/// 特批全现金;选授信按剩余额度分配(跨界腿拆单为 授信+现金 两条),未选/现金直接现金。
|
||||
/// 特批全现金;按授信分配(腿选授信,或腿未选回退交易级 margin_fund_source=授信)的腿按剩余额度占用,
|
||||
/// 跨界腿拆单为 授信+现金 两条(原腿保留授信部分、差额拆出新现金腿);现金直接现金。
|
||||
/// 授信腿只写授信出入表占用(占用记正数,绑定腿 position_id,冗余 trade_id),不产生资金流水;
|
||||
/// 现金腿走 SaveSwapTradeClientCash 幂等 upsert 产生 应付预付金 记录。
|
||||
/// marginLegs 需为已过滤(IsDeductPrincipal 等)的预付金腿(InterestMode=5/6)。
|
||||
@@ -74,7 +146,9 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
Leg = x,
|
||||
Amount = Convert.ToDouble(x.InterestPrincipalFix * (x.InterestDirection == 1 ? 1 : -1)),
|
||||
PreferCredit = x.FundTag == ConsFundTag.Credit
|
||||
//优先级:腿上显式选择 > 交易级 margin_fund_source 回退(§2.3 情形1)> 默认现金,
|
||||
//与 TradeCanBeConfirm 校验分流共用 ConsFundTag.PreferCredit 保证口径一致
|
||||
PreferCredit = ConsFundTag.PreferCredit(x.FundTag, td.MarginFundSource)
|
||||
})
|
||||
.Where(x => x.Amount > 0)
|
||||
.OrderBy(x => x.Leg.HappenDate ?? DateTime.MaxValue)
|
||||
@@ -82,12 +156,12 @@ namespace YLErp.Modules.SwapModule
|
||||
.ToList();
|
||||
var plans = FundTagCalc.AllocateByLegPreference(allocateLegs, creditAvailable, ignoreMoneyCheck);
|
||||
|
||||
//先落库拆分的新腿(需要 id 才能绑定占用记录)
|
||||
//先落库拆分的新现金腿(需要 id 才能绑定现金流水)
|
||||
foreach (var plan in plans.Where(p => p.NeedSplit))
|
||||
{
|
||||
plan.CreditLeg = SplitLeg(td, plan);
|
||||
plan.CashLeg = SplitLeg(td, plan);
|
||||
}
|
||||
//标签定稿(覆盖录入选择):拆单的两条腿在 SplitLeg 内已分别标 Cash/Credit;
|
||||
//标签定稿(覆盖录入选择):拆单的两条腿在 SplitLeg 内已分别定稿(原腿=授信、新腿=现金);
|
||||
//整腿授信→Credit、整腿现金/负应付(客户净收取)腿→Cash
|
||||
foreach (var leg in marginLegs)
|
||||
{
|
||||
@@ -110,26 +184,27 @@ namespace YLErp.Modules.SwapModule
|
||||
var happenDate = leg.HappenDate ?? td.TradeDate ?? DateTime.Now;
|
||||
if (plan != null && plan.CreditAmount > 0)
|
||||
{
|
||||
//整腿授信 或 拆单后的授信部分:不产生资金流水,只写占用(拆单绑新拆出的授信腿)。
|
||||
//授信部分(整腿授信 或 拆单后保留在原腿的可用额度部分):不产生资金流水,只写占用(占用绑原腿)。
|
||||
//占用记正数(BUG-01 修正:已使用授信=Σ(amount) 占用上升;2026-08-20"与资金流水同号入金负"口径已废弃)
|
||||
creditService.Occupy(td.ClientId, plan.CreditLeg?.id ?? leg.id, td.id, plan.CreditAmount, happenDate,
|
||||
creditService.Occupy(td.ClientId, 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.系统操作_应付预付金);
|
||||
cashService.SaveSwapTradeClientCash(td, recordAmount, happenDate, plan?.CashLeg?.id ?? leg.id, ClientCashInCashOut.系统操作_应付预付金);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 拆单:把跨界腿拆为 授信+现金 两条。原腿保留现金部分并标 Cash(资金来源同步改现金,与最终标签一致),
|
||||
/// 克隆一条授信腿(InterestPrincipalFix 按授信金额折算)标 Credit,返回新腿。
|
||||
/// 拆出的腿为普通初始腿,后续编辑/回退/平仓链路按既有腿处理。
|
||||
/// 拆单:把跨界腿拆为 授信+现金 两条。原腿保留授信部分(可用额度)并标 Credit(占用记录绑原腿),
|
||||
/// 克隆一条现金腿(授信不足的差额,InterestPrincipalFix 按现金金额折算)标 Cash,返回新腿
|
||||
/// (现金流水绑新腿)。拆出的腿为普通初始腿,后续编辑/回退/平仓链路按既有腿处理。
|
||||
/// </summary>
|
||||
private swap_position SplitLeg(trade td, LegFundPlan plan)
|
||||
{
|
||||
@@ -137,22 +212,22 @@ namespace YLErp.Modules.SwapModule
|
||||
//应付额 = fix × (dir==1 ? 1 : -1),反推 fix 用同一比例(±1 自反)
|
||||
var payableRatio = leg.InterestDirection == 1 ? 1 : -1;
|
||||
var originalFix = leg.InterestPrincipalFix;
|
||||
leg.InterestPrincipalFix = Math.Round(Convert.ToDecimal(plan.CashAmount) * payableRatio, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
leg.FundTag = ConsFundTag.Cash;
|
||||
leg.InterestPrincipalFix = Math.Round(Convert.ToDecimal(plan.CreditAmount) * payableRatio, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
leg.FundTag = ConsFundTag.Credit;
|
||||
|
||||
var creditLeg = leg.Clone();
|
||||
creditLeg.id = 0;
|
||||
creditLeg.PositionId = 0;
|
||||
//授信腿倒挤 = 原 fix − 现金 fix(BUG-20:两腿分别独立舍入会有分位尾差,倒挤保证两腿合计与原 fix 守恒)
|
||||
creditLeg.InterestPrincipalFix = originalFix - leg.InterestPrincipalFix;
|
||||
creditLeg.FundTag = ConsFundTag.Credit;
|
||||
creditLeg.OptId = UserId;
|
||||
creditLeg.OptName = UserName;
|
||||
creditLeg.OptTime = DateTime.Now;
|
||||
DbContext.swap_position.Add(creditLeg);
|
||||
var cashLeg = leg.Clone();
|
||||
cashLeg.id = 0;
|
||||
cashLeg.PositionId = 0;
|
||||
//现金腿倒挤 = 原 fix − 授信 fix(两腿分别独立舍入会有分位尾差,倒挤保证两腿合计与原 fix 守恒)
|
||||
cashLeg.InterestPrincipalFix = originalFix - leg.InterestPrincipalFix;
|
||||
cashLeg.FundTag = ConsFundTag.Cash;
|
||||
cashLeg.OptId = UserId;
|
||||
cashLeg.OptName = UserName;
|
||||
cashLeg.OptTime = DateTime.Now;
|
||||
DbContext.swap_position.Add(cashLeg);
|
||||
DbContext.SaveChanges();
|
||||
creditLeg.PosiNumber = $"{td.TradeNumber}-{creditLeg.id}";
|
||||
return creditLeg;
|
||||
cashLeg.PosiNumber = $"{td.TradeNumber}-{cashLeg.id}";
|
||||
return cashLeg;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -86,8 +86,19 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <param name="td"></param>
|
||||
/// <param name="ignoreMoneyCheck"></param>
|
||||
/// <returns></returns>
|
||||
public trade SaveTrade(trade req, bool ignoreMoneyCheck = false)
|
||||
public trade SaveTrade(trade req, bool ignoreMoneyCheck = false, bool allowMarginCreditSplit = false)
|
||||
{
|
||||
//资金来源必填(现金/授信,默认现金):保存前归一,兜住 DMA 自动建仓等绕过录入页的链路
|
||||
if (req.TradeType == "收益互换" && string.IsNullOrWhiteSpace(req.MarginFundSource))
|
||||
{
|
||||
req.MarginFundSource = ConsFundTag.Cash;
|
||||
}
|
||||
// §2.3 保存前授信拆单(2026-08-26 业务确认):保存检查授信→不足拦截(UI 确认)→拆完再保存。
|
||||
// 特批(ignoreMoneyCheck)语义为全现金不占授信,跳过拆单
|
||||
if (!ignoreMoneyCheck && req.TradeType == "收益互换")
|
||||
{
|
||||
new SwapFundTagService(this).PreSplitMarginLegsByCredit(req, allowMarginCreditSplit);
|
||||
}
|
||||
var um = checkUnderlying(req);
|
||||
trade dbTrade = new trade();
|
||||
//交易保存处理(PrepareInitialMargin 在此把 trade_Initial_Margin 折算进 req.InitialMargin,
|
||||
@@ -1709,7 +1720,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
fundTagSvc.ApplyMarginFundTags(td, generateMarginLegs, cashSvc, false);
|
||||
//标签定稿(含可能的拆单)后重克隆实时持仓:TradeBack 的克隆先于定稿生成,
|
||||
//重克隆使实时腿继承定稿标签、新拆出的授信腿也获得克隆(平仓返还分流查的是实时腿标签)
|
||||
//重克隆使实时腿继承定稿标签、拆单新拆出的现金腿也获得克隆(平仓返还分流查的是实时腿标签)
|
||||
InitialPosition(td);
|
||||
// 合约维度盯市+无预付金腿:重建交易级(positionId=0)初始预付金记录(与 SwapTradeConfirm 一致,回退重补场景)。
|
||||
// 有预付金腿的互换由上面按腿重建,不在此重复生成。
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using BaseOUDAL;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 保存交易时预付金授信不足的标记异常(§2.3 保存前拆单,2026-08-26 业务确认):
|
||||
/// 保存检查授信→不足拦截→UI 确认(AdditionalProcessing/MarginCreditSplit)→带参重提后
|
||||
/// 按剩余授信物理拆分预付金腿(原腿=可用额度 标授信、新腿=差额 标现金)再保存。
|
||||
/// 属标准业务流程,不受"允许交易特批"开关控制(与 LackOfMoney 特批协议区分)。
|
||||
/// </summary>
|
||||
public class TradeMarginCreditSplitException : ServiceException
|
||||
{
|
||||
public TradeMarginCreditSplitException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user