BugFix 追加预付金腿生成
This commit is contained in:
@@ -260,8 +260,9 @@ namespace YLErp.DBModels
|
||||
public const string 系统操作_应付预付金 = "系统操作-应付预付金";
|
||||
public const string 系统操作_预付金返息 = "系统操作-预付金返息";
|
||||
/// <summary>
|
||||
/// 阶段四 §4.1 合约维度(MarginWatchRule==0)EOD 结算产生的追加保证金资金记录:
|
||||
/// 交易级单条累计值(TradeId+Action+Deal=0 幂等 upsert),负数=客户应付追加;写入见 SwapAdditionalMarginService。
|
||||
/// 阶段四 §4.1 合约维度(MarginWatchRule==0)EOD 结算产生的追加保证金资金记录,负数=客户应付追加;
|
||||
/// 腿化改造后由追保腿的现金部分簿记产生(Deal=现金腿 id,键 TradeId+Action+Deal 幂等 upsert;
|
||||
/// EOD 重跑先删旧腿连带流水后重建,见 SwapAdditionalMarginService)。
|
||||
/// </summary>
|
||||
public const string 系统操作_追加保证金 = "系统操作-追加保证金";
|
||||
public const string 人工操作_其他 = "人工操作-其他";
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// EOD 追保腿化改造测试(纯函数层:腿构造、资金来源分配、幂等识别、跨日水位)。
|
||||
/// 服务层(SettleAdditionalMargin 的 DB 写入编排)依赖真实库,无内存测试基建——
|
||||
/// 此处按服务内实际调用顺序组合 SwapAdditionalMarginCalc / ConsFundTag / FundTagCalc 纯函数验证等价语义:
|
||||
/// 增量>0 → BuildEodMarginLeg 建腿 → PreferCredit(腿标签, 交易级资金来源) 回退 → AllocateByLegPreference 分配
|
||||
/// (= ApplyMarginFundTags 内部分配),拆单守恒走 ApplySaveTimeSplit(与 SplitLeg 同口径倒挤)。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SwapAdditionalMarginLegTest
|
||||
{
|
||||
private static readonly DateTime SettleDate = new(2026, 8, 27);
|
||||
|
||||
private static trade NewTrade(string fundSource = ConsFundTag.Credit)
|
||||
=> new()
|
||||
{
|
||||
id = 2543,
|
||||
TradeNumber = "TRS2026-2543",
|
||||
StartDate = new DateTime(2026, 8, 20),
|
||||
ExerciseDate = new DateTime(2027, 8, 20),
|
||||
SettlementCurrency = "CNY",
|
||||
MarginFundSource = fundSource
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 目标>累计 → 生成 1 条 mode6 追保腿:与手工追加预付金同形态
|
||||
/// (资金腿、IsInitial、收取方向、fix=增量两位 AwayFromZero 舍入、HappenDate=结算日、FundTag=NULL 回退、OptName 打标)。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void AM_LEG_001_目标大于累计_生成追保腿()
|
||||
{
|
||||
var td = NewTrade();
|
||||
var target = SwapAdditionalMarginCalc.CalcTarget(2_808_000, 2_000_000);
|
||||
Assert.AreEqual(808_000, target, 1e-6);
|
||||
|
||||
var leg = SwapAdditionalMarginService.BuildEodMarginLeg(td, target, SettleDate, optId: 0);
|
||||
|
||||
Assert.AreEqual(2543, leg.SwapTradeId);
|
||||
Assert.AreEqual(0, leg.PositionType);
|
||||
Assert.AreEqual(0, leg.PosiDirection);
|
||||
Assert.IsTrue(leg.IsInitial);
|
||||
Assert.AreEqual((int)InterestModeEnum.追加预付金, leg.InterestMode);
|
||||
Assert.AreEqual((int)SwapDirectionEnum.收取, leg.InterestDirection);
|
||||
Assert.AreEqual(808_000m, leg.InterestPrincipalFix);
|
||||
Assert.AreEqual(SettleDate, leg.HappenDate);
|
||||
Assert.IsNull(leg.FundTag);
|
||||
Assert.AreEqual(SwapAdditionalMarginService.EodOptName, leg.OptName);
|
||||
Assert.AreEqual("EOD追保", SwapAdditionalMarginService.EodOptName);
|
||||
|
||||
//两位 AwayFromZero 舍入
|
||||
var rounded = SwapAdditionalMarginService.BuildEodMarginLeg(td, 1000.005, SettleDate, optId: 0);
|
||||
Assert.AreEqual(1000.01m, rounded.InterestPrincipalFix);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 授信充足(腿未选 → 回退交易级 Credit):整腿定稿授信占用,现金部分 0(不产生现金流水)。
|
||||
/// 占用 remark 必须带"追加保证金"前缀(累计口径与清理链路的硬性识别点)。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void AM_LEG_002_授信充足_全额授信零现金流水()
|
||||
{
|
||||
var td = NewTrade(ConsFundTag.Credit);
|
||||
var leg = SwapAdditionalMarginService.BuildEodMarginLeg(td, 1000, SettleDate, optId: 0);
|
||||
Assert.IsTrue(ConsFundTag.PreferCredit(leg.FundTag, td.MarginFundSource));
|
||||
|
||||
var plans = FundTagCalc.AllocateByLegPreference(
|
||||
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.IsFalse(plans[0].NeedSplit);
|
||||
//CashAmount==0 → ApplyMarginFundTags 不写现金流水;占用 remark 前缀断言
|
||||
Assert.IsTrue((ClientCreditInoutService.AdditionalMarginRemark + "占用")
|
||||
.StartsWith(ClientCreditInoutService.AdditionalMarginRemark));
|
||||
Assert.IsTrue((ClientCreditInoutService.AdditionalMarginRemark + "拆单授信部分")
|
||||
.StartsWith(ClientCreditInoutService.AdditionalMarginRemark));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 额度不足 → 跨界拆单:原腿保留授信部分标 Credit,克隆现金差额腿标 Cash,
|
||||
/// 两腿 fix 倒挤守恒(现金流水绑现金腿 id,Deal=现金腿id 由 ApplyMarginFundTags 保证)。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void AM_LEG_003_额度不足_拆腿加现金流水()
|
||||
{
|
||||
var td = NewTrade(ConsFundTag.Credit);
|
||||
var leg = SwapAdditionalMarginService.BuildEodMarginLeg(td, 1000, SettleDate, optId: 0);
|
||||
|
||||
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);
|
||||
|
||||
//ApplySaveTimeSplit 与 SplitLeg 同口径(原腿=授信部分、克隆现金腿倒挤守恒)
|
||||
var newLegs = FundTagCalc.ApplySaveTimeSplit(legs, plans);
|
||||
Assert.AreEqual(1, newLegs.Count);
|
||||
Assert.AreEqual(300m, leg.InterestPrincipalFix);
|
||||
Assert.AreEqual(ConsFundTag.Credit, leg.FundTag);
|
||||
var cashLeg = newLegs[0];
|
||||
Assert.AreEqual(700m, cashLeg.InterestPrincipalFix);
|
||||
Assert.AreEqual(ConsFundTag.Cash, cashLeg.FundTag);
|
||||
Assert.AreEqual(1000m, leg.InterestPrincipalFix + cashLeg.InterestPrincipalFix);
|
||||
//拆单两腿合计=增量,且现金腿为独立期初腿(流水 Deal 绑它)
|
||||
Assert.AreEqual(0, cashLeg.id);
|
||||
Assert.AreEqual(0, cashLeg.PositionId);
|
||||
Assert.IsTrue(cashLeg.IsInitial);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// margin_fund_source=Cash:腿未选回退交易级现金 → 整腿定稿现金、全额现金流水、零授信占用。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void AM_LEG_004_交易级现金来源_全额现金零占用()
|
||||
{
|
||||
var td = NewTrade(ConsFundTag.Cash);
|
||||
var leg = SwapAdditionalMarginService.BuildEodMarginLeg(td, 1000, SettleDate, optId: 0);
|
||||
Assert.IsFalse(ConsFundTag.PreferCredit(leg.FundTag, td.MarginFundSource));
|
||||
|
||||
var plans = FundTagCalc.AllocateByLegPreference(
|
||||
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.IsFalse(plans[0].NeedSplit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增量≤0(追保回落/已补足)→ 不产生新腿:目标<已补足 与 目标=0 两种情形均跳过。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void AM_LEG_005_增量非正_不产生新腿()
|
||||
{
|
||||
//已补足:目标 50,已补足 80 → 增量 -30 → 跳过
|
||||
var target = SwapAdditionalMarginCalc.CalcTarget(150, 100);
|
||||
var increment = Math.Round(target - 80, 2, MidpointRounding.AwayFromZero);
|
||||
Assert.IsTrue(increment <= 0);
|
||||
|
||||
//追保回落到应付之下:目标 0 → 增量 ≤0 → 跳过(超付不返还,负缺口走可用资金公式)
|
||||
var fallen = SwapAdditionalMarginCalc.CalcTarget(50, 100);
|
||||
Assert.AreEqual(0, fallen, 1e-6);
|
||||
Assert.IsTrue(Math.Round(fallen - 80, 2, MidpointRounding.AwayFromZero) <= 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 幂等:EOD 腿识别(OptName 打标 + mode6 + 重跑窗口)——手工腿(OptName=操作员)与
|
||||
/// 窗口外更早历史腿不受影响;重跑时旧腿金额已计入已补足 → 增量 0 不翻倍。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void AM_LEG_006_幂等识别与重跑不翻倍()
|
||||
{
|
||||
var td = NewTrade();
|
||||
var eodLeg = SwapAdditionalMarginService.BuildEodMarginLeg(td, 80, SettleDate, optId: 0);
|
||||
var manualLeg = new swap_position
|
||||
{
|
||||
InterestMode = (int)InterestModeEnum.追加预付金,
|
||||
OptName = "张三",
|
||||
HappenDate = SettleDate
|
||||
};
|
||||
var initLeg = new swap_position
|
||||
{
|
||||
InterestMode = (int)InterestModeEnum.初始预付金,
|
||||
OptName = SwapAdditionalMarginService.EodOptName,
|
||||
HappenDate = SettleDate
|
||||
};
|
||||
var earlierEodLeg = SwapAdditionalMarginService.BuildEodMarginLeg(td, 50, SettleDate.AddDays(-2), optId: 0);
|
||||
|
||||
Assert.IsTrue(SwapAdditionalMarginService.IsEodMarginLeg(eodLeg, SettleDate));
|
||||
Assert.IsFalse(SwapAdditionalMarginService.IsEodMarginLeg(manualLeg, SettleDate));
|
||||
Assert.IsFalse(SwapAdditionalMarginService.IsEodMarginLeg(initLeg, SettleDate));
|
||||
//重跑窗口起点防误删更早历史日已归属腿
|
||||
Assert.IsFalse(SwapAdditionalMarginService.IsEodMarginLeg(earlierEodLeg, SettleDate));
|
||||
Assert.IsTrue(SwapAdditionalMarginService.IsEodMarginLeg(earlierEodLeg, SettleDate.AddDays(-2)));
|
||||
|
||||
//重跑:旧腿簿记已计入已补足(现金 30 + 授信 50 = 80)→ 目标不变、增量 0
|
||||
var target = SwapAdditionalMarginCalc.CalcTarget(180, 100);
|
||||
var rerunIncrement = Math.Round(target - 30 - 50, 2, MidpointRounding.AwayFromZero);
|
||||
Assert.AreEqual(0, rerunIncrement, 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 跨日序列(映射交易2538实例):首日补足水位后,维保回落不返还、回升只补差额,累计已补足不越目标水位。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void AM_LEG_007_跨日序列_不越水位()
|
||||
{
|
||||
const double payableNet = 2_000_000; //初始预付金全走授信(占用净额)
|
||||
var fundedLegs = new List<double>();
|
||||
|
||||
//D1 维持 2,808,000 → 目标 808,000 → 腿1
|
||||
var day1Target = SwapAdditionalMarginCalc.CalcTarget(2_808_000, payableNet);
|
||||
var day1Increment = Math.Round(day1Target - fundedLegs.Sum(), 2, MidpointRounding.AwayFromZero);
|
||||
Assert.AreEqual(808_000, day1Increment, 1e-6);
|
||||
fundedLegs.Add(day1Increment);
|
||||
|
||||
//D2 维保回落至 2,500,000 → 目标 500,000 < 已补足 → 无新腿(不返还)
|
||||
var day2Target = SwapAdditionalMarginCalc.CalcTarget(2_500_000, payableNet);
|
||||
var day2Increment = Math.Round(day2Target - fundedLegs.Sum(), 2, MidpointRounding.AwayFromZero);
|
||||
Assert.IsTrue(day2Increment <= 0);
|
||||
|
||||
//D3 维保回升至 3,000,000 → 目标 1,000,000 − 已补足 808,000 = 增量 192,000 → 腿2
|
||||
var day3Target = SwapAdditionalMarginCalc.CalcTarget(3_000_000, payableNet);
|
||||
var day3Increment = Math.Round(day3Target - fundedLegs.Sum(), 2, MidpointRounding.AwayFromZero);
|
||||
Assert.AreEqual(192_000, day3Increment, 1e-6);
|
||||
fundedLegs.Add(day3Increment);
|
||||
|
||||
//D3 重跑:已补足=目标 → 增量 0,总量恒等不越水位
|
||||
var day3Rerun = Math.Round(day3Target - fundedLegs.Sum(), 2, MidpointRounding.AwayFromZero);
|
||||
Assert.AreEqual(0, day3Rerun, 1e-6);
|
||||
Assert.AreEqual(day3Target, fundedLegs.Sum(), 1e-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using YLErp.BLL;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
@@ -114,6 +115,8 @@ namespace YLErp.Modules.SwapModule
|
||||
/// 重新确认/重补时按最新标签重写,避免占用悬挂。
|
||||
/// keepAdditionalMargin=true 时保留追加保证金部分(阶段四落地):重确认自愈/修改清除只重写 应付预付金 相关占用,
|
||||
/// 追加部分由 EOD 幂等维护(该两处不删除追加资金记录,授信占用须同步保留,否则已使用授信被低估);
|
||||
/// 保护条件 = remark 前缀识别 或 绑定的 position_id 属于本交易现存 EOD 追保腿集合(腿化改造后 EOD 占用挂腿,
|
||||
/// 删除范围跟腿走:腿还在则占用保留,腿消失由 EOD 幂等清理连带删除);
|
||||
/// 删除交易(资金记录全删)传 false 全清。
|
||||
/// </summary>
|
||||
public void RemoveByTrade(int tradeId, bool keepAdditionalMargin = false)
|
||||
@@ -121,7 +124,15 @@ namespace YLErp.Modules.SwapModule
|
||||
var records = DbContext.client_credit_inout.Where(x => x.trade_id == tradeId).ToList();
|
||||
if (keepAdditionalMargin)
|
||||
{
|
||||
records = records.Where(x => !IsAdditionalMarginRecord(x)).ToList();
|
||||
var eodLegIds = DbContext.swap_position
|
||||
.Where(x => x.SwapTradeId == tradeId && x.IsInitial
|
||||
&& x.InterestMode == (int)InterestModeEnum.追加预付金
|
||||
&& x.OptName == SwapAdditionalMarginService.EodOptName)
|
||||
.Select(x => x.id)
|
||||
.ToList();
|
||||
records = records.Where(x => !IsAdditionalMarginRecord(x)
|
||||
&& !(x.position_id != null && eodLegIds.Contains(x.position_id.Value)))
|
||||
.ToList();
|
||||
}
|
||||
DbContext.client_credit_inout.RemoveRange(records);
|
||||
}
|
||||
|
||||
@@ -7,17 +7,29 @@ using YLErp.Modules.TradeModule;
|
||||
namespace YLErp.Modules.SwapModule.Margin
|
||||
{
|
||||
/// <summary>
|
||||
/// R3 阶段四 §4.1:合约维度(MarginWatchRule==0)规则15 交易日终结算产生"追加保证金"资金记录。
|
||||
/// R3 阶段四 §4.1:合约维度(MarginWatchRule==0)规则15 交易日终结算产生"追加保证金"。
|
||||
/// 交易维度追加保证金 = 维持保证金(阶段三引擎 trade_span 产出)− 已缴保证金净额
|
||||
/// (应付预付金现金净收额 + 初始授信占用净额 + 追加保证金现金累计 + 追加授信占用累计——
|
||||
/// (应付预付金现金净收额 + 初始授信占用净额——
|
||||
/// 2026-08-27 修正:授信垫付的初始预付金不产生应付预付金流水,此前未计入已缴导致每个结算日按维持全额重复开追加);
|
||||
/// 现金部分为逐结算日增量记录(BUG-03 修正:每结算日一条、Money=−increment,键 TradeId+Action+Deal+HappenDate 幂等),
|
||||
/// 需求上升只增不减;授信优先(阶段二规则):授信部分只写授信出入表(remark 前缀=追加保证金,position_id 空、冗余 trade_id)。
|
||||
/// 由 EOD 在客户资金计算之前调用:当日新记录计入当日出入金窗口并翻"已结算",重跑时 目标/已补足 不变 → 新增为 0 不重复写。
|
||||
/// 需求上升只增不减、回落不返还。
|
||||
/// 腿化改造(2026-08-27):增量>0 时生成一条与手工追加预付金同形态的交易腿(InterestMode=追加预付金、
|
||||
/// OptName="EOD追保" 打标),并复用手工确认链路的 SwapFundTagService.ApplyMarginFundTags 完成授信/现金簿记
|
||||
/// (资金来源走腿 fund_tag→交易级 margin_fund_source 回退链;授信部分写授信出入表占用绑腿、不产生流水;
|
||||
/// 现金部分出 Action=系统操作-追加保证金 流水、Deal=现金腿 id——Action 与手工腿簿记的"应付预付金"区隔,
|
||||
/// 保住 EQD-6952 资金通知书聚合口径);平仓返还(ReleaseMarginByTag)与交易回退清理由既有链路自动获得。
|
||||
/// 幂等:重跑先清(本结算日起 EOD 旧追保腿及其簿记)后建;eod_swap_position 当日行显式补写(方案A——
|
||||
/// 腿生成晚于当日 SwapPositionCompose,快照须补齐,见 SwapEodPositionService.SaveEodAdditionalMarginPosition)。
|
||||
/// 由 EOD 在客户资金计算之前调用:当日新记录计入当日出入金窗口并翻"已结算"。
|
||||
/// 客户维度(MarginWatchRule=1/NULL)不产生资金记录(§0 占用口径),不在本服务范围。
|
||||
/// </summary>
|
||||
public class SwapAdditionalMarginService : YLBaseService
|
||||
{
|
||||
/// <summary>
|
||||
/// EOD 追保腿打标(OptName):与手工追加预付金腿(OptName=操作员实名)区分,
|
||||
/// 幂等清理、RemoveByTrade 保护与时间轴回退清理均以此识别。拆单现金腿落库时被打服务身份,簿记后回打本标识。
|
||||
/// </summary>
|
||||
public const string EodOptName = "EOD追保";
|
||||
|
||||
public SwapAdditionalMarginService(OptUserInfo userInfo) : base(userInfo)
|
||||
{
|
||||
}
|
||||
@@ -26,6 +38,45 @@ namespace YLErp.Modules.SwapModule.Margin
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EOD 追保腿识别(InterestMode=追加预付金 + OptName 打标);
|
||||
/// happenDateFrom 限定重跑/回退窗口起点,防误删更早历史日已归属腿。
|
||||
/// </summary>
|
||||
public static bool IsEodMarginLeg(swap_position leg, DateTime? happenDateFrom = null)
|
||||
{
|
||||
return leg != null
|
||||
&& leg.InterestMode == (int)InterestModeEnum.追加预付金
|
||||
&& leg.OptName == EodOptName
|
||||
&& (happenDateFrom == null || leg.HappenDate >= happenDateFrom);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造 EOD 追保腿(纯函数,便于单测;落库与 PosiNumber 回填由调用方完成):
|
||||
/// 与手工追加预付金腿同形态(PositionType=0 资金腿、PosiDirection=0、IsInitial、InterestMode=追加预付金、
|
||||
/// 收取方向、fix=增量两位舍入),FundTag=null 回退交易级资金来源,OptName 打标 EOD 追保。
|
||||
/// </summary>
|
||||
public static swap_position BuildEodMarginLeg(trade td, double increment, DateTime settleDate, int optId)
|
||||
{
|
||||
return new swap_position
|
||||
{
|
||||
SwapTradeId = td.id,
|
||||
PositionType = 0,
|
||||
PosiDirection = 0,
|
||||
IsInitial = true,
|
||||
InterestMode = (int)InterestModeEnum.追加预付金,
|
||||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||||
InterestPrincipalFix = Math.Round(Convert.ToDecimal(increment), 2, MidpointRounding.AwayFromZero),
|
||||
HappenDate = settleDate,
|
||||
PosiStartDate = td.StartDate ?? settleDate,
|
||||
PosiMatuirityDate = td.ExerciseDate,
|
||||
Currency = td.SettlementCurrency,
|
||||
FundTag = null,
|
||||
OptId = optId,
|
||||
OptName = EodOptName,
|
||||
OptTime = DateTime.Now
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 结算日逐客户逐交易产生追加保证金(clientFilter 为部分结算的客户过滤,与 EOD 请求一致)。
|
||||
/// </summary>
|
||||
@@ -89,13 +140,13 @@ namespace YLErp.Modules.SwapModule.Margin
|
||||
.Select(g => new { TradeId = g.Key ?? 0, Sum = -g.Sum(x => x.Money ?? 0d) })
|
||||
.ToDictionary(x => x.TradeId, x => x.Sum);
|
||||
|
||||
//追加保证金资金记录累计值(逐日增量记录求和即累计,BUG-03;Deal=0 交易级,口径与 EOD canonical 一致:非作废+已确认/已结算)
|
||||
//追加保证金资金记录累计值(逐日增量记录求和即累计,BUG-03;口径与 EOD canonical 一致:非作废+已确认/已结算。
|
||||
//腿化改造后现金流水 Deal=现金腿 id(原 Deal==0 条件去除):该 Action 只有 EOD 会写,手工链路写应付预付金,语义天然隔离)
|
||||
var addRecordByTrade = DbContext.ClientCashInCashOut.AsNoTracking()
|
||||
.Where(x => x.TradeId != null && tradeIds.Contains(x.TradeId ?? 0)
|
||||
&& x.Action == ClientCashInCashOut.系统操作_追加保证金
|
||||
&& x.ValidState != ConsGlobal.InValid
|
||||
&& (x.State == ClientCashInCashOut.已确认 || x.State == ClientCashInCashOut.已结算)
|
||||
&& x.Deal == 0
|
||||
&& x.Money != null)
|
||||
.GroupBy(x => x.TradeId)
|
||||
.Select(g => new { TradeId = g.Key ?? 0, Funded = -g.Sum(x => x.Money ?? 0d) })
|
||||
@@ -121,9 +172,8 @@ namespace YLErp.Modules.SwapModule.Margin
|
||||
|
||||
var fundTagService = new SwapFundTagService(this);
|
||||
var cashService = new ClientCashInCashOutService(this);
|
||||
var creditService = new ClientCreditInoutService(this);
|
||||
//客户剩余可用授信逐笔扣减缓存(同一次结算内多笔追加按顺序消耗额度,与阶段二逐腿分配同语义)
|
||||
var creditRemaining = new Dictionary<int, double>();
|
||||
var eodPositionService = new SwapEodPositionService(this);
|
||||
var flowEventService = new SwapFlowEventService(this);
|
||||
|
||||
foreach (var clientGroup in trades.GroupBy(t => t.ClientId).OrderBy(g => g.Key))
|
||||
{
|
||||
@@ -151,28 +201,97 @@ namespace YLErp.Modules.SwapModule.Margin
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!creditRemaining.TryGetValue(td.ClientId, out var remain))
|
||||
//幂等清理:先删本结算日起 EOD 旧追保腿及其簿记(腿/流水/占用/快照同生共死),再按最新增量重建;
|
||||
//手工追加预付金腿(OptName≠EOD追保)不受影响
|
||||
CleanupEodMarginLegs(td, settleDate);
|
||||
|
||||
//建腿:与手工追加预付金同形态,FundTag=null 回退交易级资金来源
|
||||
var legIdsBefore = DbContext.swap_position.Where(x => x.SwapTradeId == td.id).Select(x => x.id).ToList();
|
||||
var leg = BuildEodMarginLeg(td, increment, settleDate, UserId);
|
||||
DbContext.swap_position.Add(leg);
|
||||
DbContext.SaveChanges();
|
||||
leg.PosiNumber = $"{td.TradeNumber}-{leg.id}";
|
||||
|
||||
//簿记:复用手工确认链路的标签分配(额度内授信占账、跨界拆单、现金出流水),
|
||||
//Action 独立为 追加保证金(EQD-6952 口径)、占用 remark 带"追加保证金"前缀(累计/清理链路硬性识别点);
|
||||
//resetExistingBookings=false——本交易初始预付金占用不在此重写(EOD 只簿记本次新腿,旧追保簿记上方已清)
|
||||
fundTagService.ApplyMarginFundTags(td, new List<swap_position> { leg }, cashService, ignoreMoneyCheck: false,
|
||||
cashAction: ClientCashInCashOut.系统操作_追加保证金,
|
||||
occupyRemark: ClientCreditInoutService.AdditionalMarginRemark + "占用",
|
||||
splitOccupyRemark: ClientCreditInoutService.AdditionalMarginRemark + "拆单授信部分",
|
||||
bookingDate: settleDate,
|
||||
resetExistingBookings: false);
|
||||
|
||||
//本次新腿 = 追保腿 + 可能的拆单现金腿(SplitLeg 落库时打了服务身份 OptName,回打 EOD 标识保证幂等清理覆盖完整)
|
||||
var newLegs = DbContext.swap_position.Where(x => x.SwapTradeId == td.id && x.IsInitial
|
||||
&& x.InterestMode == (int)InterestModeEnum.追加预付金 && !legIdsBefore.Contains(x.id))
|
||||
.ToList();
|
||||
newLegs.ForEach(x => x.OptName = EodOptName);
|
||||
DbContext.SaveChanges();
|
||||
|
||||
//实时持仓克隆 + 开仓事件(参照 TradeConfirmService 簿记后动作,但只针对本次新腿——
|
||||
//整交易 InitialPosition 会把浮动腿实时持仓重置回开仓态、AddPositionEvent 会为全部腿重复建开仓事件,EOD 场景不可用);
|
||||
//克隆继承定稿标签,供平仓返还分流与后续快照链使用
|
||||
foreach (var newLeg in newLegs)
|
||||
{
|
||||
remain = fundTagService.GetAvailableCredit(td.ClientId, settleDate);
|
||||
creditRemaining[td.ClientId] = remain;
|
||||
var clone = newLeg.Clone();
|
||||
clone.id = 0;
|
||||
clone.PositionId = newLeg.id;
|
||||
clone.IsInitial = false;
|
||||
DbContext.swap_position.Add(clone);
|
||||
}
|
||||
var (creditPart, cashPart) = SwapAdditionalMarginCalc.Allocate(increment, remain);
|
||||
if (creditPart > 0)
|
||||
DbContext.SaveChanges();
|
||||
flowEventService.InitEvent(newLegs, td, EodOptName);
|
||||
DbContext.SaveChanges();
|
||||
|
||||
//方案A:腿生成晚于当日 SwapPositionCompose(当日 trade_span 结算后才可算增量),显式补写当日 eod_swap_position 行,
|
||||
//使当日报表明细(PostionMarginGain 等)不漏计;下一结算日快照由 Compose 先清后建正常接管
|
||||
foreach (var newLeg in newLegs)
|
||||
{
|
||||
//授信部分不产生资金流水,只写授信出入表占用(占用记正数,BUG-01 修正口径)
|
||||
creditService.Occupy(td.ClientId, null, td.id, creditPart, settleDate,
|
||||
ClientCreditInoutService.AdditionalMarginRemark + "占用");
|
||||
creditRemaining[td.ClientId] = Math.Round(remain - creditPart, 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
if (cashPart > 0)
|
||||
{
|
||||
//现金部分按结算日逐笔增量记录(BUG-03 修正:每结算日一条、Money=−increment、键含日期幂等),
|
||||
//避免单条累计值覆盖 + HappenDate 前移使 EOD 差分窗口跨日全额重复计入;负数=客户应付追加
|
||||
cashService.SaveSwapTradeClientCash(td, -cashPart, settleDate, 0,
|
||||
ClientCashInCashOut.系统操作_追加保证金, matchDate: true);
|
||||
eodPositionService.SaveEodAdditionalMarginPosition(td, newLeg, settleDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 幂等清理:删除本交易 本结算日起 EOD 生成的旧追保腿(含其实时克隆)及其簿记——
|
||||
/// 现金流水(Action=追加保证金,Deal=腿id,按现行 DeleteTradeCashInCashOut 习惯硬删)、
|
||||
/// 授信占用(position_id=腿id)、开仓事件、方案A 补写的当日及以后 eod_swap_position 行。
|
||||
/// </summary>
|
||||
private void CleanupEodMarginLegs(trade td, DateTime settleDate)
|
||||
{
|
||||
var oldLegs = DbContext.swap_position.Where(x => x.SwapTradeId == td.id
|
||||
&& x.InterestMode == (int)InterestModeEnum.追加预付金
|
||||
&& x.OptName == EodOptName
|
||||
&& x.HappenDate >= settleDate)
|
||||
.ToList();
|
||||
if (oldLegs.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var oldLegIds = oldLegs.Select(x => x.id).ToList();
|
||||
//簿记(流水 Deal/占用 position_id/快照 PositionId/事件 PositionId)均绑期初腿(拆单现金腿也是 IsInitial 的独立腿)
|
||||
var oldInitialIds = oldLegs.Where(x => x.IsInitial).Select(x => x.id).ToList();
|
||||
var oldCashRecords = DbContext.ClientCashInCashOut.Where(x => x.TradeId == td.id
|
||||
&& x.Action == ClientCashInCashOut.系统操作_追加保证金
|
||||
&& oldInitialIds.Contains(x.Deal))
|
||||
.ToList();
|
||||
DbContext.ClientCashInCashOut.RemoveRange(oldCashRecords);
|
||||
var oldCreditRecords = DbContext.client_credit_inout.Where(x => x.trade_id == td.id
|
||||
&& x.position_id != null && oldInitialIds.Contains(x.position_id.Value))
|
||||
.ToList();
|
||||
DbContext.client_credit_inout.RemoveRange(oldCreditRecords);
|
||||
var oldEvents = DbContext.swap_flow_event.Where(x => x.SwapTradeId == td.id
|
||||
&& x.EventType == (int)SwapFlowEventTypeEnum.开仓 && oldInitialIds.Contains(x.PositionId))
|
||||
.ToList();
|
||||
DbContext.swap_flow_event.RemoveRange(oldEvents);
|
||||
var oldEodRows = DbContext.eod_swap_position.Where(x => x.SwapTradeId == td.id
|
||||
&& x.ValueDate >= settleDate && oldInitialIds.Contains(x.PositionId))
|
||||
.ToList();
|
||||
DbContext.eod_swap_position.RemoveRange(oldEodRows);
|
||||
DbContext.swap_position.RemoveRange(oldLegs);
|
||||
DbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2046,6 +2046,44 @@ namespace YLErp.Modules.SwapModule
|
||||
FinalizeInterestEodRoll(eodPayPosition, newEodPayPosition, ratio, td, valueDate, position.InterestDirection);
|
||||
PersistEodSwapPosition(newEodPayPosition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EOD 追保腿化·方案A:SettleAdditionalMargin 在当日 SwapPositionCompose/SwapEodCompose 之后生成 mode6 追保腿
|
||||
/// (增量依赖当日 trade_span,无法前移),当日快照已落表,显式补写当日 eod_swap_position 行,
|
||||
/// 使当日报表明细(风险页 PostionMarginGain 等按快照腿汇总的列)不漏计。
|
||||
/// 字段填充参照 SaveEodInterestPosition 新腿形态(无当日流水、无上日归档:利息/损益字段为 0,TdCurrency 取当日汇率)。
|
||||
/// 幂等:先清(PositionId+ValueDate)后建;下一结算日 SwapPositionCompose 先清(ClearSwapPositions ValueDate>=当日)
|
||||
/// 再从实时腿重建,补写行不会跨日残留。
|
||||
/// </summary>
|
||||
public void SaveEodAdditionalMarginPosition(trade td, swap_position leg, DateTime valueDate)
|
||||
{
|
||||
var existing = DbContext.eod_swap_position
|
||||
.Where(x => x.SwapTradeId == td.id && x.PositionId == leg.id && x.ValueDate == valueDate)
|
||||
.ToList();
|
||||
DbContext.eod_swap_position.RemoveRange(existing);
|
||||
var row = new eod_swap_position
|
||||
{
|
||||
ClientId = td.ClientId,
|
||||
SwapTradeId = td.id,
|
||||
PosiStartDate = leg.PosiStartDate,
|
||||
PosiMatuirityDate = td.ExerciseDate,
|
||||
ValueDate = valueDate,
|
||||
PositionId = leg.id,
|
||||
PosiStatus = 0,
|
||||
Invalid = false
|
||||
};
|
||||
UpdateDbOption(row);
|
||||
//持仓内容-利息腿(无当日流水,FloatRate 取 0)
|
||||
CopyInterestLegFields(row, leg, 0);
|
||||
row.InterestFeePending = 0;
|
||||
var ratio = DirectionRatio.InterestLegPnl(leg.InterestDirection, leg.InterestMode);
|
||||
row.SwapPositionValue = PositionValueCalc.Calc(row.InterestProfitSum, row.PosiProfitSum, ratio);
|
||||
row.RealizedPnl = row.RealizedInterest + row.RealizedInterestFee;
|
||||
row.TdCurrency = Convert.ToDecimal(GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true,
|
||||
DirectionRatio.RateType(leg.InterestDirection)));
|
||||
PersistEodSwapPosition(row);
|
||||
SaveAllChanges();
|
||||
}
|
||||
/// <summary>
|
||||
/// 自动互换用,当日无互换,当日无平仓
|
||||
/// </summary>
|
||||
|
||||
@@ -127,17 +127,29 @@ namespace YLErp.Modules.SwapModule
|
||||
/// 特批全现金;按授信分配(腿选授信,或腿未选回退交易级 margin_fund_source=授信)的腿按剩余额度占用,
|
||||
/// 跨界腿拆单为 授信+现金 两条(原腿保留授信部分、差额拆出新现金腿);现金直接现金。
|
||||
/// 授信腿只写授信出入表占用(占用记正数,绑定腿 position_id,冗余 trade_id),不产生资金流水;
|
||||
/// 现金腿走 SaveSwapTradeClientCash 幂等 upsert 产生 应付预付金 记录。
|
||||
/// 现金腿走 SaveSwapTradeClientCash 幂等 upsert 产生资金记录(默认 Action=应付预付金;EOD 追保腿化传入 追加保证金)。
|
||||
/// marginLegs 需为已过滤(IsDeductPrincipal 等)的预付金腿(InterestMode=5/6)。
|
||||
/// cashAction/occupyRemark/splitOccupyRemark:EOD 追保场景参数化(现金流水 Action 独立、占用 remark 带"追加保证金"前缀——
|
||||
/// 前缀是累计口径与清理链路的硬性识别点);bookingDate:授信可用额度与簿记的取值日(默认交易起始日,EOD 传结算日——
|
||||
/// 按结算日过滤授信有效期,防止已到期授信被占用);resetExistingBookings=false 时跳过"重确认自愈"的全交易占用重写
|
||||
/// (EOD 追保只簿记本次新腿,旧追保簿记由调用方幂等清理,初始预付金占用不在此重写范围、不能清)。
|
||||
/// </summary>
|
||||
public void ApplyMarginFundTags(trade td, List<swap_position> marginLegs, ClientCashInCashOutService cashService, bool ignoreMoneyCheck)
|
||||
public void ApplyMarginFundTags(trade td, List<swap_position> marginLegs, ClientCashInCashOutService cashService, bool ignoreMoneyCheck,
|
||||
string cashAction = ClientCashInCashOut.系统操作_应付预付金,
|
||||
string occupyRemark = "簿记授信占用",
|
||||
string splitOccupyRemark = "簿记拆单授信部分",
|
||||
DateTime? bookingDate = null,
|
||||
bool resetExistingBookings = true)
|
||||
{
|
||||
var creditService = new ClientCreditInoutService(this);
|
||||
//重确认/重补场景自愈:清掉本交易旧占用记录后按腿上当前选择与最新额度重写;
|
||||
//追加保证金占用(阶段四 EOD 写入)不随重写清除——其资金记录不在本方法删除范围,由 EOD 幂等维护
|
||||
creditService.RemoveByTrade(td.id, keepAdditionalMargin: true);
|
||||
if (resetExistingBookings)
|
||||
{
|
||||
creditService.RemoveByTrade(td.id, keepAdditionalMargin: true);
|
||||
}
|
||||
|
||||
var valueDate = td.TradeDate ?? DateTime.Now;
|
||||
var valueDate = bookingDate ?? td.TradeDate ?? DateTime.Now;
|
||||
var creditAvailable = GetAvailableCredit(td.ClientId, valueDate);
|
||||
//客户应付为正:资金记录符号口径为 Money<0=客户付钱,即 收取方向(dir=1)腿 fix 为正应付额——
|
||||
//正是占用授信的场景;支付方向(dir=2)为客户收钱,不占用授信,直接现金。
|
||||
@@ -187,7 +199,7 @@ namespace YLErp.Modules.SwapModule
|
||||
//授信部分(整腿授信 或 拆单后保留在原腿的可用额度部分):不产生资金流水,只写占用(占用绑原腿)。
|
||||
//占用记正数(BUG-01 修正:已使用授信=Σ(amount) 占用上升;2026-08-20"与资金流水同号入金负"口径已废弃)
|
||||
creditService.Occupy(td.ClientId, leg.id, td.id, plan.CreditAmount, happenDate,
|
||||
plan.NeedSplit ? "簿记拆单授信部分" : "簿记授信占用");
|
||||
plan.NeedSplit ? splitOccupyRemark : occupyRemark);
|
||||
}
|
||||
//资金记录沿用既有符号口径(客户付钱为负 = -应付额):授信部分不产生流水,
|
||||
//现金部分按差额产生——拆单腿的流水绑新拆出的现金腿,整腿现金/负应付腿绑原腿
|
||||
@@ -196,7 +208,7 @@ namespace YLErp.Modules.SwapModule
|
||||
: Convert.ToDouble(leg.InterestPrincipalFix * (leg.InterestDirection == 1 ? -1 : 1));
|
||||
if (recordAmount != 0)
|
||||
{
|
||||
cashService.SaveSwapTradeClientCash(td, recordAmount, happenDate, plan?.CashLeg?.id ?? leg.id, ClientCashInCashOut.系统操作_应付预付金);
|
||||
cashService.SaveSwapTradeClientCash(td, recordAmount, happenDate, plan?.CashLeg?.id ?? leg.id, cashAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using BaseOUDAL;
|
||||
using BaseOUDAL;
|
||||
using ClosedXML.Report.Options;
|
||||
using Confluent.Kafka;
|
||||
using CsvHelper;
|
||||
@@ -31,6 +31,7 @@ using YLErp.Modules.DataProviderModule;
|
||||
using YLErp.Modules.EodModule;
|
||||
using YLErp.Modules.RiskModule;
|
||||
using YLErp.Modules.SalesModule;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
using YLErp.Modules.TradeModule;
|
||||
using YLErp.Modules.TradeModule.DealModule;
|
||||
using YLErp.Modules.TradeModule.DocGenerateModule;
|
||||
@@ -1828,6 +1829,26 @@ namespace YLErp.Modules.SwapModule
|
||||
DbContext.swap_flow_event.RemoveRange(swapFlowEvents);
|
||||
var clientCashs = DbContext.ClientCashInCashOut.Where(x => x.TradeId == td.id && x.HappenDate >= valueDate).ToList();
|
||||
DbContext.ClientCashInCashOut.RemoveRange(clientCashs);
|
||||
//EOD 追保腿(腿化改造)同生共死:流水已在上方按 HappenDate>=valueDate 全删(不分 Action),
|
||||
//此处同步删除本交易 valueDate 起的 EOD 追保腿(含实时克隆)及其授信占用、开仓事件、eod 快照行,
|
||||
//回退后由下一次 EOD 幂等重建;手工追加预付金腿(OptName≠EOD追保)不受影响
|
||||
var eodMarginLegs = DbContext.swap_position.Where(x => x.SwapTradeId == td.id
|
||||
&& x.InterestMode == (int)InterestModeEnum.追加预付金
|
||||
&& x.OptName == SwapAdditionalMarginService.EodOptName
|
||||
&& x.HappenDate >= valueDate)
|
||||
.ToList();
|
||||
if (eodMarginLegs.Count > 0)
|
||||
{
|
||||
var eodMarginLegIds = eodMarginLegs.Where(x => x.IsInitial).Select(x => x.id).ToList();
|
||||
DbContext.client_credit_inout.RemoveRange(DbContext.client_credit_inout
|
||||
.Where(x => x.trade_id == td.id && x.position_id != null && eodMarginLegIds.Contains(x.position_id.Value)));
|
||||
DbContext.eod_swap_position.RemoveRange(DbContext.eod_swap_position
|
||||
.Where(x => x.SwapTradeId == td.id && eodMarginLegIds.Contains(x.PositionId)));
|
||||
DbContext.swap_flow_event.RemoveRange(DbContext.swap_flow_event
|
||||
.Where(x => x.SwapTradeId == td.id && x.EventType == (int)SwapFlowEventTypeEnum.开仓
|
||||
&& eodMarginLegIds.Contains(x.PositionId)));
|
||||
DbContext.swap_position.RemoveRange(eodMarginLegs);
|
||||
}
|
||||
DbContext.SaveChanges();
|
||||
}
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user