授信/现金资金标签分配链路 double→decimal 精度收敛
- FundTagCalc 全链路(LegAmount/LegFundPlan/UnwindTagSplit/TagRelease/MarginLegSettlement)金额类型改 decimal,消除 double 二进制误差在拆单倒挤、守恒校验中的分位尾差 - 边界处显式转换:writeCash 委托与 ClientCreditInoutService.Occupy/Release(仍 double)、GetAvailableCredit 返回值 (decimal) 强转 - SwapDealService.RecordMarginCashFlow/ReleaseMarginByFundTag 签名同步 decimal,测试桩 TestableSwapDealService 对齐 - 单测断言去掉 1e-6 容差(decimal 精确比较)
This commit is contained in:
@@ -11,7 +11,7 @@ namespace YLErp.Modules.SwapModule
|
||||
[TestClass]
|
||||
public class FundTagCalcTest
|
||||
{
|
||||
private static LegAmount Leg(long id, double amount, bool preferCredit)
|
||||
private static LegAmount Leg(long id, decimal amount, bool preferCredit)
|
||||
=> new() { Leg = new swap_position { id = id }, Amount = amount, PreferCredit = preferCredit };
|
||||
|
||||
// ================================================================
|
||||
@@ -273,7 +273,7 @@ namespace YLErp.Modules.SwapModule
|
||||
InterestPrincipalFix = fix,
|
||||
FundTag = preferCredit ? ConsFundTag.Credit : ConsFundTag.Cash
|
||||
},
|
||||
Amount = (double)fix,
|
||||
Amount = fix,
|
||||
PreferCredit = preferCredit
|
||||
};
|
||||
|
||||
|
||||
@@ -71,8 +71,8 @@ namespace YLErp.Modules.SwapModule
|
||||
new List<LegAmount> { new() { Leg = leg, Amount = 1000, PreferCredit = true } },
|
||||
creditAvailable: 5_000, ignoreMoneyCheck: false);
|
||||
|
||||
Assert.AreEqual(1000, plans[0].CreditAmount, 1e-6);
|
||||
Assert.AreEqual(0, plans[0].CashAmount, 1e-6);
|
||||
Assert.AreEqual(1000, plans[0].CreditAmount);
|
||||
Assert.AreEqual(0, plans[0].CashAmount);
|
||||
Assert.IsFalse(plans[0].NeedSplit);
|
||||
//CashAmount==0 → ApplyMarginFundTags 不写现金流水;占用 remark 前缀断言
|
||||
Assert.IsTrue((ClientCreditInoutService.AdditionalMarginRemark + "占用")
|
||||
@@ -94,8 +94,8 @@ namespace YLErp.Modules.SwapModule
|
||||
var legs = new List<LegAmount> { new() { Leg = leg, Amount = 1000, PreferCredit = true } };
|
||||
var plans = FundTagCalc.AllocateByLegPreference(legs, creditAvailable: 300, ignoreMoneyCheck: false);
|
||||
Assert.IsTrue(plans[0].NeedSplit);
|
||||
Assert.AreEqual(300, plans[0].CreditAmount, 1e-6);
|
||||
Assert.AreEqual(700, plans[0].CashAmount, 1e-6);
|
||||
Assert.AreEqual(300, plans[0].CreditAmount);
|
||||
Assert.AreEqual(700, plans[0].CashAmount);
|
||||
|
||||
//ApplySaveTimeSplit 与 SplitLeg 同口径(原腿=授信部分、克隆现金腿倒挤守恒)
|
||||
var newLegs = FundTagCalc.ApplySaveTimeSplit(legs, plans);
|
||||
@@ -126,8 +126,8 @@ namespace YLErp.Modules.SwapModule
|
||||
new List<LegAmount> { new() { Leg = leg, Amount = 1000, PreferCredit = false } },
|
||||
creditAvailable: 5_000, ignoreMoneyCheck: false);
|
||||
|
||||
Assert.AreEqual(0, plans[0].CreditAmount, 1e-6);
|
||||
Assert.AreEqual(1000, plans[0].CashAmount, 1e-6);
|
||||
Assert.AreEqual(0, plans[0].CreditAmount);
|
||||
Assert.AreEqual(1000, plans[0].CashAmount);
|
||||
Assert.IsFalse(plans[0].NeedSplit);
|
||||
}
|
||||
|
||||
|
||||
@@ -141,10 +141,10 @@ namespace YLErp.Modules.SwapModule
|
||||
return ReleaseMarginByFundTagResult
|
||||
?? new UnwindTagSplit
|
||||
{
|
||||
CashMargin = Convert.ToDouble(marginAmount),
|
||||
CashRebate = Convert.ToDouble(marginLegs.Any()
|
||||
CashMargin = marginAmount,
|
||||
CashRebate = marginLegs.Any()
|
||||
? marginLegs.Sum(x => x.InterestClosePnL)
|
||||
: marginRebate)
|
||||
: marginRebate
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -16,14 +16,14 @@ public static class FundTagCalc
|
||||
/// 客户净收取(负金额)的腿不参与授信分配。
|
||||
/// legs 需按预期占用顺序传入(HappenDate、id)。
|
||||
/// </summary>
|
||||
public static List<LegFundPlan> AllocateByLegPreference(List<LegAmount> legs, double creditAvailable, bool ignoreMoneyCheck)
|
||||
public static List<LegFundPlan> AllocateByLegPreference(List<LegAmount> legs, decimal creditAvailable, bool ignoreMoneyCheck)
|
||||
{
|
||||
var plans = new List<LegFundPlan>();
|
||||
var remaining = Math.Round(Math.Max(creditAvailable, 0), 2, MidpointRounding.AwayFromZero);
|
||||
var remaining = Math.Round(Math.Max(creditAvailable, 0m), 2, MidpointRounding.AwayFromZero);
|
||||
foreach (var leg in legs)
|
||||
{
|
||||
var amount = Math.Round(leg.Amount, 2, MidpointRounding.AwayFromZero);
|
||||
var credit = 0.0;
|
||||
var credit = 0m;
|
||||
if (!ignoreMoneyCheck && leg.PreferCredit && amount > 0)
|
||||
{
|
||||
credit = Math.Min(amount, remaining);
|
||||
@@ -64,7 +64,7 @@ public static class FundTagCalc
|
||||
//应付额 = 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.InterestPrincipalFix = Math.Round(plan.CreditAmount * payableRatio, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
position.FundTag = ConsFundTag.Credit;
|
||||
var cashLeg = position.Clone();
|
||||
cashLeg.id = 0;
|
||||
@@ -93,8 +93,8 @@ public static class FundTagCalc
|
||||
var result = new UnwindTagSplit();
|
||||
foreach (var s in settlements)
|
||||
{
|
||||
var margin = Convert.ToDouble(s.MarginAmount);
|
||||
var rebate = Convert.ToDouble(s.RebateAmount);
|
||||
var margin = s.MarginAmount;
|
||||
var rebate = s.RebateAmount;
|
||||
if (s.Tag == ConsFundTag.Credit)
|
||||
{
|
||||
result.CreditMargin += margin;
|
||||
@@ -128,7 +128,7 @@ public static class FundTagCalc
|
||||
public class LegAmount
|
||||
{
|
||||
public swap_position Leg { get; set; }
|
||||
public double Amount { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
/// <summary>腿上是否选了授信(swap_position.fund_tag=='Credit')</summary>
|
||||
public bool PreferCredit { get; set; }
|
||||
}
|
||||
@@ -140,11 +140,11 @@ public class LegFundPlan
|
||||
{
|
||||
public swap_position Leg { get; set; }
|
||||
/// <summary>腿原簿记金额</summary>
|
||||
public double Amount { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
/// <summary>授信部分金额</summary>
|
||||
public double CreditAmount { get; set; }
|
||||
public decimal CreditAmount { get; set; }
|
||||
/// <summary>现金部分金额</summary>
|
||||
public double CashAmount { get; set; }
|
||||
public decimal CashAmount { get; set; }
|
||||
/// <summary>拆单时新拆出的现金腿(授信不足的差额;占用记录绑原腿、现金流水绑它)</summary>
|
||||
public swap_position CashLeg { get; set; }
|
||||
public bool NeedSplit => CreditAmount > 0 && CashAmount > 0;
|
||||
@@ -198,7 +198,7 @@ public static class MarginSettlementBuilder
|
||||
public class TagRelease
|
||||
{
|
||||
public long PositionId { get; set; }
|
||||
public double Amount { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -207,13 +207,13 @@ public class TagRelease
|
||||
public class UnwindTagSplit
|
||||
{
|
||||
/// <summary>现金部分返还本金(产生 应付预付金 资金流水)</summary>
|
||||
public double CashMargin { get; set; }
|
||||
public decimal CashMargin { get; set; }
|
||||
/// <summary>授信部分返还本金(写授信出入表"释放",不产生资金流水)</summary>
|
||||
public double CreditMargin { get; set; }
|
||||
public decimal CreditMargin { get; set; }
|
||||
/// <summary>现金部分预付金返息(产生 预付金返息 资金流水)</summary>
|
||||
public double CashRebate { get; set; }
|
||||
public decimal CashRebate { get; set; }
|
||||
/// <summary>授信部分预付金返息(授信不进资金,不产生资金流水)</summary>
|
||||
public double CreditRebate { get; set; }
|
||||
public decimal CreditRebate { get; set; }
|
||||
/// <summary>按腿的授信释放明细(position_id 匹配原占用记录)</summary>
|
||||
public List<TagRelease> Releases { get; set; } = new List<TagRelease>();
|
||||
}
|
||||
|
||||
@@ -1541,11 +1541,11 @@ namespace YLErp.Modules.SwapModule
|
||||
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) };
|
||||
: new UnwindTagSplit { CashMargin = marginAmount, CashRebate = marginRebate };
|
||||
if (split.CashMargin != 0)
|
||||
writeCash(td, split.CashMargin, ClientCashInCashOut.系统操作_应付预付金, valueDate);
|
||||
writeCash(td, (double)split.CashMargin, ClientCashInCashOut.系统操作_应付预付金, valueDate);
|
||||
if (split.CashRebate != 0)
|
||||
writeCash(td, -split.CashRebate, ClientCashInCashOut.系统操作_预付金返息, valueDate);
|
||||
writeCash(td, (double)(-split.CashRebate), ClientCashInCashOut.系统操作_预付金返息, valueDate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1561,7 +1561,7 @@ namespace YLErp.Modules.SwapModule
|
||||
//结算事件缺失(异常数据)时保底按传入总额走现金,不丢资金记录
|
||||
if (settlements.Count == 0)
|
||||
{
|
||||
return new UnwindTagSplit { CashMargin = Convert.ToDouble(marginAmount), CashRebate = Convert.ToDouble(marginRebate) };
|
||||
return new UnwindTagSplit { CashMargin = marginAmount, CashRebate = marginRebate };
|
||||
}
|
||||
return split;
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ namespace YLErp.Modules.SwapModule
|
||||
.Select(x => new LegAmount
|
||||
{
|
||||
Leg = x,
|
||||
Amount = Convert.ToDouble(x.InterestPrincipalFix * (x.InterestDirection == 1 ? 1 : -1)),
|
||||
Amount = x.InterestPrincipalFix * (x.InterestDirection == 1 ? 1 : -1),
|
||||
PreferCredit = true
|
||||
})
|
||||
.Where(x => x.Amount > 0)
|
||||
@@ -89,7 +89,7 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
return;
|
||||
}
|
||||
var plans = FundTagCalc.AllocateByLegPreference(allocateLegs, creditAvailable, ignoreMoneyCheck: false);
|
||||
var plans = FundTagCalc.AllocateByLegPreference(allocateLegs, (decimal)creditAvailable, ignoreMoneyCheck: false);
|
||||
//授信不足的腿 = 偏好授信但授信没覆盖全额(含额度为0/被前腿耗尽的整体转现金)
|
||||
var shortPlans = plans.Where(p => p.CreditAmount < p.Amount).ToList();
|
||||
if (shortPlans.Count == 0)
|
||||
@@ -157,7 +157,7 @@ namespace YLErp.Modules.SwapModule
|
||||
.Select(x => new LegAmount
|
||||
{
|
||||
Leg = x,
|
||||
Amount = Convert.ToDouble(x.InterestPrincipalFix * (x.InterestDirection == 1 ? 1 : -1)),
|
||||
Amount = x.InterestPrincipalFix * (x.InterestDirection == 1 ? 1 : -1),
|
||||
//优先级:腿上显式选择 > 交易级 margin_fund_source 回退(§2.3 情形1)> 默认现金,
|
||||
//与 TradeCanBeConfirm 校验分流共用 ConsFundTag.PreferCredit 保证口径一致
|
||||
PreferCredit = ConsFundTag.PreferCredit(x.FundTag, td.MarginFundSource)
|
||||
@@ -166,7 +166,7 @@ namespace YLErp.Modules.SwapModule
|
||||
.OrderBy(x => x.Leg.HappenDate ?? DateTime.MaxValue)
|
||||
.ThenBy(x => x.Leg.id)
|
||||
.ToList();
|
||||
var plans = FundTagCalc.AllocateByLegPreference(allocateLegs, creditAvailable, ignoreMoneyCheck);
|
||||
var plans = FundTagCalc.AllocateByLegPreference(allocateLegs, (decimal)creditAvailable, ignoreMoneyCheck);
|
||||
|
||||
//先落库拆分的新现金腿(需要 id 才能绑定现金流水)
|
||||
foreach (var plan in plans.Where(p => p.NeedSplit))
|
||||
@@ -198,13 +198,13 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
//授信部分(整腿授信 或 拆单后保留在原腿的可用额度部分):不产生资金流水,只写占用(占用绑原腿)。
|
||||
//占用记正数(BUG-01 修正:已使用授信=Σ(amount) 占用上升;2026-08-20"与资金流水同号入金负"口径已废弃)
|
||||
creditService.Occupy(td.ClientId, leg.id, td.id, plan.CreditAmount, happenDate,
|
||||
creditService.Occupy(td.ClientId, leg.id, td.id, (double)plan.CreditAmount, happenDate,
|
||||
plan.NeedSplit ? splitOccupyRemark : occupyRemark);
|
||||
}
|
||||
//资金记录沿用既有符号口径(客户付钱为负 = -应付额):授信部分不产生流水,
|
||||
//现金部分按差额产生——拆单腿的流水绑新拆出的现金腿,整腿现金/负应付腿绑原腿
|
||||
var recordAmount = plan != null
|
||||
? -plan.CashAmount
|
||||
? (double)(-plan.CashAmount)
|
||||
: Convert.ToDouble(leg.InterestPrincipalFix * (leg.InterestDirection == 1 ? -1 : 1));
|
||||
if (recordAmount != 0)
|
||||
{
|
||||
@@ -256,7 +256,7 @@ namespace YLErp.Modules.SwapModule
|
||||
var creditService = new ClientCreditInoutService(this);
|
||||
foreach (var release in split.Releases)
|
||||
{
|
||||
creditService.Release(td.ClientId, release.PositionId, td.id, release.Amount, valueDate, "平仓/到期释放");
|
||||
creditService.Release(td.ClientId, release.PositionId, td.id, (double)release.Amount, valueDate, "平仓/到期释放");
|
||||
}
|
||||
}
|
||||
return split;
|
||||
|
||||
Reference in New Issue
Block a user