Merge remote-tracking branch 'origin/glms/feature/1.4.2' into glms/feature/1.4.2
This commit is contained in:
@@ -24,5 +24,25 @@ namespace YLErp.DBModels
|
||||
{
|
||||
return fundTag == Credit ? Credit : Cash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 腿是否按授信分配(§2.3 情形1 回退口径):腿上显式选择优先(Credit=授信、Cash=现金);
|
||||
/// 腿未选(默认)时取交易级资金来源(trade.margin_fund_source,必填默认现金)——授信→按授信分配
|
||||
/// (额度不足拆单),现金→现金。交易级空值按现金(存量防御,SaveTrade 已归一)。
|
||||
/// 标签定稿(ApplyMarginFundTags)与簿记资金校验(RealtimePnlCalc.TradeCanBeConfirm)
|
||||
/// 共用本口径,保证校验与定稿一致。
|
||||
/// </summary>
|
||||
public static bool PreferCredit(string legFundTag, string tradeFundSource)
|
||||
{
|
||||
if (legFundTag == Credit)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (legFundTag == Cash)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return string.IsNullOrEmpty(legFundTag) && tradeFundSource == Credit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,4 +220,86 @@ namespace YLErp.DBModels
|
||||
/// </summary>
|
||||
public decimal MarginInterestLoss { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EQD-7084 新“框架合约”Tab 的新增字段及拆分后的估值口径。
|
||||
/// 该模型不映射数据库,仅由新查询接口计算返回。
|
||||
/// </summary>
|
||||
public class EodSwapRiskNewFields
|
||||
{
|
||||
/// <summary>
|
||||
/// 浮动收益端标的类型,仅供前端按债券/非债券选择期初价格精度使用,
|
||||
/// 不参与任何收益或估值计算。
|
||||
/// </summary>
|
||||
public string UnderlyingInstrumentType { get; set; }
|
||||
|
||||
/// <summary>浮动收益端多空方向。</summary>
|
||||
public string UnderlyingDirection { get; set; }
|
||||
|
||||
/// <summary>浮动收益端标的代码。</summary>
|
||||
public string UnderlyingCode { get; set; }
|
||||
|
||||
/// <summary>期初标的价格;债券按百分价格展示。</summary>
|
||||
public decimal? InitialPrice { get; set; }
|
||||
|
||||
/// <summary>名义数量,取合约名义本金。</summary>
|
||||
public decimal NotionalQuantity { get; set; }
|
||||
|
||||
/// <summary>合约起始日。</summary>
|
||||
public DateTime? ContractStartDate { get; set; }
|
||||
|
||||
/// <summary>合约到期日。</summary>
|
||||
public DateTime? ContractMaturityDate { get; set; }
|
||||
|
||||
/// <summary>利息端基准:FR007 或固定利率。</summary>
|
||||
public string InterestBenchmark { get; set; }
|
||||
|
||||
/// <summary>普通利息腿当前交易日适用利率合计。</summary>
|
||||
public decimal InterestRatePrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开平仓费用。日终腿已按我方收益方向归一:我方支付为负、我方收取为正;
|
||||
/// 新 Tab 单独展示该金额,但估值中仍须计入一次。
|
||||
/// </summary>
|
||||
public decimal OpeningClosingFee { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 不含开平仓费用的浮动端待实现收益,来源为日终浮动腿的 PosiMtmPnL;
|
||||
/// 不可再由旧口径的 PosiProfitSum 反推,避免把费用重新混入本列。
|
||||
/// </summary>
|
||||
public decimal FloatingUnrealizedPnl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排除初始/维持保证金腿后的普通利息端待实现收益。保证金利息保留在其独立两列,
|
||||
/// 且只通过 MarginInterestAmount 参与估值,以满足“利息端仅展示利息端盈亏”的新口径。
|
||||
/// </summary>
|
||||
public decimal OrdinaryInterestPnl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 保证金利息净额,仅供两种合约估值维持旧总额;前端不直接绑定该字段,
|
||||
/// 以防它再次落入“合约利息端待实现收益”。
|
||||
/// </summary>
|
||||
public decimal MarginInterestAmount { get; set; }
|
||||
|
||||
/// <summary>收取对手方保证金利息。</summary>
|
||||
public decimal MarginInterestGain { get; set; }
|
||||
|
||||
/// <summary>支付对手方保证金利息。</summary>
|
||||
public decimal MarginInterestLoss { get; set; }
|
||||
|
||||
/// <summary>到期轧差口径估值;仅 DividendPayDate=0 时有值,且包含期间付息/分红。</summary>
|
||||
public decimal? MaturityNettingValuation { get; set; }
|
||||
|
||||
/// <summary>期间支付派息口径估值;仅 DividendPayDate 非 0 时有值,不重复计入期间付息/分红。</summary>
|
||||
public decimal? PeriodPaymentValuation { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EQD-7084 新“框架合约”Tab 响应。继承旧响应以保持原有列字段完全一致,
|
||||
/// 新接口只额外序列化新增字段。
|
||||
/// </summary>
|
||||
public class EodSwapRiskNewResponse : EodSwapResponse
|
||||
{
|
||||
public EodSwapRiskNewFields NewFields { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace YLErp.DBModels
|
||||
{
|
||||
public enum PushStateEnum
|
||||
{
|
||||
待推送 = 0,
|
||||
成功 = 1,
|
||||
失败 = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 外发推送失败状态。只记录定位信息,不保存报文。
|
||||
/// </summary>
|
||||
[Table("push_status")]
|
||||
public class PushStatus
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public long id { get; set; }
|
||||
|
||||
[Column("value_date")]
|
||||
public DateTime ValueDate { get; set; }
|
||||
|
||||
[Column("push_type")]
|
||||
public int PushType { get; set; }
|
||||
|
||||
[Column("record_id")]
|
||||
public long RecordId { get; set; }
|
||||
|
||||
[Column("state")]
|
||||
public PushStateEnum State { get; set; }
|
||||
|
||||
[Column("retry_count")]
|
||||
public int RetryCount { get; set; }
|
||||
|
||||
[Column("last_error")]
|
||||
public string LastError { get; set; }
|
||||
|
||||
[Column("push_time")]
|
||||
public DateTime? PushTime { get; set; }
|
||||
|
||||
[Column("create_time")]
|
||||
public DateTime CreateTime { get; set; }
|
||||
|
||||
[Column("update_time")]
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -677,6 +677,17 @@ namespace YLErp.DBModels
|
||||
[TradeAuditExclude]
|
||||
public string MarginTemplateName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 保证金资金来源(收益互换,必填,默认现金):预付金腿未选资金标签(默认)时的定稿依据(§2.3 情形1)。
|
||||
/// 值与 swap_position.fund_tag 同词表:Credit=优先授信(额度不足自动拆单)、Cash=现金。
|
||||
/// 录入页必选(新交易默认 Cash),SaveTrade 对空值归一为 Cash(兜住 DMA 等绕过页面的链路);
|
||||
/// 历史来源:旧版 trade.MarginTemplateName 曾以字典文本(授信保证金/现金保证金)承载该语义,模板V2迁移后由本列承接。
|
||||
/// </summary>
|
||||
[DisplayName("资金来源")]
|
||||
[TradeAuditExclude]
|
||||
[Column("margin_fund_source")]
|
||||
public string MarginFundSource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 预付金算法
|
||||
/// </summary>
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -245,6 +245,12 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator
|
||||
}
|
||||
dic["主协议编号"] = mainProtocolCode ?? "";
|
||||
|
||||
// 主协议类型:仅 NAFMII 使用中国银行间市场协议,其余类型按 SAC 展示。
|
||||
var mainProtocolType = Context.GetClientMeta(client.id, "MainProtocolType")?.MetaValue;
|
||||
var isNafmii = mainProtocolType == "1";
|
||||
dic["IsSac"] = !isNafmii;
|
||||
dic["IsNafmii"] = isNafmii;
|
||||
|
||||
// 补充协议编号:优先取 client 表字段,为空时从 client_meta 表兜底
|
||||
var supProtocolCode = client.SupProtocolCode;
|
||||
if (string.IsNullOrWhiteSpace(supProtocolCode))
|
||||
@@ -563,7 +569,8 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator
|
||||
|
||||
// 参考标的证券全称和参考标的名义份额(复用上方已声明的bond)
|
||||
dic["参考标的证券全称"] = underlying != null
|
||||
? (JsonHelper.Deserialize<UnderlyingBond>(underlying.ExJson)?.UnderlyingFullName ?? underlying.UnderlyingName)
|
||||
// ? (JsonHelper.Deserialize<UnderlyingBond>(underlying.ExJson)?.UnderlyingFullName ?? underlying.UnderlyingName)
|
||||
? (underlying.UnderlyingName ?? "")
|
||||
: "";
|
||||
dic["参考标的名义份额"] = swapPosition != null
|
||||
? ((double)swapPosition.PosiQuantity).ToString("0.##")
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace YLErp.Modules.EodModule
|
||||
{
|
||||
[TestClass]
|
||||
public class BondPaymentListQueryBoundaryTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void SearchList_OnlyQueriesBondPaymentTable()
|
||||
{
|
||||
var source = ReadBondPaymentServiceSource();
|
||||
var searchList = ExtractMethod(source, "public SearchListResult<BondPaymentDto> SearchList", "public BondPayment SaveBondPayment");
|
||||
|
||||
Assert.IsFalse(
|
||||
searchList.Contains("ex_dividend_info", StringComparison.Ordinal),
|
||||
"债券付息列表只能查询 bond_payment_info,不能把公司行为除权表拼入展示结果。");
|
||||
StringAssert.Contains(searchList, "DbContext.bondPayment");
|
||||
}
|
||||
|
||||
private static string ExtractMethod(string source, string startMarker, string endMarker)
|
||||
{
|
||||
var start = source.IndexOf(startMarker, StringComparison.Ordinal);
|
||||
Assert.IsTrue(start >= 0, $"Could not find method: {startMarker}");
|
||||
var end = source.IndexOf(endMarker, start + startMarker.Length, StringComparison.Ordinal);
|
||||
Assert.IsTrue(end >= 0, $"Could not find method end: {endMarker}");
|
||||
return source.Substring(start, end - start);
|
||||
}
|
||||
|
||||
private static string ReadBondPaymentServiceSource()
|
||||
{
|
||||
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (directory != null)
|
||||
{
|
||||
var path = Path.Combine(directory.FullName, "YLErpDAL", "Modules", "EodModule", "BondPaymentService.cs");
|
||||
if (File.Exists(path))
|
||||
{
|
||||
return File.ReadAllText(path);
|
||||
}
|
||||
directory = directory.Parent;
|
||||
}
|
||||
|
||||
Assert.Fail("Could not locate BondPaymentService.cs from the test output directory.");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,5 +66,23 @@ namespace YLErp.Modules.EodModule
|
||||
|
||||
Assert.AreEqual(204000m, actual);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalcPayment_CorporateActionUsesPreCorporateActionQuantity()
|
||||
{
|
||||
var payments = new List<BondPayment>
|
||||
{
|
||||
new BondPayment { payment_interest = 2m },
|
||||
new BondPayment { payment_interest = 10m, IsCorporateActionCashDividend = true }
|
||||
};
|
||||
var method = typeof(BondPaymentService).GetMethod(
|
||||
nameof(BondPaymentService.CalcPayment),
|
||||
new[] { typeof(List<BondPayment>), typeof(decimal), typeof(decimal), typeof(decimal), typeof(decimal?) });
|
||||
|
||||
Assert.IsNotNull(method, "公司行为现金分红需要支持单独传入除权前数量。");
|
||||
var actual = (decimal)method.Invoke(CreateService(), new object[] { payments, 2000m, 1m, 1m, (decimal?)1000m });
|
||||
|
||||
Assert.AreEqual(1040m, actual, "原生付息按当前 2000 份计算为 40,公司行为分红按除权前 1000 份计算为 1000。");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace YLErp.Modules.EodModule
|
||||
{
|
||||
[TestClass]
|
||||
public class DividendInfoEditGuardMessageTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void AddDividendInfos_ReportsTheReferencingTradeNumberWhenAnExecutedActionIsEdited()
|
||||
{
|
||||
var source = ReadDividendServiceSource();
|
||||
var addDividendInfos = ExtractMethod(source, "public bool AddDividendInfos", "public bool checkDividendInfoExecuteStatus");
|
||||
var controllerSource = ReadExDividendInfoControllerSource();
|
||||
var deleteDividend = ExtractMethod(controllerSource, "public JsonResult deleteDividend", "public JsonResult ImportDividendInfo");
|
||||
|
||||
Assert.IsTrue(addDividendInfos.Contains("不可修改,有交易【", StringComparison.Ordinal));
|
||||
Assert.IsTrue(addDividendInfos.Contains("使用了该条除权除息数据", StringComparison.Ordinal));
|
||||
Assert.IsTrue(deleteDividend.Contains("不可修改,有交易【", StringComparison.Ordinal));
|
||||
Assert.IsTrue(deleteDividend.Contains("使用了该条除权除息数据", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
private static string ExtractMethod(string source, string startMarker, string endMarker)
|
||||
{
|
||||
var start = source.IndexOf(startMarker, StringComparison.Ordinal);
|
||||
Assert.IsTrue(start >= 0, $"Could not find method: {startMarker}");
|
||||
var end = source.IndexOf(endMarker, start + startMarker.Length, StringComparison.Ordinal);
|
||||
Assert.IsTrue(end >= 0, $"Could not find method end: {endMarker}");
|
||||
return source.Substring(start, end - start);
|
||||
}
|
||||
|
||||
private static string ReadDividendServiceSource()
|
||||
{
|
||||
return ReadSource("YLErpDAL", "Modules", "TradeModule", "DealModule", "DividendService.cs");
|
||||
}
|
||||
|
||||
private static string ReadExDividendInfoControllerSource()
|
||||
{
|
||||
return ReadSource("YLErpWeb", "Controllers", "ex_dividend_infoController.cs");
|
||||
}
|
||||
|
||||
private static string ReadSource(params string[] relativePath)
|
||||
{
|
||||
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (directory != null)
|
||||
{
|
||||
var path = Path.Combine(new[] { directory.FullName }.Concat(relativePath).ToArray());
|
||||
if (File.Exists(path))
|
||||
{
|
||||
return File.ReadAllText(path);
|
||||
}
|
||||
directory = directory.Parent;
|
||||
}
|
||||
|
||||
Assert.Fail("Could not locate DividendService.cs from the test output directory.");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
using YLErp.Abstract;
|
||||
using YLErp.Helpers;
|
||||
using YLErp.Modules.EodModule;
|
||||
|
||||
namespace YLErp.Modules.EodModuleTests
|
||||
{
|
||||
[TestClass]
|
||||
public class TrsContractKafkaPushServiceTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void Push_空日快照_发送一条空消息并使用业务日期作为Key()
|
||||
{
|
||||
var valueDate = new DateTime(2026, 8, 24);
|
||||
var producer = new RecordingKafkaProducer();
|
||||
var service = CreateService(producer, valueDate);
|
||||
|
||||
service.Push(valueDate);
|
||||
|
||||
Assert.AreEqual(1, producer.Messages.Count);
|
||||
Assert.AreEqual("onederiv.trs.contract.v1", producer.Messages[0].Topic);
|
||||
Assert.AreEqual("2026-08-24", producer.Messages[0].Key);
|
||||
var payload = JsonHelper.Deserialize<TrsContractSnapshot>(producer.Messages[0].Message);
|
||||
Assert.AreEqual("2026-08-24", payload.ValueDate);
|
||||
Assert.AreEqual(0, payload.ContractCount);
|
||||
Assert.AreEqual(0, payload.Contracts.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Push_区间内每天分别调用_每个日期各发送一条快照()
|
||||
{
|
||||
var valueDates = new[]
|
||||
{
|
||||
new DateTime(2026, 8, 20),
|
||||
new DateTime(2026, 8, 21),
|
||||
new DateTime(2026, 8, 24)
|
||||
};
|
||||
var producer = new RecordingKafkaProducer();
|
||||
var service = new TestableTrsContractKafkaPushService(producer, valueDates.ToDictionary(x => x, CreateEmptySnapshot));
|
||||
|
||||
foreach (var valueDate in valueDates)
|
||||
{
|
||||
service.Push(valueDate);
|
||||
}
|
||||
|
||||
CollectionAssert.AreEqual(
|
||||
new[] { "2026-08-20", "2026-08-21", "2026-08-24" },
|
||||
producer.Messages.Select(x => x.Key).ToArray());
|
||||
CollectionAssert.AreEqual(
|
||||
new[] { "2026-08-20", "2026-08-21", "2026-08-24" },
|
||||
producer.Messages.Select(x => JsonHelper.Deserialize<TrsContractSnapshot>(x.Message).ValueDate).ToArray());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Push_首次失败后成功_停止重试且不记录最终失败()
|
||||
{
|
||||
var valueDate = new DateTime(2026, 8, 24);
|
||||
var producer = new RecordingKafkaProducer { FailuresBeforeSuccess = 1 };
|
||||
var service = CreateService(producer, valueDate);
|
||||
|
||||
service.Push(valueDate);
|
||||
|
||||
Assert.AreEqual(2, producer.AttemptCount);
|
||||
Assert.AreEqual(1, producer.Messages.Count);
|
||||
Assert.AreEqual(0, service.FailureRecords.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Push_连续失败三次_记录最终失败和三次尝试()
|
||||
{
|
||||
var valueDate = new DateTime(2026, 8, 24);
|
||||
var producer = new RecordingKafkaProducer { FailuresBeforeSuccess = int.MaxValue };
|
||||
var service = CreateService(producer, valueDate);
|
||||
|
||||
service.Push(valueDate);
|
||||
|
||||
Assert.AreEqual(3, producer.AttemptCount);
|
||||
Assert.AreEqual(0, producer.Messages.Count);
|
||||
Assert.AreEqual(1, service.FailureRecords.Count);
|
||||
Assert.AreEqual(valueDate, service.FailureRecords[0].ValueDate);
|
||||
Assert.AreEqual(3, service.FailureRecords[0].RetryCount);
|
||||
Assert.IsInstanceOfType(service.FailureRecords[0].Exception, typeof(InvalidOperationException));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void BuildContract_字段使用日终快照和约定来源()
|
||||
{
|
||||
var valueDate = new DateTime(2026, 8, 24);
|
||||
var eodSwap = new eod_swap
|
||||
{
|
||||
id = 10,
|
||||
ValueDate = valueDate,
|
||||
SwapTradeId = 7,
|
||||
SwapTradeNo = "TRS-001",
|
||||
BookId = 3,
|
||||
ClientId = 8,
|
||||
NotionalValue = 1000000m,
|
||||
dv01 = 12.34m,
|
||||
InitMarginGain = 100m,
|
||||
InitMarginLoss = 0m
|
||||
};
|
||||
var trade = new trade
|
||||
{
|
||||
id = 7,
|
||||
UnderlyingCode = "600000.SH",
|
||||
UnderlyingAssetName = "浦发银行",
|
||||
UnderlyingInstrumentType = "Stock",
|
||||
StartDate = new DateTime(2026, 8, 1),
|
||||
ExerciseDate = new DateTime(2027, 8, 1)
|
||||
};
|
||||
var positions = new List<eod_swap_position>
|
||||
{
|
||||
new() { SwapTradeId = 7, PositionId = 101, UnderlyingCode = "600000.SH", PositionType = 1 },
|
||||
new() { SwapTradeId = 7, PositionId = 102, InterestMode = (int)InterestModeEnum.固定值, InterestRateDefault = 0.0123m, InterestDirection = 2 }
|
||||
};
|
||||
var swapPositions = new Dictionary<long, swap_position>
|
||||
{
|
||||
[101] = new() { id = 101, category_tag = "互换利率" },
|
||||
[102] = new() { id = 102, category_tag = "互换利率" }
|
||||
};
|
||||
|
||||
var item = TrsContractKafkaPushService.BuildContract(
|
||||
eodSwap,
|
||||
new Dictionary<int, trade> { [7] = trade },
|
||||
positions,
|
||||
swapPositions);
|
||||
|
||||
Assert.AreEqual("2026-08-24", item.TradeDate);
|
||||
Assert.AreEqual(3, item.BookId);
|
||||
Assert.AreEqual("TRS-001", item.SwapTradeNo);
|
||||
Assert.AreEqual(8, item.ClientId);
|
||||
Assert.AreEqual("600000.SH", item.UnderlyingCode);
|
||||
Assert.AreEqual("浦发银行", item.UnderlyingName);
|
||||
Assert.AreEqual("Stock", item.UnderlyingInstrumentType);
|
||||
Assert.AreEqual(1000000m, item.NotionalValue);
|
||||
Assert.AreEqual("2026-08-01", item.StartDate);
|
||||
Assert.AreEqual("2027-08-01", item.MaturityDate);
|
||||
Assert.AreEqual(12.34m, item.Dv01);
|
||||
Assert.AreEqual(0.0123m, item.FixedRate);
|
||||
Assert.AreEqual(2, item.InterestDirection);
|
||||
Assert.AreEqual(1, item.FloatingDirection);
|
||||
Assert.AreEqual(100m, item.InitMarginGain);
|
||||
Assert.AreEqual(0m, item.InitMarginLoss);
|
||||
}
|
||||
|
||||
private static TestableTrsContractKafkaPushService CreateService(RecordingKafkaProducer producer, DateTime valueDate)
|
||||
{
|
||||
return new TestableTrsContractKafkaPushService(
|
||||
producer,
|
||||
new Dictionary<DateTime, TrsContractSnapshot> { [valueDate] = CreateEmptySnapshot(valueDate) });
|
||||
}
|
||||
|
||||
private static TrsContractSnapshot CreateEmptySnapshot(DateTime valueDate)
|
||||
{
|
||||
return new TrsContractSnapshot
|
||||
{
|
||||
SchemaVersion = "v1",
|
||||
ValueDate = valueDate.ToString("yyyy-MM-dd"),
|
||||
PushTime = "2026-08-24 12:00:00",
|
||||
ContractCount = 0,
|
||||
Contracts = new List<TrsContractSnapshotItem>()
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class TestableTrsContractKafkaPushService : TrsContractKafkaPushService
|
||||
{
|
||||
private readonly IReadOnlyDictionary<DateTime, TrsContractSnapshot> _snapshots;
|
||||
|
||||
public List<(DateTime ValueDate, int RetryCount, Exception Exception)> FailureRecords { get; } = new();
|
||||
|
||||
public TestableTrsContractKafkaPushService(IKafkaProduce producer, IReadOnlyDictionary<DateTime, TrsContractSnapshot> snapshots)
|
||||
: base(new YLContext(), producer, "onederiv.trs.contract.v1")
|
||||
{
|
||||
_snapshots = snapshots;
|
||||
}
|
||||
|
||||
protected override TrsContractSnapshot BuildSnapshot(DateTime valueDate)
|
||||
{
|
||||
return _snapshots[valueDate];
|
||||
}
|
||||
|
||||
protected override void RecordFailures(DateTime valueDate, int retryCount, Exception exception)
|
||||
{
|
||||
FailureRecords.Add((valueDate, retryCount, exception));
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RecordingKafkaProducer : IKafkaProduce
|
||||
{
|
||||
public int FailuresBeforeSuccess { get; set; }
|
||||
public int AttemptCount { get; private set; }
|
||||
public List<(string Topic, string Key, string Message)> Messages { get; } = new();
|
||||
|
||||
public void Produce(string topic, string message)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void Produce(string topic, string key, string message)
|
||||
{
|
||||
AttemptCount++;
|
||||
if (AttemptCount <= FailuresBeforeSuccess)
|
||||
{
|
||||
throw new InvalidOperationException("Kafka unavailable");
|
||||
}
|
||||
|
||||
Messages.Add((topic, key, message));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using YLErp.DBModels;
|
||||
@@ -20,6 +21,7 @@ namespace YLErp.Modules.SwapModule
|
||||
public void RegistrationSnapshot_IsPending_AndKeepsBeforeFields()
|
||||
{
|
||||
var info = CreateAction(77, ConsGlobal.InstrumentType.Stock);
|
||||
info.GiveShareAmount = 1m;
|
||||
var before = CreateEodPosition(9, info.UnderlyingCode, 1000m, 100m);
|
||||
|
||||
var snapshot = SwapEodPositionService.BuildCorporateActionEventData(
|
||||
@@ -37,8 +39,9 @@ namespace YLErp.Modules.SwapModule
|
||||
Assert.IsFalse(snapshot.Applied);
|
||||
|
||||
var reason = SwapEventService.BuildCorporateActionEventReason(snapshot);
|
||||
StringAssert.Contains(reason, "BeforeQuantity=1000");
|
||||
StringAssert.Contains(reason, "AfterQuantity=0");
|
||||
StringAssert.Contains(reason, "股权登记日:2026-08-14 发生公司行为(送股)");
|
||||
StringAssert.Contains(reason, "调整前:名义本金:100000 期初标的价格:100 持仓数量:1000");
|
||||
StringAssert.Contains(reason, "调整后:名义本金:0 期初标的价格:0 持仓数量:0");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -65,6 +68,23 @@ namespace YLErp.Modules.SwapModule
|
||||
Assert.IsFalse(SwapEodPositionService.IsCorporateActionInstrument(ConsGlobal.InstrumentType.TBonds));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CorporateActionReason_ShowsOneActionTypeOrActualCashDividend()
|
||||
{
|
||||
AssertActionDescription(
|
||||
new CorporateActionEventData { ExDividendDate = RecordDate, RationedSharesAmount = 1m, GiveShareAmount = 1m, Split = 2m, GiveCashAmount = 10m, CashFlowChange = 1000m },
|
||||
"发生公司行为(配股)");
|
||||
AssertActionDescription(
|
||||
new CorporateActionEventData { ExDividendDate = RecordDate, GiveShareAmount = 1m, Split = 2m, GiveCashAmount = 10m, CashFlowChange = 1000m },
|
||||
"发生公司行为(送股)");
|
||||
AssertActionDescription(
|
||||
new CorporateActionEventData { ExDividendDate = RecordDate, Split = 2m, GiveCashAmount = 10m, CashFlowChange = 1000m },
|
||||
"发生公司行为(拆分)");
|
||||
AssertActionDescription(
|
||||
new CorporateActionEventData { ExDividendDate = RecordDate, GiveCashAmount = 11m, CashFlowChange = 220000m },
|
||||
"发生公司行为(产生分红:220000)");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Rerun_DoesNotCreateDuplicateCorporateActionEvent()
|
||||
{
|
||||
@@ -113,7 +133,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void OperationHistory_PreservesPendingCorporateActionForAudit()
|
||||
public void OperationHistory_HidesPendingCorporateActionUntilItIsApplied()
|
||||
{
|
||||
var info = CreateAction(80, ConsGlobal.InstrumentType.Stock);
|
||||
var pendingData = SwapEodPositionService.BuildCorporateActionEventData(
|
||||
@@ -133,12 +153,17 @@ namespace YLErp.Modules.SwapModule
|
||||
new swap_event { id = 3, EventType = (int)SwapEventTypeEnum.互换, EventData = "{}" }
|
||||
};
|
||||
|
||||
// 操作历史不再隐藏登记日待生效事件;Applied=false 是事件状态,不是展示过滤条件。
|
||||
Assert.AreEqual(3, events.Count);
|
||||
Assert.IsTrue(SwapEventService.TryDeserializeCorporateActionEventData(events[0], out var pendingSnapshot));
|
||||
Assert.IsFalse(pendingSnapshot.Applied);
|
||||
Assert.IsTrue(SwapEventService.TryDeserializeCorporateActionEventData(events[1], out var appliedSnapshot));
|
||||
Assert.IsTrue(appliedSnapshot.Applied);
|
||||
var filter = typeof(SwapEventService).GetMethod(
|
||||
"FilterOperationHistory",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
Assert.IsNotNull(filter, "操作历史必须过滤登记日创建的待生效公司行为事件。");
|
||||
|
||||
var visibleEvents = (List<swap_event>)filter.Invoke(null, new object[] { events });
|
||||
|
||||
Assert.AreEqual(2, visibleEvents.Count);
|
||||
Assert.IsFalse(visibleEvents.Any(x => x.id == 1));
|
||||
Assert.IsTrue(visibleEvents.Any(x => x.id == 2));
|
||||
Assert.IsTrue(visibleEvents.Any(x => x.id == 3));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -283,6 +308,11 @@ namespace YLErp.Modules.SwapModule
|
||||
};
|
||||
}
|
||||
|
||||
private static void AssertActionDescription(CorporateActionEventData data, string expected)
|
||||
{
|
||||
StringAssert.Contains(SwapEventService.BuildCorporateActionEventReason(data), expected);
|
||||
}
|
||||
|
||||
private static eod_swap_position CreateEodPosition(long positionId, string code, decimal quantity, decimal price)
|
||||
{
|
||||
return new eod_swap_position
|
||||
|
||||
@@ -206,5 +206,106 @@ namespace YLErp.Modules.SwapModule
|
||||
Assert.AreEqual(ConsFundTag.Cash, settlements[1].Tag);
|
||||
Assert.AreEqual(-300m, settlements[1].MarginAmount);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// §2.3 情形1 交易级资金来源回退(ConsFundTag.PreferCredit):
|
||||
// 腿上显式选择优先 > 交易级 margin_fund_source 回退 > 默认现金。
|
||||
// 标签定稿(ApplyMarginFundTags)与资金校验(TradeCanBeConfirm)共用本口径。
|
||||
// ================================================================
|
||||
|
||||
[TestMethod]
|
||||
public void FT_031_腿选授信或现金_交易级字段不覆盖腿上显式选择()
|
||||
{
|
||||
Assert.IsTrue(ConsFundTag.PreferCredit(ConsFundTag.Credit, null));
|
||||
Assert.IsTrue(ConsFundTag.PreferCredit(ConsFundTag.Credit, ConsFundTag.Cash));
|
||||
Assert.IsFalse(ConsFundTag.PreferCredit(ConsFundTag.Cash, ConsFundTag.Credit));
|
||||
Assert.IsFalse(ConsFundTag.PreferCredit(ConsFundTag.Cash, null));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FT_032_腿未选_按交易级资金来源回退()
|
||||
{
|
||||
Assert.IsTrue(ConsFundTag.PreferCredit(null, ConsFundTag.Credit));
|
||||
Assert.IsTrue(ConsFundTag.PreferCredit("", ConsFundTag.Credit));
|
||||
Assert.IsFalse(ConsFundTag.PreferCredit(null, ConsFundTag.Cash));
|
||||
//交易级也未设置 → 默认现金
|
||||
Assert.IsFalse(ConsFundTag.PreferCredit(null, null));
|
||||
Assert.IsFalse(ConsFundTag.PreferCredit("", ""));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// §2.3 保存前授信拆单(FundTagCalc.ApplySaveTimeSplit,2026-08-26 业务确认:
|
||||
// 保存检查授信→不足拦截确认→拆完再保存;原腿=可用额度标授信、新腿=现金差额)
|
||||
// ================================================================
|
||||
|
||||
private static LegAmount MarginLeg(long id, decimal fix, bool preferCredit = true)
|
||||
=> new()
|
||||
{
|
||||
Leg = new swap_position
|
||||
{
|
||||
id = id,
|
||||
InterestDirection = 1,
|
||||
InterestPrincipalFix = fix,
|
||||
FundTag = preferCredit ? ConsFundTag.Credit : ConsFundTag.Cash
|
||||
},
|
||||
Amount = (double)fix,
|
||||
PreferCredit = preferCredit
|
||||
};
|
||||
|
||||
[TestMethod]
|
||||
public void FT_033_保存前拆单_额度不足_原腿授信新腿现金差额守恒()
|
||||
{
|
||||
var legs = new List<LegAmount> { MarginLeg(101, 1000m) };
|
||||
var plans = FundTagCalc.AllocateByLegPreference(legs, 300, ignoreMoneyCheck: false);
|
||||
var newLegs = FundTagCalc.ApplySaveTimeSplit(legs, plans);
|
||||
|
||||
Assert.AreEqual(1, newLegs.Count);
|
||||
//原腿保留可用额度部分并标授信
|
||||
Assert.AreEqual(300m, legs[0].Leg.InterestPrincipalFix);
|
||||
Assert.AreEqual(ConsFundTag.Credit, legs[0].Leg.FundTag);
|
||||
//新现金腿=差额,倒挤守恒
|
||||
Assert.AreEqual(700m, newLegs[0].InterestPrincipalFix);
|
||||
Assert.AreEqual(ConsFundTag.Cash, newLegs[0].FundTag);
|
||||
Assert.AreEqual(0, newLegs[0].id);
|
||||
Assert.IsNull(newLegs[0].Obervation);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FT_034_保存前拆单_可用授信为零_整腿定稿现金不拆()
|
||||
{
|
||||
var legs = new List<LegAmount> { MarginLeg(101, 1000m) };
|
||||
var plans = FundTagCalc.AllocateByLegPreference(legs, 0, ignoreMoneyCheck: false);
|
||||
var newLegs = FundTagCalc.ApplySaveTimeSplit(legs, plans);
|
||||
|
||||
Assert.AreEqual(0, newLegs.Count);
|
||||
Assert.AreEqual(1000m, legs[0].Leg.InterestPrincipalFix);
|
||||
Assert.AreEqual(ConsFundTag.Cash, legs[0].Leg.FundTag);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FT_035_保存前拆单_额度充足_全额授信定稿不拆_现金腿不动()
|
||||
{
|
||||
var legs = new List<LegAmount> { MarginLeg(101, 1000m), MarginLeg(102, 500m, preferCredit: false) };
|
||||
var plans = FundTagCalc.AllocateByLegPreference(legs, 5000, ignoreMoneyCheck: false);
|
||||
var newLegs = FundTagCalc.ApplySaveTimeSplit(legs, plans);
|
||||
|
||||
Assert.AreEqual(0, newLegs.Count);
|
||||
Assert.AreEqual(ConsFundTag.Credit, legs[0].Leg.FundTag);
|
||||
Assert.AreEqual(1000m, legs[0].Leg.InterestPrincipalFix);
|
||||
Assert.AreEqual(ConsFundTag.Cash, legs[1].Leg.FundTag);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FT_036_保存前拆单_支付方向腿按方向比折算()
|
||||
{
|
||||
var leg = new swap_position { id = 101, InterestDirection = 2, InterestPrincipalFix = -1000m, FundTag = ConsFundTag.Credit };
|
||||
var legs = new List<LegAmount> { new() { Leg = leg, Amount = 1000, PreferCredit = true } };
|
||||
var plans = FundTagCalc.AllocateByLegPreference(legs, 300, ignoreMoneyCheck: false);
|
||||
var newLegs = FundTagCalc.ApplySaveTimeSplit(legs, plans);
|
||||
|
||||
//应付额 = fix × -1(dir=2),授信部分 300 → fix = -300;现金差额倒挤 = -700
|
||||
Assert.AreEqual(-300m, leg.InterestPrincipalFix);
|
||||
Assert.AreEqual(-700m, newLegs[0].InterestPrincipalFix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
|
||||
namespace YLErp.Modules.SwapModule;
|
||||
|
||||
/// <summary>
|
||||
/// EQD-7084 新“框架合约”Tab 的口径测试。
|
||||
/// 纯计算测试不依赖数据库,直接锁定 EodPnlCalculator 的新口径。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SwapEodPositionRiskNewTabTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void 新口径_普通利息排除保证金_浮动收益剔除费用并保留估值总额()
|
||||
{
|
||||
var floating = new[]
|
||||
{
|
||||
// EOD 归一后,费用已经按我方收益视角落库;支付费用为负数。
|
||||
FloatingLeg("600000", 1, 100m, 0m, -12m, "普通收益互换")
|
||||
};
|
||||
var interests = new[]
|
||||
{
|
||||
InterestLeg(1, (int)InterestModeEnum.固定值, 80m, 0.02m, 0.02m),
|
||||
InterestLeg(1, (int)InterestModeEnum.初始预付金, 20m, 0.01m, 0.01m)
|
||||
};
|
||||
|
||||
var fields = InvokeCalculation(
|
||||
floating,
|
||||
interests,
|
||||
structureType: "普通收益互换",
|
||||
notionalValue: 1_000m,
|
||||
startDate: new DateTime(2026, 1, 1),
|
||||
maturityDate: new DateTime(2026, 12, 31),
|
||||
periodAmount: 5m,
|
||||
dividendPayDate: 0);
|
||||
|
||||
Assert.AreEqual(100m, GetDecimal(fields, "FloatingUnrealizedPnl"), 0.0001m,
|
||||
"新浮动端待实现收益应排除 PosiFeePending:PosiProfitSum(88) - PosiFeePending(-12) = 100");
|
||||
Assert.AreEqual(-12m, GetDecimal(fields, "OpeningClosingFee"), 0.0001m,
|
||||
"开平仓费用直接使用 EOD 已归一的 PosiFeePending");
|
||||
Assert.AreEqual(80m, GetDecimal(fields, "OrdinaryInterestPnl"), 0.0001m,
|
||||
"利息端待实现收益应排除初始/维持保证金腿");
|
||||
Assert.AreEqual(-20m, GetDecimal(fields, "MarginInterestAmount"), 0.0001m,
|
||||
"保证金利息仍应按保证金腿方向计入估值");
|
||||
Assert.AreEqual(153m, GetDecimal(fields, "MaturityNettingValuation"), 0.0001m,
|
||||
"估值应保持旧口径:100 - 12 + 80 - 20 + 5 = 153;费用只计一次");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 新口径_当前利率合计使用普通利息腿TdInterestRate_并识别FR007()
|
||||
{
|
||||
var fr007Leg = InterestLeg(2, (int)InterestModeEnum.合约名义本金规模, 20m, 0.03m, 0.018m);
|
||||
fr007Leg.FloatRateUnderlyingCode = "FR007";
|
||||
fr007Leg.FloatRate = 0.015m;
|
||||
var fields = InvokeCalculation(
|
||||
new[] { FloatingLeg("600001", 2, 100m, 0m, 0m, "普通收益互换") },
|
||||
new[]
|
||||
{
|
||||
InterestLeg(1, (int)InterestModeEnum.固定值, 10m, 0.02m, 0.0125m),
|
||||
fr007Leg
|
||||
},
|
||||
structureType: "普通收益互换",
|
||||
notionalValue: 100m,
|
||||
startDate: new DateTime(2026, 2, 1),
|
||||
maturityDate: new DateTime(2026, 8, 1),
|
||||
periodAmount: 0m,
|
||||
dividendPayDate: 1);
|
||||
|
||||
Assert.AreEqual(0.0305m, GetDecimal(fields, "InterestRatePrice"), 0.0000001m,
|
||||
"利率端价格应为普通利息腿当前 TdInterestRate 合计,而非默认利差合计");
|
||||
Assert.AreEqual("FR007", GetString(fields, "InterestBenchmark"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 新口径_普通利息腿无FR007时基准为固定利率()
|
||||
{
|
||||
var fields = InvokeCalculation(
|
||||
new[] { FloatingLeg("600002", 1, 100m, 0m, 0m, "普通收益互换") },
|
||||
new[] { InterestLeg(1, (int)InterestModeEnum.固定值, 10m, 0.02m, 0.0125m) },
|
||||
structureType: "普通收益互换",
|
||||
notionalValue: 100m,
|
||||
startDate: new DateTime(2026, 2, 1),
|
||||
maturityDate: new DateTime(2026, 8, 1),
|
||||
periodAmount: 0m,
|
||||
dividendPayDate: 1);
|
||||
|
||||
Assert.AreEqual("固定利率", GetString(fields, "InterestBenchmark"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 新口径_债券期初价格按风险页约定放大百分价格_并保留合同字段()
|
||||
{
|
||||
var fields = InvokeCalculation(
|
||||
new[] { FloatingLeg("110000", 1, 99.12m, 0m, 0m, "普通债券类收益互换", "Bond") },
|
||||
new[] { InterestLeg(1, (int)InterestModeEnum.固定值, 1m, 0.01m, 0.01m) },
|
||||
structureType: "普通债券类收益互换",
|
||||
notionalValue: 9_900m,
|
||||
startDate: new DateTime(2026, 3, 1),
|
||||
maturityDate: new DateTime(2027, 3, 1),
|
||||
periodAmount: 0m,
|
||||
dividendPayDate: 1);
|
||||
|
||||
Assert.AreEqual(99.12m, GetDecimal(fields, "InitialPrice"), 0.0001m,
|
||||
"债券日终 PosiGrossPrice 已由 SetPosiPrice 按风险页口径缩放,新接口不能再次乘 100");
|
||||
Assert.AreEqual(9_900m, GetDecimal(fields, "NotionalQuantity"), 0.0001m);
|
||||
Assert.AreEqual("多头", GetString(fields, "UnderlyingDirection"));
|
||||
Assert.AreEqual("110000", GetString(fields, "UnderlyingCode"));
|
||||
Assert.AreEqual("Bond", GetString(fields, "UnderlyingInstrumentType"));
|
||||
Assert.AreEqual(new DateTime(2026, 3, 1), GetDate(fields, "ContractStartDate"));
|
||||
Assert.AreEqual(new DateTime(2027, 3, 1), GetDate(fields, "ContractMaturityDate"));
|
||||
}
|
||||
|
||||
private static object InvokeCalculation(
|
||||
IEnumerable<eod_swap_position> floating,
|
||||
IEnumerable<eod_swap_position> interests,
|
||||
string structureType,
|
||||
decimal notionalValue,
|
||||
DateTime startDate,
|
||||
DateTime maturityDate,
|
||||
decimal periodAmount,
|
||||
int dividendPayDate)
|
||||
{
|
||||
return EodPnlCalculator.CalculateEodSwapRiskNewFields(
|
||||
floating,
|
||||
interests,
|
||||
structureType,
|
||||
notionalValue,
|
||||
startDate,
|
||||
maturityDate,
|
||||
periodAmount,
|
||||
dividendPayDate);
|
||||
}
|
||||
|
||||
private static decimal GetDecimal(object fields, string name)
|
||||
=> Convert.ToDecimal(fields.GetType().GetProperty(name)!.GetValue(fields));
|
||||
|
||||
private static string GetString(object fields, string name)
|
||||
=> (string)fields.GetType().GetProperty(name)!.GetValue(fields)!;
|
||||
|
||||
private static DateTime GetDate(object fields, string name)
|
||||
=> (DateTime)fields.GetType().GetProperty(name)!.GetValue(fields)!;
|
||||
|
||||
private static eod_swap_position FloatingLeg(
|
||||
string code,
|
||||
int positionType,
|
||||
decimal mtm,
|
||||
decimal dividend,
|
||||
decimal fee,
|
||||
string structureType,
|
||||
string instrumentType = null)
|
||||
=> new()
|
||||
{
|
||||
UnderlyingCode = code,
|
||||
UnderlyingInstrumentType = instrumentType ?? structureType,
|
||||
PositionType = positionType,
|
||||
PosiGrossPrice = mtm,
|
||||
PosiMtmPnL = mtm,
|
||||
PosiDividendSum = dividend,
|
||||
PosiFeePending = fee,
|
||||
PosiProfitSum = mtm + dividend + fee,
|
||||
PosiNotionalValue = 100m
|
||||
};
|
||||
|
||||
private static eod_swap_position InterestLeg(
|
||||
int direction,
|
||||
int mode,
|
||||
decimal profit,
|
||||
decimal defaultRate,
|
||||
decimal currentRate)
|
||||
=> new()
|
||||
{
|
||||
InterestDirection = direction,
|
||||
InterestMode = mode,
|
||||
InterestProfitSum = profit,
|
||||
InterestRateDefault = defaultRate,
|
||||
TdInterestRate = currentRate
|
||||
};
|
||||
}
|
||||
@@ -9,5 +9,7 @@ namespace YLErp.Abstract
|
||||
public interface IKafkaProduce
|
||||
{
|
||||
void Produce(string topic, string message);
|
||||
|
||||
void Produce(string topic, string key, string message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2327,8 +2327,9 @@ namespace YLErp.BLL.Eod
|
||||
if (trade.ExerciseDate.Value.Date >= valuedateBLL.ValueDate.Date)
|
||||
{
|
||||
// R4 簿记资金校验口径(2026-08-21 业务强调"走了资金的就不能占用授信"):
|
||||
// 按腿的资金走向分流——走现金的部分(未选/选现金腿 + 成交金额)只认现金结存;
|
||||
// 选授信的腿认 剩余可用授信(有效授信−已使用授信,授信出入表 Σ(amount)),
|
||||
// 按腿的资金走向分流——走现金的部分(选现金/未选且交易级资金来源非授信 + 成交金额)只认现金结存;
|
||||
// 按授信的腿(腿选授信,或腿未选回退交易级 margin_fund_source=授信,ConsFundTag.PreferCredit)
|
||||
// 认 剩余可用授信(有效授信−已使用授信,授信出入表 Σ(amount)),
|
||||
// 授信不够覆盖的部分回落现金,同样只认现金结存。杜绝"现金腿拿授信垫付校验→现金透支"。
|
||||
var marginModes = new[] { (int)InterestModeEnum.追加预付金, (int)InterestModeEnum.初始预付金 };
|
||||
var legs = trade.swap_positions?.Where(x => marginModes.Contains(x.InterestMode)).ToList();
|
||||
@@ -2346,7 +2347,7 @@ namespace YLErp.BLL.Eod
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (leg.FundTag == YLErp.DBModels.ConsFundTag.Credit)
|
||||
if (YLErp.DBModels.ConsFundTag.PreferCredit(leg.FundTag, trade.MarginFundSource))
|
||||
{
|
||||
creditPayable += payable;
|
||||
}
|
||||
|
||||
@@ -354,6 +354,7 @@ namespace YLErp.BLL
|
||||
public DbSet<eod_swap_position> eod_swap_position { get; set; }
|
||||
public DbSet<swap_event> swap_event { get; set; }
|
||||
public DbSet<eod_swap> eod_swap { get; set; }
|
||||
public DbSet<PushStatus> push_status { get; set; }
|
||||
public DbSet<TradeObervation> trade_obervation { get; set; }
|
||||
|
||||
public DbSet<SystemLog> SystemLogs { get; set; }
|
||||
|
||||
@@ -63,19 +63,29 @@ namespace YLErp.Helpers
|
||||
|
||||
public void Produce(string topic,string message)
|
||||
{
|
||||
var kafkaMessage = new Message<string, string>
|
||||
{
|
||||
Key=null,
|
||||
Value = message
|
||||
};
|
||||
try
|
||||
{
|
||||
_producer.ProduceAsync(topic, kafkaMessage).GetAwaiter().GetResult();
|
||||
ProduceCore(topic, null, message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Topic:{topic} send failed",ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void Produce(string topic, string key, string message)
|
||||
{
|
||||
ProduceCore(topic, key, message);
|
||||
}
|
||||
|
||||
private void ProduceCore(string topic, string key, string message)
|
||||
{
|
||||
var kafkaMessage = new Message<string, string>
|
||||
{
|
||||
Key = key,
|
||||
Value = message
|
||||
};
|
||||
_producer.ProduceAsync(topic, kafkaMessage).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace YLErp.Model
|
||||
/// <summary>
|
||||
/// TRS合约数据推送topic(对外,如onebp等)
|
||||
/// </summary>
|
||||
public string ContractTopic { get; set; } = "onederi.trs.onebp.contract.v1";
|
||||
public string ContractTopic { get; set; } = "onederiv.trs.contract.v1";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -59,43 +59,7 @@ namespace YLErp.Modules.EodModule
|
||||
create_time = source.create_time,
|
||||
update_time = source.update_time
|
||||
};
|
||||
// 不再依赖 bond-sync 镜像:Stock/Fund 公司行为直接作为展示行返回。
|
||||
// 展示金额按“每 10 份派现金额”换算为 GiveCashAmount / 10;EOD 计算仍使用
|
||||
// GetBondPayments 的内部单位口径,不受此处展示换算影响。
|
||||
var corporateQuery = from un in queryUn
|
||||
join dividend in DbContext.ex_dividend_info.AsNoTracking()
|
||||
on un.UnderlyingCode equals dividend.UnderlyingCode
|
||||
where dividend.ValidStatus
|
||||
&& dividend.EffectiveDate.HasValue
|
||||
&& dividend.EffectiveDate.Value >= valueDtStart
|
||||
&& dividend.EffectiveDate.Value < valueDtEnd
|
||||
&& dividend.GiveCashAmount != 0
|
||||
&& (string.IsNullOrEmpty(req.UnderlyingCode)
|
||||
|| dividend.UnderlyingCode.Contains(req.UnderlyingCode))
|
||||
&& (un.UnderlyingInstrumentType == ConsGlobal.InstrumentType.Stock
|
||||
|| un.UnderlyingInstrumentType == ConsGlobal.InstrumentType.Fund)
|
||||
&& (string.IsNullOrEmpty(req.DataSource)
|
||||
|| "公司行为除权表".Contains(req.DataSource))
|
||||
select new BondPaymentDto
|
||||
{
|
||||
id = -dividend.id,
|
||||
channel_source = "公司行为除权表",
|
||||
MarketName = un.MarketName,
|
||||
security_id = un.UnderlyingCode,
|
||||
symbol = un.UnderlyingName,
|
||||
coupon_rate = null,
|
||||
payment_date = dividend.EffectiveDate,
|
||||
payment_interest = dividend.GiveCashAmount / 10m,
|
||||
payment_parvalue = null,
|
||||
paying_price = dividend.GiveCashAmount / 10m,
|
||||
create_time = dividend.OptDate,
|
||||
update_time = dividend.OptDate
|
||||
};
|
||||
// EF Core 无法翻译两个对 BondPaymentDto 继承属性赋值集合不完全一致的投影
|
||||
// 直接 Concat;分别执行后在内存合并,不改变两组查询的筛选口径。
|
||||
var rows = query.ToList();
|
||||
rows.AddRange(corporateQuery.ToList());
|
||||
var result = rows.AsQueryable().ToSearchList(req);
|
||||
var result = query.ToSearchList(req);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -225,10 +189,26 @@ namespace YLErp.Modules.EodModule
|
||||
decimal qty,
|
||||
decimal longRatio,
|
||||
decimal payDirection)
|
||||
{
|
||||
return CalcPayment(payments, qty, longRatio, payDirection, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 公司行为现金分红按登记日权益数量计算;同一窗口中的原生债券付息仍按当前持仓数量计算。
|
||||
/// </summary>
|
||||
public decimal CalcPayment(
|
||||
List<BondPayment> payments,
|
||||
decimal qty,
|
||||
decimal longRatio,
|
||||
decimal payDirection,
|
||||
decimal? corporateActionQty)
|
||||
{
|
||||
var actualAmount = (payments ?? new List<BondPayment>()).Sum(payment =>
|
||||
{
|
||||
var paymentAmount = (payment.payment_interest ?? 0m) * qty;
|
||||
var paymentQty = payment.IsCorporateActionCashDividend
|
||||
? corporateActionQty ?? qty
|
||||
: qty;
|
||||
var paymentAmount = (payment.payment_interest ?? 0m) * paymentQty;
|
||||
// bond_payment_info 原生期间付息按每 100 份存储;由 ex_dividend_info 补充的
|
||||
// 公司行为现金分红按每 10 份存储。Fund 标的可能同时命中两类记录,故必须逐条分流。
|
||||
return payment.IsCorporateActionCashDividend
|
||||
|
||||
@@ -10,6 +10,12 @@ using YLErp.Modules.SystemModule;
|
||||
using YLErp.Modules.TradeDalModule;
|
||||
using YLErp.Modules.TradeModule.DealModule;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YieldChain.Commons;
|
||||
using YLErp.Abstract;
|
||||
using YLErp.Model;
|
||||
|
||||
namespace YLErp.Modules.EodModule.SettlementModule
|
||||
{
|
||||
/// <summary>
|
||||
@@ -445,12 +451,35 @@ where {nameof(t.TaskStartTime)}>'{startDateStr}' and {nameof(t.TaskState)}={(int
|
||||
}
|
||||
ClientBalanceUtility.saveClientRiskMonitor(eodTask.ValueDate);
|
||||
new EodFileService(this.OptUser).GenerateFileAfterEod(eodTask.ValueDate);
|
||||
PushTrsContractSnapshot(eodTask.ValueDate);
|
||||
//执行下一日
|
||||
eodTask.ValueDate = eodTask.ValueDate.AddDays(1);
|
||||
eodTask.TaskEndTime = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
private void PushTrsContractSnapshot(DateTime valueDate)
|
||||
{
|
||||
try
|
||||
{
|
||||
var provider = YLServiceLocator.ServiceProvider;
|
||||
var kafkaProduce = provider?.GetService<IKafkaProduce>();
|
||||
var kafkaOptions = provider?.GetService<IOptions<KafkaConfig>>();
|
||||
if (kafkaProduce == null || kafkaOptions?.Value == null)
|
||||
{
|
||||
LogFactory.GetLogger("TRS合约日终Kafka推送").Error("Kafka service or configuration is unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
using var pushDbContext = DbContextFactory.GetYLDbContext();
|
||||
new TrsContractKafkaPushService(pushDbContext, kafkaProduce, kafkaOptions.Value.ContractTopic).Push(valueDate);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogFactory.GetLogger("TRS合约日终Kafka推送").Error($"TRS contract snapshot task failed, valueDate:{valueDate:yyyy-MM-dd}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找第一个可用的任务
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
using YLErp.Abstract;
|
||||
using YLErp.BLL;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Helpers;
|
||||
|
||||
namespace YLErp.Modules.EodModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 收盘后按交易日推送 TRS 合约全量快照。
|
||||
/// </summary>
|
||||
public class TrsContractKafkaPushService
|
||||
{
|
||||
private const string DateFormat = "yyyy-MM-dd";
|
||||
private const string DateTimeFormat = "yyyy-MM-dd HH:mm:ss";
|
||||
private const string InterestCategory = "互换利率";
|
||||
private const int TrsContractPushType = 1;
|
||||
private const int MaxAttempts = 3;
|
||||
|
||||
private readonly YLContext _dbContext;
|
||||
private readonly IKafkaProduce _kafkaProduce;
|
||||
private readonly string _topic;
|
||||
private readonly IYcLogger _logger;
|
||||
|
||||
public TrsContractKafkaPushService(YLContext dbContext, IKafkaProduce kafkaProduce, string topic)
|
||||
{
|
||||
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
|
||||
_kafkaProduce = kafkaProduce ?? throw new ArgumentNullException(nameof(kafkaProduce));
|
||||
_topic = string.IsNullOrWhiteSpace(topic) ? throw new ArgumentException("Kafka topic is empty", nameof(topic)) : topic;
|
||||
_logger = LogFactory.GetLogger(nameof(TrsContractKafkaPushService));
|
||||
}
|
||||
|
||||
public void Push(DateTime valueDate)
|
||||
{
|
||||
valueDate = valueDate.Date;
|
||||
|
||||
TrsContractSnapshot snapshot;
|
||||
string payload;
|
||||
try
|
||||
{
|
||||
snapshot = BuildSnapshot(valueDate);
|
||||
payload = JsonHelper.Serialize(snapshot, true, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"TRS contract snapshot build failed, valueDate:{valueDate:yyyy-MM-dd}", ex);
|
||||
RecordFailures(valueDate, 0, ex);
|
||||
return;
|
||||
}
|
||||
|
||||
var key = valueDate.ToString(DateFormat);
|
||||
Exception lastException = null;
|
||||
for (var attempt = 1; attempt <= MaxAttempts; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
_kafkaProduce.Produce(_topic, key, payload);
|
||||
_logger.Info($"TRS contract snapshot sent, valueDate:{key}, topic:{_topic}, count:{snapshot.ContractCount}, attempt:{attempt}");
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lastException = ex;
|
||||
_logger.Error($"TRS contract snapshot send failed, valueDate:{key}, topic:{_topic}, attempt:{attempt}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Error($"TRS contract snapshot send exhausted retries, valueDate:{key}, topic:{_topic}, attempts:{MaxAttempts}");
|
||||
RecordFailures(valueDate, MaxAttempts, lastException);
|
||||
}
|
||||
|
||||
protected virtual void RecordFailures(DateTime valueDate, int retryCount, Exception exception)
|
||||
{
|
||||
try
|
||||
{
|
||||
var recordIds = _dbContext.eod_swap
|
||||
.Where(x => x.ValueDate == valueDate)
|
||||
.Select(x => x.id)
|
||||
.ToList();
|
||||
if (recordIds.Count == 0)
|
||||
{
|
||||
recordIds.Add(0);
|
||||
}
|
||||
|
||||
var now = DateTime.Now;
|
||||
var statuses = _dbContext.push_status
|
||||
.Where(x => x.ValueDate == valueDate
|
||||
&& x.PushType == TrsContractPushType
|
||||
&& recordIds.Contains(x.RecordId))
|
||||
.ToList();
|
||||
var error = exception?.ToString();
|
||||
if (error?.Length > 2000)
|
||||
{
|
||||
error = error.Substring(0, 2000);
|
||||
}
|
||||
|
||||
foreach (var recordId in recordIds)
|
||||
{
|
||||
var status = statuses.FirstOrDefault(x => x.RecordId == recordId);
|
||||
if (status == null)
|
||||
{
|
||||
status = new PushStatus
|
||||
{
|
||||
ValueDate = valueDate,
|
||||
PushType = TrsContractPushType,
|
||||
RecordId = recordId,
|
||||
CreateTime = now
|
||||
};
|
||||
_dbContext.push_status.Add(status);
|
||||
}
|
||||
|
||||
status.State = PushStateEnum.失败;
|
||||
status.RetryCount = retryCount;
|
||||
status.LastError = error;
|
||||
status.PushTime = now;
|
||||
status.UpdateTime = now;
|
||||
}
|
||||
|
||||
_dbContext.SaveChanges();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"TRS contract push failure status save failed, valueDate:{valueDate:yyyy-MM-dd}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual TrsContractSnapshot BuildSnapshot(DateTime valueDate)
|
||||
{
|
||||
var eodSwaps = _dbContext.eod_swap
|
||||
.Where(x => x.ValueDate == valueDate)
|
||||
.AsNoTracking()
|
||||
.ToList();
|
||||
var tradeIds = eodSwaps.Select(x => x.SwapTradeId).Distinct().ToList();
|
||||
var trades = _dbContext.trade
|
||||
.Where(x => tradeIds.Contains(x.id))
|
||||
.AsNoTracking()
|
||||
.ToDictionary(x => x.id);
|
||||
var eodPositions = _dbContext.eod_swap_position
|
||||
.Where(x => x.ValueDate == valueDate && tradeIds.Contains(x.SwapTradeId) && !x.Invalid)
|
||||
.AsNoTracking()
|
||||
.ToList();
|
||||
var positionIds = eodPositions.Select(x => x.PositionId).Distinct().ToList();
|
||||
var swapPositions = _dbContext.swap_position
|
||||
.Where(x => positionIds.Contains(x.id) && !x.Invalid && x.category_tag == InterestCategory)
|
||||
.AsNoTracking()
|
||||
.ToDictionary(x => x.id);
|
||||
|
||||
var contracts = eodSwaps.Select(eodSwap => BuildContract(eodSwap, trades, eodPositions, swapPositions)).ToList();
|
||||
return new TrsContractSnapshot
|
||||
{
|
||||
SchemaVersion = "v1",
|
||||
ValueDate = valueDate.ToString(DateFormat),
|
||||
PushTime = DateTime.Now.ToString(DateTimeFormat),
|
||||
ContractCount = contracts.Count,
|
||||
Contracts = contracts
|
||||
};
|
||||
}
|
||||
|
||||
internal static TrsContractSnapshotItem BuildContract(
|
||||
eod_swap eodSwap,
|
||||
IReadOnlyDictionary<int, trade> trades,
|
||||
IReadOnlyCollection<eod_swap_position> eodPositions,
|
||||
IReadOnlyDictionary<long, swap_position> swapPositions)
|
||||
{
|
||||
if (!trades.TryGetValue(eodSwap.SwapTradeId, out var trade))
|
||||
{
|
||||
throw new InvalidOperationException($"TRS trade not found, swapTradeId:{eodSwap.SwapTradeId}");
|
||||
}
|
||||
|
||||
var positions = eodPositions.Where(x => x.SwapTradeId == eodSwap.SwapTradeId).ToList();
|
||||
var floating = positions.Where(x => !string.IsNullOrWhiteSpace(x.UnderlyingCode) && swapPositions.ContainsKey(x.PositionId)).ToList();
|
||||
var interest = positions.Where(x => string.IsNullOrWhiteSpace(x.UnderlyingCode)
|
||||
&& ConsTrade.InterestModels.Contains(x.InterestMode)
|
||||
&& swapPositions.TryGetValue(x.PositionId, out var swapPosition)
|
||||
&& swapPosition.category_tag == InterestCategory).ToList();
|
||||
|
||||
if (floating.Count != 1 || interest.Count != 1)
|
||||
{
|
||||
throw new InvalidOperationException($"TRS legs invalid, swapTradeId:{eodSwap.SwapTradeId}, floating:{floating.Count}, interest:{interest.Count}");
|
||||
}
|
||||
|
||||
var interestLeg = interest[0];
|
||||
var floatingLeg = floating[0];
|
||||
return new TrsContractSnapshotItem
|
||||
{
|
||||
TradeDate = eodSwap.ValueDate.ToString(DateFormat),
|
||||
BookId = eodSwap.BookId,
|
||||
SwapTradeNo = eodSwap.SwapTradeNo,
|
||||
ClientId = eodSwap.ClientId,
|
||||
UnderlyingCode = trade.UnderlyingCode,
|
||||
UnderlyingName = trade.UnderlyingAssetName,
|
||||
UnderlyingInstrumentType = trade.UnderlyingInstrumentType,
|
||||
NotionalValue = eodSwap.NotionalValue,
|
||||
Dv01 = eodSwap.dv01 ?? 0,
|
||||
StartDate = trade.StartDate?.ToString(DateFormat),
|
||||
MaturityDate = trade.ExerciseDate?.ToString(DateFormat),
|
||||
FixedRate = interestLeg.InterestRateDefault,
|
||||
InterestDirection = interestLeg.InterestDirection,
|
||||
FloatingDirection = floatingLeg.PositionType,
|
||||
InitMarginGain = eodSwap.InitMarginGain,
|
||||
InitMarginLoss = eodSwap.InitMarginLoss
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class TrsContractSnapshot
|
||||
{
|
||||
public string SchemaVersion { get; set; }
|
||||
public string ValueDate { get; set; }
|
||||
public string PushTime { get; set; }
|
||||
public int ContractCount { get; set; }
|
||||
public List<TrsContractSnapshotItem> Contracts { get; set; }
|
||||
}
|
||||
|
||||
public class TrsContractSnapshotItem
|
||||
{
|
||||
public string TradeDate { get; set; }
|
||||
public int BookId { get; set; }
|
||||
public string SwapTradeNo { get; set; }
|
||||
public int ClientId { get; set; }
|
||||
public string UnderlyingCode { get; set; }
|
||||
public string UnderlyingName { get; set; }
|
||||
public string UnderlyingInstrumentType { get; set; }
|
||||
public decimal NotionalValue { get; set; }
|
||||
public decimal Dv01 { get; set; }
|
||||
public string StartDate { get; set; }
|
||||
public string MaturityDate { get; set; }
|
||||
public decimal FixedRate { get; set; }
|
||||
public int InterestDirection { get; set; }
|
||||
public int FloatingDirection { get; set; }
|
||||
public decimal InitMarginGain { get; set; }
|
||||
public decimal InitMarginLoss { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using YLErp;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
using YLErp.Modules.SwapModule.ReturnLegs;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
@@ -180,5 +182,101 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
position.RealizedPnl = position.RealizedInterest + position.RealizedInterestFee;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算 EQD-7084 新“框架合约”Tab 的纯展示口径。
|
||||
/// 浮动腿盯市收益、开平仓费用和普通利息分别计算;保证金腿的利息
|
||||
/// 仅作为估值组成项保留一次,不混入新 Tab 的普通利息列。
|
||||
/// </summary>
|
||||
public static EodSwapRiskNewFields CalculateEodSwapRiskNewFields(
|
||||
IEnumerable<eod_swap_position> floatingLegs,
|
||||
IEnumerable<eod_swap_position> interestLegs,
|
||||
string structureType,
|
||||
decimal notionalValue,
|
||||
DateTime? startDate,
|
||||
DateTime? maturityDate,
|
||||
decimal periodAmount,
|
||||
int dividendPayDate)
|
||||
{
|
||||
// 日终明细以 UnderlyingCode 是否存在区分浮动腿和利息腿;调用方即使传入混合集合,
|
||||
// 这里也会重新过滤,避免保证金/利息数据被带入浮动端新口径。
|
||||
var floating = (floatingLegs ?? Enumerable.Empty<eod_swap_position>())
|
||||
.Where(x => x != null && !string.IsNullOrEmpty(x.UnderlyingCode))
|
||||
.ToList();
|
||||
var interests = (interestLegs ?? Enumerable.Empty<eod_swap_position>())
|
||||
.Where(x => x != null && string.IsNullOrEmpty(x.UnderlyingCode))
|
||||
.ToList();
|
||||
// MarginModes 覆盖初始/维持保证金相关腿。它们的利息不属于需求中的“利息端待实现收益”,
|
||||
// 但必须单独保留,以使两个合约估值与旧口径总额保持一致。
|
||||
var ordinaryInterests = interests.Where(x => !MarginModes.Contains(x.InterestMode)).ToList();
|
||||
var marginInterests = interests.Where(x => MarginModes.Contains(x.InterestMode)).ToList();
|
||||
var firstFloating = floating.FirstOrDefault();
|
||||
|
||||
// PosiGrossPrice 已是 EOD 归档口径的期初全价;债券价格不可在报表接口再次乘 100。
|
||||
var initialPrice = firstFloating?.PosiGrossPrice;
|
||||
// PosiFeePending 是日终归一后的我方损益方向:支付费用为负、收取费用为正。
|
||||
// 本列独立展示它,下面的 valuation 再加回一次,不能因展示拆列而改变合约估值。
|
||||
var openingClosingFee = floating.Sum(x => x.PosiFeePending);
|
||||
// PosiMtmPnL 已排除分红和费用,避免从 PosiProfitSum 重复拆分历史费用。
|
||||
var floatingUnrealizedPnl = floating.Sum(x => x.PosiMtmPnL);
|
||||
var ordinaryInterestPnl = ordinaryInterests.Sum(x =>
|
||||
x.InterestProfitSum * DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode));
|
||||
var marginInterestAmount = marginInterests.Sum(x =>
|
||||
x.InterestProfitSum * DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode));
|
||||
|
||||
// 新口径估值 = 去费用浮动收益 + 开平仓费用 + 普通利息 + 保证金利息。
|
||||
// “浮动端待实现收益”列不包含费用,而合约估值仍沿用旧总额,故费用只能在此加一次。
|
||||
var valuation = floatingUnrealizedPnl
|
||||
+ openingClosingFee
|
||||
+ ordinaryInterestPnl
|
||||
+ marginInterestAmount;
|
||||
var result = new EodSwapRiskNewFields
|
||||
{
|
||||
UnderlyingInstrumentType = firstFloating?.UnderlyingInstrumentType,
|
||||
UnderlyingDirection = string.Join(",", floating
|
||||
.Select(x => x.PositionType == (int)PositionTypeFlag.Long ? "多头"
|
||||
: x.PositionType == (int)PositionTypeFlag.Short ? "空头" : "")
|
||||
.Where(x => !string.IsNullOrEmpty(x))
|
||||
.Distinct()),
|
||||
UnderlyingCode = string.Join(",", floating
|
||||
.Select(x => x.UnderlyingCode)
|
||||
.Where(x => !string.IsNullOrEmpty(x))
|
||||
.Distinct()),
|
||||
InitialPrice = initialPrice,
|
||||
NotionalQuantity = notionalValue,
|
||||
ContractStartDate = startDate,
|
||||
ContractMaturityDate = maturityDate,
|
||||
// 只要普通利息腿存在 FR007,即按需求显示 FR007;保证金腿不影响该展示基准。
|
||||
InterestBenchmark = ordinaryInterests.Any(x =>
|
||||
!string.IsNullOrWhiteSpace(x.FloatRateUnderlyingCode)
|
||||
&& x.FloatRateUnderlyingCode.IndexOf("FR007", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
? "FR007" : "固定利率",
|
||||
// 使用日终当日实际适用的 TdInterestRate 合计,而非合同初始利率或利差字段。
|
||||
InterestRatePrice = ordinaryInterests.Sum(x => x.TdInterestRate),
|
||||
OpeningClosingFee = openingClosingFee,
|
||||
FloatingUnrealizedPnl = floatingUnrealizedPnl,
|
||||
OrdinaryInterestPnl = ordinaryInterestPnl,
|
||||
MarginInterestAmount = marginInterestAmount,
|
||||
MarginInterestGain = marginInterests
|
||||
.Where(x => x.InterestDirection == (int)SwapDirectionEnum.支付)
|
||||
.Sum(x => Math.Abs(x.InterestIncomeSum)),
|
||||
MarginInterestLoss = marginInterests
|
||||
.Where(x => x.InterestDirection == (int)SwapDirectionEnum.收取)
|
||||
.Sum(x => -Math.Abs(x.InterestIncomeSum))
|
||||
};
|
||||
|
||||
// DividendPayDate=0 表示到期才与本金轧差,期间付息/分红需要加进该口径;
|
||||
// 其余支付方式则由现金支付承担期间金额,估值字段不再包含 periodAmount。
|
||||
if (dividendPayDate == 0)
|
||||
{
|
||||
result.MaturityNettingValuation = valuation + periodAmount;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.PeriodPaymentValuation = valuation;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -357,6 +357,19 @@ namespace YLErp.Modules.SwapModule
|
||||
directionRatio);
|
||||
}
|
||||
|
||||
protected virtual decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate,
|
||||
decimal qty, int shortRatio, int directionRatio, decimal? corporateActionQty)
|
||||
{
|
||||
if (!corporateActionQty.HasValue)
|
||||
{
|
||||
return CalcBondPayment(underlyingCode, fromDate, toDate, qty, shortRatio, directionRatio);
|
||||
}
|
||||
|
||||
var service = new BondPaymentService(UserInfo);
|
||||
var payments = service.GetBondPayments(underlyingCode, fromDate, toDate);
|
||||
return service.CalcPayment(payments, qty, shortRatio, directionRatio, corporateActionQty);
|
||||
}
|
||||
|
||||
// ---- SwapPositionCompose 路径专用 seam(借鉴 testable 分支)----
|
||||
|
||||
/// <summary>查找收盘所需的活跃互换交易(生产: DbContext.trade.Where;测试: 内存列表)</summary>
|
||||
@@ -439,7 +452,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <summary>
|
||||
/// 获取公司行为公式使用的收盘价。
|
||||
/// EffectiveDate 是真正切换持仓基线的日期,但除权系数的收盘价仍属于登记日
|
||||
/// ExDividendDate;不能在 8 月 17 日 EOD 误取 8 月 17 日收盘价重算 8 月 14 日
|
||||
/// ExDividendDate;不能在 除权日 EOD 误取 除权日收盘价重算 登记日
|
||||
/// 登记日形成的系数。测试实现可以返回快照中的回退值,生产实现从登记日行情读取。
|
||||
/// </summary>
|
||||
protected virtual decimal GetFundCorporateActionClosePrice(
|
||||
@@ -516,18 +529,22 @@ namespace YLErp.Modules.SwapModule
|
||||
// 公司行为只取 settleDate 当天的有效单行;同一标的出现多条记录必须中止本次收盘,
|
||||
// 否则 ToDictionary 会抛重复键,无法证明哪一条系数应生效。
|
||||
var corporateActionInfos = FindCorporateActionInfos(settleDate) ?? new List<ex_dividend_info>();
|
||||
// 除权日信息
|
||||
var exDividendInfos = corporateActionInfos
|
||||
.Where(x => x != null
|
||||
&& x.ValidStatus
|
||||
&& x.EffectiveDate.HasValue
|
||||
&& x.EffectiveDate.Value.Date == settleDate.Date)
|
||||
.ToList();
|
||||
// 登记日信息
|
||||
var registrationInfos = corporateActionInfos
|
||||
.Where(x => x != null
|
||||
&& x.ValidStatus
|
||||
&& x.ExDividendDate.HasValue
|
||||
&& x.ExDividendDate.Value.Date == settleDate.Date)
|
||||
.ToList();
|
||||
|
||||
// 公司行为去重 - 除权日
|
||||
var duplicateDividend = exDividendInfos
|
||||
.GroupBy(x => x.UnderlyingCode, StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(x => x.Count() > 1);
|
||||
@@ -535,7 +552,8 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
throw new InvalidOperationException($"标的【{duplicateDividend.Key}】在【{settleDate:yyyy-MM-dd}】存在多条有效除权记录");
|
||||
}
|
||||
// 公司行为去重 - 拦截
|
||||
|
||||
// 公司行为去重 - 登记日
|
||||
var duplicateRegistration = registrationInfos
|
||||
.GroupBy(x => x.UnderlyingCode, StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(x => x.Count() > 1);
|
||||
@@ -545,6 +563,8 @@ namespace YLErp.Modules.SwapModule
|
||||
// 有多条有效记录时,系统无法证明应采用哪一条派现金额,必须中止收盘。
|
||||
throw new InvalidOperationException($"标的【{duplicateRegistration.Key}】在【{settleDate:yyyy-MM-dd}】存在多条有效登记日记录");
|
||||
}
|
||||
|
||||
// 根据标的代码 创建map
|
||||
var exDividendByCode = exDividendInfos.ToDictionary(
|
||||
x => x.UnderlyingCode,
|
||||
x => x,
|
||||
@@ -588,22 +608,27 @@ namespace YLErp.Modules.SwapModule
|
||||
var flowEvents = FindFlowEvents(td.id, settleDate);
|
||||
var preDealDate = GetPreDealDate(td.id, settleDate, eventTyps);//上一次平仓/互换/自动互换处理日期
|
||||
List<swap_flow_event> autoInterests = new List<swap_flow_event>();//自动互换利息腿信息
|
||||
|
||||
// 处理浮动腿前先准备当日开盘基线:登记日 EOD 仍保存
|
||||
// 1000 份/100 元,除权日收盘时先把上一 EOD 的基线转换为
|
||||
// 2000 份/50 元,再处理当日平仓 300 份,最终才会得到 1700 份/50 元。
|
||||
// 不能等 DealFloatPositions 处理完平仓后再把 700 份乘 2,否则会错误得到
|
||||
// 1400 份;也不能直接修改数据库里的上一 EOD,否则登记日报表会被污染。
|
||||
|
||||
// 重置基线
|
||||
// 不能等 DealFloatPositions 处理完平仓后再把 700 份乘 2,
|
||||
// 否则会错误得到 1400 份;也不能直接修改数据库里的上一 EOD,否则登记日报表会被污染。
|
||||
// 重置基线 - 除权日
|
||||
var openingEodPositions = PrepareFundOpeningEodPositions(
|
||||
eodPositions,
|
||||
eodPositions, // 上一日终持仓
|
||||
exDividendByCode,
|
||||
settleDate);
|
||||
|
||||
// 构建公司行为前eod持仓
|
||||
var corporateActionBeforePositions = BuildCorporateActionBeforePositions(
|
||||
eodPositions,
|
||||
eodPositions, // 上一日终持仓
|
||||
posiList);
|
||||
var corporateActionCashDividendBeforePositions = corporateActionBeforePositions
|
||||
.Where(position => !string.IsNullOrWhiteSpace(position.UnderlyingCode)
|
||||
&& exDividendByCode.TryGetValue(position.UnderlyingCode, out var dividend)
|
||||
&& dividend.GiveCashAmount != 0m)
|
||||
.ToList();
|
||||
|
||||
// 交易首日恰逢 EffectiveDate 时,在内存克隆上生成除权后的开盘基线,应用生效日公司行为。
|
||||
// 有上一份 EOD 时沿用 PrepareFundOpeningEodPositions,避免重复套系数。
|
||||
@@ -613,18 +638,20 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
// 处理浮动腿归档
|
||||
var curEodPosis = DealFloatPositions(
|
||||
floatPositionsForCompose,
|
||||
realPosiList,
|
||||
openingEodPositions,
|
||||
todyEodPositions,
|
||||
settleDate,
|
||||
td,
|
||||
preSettleDate,
|
||||
flowEvents);
|
||||
floatPositionsForCompose, // 初始腿
|
||||
realPosiList, // 实时腿
|
||||
openingEodPositions, // 开盘基线
|
||||
todyEodPositions, // 当日终持仓
|
||||
settleDate, // 收盘日期
|
||||
td, // 交易
|
||||
preSettleDate, // 上一交易日
|
||||
flowEvents, // 流水事件
|
||||
corporateActionCashDividendBeforePositions);
|
||||
|
||||
// 现金分红不在登记日直接累加;Copy/Update EOD 通过 CalcBondPayment
|
||||
// 读取 EffectiveDate 命中的 ex_dividend_info,并生成 TdPosiDividend。
|
||||
// 这样登记日快照不提前变化,且公司行为分红与债券付息共用同一待实现余额。
|
||||
// 公司行为事件
|
||||
RecordCorporateActionEvents(
|
||||
td,
|
||||
curEodPosis,
|
||||
@@ -632,8 +659,9 @@ namespace YLErp.Modules.SwapModule
|
||||
registrationInfos,
|
||||
exDividendInfos,
|
||||
settleDate);
|
||||
// 登记日 EOD 仍保存除权前快照,但下一交易日开盘读取的实时浮动腿需要
|
||||
// 先切换到生效后的 Q/P。该更新基于当日 EOD 恢复后再套系数,重收盘不会重复放大。
|
||||
// 登记日 EOD 仍保存除权前快照,
|
||||
// 但下一交易日开盘读取的实时浮动腿需要先切换到生效后的 Q/P。
|
||||
// 该更新基于当日 EOD 恢复后再套系数,重收盘不会重复放大。
|
||||
UpdateRealtimeCorporateActionPositions(td, curEodPosis, registrationInfos, exDividendInfos, settleDate);
|
||||
var posiLongNotional = curEodPosis.Where(s => s.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue);
|
||||
var posiShortNotional = curEodPosis.Where(s => s.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue);
|
||||
@@ -668,8 +696,8 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <summary>
|
||||
/// 把上一实际 EOD 复制成“当日开盘基线”,并在需要时套用当日生效的 Stock/Fund 公司行为。
|
||||
/// 原始上一 EOD 只读保留在数据库中,确保登记日 EOD 报表仍展示除权前 Q/P。
|
||||
/// 例如 1000 份/100 元、10 送 10 的记录在 8 月 14 日 EOD 仍是 1000/100;
|
||||
/// 8 月 17 日处理当日流水前,内存基线先转为 2000/50,再平仓 300 份得到 1700/50。
|
||||
/// 例如 1000 份/100 元、10 送 10 的记录在 登记日 EOD 仍是 1000/100;
|
||||
/// 除权日处理当日流水前,内存基线先转为 2000/50,再平仓 300 份得到 1700/50。
|
||||
/// </summary>
|
||||
protected List<eod_swap_position> PrepareFundOpeningEodPositions(
|
||||
IReadOnlyCollection<eod_swap_position> previousEodPositions,
|
||||
@@ -729,6 +757,7 @@ namespace YLErp.Modules.SwapModule
|
||||
var dividendTaxRate = 0m;
|
||||
foreach (var position in positions)
|
||||
{
|
||||
// 不是浮动腿 或者 不是 Fund Stock类型的标的 或者 没有除权信息 或者 除权日不是结算日 - 跳过
|
||||
if (position.PosiDirection <= 0
|
||||
|| !IsTrsCorporateActionInstrument(position.UnderlyingInstrumentType)
|
||||
|| string.IsNullOrWhiteSpace(position.UnderlyingCode)
|
||||
@@ -739,7 +768,7 @@ namespace YLErp.Modules.SwapModule
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取除权参考价
|
||||
// 获取除权参考价 - 登记日收盘价
|
||||
var corporateActionClosePrice = GetFundCorporateActionClosePrice(
|
||||
dividendInfo,
|
||||
position.UnderlyingPrice);
|
||||
@@ -773,8 +802,11 @@ namespace YLErp.Modules.SwapModule
|
||||
position.PosiNetFeePrice = adjusted.NetFeePrice;
|
||||
position.PosiNetNoFeePrice = adjusted.NetNoFeePrice;
|
||||
|
||||
// 多空方向
|
||||
var shortRatio = DirectionRatio.LongShort(position.PositionType);
|
||||
// 收付方向
|
||||
var directionRatio = DirectionRatio.ReceivePay(position.PosiDirection);
|
||||
// 处理价格的正负号(收支方向)
|
||||
position.PosiNotionalValue = Math.Round(
|
||||
position.PosiGrossPrice * position.PosiQuantity * position.ContractSize,
|
||||
ConsGlobal.MoneyRound,
|
||||
@@ -900,8 +932,9 @@ namespace YLErp.Modules.SwapModule
|
||||
return;
|
||||
}
|
||||
|
||||
// 登记日收盘后即切换实时 BOD。EffectiveDate 只用于确认这条记录仍是未来生效的
|
||||
// 公司行为;无论登记日与生效日之间有一个还是多个非交易日,都不能漏掉这次切换。
|
||||
// 登记日收盘后即切换实时 BOD。
|
||||
// EffectiveDate 只用于确认这条记录仍是未来生效的公司行为;
|
||||
// 无论登记日与生效日之间有一个还是多个非交易日,都不能漏掉这次切换。
|
||||
var pendingInfos = (registrationInfos ?? Array.Empty<ex_dividend_info>())
|
||||
.Where(x => x.EffectiveDate.HasValue && x.EffectiveDate.Value.Date > settleDate.Date)
|
||||
.ToList();
|
||||
@@ -912,6 +945,7 @@ namespace YLErp.Modules.SwapModule
|
||||
&& IsTrsCorporateActionInstrument(x.UnderlyingInstrumentType)
|
||||
&& !string.IsNullOrWhiteSpace(x.UnderlyingCode)))
|
||||
{
|
||||
// 实时腿
|
||||
var realtime = DbContext.swap_position.FirstOrDefault(x => x.SwapTradeId == td.id
|
||||
&& !x.Invalid
|
||||
&& !x.IsInitial
|
||||
@@ -920,7 +954,8 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// 对每条当日 EOD 浮动腿,按标的代码在 pendingInfos 中找匹配的公司行为。
|
||||
var pending = pendingInfos.FirstOrDefault(x => string.Equals(
|
||||
x.UnderlyingCode, eod.UnderlyingCode, StringComparison.OrdinalIgnoreCase));
|
||||
if (pending != null)
|
||||
@@ -966,6 +1001,7 @@ namespace YLErp.Modules.SwapModule
|
||||
return;
|
||||
}
|
||||
|
||||
// 登记日信息合并除权日信息
|
||||
var infos = (registrationInfos ?? Array.Empty<ex_dividend_info>())
|
||||
.Concat(effectiveInfos ?? Array.Empty<ex_dividend_info>())
|
||||
.Where(x => x != null && x.ValidStatus && !string.IsNullOrWhiteSpace(x.UnderlyingCode))
|
||||
@@ -983,6 +1019,7 @@ namespace YLErp.Modules.SwapModule
|
||||
return;
|
||||
}
|
||||
|
||||
// 跟据交易id查当前交易关联事件
|
||||
var existingEvents = FindCorporateActionEvents(td.id);
|
||||
foreach (var current in currentPositions.Where(x => x != null && x.PosiDirection > 0
|
||||
&& IsTrsCorporateActionInstrument(x.UnderlyingInstrumentType)))
|
||||
@@ -1004,16 +1041,18 @@ namespace YLErp.Modules.SwapModule
|
||||
&& x.Data.ExDividendInfoId == info.id
|
||||
&& x.Data.PositionId == current.PositionId)
|
||||
.ToList();
|
||||
// 寻找applied = false的(登记日记录的)
|
||||
var eventData = matchingEvents.FirstOrDefault(x => !x.Data.Applied)
|
||||
?? matchingEvents.FirstOrDefault();
|
||||
var previous = previousPositions?.FirstOrDefault(x => x != null && x.PositionId == current.PositionId);
|
||||
var previous = previousPositions?.FirstOrDefault(x => x != null
|
||||
&& x.PositionId == current.PositionId);
|
||||
// 登记日 false 除权日 true
|
||||
var isEffective = info.EffectiveDate.HasValue
|
||||
&& info.EffectiveDate.Value.Date <= settleDate.Date
|
||||
&& effectiveInfos != null
|
||||
&& effectiveInfos.Any(x => x.id == info.id);
|
||||
|
||||
// 如果没有匹配到事件或事件未生效,则创建新事件。
|
||||
// 如果没有匹配到事件或今天不是除权日 但找到的事件的applied=true(异常事件/重收盘),则创建新事件。
|
||||
if (eventData == null || (!isEffective && eventData.Data.Applied))
|
||||
{
|
||||
// 创建新事件
|
||||
@@ -1184,6 +1223,7 @@ namespace YLErp.Modules.SwapModule
|
||||
decimal dividendTaxRate,
|
||||
int grossPriceRound)
|
||||
{
|
||||
// 计算除权系数 - adjustCashDividendPrice = false (现金分红模式)
|
||||
var factors = DividendService.CalculateCorporateActionFactors(
|
||||
dividendInfo,
|
||||
closePrice,
|
||||
@@ -1414,7 +1454,8 @@ namespace YLErp.Modules.SwapModule
|
||||
DateTime settleDate,
|
||||
trade td,
|
||||
DateTime preSettleDate,
|
||||
List<swap_flow_event> flowEvents)
|
||||
List<swap_flow_event> flowEvents,
|
||||
IReadOnlyCollection<eod_swap_position> corporateActionBeforePositions = null)
|
||||
{
|
||||
string settleDateStr = settleDate.ToString("yyyy-MM-dd");
|
||||
string preSettleDateStr = preSettleDate.ToString("yyyy-MM-dd");
|
||||
@@ -1436,18 +1477,20 @@ namespace YLErp.Modules.SwapModule
|
||||
var tdEodPosition = todyEodPositions.FirstOrDefault(x => x.PositionId == posi.id);//当前结算日日终持仓信息
|
||||
var unwindEvents = flowEvents.Where(x => x.PositionId == posi.id).ToList();//当前日平仓信息
|
||||
var realPosition = realPosiList.FirstOrDefault(s => s.PositionId == posi.id);
|
||||
var corporateActionBeforeQuantity = corporateActionBeforePositions?
|
||||
.FirstOrDefault(x => x.PositionId == posi.id)?.PosiQuantity;
|
||||
eod_swap_position eodPosi = new eod_swap_position();
|
||||
if (eodPosition == null)
|
||||
{
|
||||
eodPosi = SaveCurrentEodInitalPosi(posi, td, settleDate, preSettleDate, unwindEvents);
|
||||
eodPosi = SaveCurrentEodInitalPosi(posi, td, settleDate, preSettleDate, unwindEvents, corporateActionBeforeQuantity);
|
||||
}
|
||||
else if (unwindEvents.Count() == 0)
|
||||
{
|
||||
eodPosi = CopyEodPosition(eodPosition, tdEodPosition, td, settleDate, preSettleDate);
|
||||
eodPosi = CopyEodPosition(eodPosition, tdEodPosition, td, settleDate, preSettleDate, corporateActionBeforeQuantity);
|
||||
}
|
||||
else
|
||||
{
|
||||
eodPosi = UpdateEodPosition(posi, eodPosition, tdEodPosition, td, settleDate, preSettleDate, unwindEvents);
|
||||
eodPosi = UpdateEodPosition(posi, eodPosition, tdEodPosition, td, settleDate, preSettleDate, unwindEvents, corporateActionBeforeQuantity);
|
||||
}
|
||||
Log.Info($"eodPosi为:{JsonHelper.Serialize(eodPosi, false)}");
|
||||
list.Add(eodPosi);
|
||||
@@ -2634,7 +2677,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <param name="todayPositions">当日日终归档信息</param>
|
||||
/// <param name="swap_Deals">当日平仓/互换事件信息</param>
|
||||
/// <param name="td">交易信息</param>
|
||||
protected eod_swap_position CopyEodPosition(eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate)
|
||||
protected eod_swap_position CopyEodPosition(eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate, decimal? corporateActionBeforeQuantity = null)
|
||||
{
|
||||
if (curretEod == null)
|
||||
{
|
||||
@@ -2656,7 +2699,7 @@ namespace YLErp.Modules.SwapModule
|
||||
decimal tax = um.ValueAddedTax ?? 0;
|
||||
if (valueDate > td.StartDate.Value && curretEod.PosiQuantity > 0)
|
||||
{
|
||||
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio);
|
||||
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio, corporateActionBeforeQuantity);
|
||||
curretEod.TdPosiDividend = DividendCalc.AfterTax(payment, tax);
|
||||
}
|
||||
curretEod.PosiDividendSum = eod.PosiQuantity > 0 ? Math.Round(eod.PosiDividendSum + curretEod.TdPosiDividend, 2) : 0;
|
||||
@@ -2718,7 +2761,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <param name="curretEod"></param>
|
||||
/// <param name="td"></param>
|
||||
/// <param name="valueDate"></param>
|
||||
protected eod_swap_position UpdateEodPosition(swap_position swapPosition, eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate, List<swap_flow_event> unwindEvents)
|
||||
protected eod_swap_position UpdateEodPosition(swap_position swapPosition, eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate, List<swap_flow_event> unwindEvents, decimal? corporateActionBeforeQuantity = null)
|
||||
{
|
||||
if (curretEod == null)
|
||||
{
|
||||
@@ -2752,7 +2795,7 @@ namespace YLErp.Modules.SwapModule
|
||||
// 修改,互换事件会影响待实现的分红的,现在要算上
|
||||
if (valueDate > td.StartDate.Value && (curretEod.PosiQuantity > 0))
|
||||
{
|
||||
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio);
|
||||
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio, corporateActionBeforeQuantity);
|
||||
curretEod.TdPosiDividend = DividendCalc.AfterTax(payment, tax);
|
||||
}
|
||||
curretEod.RealizedMtmPnL = eod.RealizedMtmPnL + curretEod.TdCloseMtmPnl;
|
||||
@@ -2879,7 +2922,8 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <param name="position"></param>
|
||||
/// <param name="td"></param>
|
||||
/// <param name="settleDate"></param>
|
||||
protected eod_swap_position SaveCurrentEodInitalPosi(swap_position position, trade td, DateTime settleDate, DateTime preSettleDate, List<swap_flow_event> unwindEvents)
|
||||
protected eod_swap_position SaveCurrentEodInitalPosi(swap_position position, trade td, DateTime settleDate,
|
||||
DateTime preSettleDate, List<swap_flow_event> unwindEvents, decimal? corporateActionBeforeQuantity = null)
|
||||
{
|
||||
eod_swap_position curretEod = new eod_swap_position();
|
||||
var um = GetUnderlyingData(position.UnderlyingCode);
|
||||
@@ -2932,7 +2976,7 @@ namespace YLErp.Modules.SwapModule
|
||||
if (!hasSwapEvent && settleDate > td.StartDate.Value && curretEod.PosiQuantity > 0)
|
||||
{
|
||||
decimal tax = um.ValueAddedTax ?? 0;
|
||||
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, td.StartDate.Value, settleDate, curretEod.PosiQuantity, shortRatio, directionRatio);
|
||||
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, td.StartDate.Value, settleDate, curretEod.PosiQuantity, shortRatio, directionRatio, corporateActionBeforeQuantity);
|
||||
payment = DividendCalc.AfterTax(payment, tax);
|
||||
//var consumedDividend = CalcConsumedDividend(curretEod, unwindEvents); 首日应该没有分红
|
||||
curretEod.TdPosiDividend = payment;
|
||||
@@ -3370,6 +3414,91 @@ namespace YLErp.Modules.SwapModule
|
||||
return retListResult;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询 EQD-7084 新“框架合约”字段。
|
||||
/// 旧查询负责筛选、排序、分页及旧字段计算;新字段只基于当前页对应的日终腿补充计算,
|
||||
/// 避免改变旧接口的返回口径。
|
||||
/// </summary>
|
||||
public SearchListResult<EodSwapRiskNewResponse> SearchEodSwapNewList(EodSwapQueryRequest req)
|
||||
{
|
||||
// 新 Tab 与旧 Tab 共享同一套权限、筛选、排序和分页边界;先复用旧查询,
|
||||
// 再只替换需求明确调整的展示字段,避免新接口悄然改变旧口径或查询范围。
|
||||
var oldResult = SearchEodSwapList(req);
|
||||
var oldRows = oldResult.rows?.ToList() ?? new List<EodSwapResponse>();
|
||||
var tradeIds = oldRows.Select(x => x.position.SwapTradeId).Distinct().ToList();
|
||||
var valueDates = oldRows.Select(x => x.position.ValueDate).Distinct().ToList();
|
||||
|
||||
if (tradeIds.Count == 0)
|
||||
{
|
||||
return new SearchListResult<EodSwapRiskNewResponse>(oldResult,
|
||||
Enumerable.Empty<EodSwapRiskNewResponse>());
|
||||
}
|
||||
|
||||
// 当前页的交易、日终明细和扩展信息各批量读取一次,随后在内存按“交易 + 日终日”配对。
|
||||
// 不在 rows.Select 内查询数据库,避免分页结果产生 N+1 查询。
|
||||
var trades = DbContext.trade
|
||||
.Where(x => tradeIds.Contains(x.id))
|
||||
.Select(x => new { x.id, x.StartDate, x.ExerciseDate })
|
||||
.ToDictionary(x => x.id);
|
||||
var eodPositionDetails = DbContext.eod_swap_position
|
||||
.Where(x => tradeIds.Contains(x.SwapTradeId)
|
||||
&& valueDates.Contains(x.ValueDate)
|
||||
&& !x.Invalid)
|
||||
.ToList();
|
||||
var tradeExtends = DbContext.trade_extend
|
||||
.Where(x => tradeIds.Contains(x.TradeId))
|
||||
.ToList();
|
||||
|
||||
var rows = oldRows.Select(item =>
|
||||
{
|
||||
// 同一交易可出现在多个日终日;必须同时匹配 ValueDate,不能把其他日期的腿混入本行。
|
||||
var details = eodPositionDetails
|
||||
.Where(x => x.SwapTradeId == item.position.SwapTradeId
|
||||
&& x.ValueDate == item.position.ValueDate)
|
||||
.ToList();
|
||||
var floatingLegs = details.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList();
|
||||
var interestLegs = details.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)).ToList();
|
||||
var tradeExtend = tradeExtends.FirstOrDefault(x => x.TradeId == item.position.SwapTradeId);
|
||||
// 缺少扩展信息时按“期间支付”处理,和旧接口的默认值保持一致。
|
||||
var dividendPayDate = tradeExtend?.ExtendObj?.DividendPayDate ?? 1;
|
||||
trades.TryGetValue(item.position.SwapTradeId, out var tradeInfo);
|
||||
|
||||
return new EodSwapRiskNewResponse
|
||||
{
|
||||
position = item.position,
|
||||
TradeDate = item.TradeDate,
|
||||
SwapTradeNo = item.SwapTradeNo,
|
||||
ClientName = item.ClientName,
|
||||
StructureType = item.StructureType,
|
||||
AssetBookName = item.AssetBookName,
|
||||
ClientId = item.ClientId,
|
||||
SwapTradeTypeStr = item.SwapTradeTypeStr,
|
||||
UnderlyingType = item.UnderlyingType,
|
||||
PeriodAmount = item.PeriodAmount,
|
||||
FloatingUnrealizedPnl = item.FloatingUnrealizedPnl,
|
||||
InterestPaymentMethod = item.InterestPaymentMethod,
|
||||
MaturityNettingValuation = item.MaturityNettingValuation,
|
||||
PeriodPaymentValuation = item.PeriodPaymentValuation,
|
||||
MarginInterestGain = item.MarginInterestGain,
|
||||
MarginInterestLoss = item.MarginInterestLoss,
|
||||
// 所有 EQD-7084 差异集中在 NewFields;上方复制的旧字段用于保留原报表的
|
||||
// 基本信息、DV、期间金额及已实现收益,前端再将六个差异列绑定到 NewFields。
|
||||
NewFields = CalculateEodSwapRiskNewFields(
|
||||
floatingLegs,
|
||||
interestLegs,
|
||||
item.StructureType,
|
||||
item.position.NotionalValue,
|
||||
tradeInfo?.StartDate,
|
||||
tradeInfo?.ExerciseDate,
|
||||
item.PeriodAmount,
|
||||
dividendPayDate)
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
return new SearchListResult<EodSwapRiskNewResponse>(oldResult, rows);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取互换交易日终持仓数据
|
||||
/// </summary>
|
||||
@@ -3448,6 +3577,29 @@ namespace YLErp.Modules.SwapModule
|
||||
return retListResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算 EQD-7084 新“框架合约”Tab 的字段口径。
|
||||
/// 纯函数只依赖日终浮动腿、利息腿和交易级展示参数,供查询接口及无库单测共用。
|
||||
/// </summary>
|
||||
public static EodSwapRiskNewFields CalculateEodSwapRiskNewFields(
|
||||
IEnumerable<eod_swap_position> floatingLegs,
|
||||
IEnumerable<eod_swap_position> interestLegs,
|
||||
string structureType,
|
||||
decimal notionalValue,
|
||||
DateTime? startDate,
|
||||
DateTime? ExerciseDate,
|
||||
decimal periodAmount,
|
||||
int dividendPayDate)
|
||||
=> EodPnlCalculator.CalculateEodSwapRiskNewFields(
|
||||
floatingLegs,
|
||||
interestLegs,
|
||||
structureType,
|
||||
notionalValue,
|
||||
startDate,
|
||||
ExerciseDate,
|
||||
periodAmount,
|
||||
dividendPayDate);
|
||||
|
||||
/// <summary>
|
||||
/// 互换持仓明细查询
|
||||
/// </summary>
|
||||
@@ -3590,7 +3742,7 @@ namespace YLErp.Modules.SwapModule
|
||||
else if (isEtf)
|
||||
{
|
||||
item.PeriodAmount = null;
|
||||
item.DividendAmount = pendingDividend;
|
||||
item.DividendAmount = -pendingDividend; // 每日估值报告是客户视角 取值与日终持仓风险相反
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -3634,9 +3786,10 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
item.FloatRateAbs = item.position.PosiNotionalValue == 0 ? 0 : item.InterestAmount / item.position.PosiNotionalValue;
|
||||
}
|
||||
// 交易录入的债券类收益互换价格以小数保存,展示时转为百分比价格;
|
||||
// 普通收益互换录入的是数量/原始数值,不做乘 100 转换。
|
||||
SetPosiPrice(item.position, item.StructureType == "普通债券类收益互换");
|
||||
// 是否 ×100 由标的资产类型决定(债券价格以小数保存,展示时转为百分比价格),
|
||||
// 与存储层 GetStorageDeliveryPriceRound / GetSwapValuationPrice 的 IsBond 口径一致,
|
||||
// 不依赖簿记结构类型 StructureType。
|
||||
SetPosiPrice(item.position);
|
||||
}
|
||||
return retListResult;
|
||||
}
|
||||
@@ -3681,10 +3834,10 @@ namespace YLErp.Modules.SwapModule
|
||||
position.SwapPositionValue = -position.SwapPositionValue;
|
||||
position.PosiDividendSum = -position.PosiDividendSum;
|
||||
}
|
||||
private void SetPosiPrice(eod_swap_position position, bool? useBondPriceScale = null)
|
||||
private void SetPosiPrice(eod_swap_position position)
|
||||
{
|
||||
var um = DataCacheProvider.GetUnderlyingDataSource().GetData(position.UnderlyingCode);
|
||||
if (useBondPriceScale ?? (um != null && um.IsBond()))
|
||||
if (um != null && um.IsBond())
|
||||
{
|
||||
position.PosiNetPrice *= 100;
|
||||
position.UnderlyingPrice *= 100;
|
||||
|
||||
@@ -207,8 +207,8 @@ namespace YLErp.Modules.SwapModule
|
||||
return events;
|
||||
}
|
||||
/// <summary>
|
||||
/// 获取交易操作历史。登记日创建但尚未到 EffectiveDate 的公司行为事件也保留,
|
||||
/// 由 EventData.Applied=false 表示“待生效”,保证审计日志完整可追溯。
|
||||
/// 获取交易操作历史。登记日创建的待生效公司行为仍保留在审计数据中,
|
||||
/// 但在 EffectiveDate 将其更新为 Applied=true 前不对操作历史展示。
|
||||
/// </summary>
|
||||
/// <param name="tradeId">交易id</param>
|
||||
/// <returns></returns>
|
||||
@@ -218,7 +218,30 @@ namespace YLErp.Modules.SwapModule
|
||||
.Where(x => x.SwapTradeId == tradeId)
|
||||
.OrderByDescending(o => o.id)
|
||||
.ToList();
|
||||
return list;
|
||||
return FilterOperationHistory(list);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 过滤尚未生效的公司行为事件。非公司行为、已生效事件和无法识别的历史事件均保留,
|
||||
/// 避免过滤条件误伤既有操作记录。
|
||||
/// </summary>
|
||||
private static List<swap_event> FilterOperationHistory(IEnumerable<swap_event> events)
|
||||
{
|
||||
if (events == null)
|
||||
{
|
||||
return new List<swap_event>();
|
||||
}
|
||||
|
||||
return events
|
||||
.Where(x => !IsPendingCorporateActionEvent(x))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static bool IsPendingCorporateActionEvent(swap_event swapEvent)
|
||||
{
|
||||
return swapEvent?.EventType == (int)SwapEventTypeEnum.公司行为
|
||||
&& TryDeserializeCorporateActionEventData(swapEvent, out var data)
|
||||
&& !data.Applied;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -247,8 +270,8 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 公司行为说明使用稳定的键值格式,完整保留调整前后名义本金、价格、数量、
|
||||
/// 待实现分红和现金流变化,操作历史无需重新计算即可核对。
|
||||
/// 公司行为说明仅展示调整前后的名义本金、期初标的价格和持仓数量,
|
||||
/// 便于操作历史直接比对持仓基线。
|
||||
/// </summary>
|
||||
public static string BuildCorporateActionEventReason(CorporateActionEventData data)
|
||||
{
|
||||
@@ -257,37 +280,38 @@ namespace YLErp.Modules.SwapModule
|
||||
return "公司行为快照为空";
|
||||
}
|
||||
|
||||
// 使用 InvariantCulture 固定小数与日期格式,说明文本不随服务器区域设置变化。
|
||||
// 使用 InvariantCulture 固定小数和日期格式,说明文本不随服务器区域设置变化。
|
||||
string D(decimal value) => value.ToString(CultureInfo.InvariantCulture);
|
||||
string Date(DateTime? value) => value.HasValue
|
||||
? value.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)
|
||||
: "";
|
||||
|
||||
return string.Join("; ", new[]
|
||||
string ActionDescription()
|
||||
{
|
||||
$"公司行为[{data.UnderlyingCode}]",
|
||||
$"ExDividendDate={Date(data.ExDividendDate)}",
|
||||
$"EffectiveDate={Date(data.EffectiveDate)}",
|
||||
$"ExDividendInfoId={data.ExDividendInfoId}",
|
||||
$"PositionId={data.PositionId}",
|
||||
$"GiveCashAmount={D(data.GiveCashAmount)}",
|
||||
$"GiveShareAmount={D(data.GiveShareAmount)}",
|
||||
$"Split={(data.Split.HasValue ? D(data.Split.Value) : "")}",
|
||||
$"RationedSharesAmount={D(data.RationedSharesAmount)}",
|
||||
$"RationedSharesPrice={D(data.RationedSharesPrice)}",
|
||||
"调整前",
|
||||
$"BeforeNotional={D(data.BeforeNotional)}",
|
||||
$"BeforePrice={D(data.BeforePrice)}",
|
||||
$"BeforeQuantity={D(data.BeforeQuantity)}",
|
||||
$"BeforePendingDividend={D(data.BeforePendingDividend)}",
|
||||
"调整后",
|
||||
$"AfterNotional={D(data.AfterNotional)}",
|
||||
$"AfterPrice={D(data.AfterPrice)}",
|
||||
$"AfterQuantity={D(data.AfterQuantity)}",
|
||||
$"AfterPendingDividend={D(data.AfterPendingDividend)}",
|
||||
$"CashFlowChange={D(data.CashFlowChange)}",
|
||||
$"Applied={data.Applied}"
|
||||
});
|
||||
if (data.RationedSharesAmount != 0m)
|
||||
{
|
||||
return "配股";
|
||||
}
|
||||
if (data.GiveShareAmount != 0m)
|
||||
{
|
||||
return "送股";
|
||||
}
|
||||
if (data.Split.HasValue && data.Split.Value != 1m)
|
||||
{
|
||||
return "拆分";
|
||||
}
|
||||
if (data.GiveCashAmount != 0m)
|
||||
{
|
||||
// return $"产生分红:{D(data.CashFlowChange)}";
|
||||
return $"产生分红";
|
||||
}
|
||||
return "公司行为";
|
||||
}
|
||||
|
||||
return $"股权登记日:{Date(data.ExDividendDate)} 发生公司行为({ActionDescription()})"
|
||||
+ Environment.NewLine
|
||||
+ $"调整前:名义本金:{D(data.BeforeNotional)} 期初标的价格:{D(data.BeforePrice)} 持仓数量:{D(data.BeforeQuantity)}"
|
||||
+ Environment.NewLine
|
||||
+ $"调整后:名义本金:{D(data.AfterNotional)} 期初标的价格:{D(data.AfterPrice)} 持仓数量:{D(data.AfterQuantity)}";
|
||||
}
|
||||
|
||||
public void DeleteEvent(int tradeId)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -773,9 +773,9 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
bool adjustCashDividendPrice = true)
|
||||
{
|
||||
// 价格调整模式除权参考价 =
|
||||
// 收盘价 * 10 - 【每股派息 * 10 * (1-分红税率)】 + 配股数 * 配股价
|
||||
// - -----------------------------------------------------
|
||||
// (10 + 送股数 + 配股数) * 拆股倍数
|
||||
// 登记日收盘价 * 10 - 【每股派息 * 10 * (1-分红税率)】 + 配股数 * 配股价
|
||||
// ---------------------------------------------------------------
|
||||
// (10 + 送股数 + 配股数) * 拆股倍数
|
||||
// 场内链路默认继续把现金派息计入除权参考价;
|
||||
// TRS Stock/Fund 现金模式显式关闭该项 :“【】” 号内数据。
|
||||
var cashPriceAdjustment = adjustCashDividendPrice
|
||||
@@ -784,8 +784,8 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
// 拆股倍数
|
||||
var splitFactor = GetSplitFactor(info);
|
||||
// 除权参考价(TRS) :
|
||||
// 收盘价 * 10 + 配股数 * 配股价
|
||||
// ------------------------------
|
||||
// 登记日收盘价 * 10 + 配股数 * 配股价
|
||||
// -------------------------------------
|
||||
// (10 + 送股数 + 配股数) * 拆股倍数
|
||||
var exDividendPrice = ((closePrice * 10m - cashPriceAdjustment
|
||||
+ info.RationedSharesAmount * info.RationedSharesPrice)
|
||||
@@ -1159,9 +1159,10 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
}
|
||||
else
|
||||
{
|
||||
if (checkDividendInfoExecuteStatus(dividend))
|
||||
var executingTradeNumber = GetDividendInfoExecutingTradeNumber(dividend);
|
||||
if (!string.IsNullOrWhiteSpace(executingTradeNumber))
|
||||
{
|
||||
errMsg = $"{dividend.UnderlyingCode} {dividend.ExDividendDate?.ToString("yyyy-MM-dd")}除权信息保存失败,该信息已被执行,不允许修改!";
|
||||
errMsg = $"不可修改,有交易【{executingTradeNumber}】使用了该条除权除息数据";
|
||||
return false;
|
||||
}
|
||||
var conflictingDividend = FindExDividendByBusinessKey(underlying.id, itemDate, dividend.id);
|
||||
@@ -1223,6 +1224,14 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
/// <param name="info"></param>
|
||||
/// <returns></returns>
|
||||
public bool checkDividendInfoExecuteStatus(ex_dividend_info info)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(GetDividendInfoExecutingTradeNumber(info));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回仍在引用已执行公司行为的交易编号;无引用时返回空字符串。
|
||||
/// </summary>
|
||||
public string GetDividendInfoExecutingTradeNumber(ex_dividend_info info)
|
||||
{
|
||||
// TRS 公司行为以 EffectiveDate 为真正生效边界。登记日创建待生效事件不应锁定
|
||||
// 维护;只有交易已经完成 EffectiveDate(例如收盘到 7 月 30 日,而真实除权日为
|
||||
@@ -1230,34 +1239,37 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
if (info?.EffectiveDate.HasValue == true)
|
||||
{
|
||||
var effectiveDate = info.EffectiveDate.Value.Date;
|
||||
var trsTradeIds = DbContext.trade
|
||||
var trsTrades = DbContext.trade
|
||||
.Where(x => x.ValidState != ConsGlobal.InValid
|
||||
&& x.TradeType == "收益互换"
|
||||
&& x.UnderlyingCode == info.UnderlyingCode
|
||||
&& x.TradeDate <= effectiveDate
|
||||
&& x.ExerciseDate >= effectiveDate)
|
||||
.Select(x => x.id)
|
||||
.Select(x => new { x.id, x.TradeNumber })
|
||||
.ToList();
|
||||
if (trsTradeIds.Count > 0)
|
||||
if (trsTrades.Count > 0)
|
||||
{
|
||||
// 是否仍被交易引用以当前有效 EOD 为准。公司行为事件本身是不可篡改
|
||||
// 历史,交易回退后仍会保留;若仅凭 Applied 事件锁定,回退到登记日前
|
||||
// 也无法纠错。生效日及以后还有有效 EOD 才表示当前仍已执行。
|
||||
var hasAppliedEod = DbContext.eod_swap_position.Any(x =>
|
||||
var trsTradeIds = trsTrades.Select(x => x.id).ToList();
|
||||
var appliedTradeId = DbContext.eod_swap_position.Where(x =>
|
||||
trsTradeIds.Contains(x.SwapTradeId)
|
||||
&& !x.Invalid
|
||||
&& x.UnderlyingCode == info.UnderlyingCode
|
||||
&& x.ValueDate >= effectiveDate);
|
||||
if (hasAppliedEod)
|
||||
&& x.ValueDate >= effectiveDate)
|
||||
.Select(x => x.SwapTradeId)
|
||||
.FirstOrDefault();
|
||||
if (appliedTradeId > 0)
|
||||
{
|
||||
return true;
|
||||
return trsTrades.First(x => x.id == appliedTradeId).TradeNumber;
|
||||
}
|
||||
|
||||
// EffectiveDate 已存在时,当前有效 EOD 是唯一执行状态来源。
|
||||
// 回退会清理生效日及之后的 EOD,但不会删除 eodStatus 或不可篡改的
|
||||
// 公司行为审计事件;此处不能继续落入旧的登记日 eodStatus 判断,
|
||||
// 否则交易已回退仍会被错误判定为“已执行”而无法修改。
|
||||
return false;
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1268,20 +1280,22 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
var tradeQuery = from t in DbContext.trade.Where(O => O.UnderlyingCode == info.UnderlyingCode && O.TradeDate <= info.ExDividendDate && O.ExerciseDate >= info.ExDividendDate && O.DividendDate >= O.TradeDate)
|
||||
join et in DbContext.eod_trade.Where(O => ConsTrade.LiveTradeStatusList.Contains(O.TradeStatus))
|
||||
on new { t.id, ValueDate = t.TradeDate.Value } equals new { id = et.TradeId, et.ValueDate }
|
||||
select et.id;
|
||||
if (tradeQuery.Any())
|
||||
select t.TradeNumber;
|
||||
var executingTradeNumber = tradeQuery.FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(executingTradeNumber))
|
||||
{
|
||||
return true;
|
||||
return executingTradeNumber;
|
||||
}
|
||||
//查询篮子标的对应交易是否执行过收盘操作;
|
||||
var umList = DataCacheProvider.GetUnderlyingDataSource().AsQueryable(O => O.CommodityCode == "篮子标的" && O.SubData != null && O.SubData.Contains(info.UnderlyingCode)).Select(O => O.UnderlyingCode).ToArray();
|
||||
tradeQuery = from t in DbContext.trade.Where(O => umList.Contains(O.UnderlyingCode) && O.TradeDate <= info.ExDividendDate && O.ExerciseDate >= info.ExDividendDate && O.DividendDate >= O.TradeDate)
|
||||
join et in DbContext.eod_trade.Where(O => ConsTrade.LiveTradeStatusList.Contains(O.TradeStatus))
|
||||
on new { t.id, ValueDate = t.TradeDate.Value } equals new { id = et.TradeId, et.ValueDate }
|
||||
select et.id;
|
||||
if (tradeQuery.Any())
|
||||
select t.TradeNumber;
|
||||
executingTradeNumber = tradeQuery.FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(executingTradeNumber))
|
||||
{
|
||||
return true;
|
||||
return executingTradeNumber;
|
||||
}
|
||||
//查询多标的对应交易是否执行过收盘操作;
|
||||
tradeQuery = from ts in DbContext.trade_swap_detail.Where(O => O.UnderlyingCode == info.UnderlyingCode)
|
||||
@@ -1289,13 +1303,14 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
on ts.TradeId equals t.id
|
||||
join et in DbContext.eod_trade.Where(O => ConsTrade.LiveTradeStatusList.Contains(O.TradeStatus))
|
||||
on new { t.id, ValueDate = t.TradeDate.Value } equals new { id = et.TradeId, et.ValueDate }
|
||||
select et.id;
|
||||
if (tradeQuery.Any())
|
||||
select t.TradeNumber;
|
||||
executingTradeNumber = tradeQuery.FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(executingTradeNumber))
|
||||
{
|
||||
return true;
|
||||
return executingTradeNumber;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public List<DividendTrade> QueryDividendTrade(DividendTradeReq req)
|
||||
|
||||
@@ -378,10 +378,11 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
}
|
||||
var cashService = new ClientCashInCashOutService(this);
|
||||
cashService.SaveSwapTradeClientCash(td, td.TradePrice ?? 0, happenDate,0);
|
||||
// R4 授信/现金标签:预付金腿定稿资金标签(选授信按剩余授信分配,不足跨界腿拆单),
|
||||
// R4 授信/现金标签:预付金腿定稿资金标签(按授信分配的腿——腿选授信或未选回退交易级资金来源——
|
||||
// 按剩余授信分配,不足跨界腿拆单为 原腿授信+新现金腿),
|
||||
// 授信部分不产生资金流水(只写授信出入表占用),现金部分产生 应付预付金 记录;特批全现金。
|
||||
// 必须在 InitialPosition/AddPositionEvent 之前执行:实时持仓克隆与初始事件要继承"定稿后"的标签,
|
||||
// 拆单新拆出的授信腿也要被克隆、建事件(否则平仓返还分流会查到克隆腿上的旧标签/漏腿)。
|
||||
// 拆单新拆出的现金腿也要被克隆、建事件(否则平仓返还分流会查到克隆腿上的旧标签/漏腿)。
|
||||
var generateMarginLegs = new List<swap_position>();
|
||||
foreach (var marginPosition in td.swap_positions)
|
||||
{
|
||||
|
||||
@@ -25,6 +25,11 @@ namespace YLErp.BLL
|
||||
/// </summary>
|
||||
public const string LackOfMoney = "LackOfMoney";
|
||||
|
||||
/// <summary>
|
||||
/// 保存交易时预付金授信不足:UI 确认后按 剩余授信+现金差额 拆分预付金腿再保存(§2.3 保存前拆单)
|
||||
/// </summary>
|
||||
public const string MarginCreditSplit = "MarginCreditSplit";
|
||||
|
||||
public const string RiskWarningConfirm = "RiskWarningConfirm";
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -104,6 +104,8 @@ namespace YLErp.Web.Controllers
|
||||
TraderId = CurUser.UserId,
|
||||
TraderName = CurUser.UserName,
|
||||
MarginTemplateName = defaultMarginTemplateName,
|
||||
//资金来源必填(现金/授信),新交易默认现金
|
||||
MarginFundSource = ConsFundTag.Cash,
|
||||
OpponentRole = "乙方",
|
||||
OriginalStockEqvNotional = 0,
|
||||
StructureType = "普通债券类收益互换",
|
||||
@@ -492,18 +494,29 @@ namespace YLErp.Web.Controllers
|
||||
req.id = DecryptInt(req.EncryptId);
|
||||
}
|
||||
//特批放行判定与确认/审批环节同口径(processtradelogController/ApprovalService):
|
||||
//系统参数 允许交易特批(SpecialOperateForTrade) 开启 且 显式带 LackOfMoney 标记重提
|
||||
var ignoreMoneyCheck = valuedateBLL.SystemDate.SpecialOperateForTrade == 1 && additionalProcessing == tradeBLL.LackOfMoney;
|
||||
//系统参数 允许交易特批(SpecialOperateForTrade) 开启 且 显式带 LackOfMoney 标记重提。
|
||||
//additionalProcessing 支持逗号分隔多标记(保存前授信拆单与资金特批可链式确认):
|
||||
//MarginCreditSplit=预付金授信不足拆单确认(标准流程,不受特批开关控制)
|
||||
var processings = (additionalProcessing ?? "").Split(',', StringSplitOptions.RemoveEmptyEntries).ToHashSet();
|
||||
var ignoreMoneyCheck = valuedateBLL.SystemDate.SpecialOperateForTrade == 1 && processings.Contains(tradeBLL.LackOfMoney);
|
||||
var allowMarginCreditSplit = processings.Contains(tradeBLL.MarginCreditSplit);
|
||||
try
|
||||
{
|
||||
bool edit = req.id != 0;
|
||||
var r= swapTradeService.SaveTrade(req, ignoreMoneyCheck);
|
||||
var r= swapTradeService.SaveTrade(req, ignoreMoneyCheck, allowMarginCreditSplit);
|
||||
Task.Run(() =>
|
||||
{
|
||||
RealtimePnlCalc.RealtimeSwapPosition(new OptUserInfo(0, "互换实时持仓服务", OptUserFrom.Service));
|
||||
});
|
||||
return JsonSuccess("更新成功", r);
|
||||
}
|
||||
catch (TradeMarginCreditSplitException e)
|
||||
{
|
||||
//保存前授信拆单(§2.3):预付金授信不足,UI 确认后带 additionalProcessing=MarginCreditSplit
|
||||
//重提,按 剩余授信+现金差额 物理拆腿后保存
|
||||
LogFactory.GetLogger("交易保存").Info("保存授信不足待拆单确认:" + e.Message);
|
||||
return JsonSuccessData(new { proccessType = "AdditionalProcessing", type = tradeBLL.MarginCreditSplit, message = e.Message });
|
||||
}
|
||||
catch (TradeLackOfMoneyException e)
|
||||
{
|
||||
//保存环节资金不足:开关开启时按确认/审批同一协议返回 AdditionalProcessing/LackOfMoney,
|
||||
@@ -965,6 +978,23 @@ namespace YLErp.Web.Controllers
|
||||
var retListResult = service.SearchEodSwapList(req);
|
||||
return Json(retListResult);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 日终持仓-互换新框架合约查询。
|
||||
/// 先与旧框架合约接口执行相同的账簿、资产单元和客户权限收敛,
|
||||
/// 再返回 EQD-7084 拆分后的展示字段;不能直接绕过这些条件调用服务层。
|
||||
/// </summary>
|
||||
/// <param name="req"></param>
|
||||
/// <returns></returns>
|
||||
public JsonResult EodSwapRiskNewQuery(EodSwapQueryRequest req)
|
||||
{
|
||||
req.BookIds = AssetUnitModel.IntersectAssetUnits(req.AssetIdGroupList, req.BookIds).ToList();
|
||||
req.UserAssets = CurUser.GetAssetUnitIds();
|
||||
req.UserClients = CurUser.GetClientIdsByCurUser();
|
||||
var service = new SwapEodPositionService(CurUser);
|
||||
var retListResult = service.SearchEodSwapNewList(req);
|
||||
return Json(retListResult);
|
||||
}
|
||||
#endregion
|
||||
#region 结算报告
|
||||
/// <summary>
|
||||
|
||||
@@ -105,9 +105,11 @@ namespace YLErp.Web.Controllers
|
||||
{
|
||||
return JsonError("未找到有效的除权除息信息");
|
||||
}
|
||||
if (new DividendService(CurUser).checkDividendInfoExecuteStatus(r))
|
||||
var dividendService = new DividendService(CurUser);
|
||||
var executingTradeNumber = dividendService.GetDividendInfoExecutingTradeNumber(r);
|
||||
if (!string.IsNullOrWhiteSpace(executingTradeNumber))
|
||||
{
|
||||
return JsonError("该条除权信息已被执行,不允许删除!");
|
||||
return JsonError($"不可修改,有交易【{executingTradeNumber}】使用了该条除权除息数据");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -46,7 +46,12 @@
|
||||
<a href="/swaptrade2/EodPositionRisks?index=1">日终持仓</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/swaptrade2/EodPositionRisks?index=2">框架合约</a>
|
||||
@* index=2 固定保留历史报表与导出配置,供新旧口径并行核对。 *@
|
||||
<a href="/swaptrade2/EodPositionRisks?index=2">框架合约(旧口径)</a>
|
||||
</li>
|
||||
<li>
|
||||
@* index=3 才使用 EQD-7084 新查询与拆分字段,不能复用旧 Tab 的列配置。 *@
|
||||
<a href="/swaptrade2/EodPositionRisks?index=3">框架合约</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -102,6 +102,10 @@
|
||||
page.Trade.StartDate = page.Trade.StartDate ? page.Trade.StartDate.substr(0, 10) : "";
|
||||
page.Trade.TradeDate = page.Trade.TradeDate ? page.Trade.TradeDate.substr(0, 10) : "";
|
||||
page.Trade.ExerciseDate = page.Trade.ExerciseDate ? page.Trade.ExerciseDate.substr(0, 10) : "";
|
||||
//资金来源必填(现金/授信),存量空值按默认现金归一,避免下拉空值匹配不到选项
|
||||
if (!page.Trade.MarginFundSource) {
|
||||
page.Trade.MarginFundSource = "Cash";
|
||||
}
|
||||
</script>
|
||||
<script src="@HtmlUtil.BasicDataJs("品种","客户","簿记","交易员")"></script>
|
||||
<script src="~/front/calendar?v=@(HtmlUtil.JsVersion)"></script>
|
||||
@@ -289,12 +293,19 @@
|
||||
<div class="form-group">
|
||||
<label class="formlabel half">保证金模板</label>
|
||||
<select v-model="trade.MarginTemplateName">
|
||||
<option value="">请选择</option>
|
||||
<option value="">默认</option>
|
||||
<option v-for="item in page.swapMarginTemplateItems" :key="item.Value" :value="item.Value">
|
||||
{{ item.Text }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="formlabel half">资金来源</label>
|
||||
<select v-model="trade.MarginFundSource" title="必填,默认现金。预付金腿未选资金标签(默认)时按此定稿:授信=优先占用授信额度(不足自动拆分为授信+现金两条);现金=现金">
|
||||
<option value="Cash">现金</option>
|
||||
<option value="Credit">授信</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
@@ -332,8 +343,8 @@
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select v-model="item.FundTag" style="width:86px;" title="资金标签:确认成交时按此选择定稿——授信检查剩余额度,不足自动拆分为授信+现金两条;未选默认现金">
|
||||
<option value="">默认(现金)</option>
|
||||
<select v-model="item.FundTag" style="width:86px;" title="资金标签:确认成交时按此定稿——授信检查剩余额度,不足自动拆分为授信+现金两条;默认=取交易上的资金来源">
|
||||
<option value="">默认</option>
|
||||
<option value="Cash">现金</option>
|
||||
<option value="Credit">授信</option>
|
||||
</select>
|
||||
|
||||
@@ -253,7 +253,11 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>保证金模板</td>
|
||||
<td class="color-bule">@trade.MarginTemplateName</td>
|
||||
<td class="color-bule">@(string.IsNullOrWhiteSpace(trade.MarginTemplateName) ? "默认" : trade.MarginTemplateName)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>资金来源</td>
|
||||
<td class="color-bule">@(trade.MarginFundSource == ConsFundTag.Credit ? "授信" : "现金")</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -288,8 +292,8 @@
|
||||
<td class="@bgclass" style="width:116px !important;">@((SwapDirectionEnum)item.InterestDirection)</td>
|
||||
<td>@((InterestModeEnum)item.InterestMode)</td>
|
||||
<td>
|
||||
@*R4 资金标签(fund_tag 单列):录入时为用户选择,确认成交后为系统定稿(授信/现金)*@
|
||||
@(item.FundTag == ConsFundTag.Credit ? "授信" : "现金")
|
||||
@*R4 资金标签(fund_tag 单列):录入时为用户选择(默认=取交易上的资金来源),确认成交后为系统定稿(授信/现金)*@
|
||||
@(item.FundTag == ConsFundTag.Credit ? "授信" : item.FundTag == ConsFundTag.Cash ? "现金" : "默认")
|
||||
</td>
|
||||
<td>@item.HappenDate.OtcFormatDate()</td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(item.InterestPrincipalFix)" data-kind="amount"></span></td>
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"CompressionType": 0, // 消息压缩方式,可以是 None(0)、Gzip(1)、Snappy(2)、Lz4(3)、Zstd(4) 中的一种,
|
||||
"MessageTimeoutMs": 3000, // 控制生产者等待消息确认的时间,单位是毫秒
|
||||
"ClientRateTopic": "ylClientRateTopic", //客户互换费率生产topic
|
||||
"ContractTopic": "onederiv.trs.contract.v1", //TRS合约日终推送topic
|
||||
"HedgingAccountTopic": "ylHedgingAccountTopic", //对冲账户生产topic
|
||||
"ReqAccountCapitalTopic": "ReqAccountCapital", //账户资金请求topic
|
||||
"OnRspAccountCapitalTopic": "OnRspAccountCapital", //账户资金请求返回topic
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"CompressionType": 0, // 消息压缩方式,可以是 None(0)、Gzip(1)、Snappy(2)、Lz4(3)、Zstd(4) 中的一种,
|
||||
"MessageTimeoutMs": 3000, // 控制生产者等待消息确认的时间,单位是毫秒
|
||||
"ClientRateTopic": "ylClientRateTopic", //客户互换费率生产topic
|
||||
"ContractTopic": "onederiv.trs.contract.v1", //TRS合约日终推送topic
|
||||
"HedgingAccountTopic": "ylHedgingAccountTopic", //对冲账户生产topic
|
||||
"ReqAccountCapitalTopic": "ReqAccountCapital", //账户资金请求topic
|
||||
"OnRspAccountCapitalTopic": "OnRspAccountCapital", //账户资金请求返回topic
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"CompressionType": 0, // 消息压缩方式,可以是 None(0)、Gzip(1)、Snappy(2)、Lz4(3)、Zstd(4) 中的一种,
|
||||
"MessageTimeoutMs": 3000, // 控制生产者等待消息确认的时间,单位是毫秒
|
||||
"ClientRateTopic": "ylClientRateTopic", //客户互换费率生产topic
|
||||
"ContractTopic": "onederiv.trs.contract.v1", //TRS合约日终推送topic
|
||||
"HedgingAccountTopic": "ylHedgingAccountTopic", //对冲账户生产topic
|
||||
"AccountCapitalTopicGroupId": "YiLian_OnRspAccountCapitalConsumer", //账户资金消费组
|
||||
"ReqAccountCapitalTopic": "ReqAccountCapital", //账户资金请求topic
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"CompressionType": 0, // 消息压缩方式,可以是 None(0)、Gzip(1)、Snappy(2)、Lz4(3)、Zstd(4) 中的一种,
|
||||
"MessageTimeoutMs": 3000, // 控制生产者等待消息确认的时间,单位是毫秒
|
||||
"ClientRateTopic": "ylClientRateTopic", //客户互换费率生产topic
|
||||
"ContractTopic": "onederiv.trs.contract.v1", //TRS合约日终推送topic
|
||||
"HedgingAccountTopic": "ylHedgingAccountTopic", //对冲账户生产topic
|
||||
"AccountCapitalTopicGroupId": "YiLian_OnRspAccountCapitalConsumer", //账户资金消费组
|
||||
"ReqAccountCapitalTopic": "ReqAccountCapital", //账户资金请求topic
|
||||
|
||||
@@ -4,7 +4,8 @@ const vm = require('vm');
|
||||
|
||||
function loadEodPositionRiskHelpers() {
|
||||
const filePath = path.join(__dirname, '../wwwroot/Scripts/app/swaptrade/EodPositionRisks.js');
|
||||
const code = fs.readFileSync(filePath, 'utf8') + '\nmodule.exports = { TradeDirectionFormat };';
|
||||
const source = fs.readFileSync(filePath, 'utf8');
|
||||
const code = source + '\nmodule.exports = { TradeDirectionFormat, colModelGridEodSwap, colModelGridEodSwapNew, eodSwapGroupConfig, eodSwapRiskNewGroupConfig };';
|
||||
const sandbox = {
|
||||
module: { exports: {} },
|
||||
exports: {},
|
||||
@@ -13,14 +14,25 @@ function loadEodPositionRiskHelpers() {
|
||||
numberFormat() {
|
||||
return function () { };
|
||||
}
|
||||
},
|
||||
otcformat: {
|
||||
trading: {
|
||||
notional() { return ''; },
|
||||
StockEqvNotional() { return ''; },
|
||||
tradePrice() { return ''; }
|
||||
}
|
||||
},
|
||||
swapPricePrecision: {
|
||||
format() { return ''; }
|
||||
}
|
||||
};
|
||||
|
||||
vm.runInNewContext(code, sandbox, { filename: filePath });
|
||||
return sandbox.module.exports;
|
||||
return { helpers: sandbox.module.exports, source };
|
||||
}
|
||||
|
||||
const { TradeDirectionFormat } = loadEodPositionRiskHelpers();
|
||||
const loaded = loadEodPositionRiskHelpers();
|
||||
const { TradeDirectionFormat, colModelGridEodSwap, colModelGridEodSwapNew, eodSwapGroupConfig, eodSwapRiskNewGroupConfig } = loaded.helpers;
|
||||
|
||||
describe('互换日终持仓交易方向', () => {
|
||||
test.each([
|
||||
@@ -37,3 +49,54 @@ describe('互换日终持仓交易方向', () => {
|
||||
expect(TradeDirectionFormat(1, {}, { eodPosition: { PosiDirection: 0, PositionType: 1 } })).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('EQD-7084 新框架合约前端接线', () => {
|
||||
test('新列模型保留旧列并追加九个字段,六个展示列绑定 NewFields', () => {
|
||||
const oldColumns = colModelGridEodSwap();
|
||||
const newColumns = colModelGridEodSwapNew();
|
||||
const oldNames = oldColumns.map(column => column.name);
|
||||
const newNames = newColumns.map(column => column.name);
|
||||
const replacements = {
|
||||
FloatingUnrealizedPnl: 'NewFields.FloatingUnrealizedPnl',
|
||||
'position.InterestPnL': 'NewFields.OrdinaryInterestPnl',
|
||||
MarginInterestGain: 'NewFields.MarginInterestGain',
|
||||
MarginInterestLoss: 'NewFields.MarginInterestLoss',
|
||||
MaturityNettingValuation: 'NewFields.MaturityNettingValuation',
|
||||
PeriodPaymentValuation: 'NewFields.PeriodPaymentValuation'
|
||||
};
|
||||
const newFields = [
|
||||
'NewFields.UnderlyingDirection',
|
||||
'NewFields.UnderlyingCode',
|
||||
'NewFields.InitialPrice',
|
||||
'NewFields.NotionalQuantity',
|
||||
'NewFields.ContractStartDate',
|
||||
'NewFields.ContractMaturityDate',
|
||||
'NewFields.InterestBenchmark',
|
||||
'NewFields.InterestRatePrice',
|
||||
'NewFields.OpeningClosingFee'
|
||||
];
|
||||
|
||||
expect(newColumns).toHaveLength(oldColumns.length + 9);
|
||||
Object.entries(replacements).forEach(([oldName, newName]) => {
|
||||
expect(newNames).toContain(newName);
|
||||
expect(newNames).not.toContain(oldName);
|
||||
expect(newColumns.find(column => column.name === newName).label)
|
||||
.toBe(oldColumns.find(column => column.name === oldName).label);
|
||||
});
|
||||
oldNames
|
||||
.filter(oldName => !Object.prototype.hasOwnProperty.call(replacements, oldName))
|
||||
.forEach(oldName => expect(newNames).toContain(oldName));
|
||||
newFields.forEach(field => expect(newNames).toContain(field));
|
||||
});
|
||||
|
||||
test('index=2 保留旧 endpoint/config,index=3 使用独立 endpoint/config 且界面不启用分组', () => {
|
||||
expect(loaded.source).toContain("queryurl = '/swaptrade2/EodSwapRiskQuery';");
|
||||
expect(loaded.source).toContain("cloumnTargetName = \"eodSwapList\";");
|
||||
expect(loaded.source).toContain("queryurl = '/swaptrade2/EodSwapRiskNewQuery';");
|
||||
expect(loaded.source).toContain("cloumnTargetName = \"eodSwapRiskNewList\";");
|
||||
expect(loaded.source).toContain('eodSwapRiskNewExportColumnNames');
|
||||
expect(loaded.source).not.toMatch(/main\.initCollapsibleGroupHeaders\s*\(/);
|
||||
expect(eodSwapGroupConfig).not.toBe(eodSwapRiskNewGroupConfig);
|
||||
expect(eodSwapRiskNewGroupConfig.some(group => group.columns.includes('NewFields.InitialPrice'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
var queryurl = '/swaptrade2/EodPositionRiskQuery';
|
||||
var cloumnTargetName = "eodSwapPositionList";
|
||||
var eodSwapExportColumnNames = [];
|
||||
var eodSwapRiskNewExportColumnNames = [];
|
||||
$(function () {
|
||||
var PostData = {};
|
||||
$("#DateValueDate").datepicker({
|
||||
@@ -24,6 +25,18 @@ $(function () {
|
||||
}).map(function (col) {
|
||||
return col.name;
|
||||
});
|
||||
} else if (page.tabIndex == 3) {
|
||||
// 新旧口径并行:独立 endpoint、列设置 key 与标准导出列,避免用户在新 Tab 调列后影响旧报表。
|
||||
queryurl = '/swaptrade2/EodSwapRiskNewQuery';
|
||||
$("#myTab li:first").removeClass("active");
|
||||
$("#myTab li:eq(2)").addClass("active");
|
||||
cloumnTargetName = "eodSwapRiskNewList";
|
||||
colModelGrid = colModelGridEodSwapNew();
|
||||
eodSwapRiskNewExportColumnNames = colModelGrid.filter(function (col) {
|
||||
return !col.optionHide;
|
||||
}).map(function (col) {
|
||||
return col.name;
|
||||
});
|
||||
}
|
||||
PostData.ValueDate = $("#DateValueDate").val();
|
||||
var grid = jQuery('#listGrid').jqGrid({
|
||||
@@ -44,7 +57,8 @@ $(function () {
|
||||
pagerpos: 'left',
|
||||
rowNum: 25,
|
||||
rowList: [25, 50, 100, 200, 10000],
|
||||
footerrow: page.tabIndex == 2,
|
||||
// 两个框架合约 Tab 都需要承载后端返回的 DV 汇总;普通日终持仓维持原行为。
|
||||
footerrow: page.tabIndex == 2 || page.tabIndex == 3,
|
||||
loadComplete: gridComplete,
|
||||
onPaging: onJqgridPaging,
|
||||
grouping: true
|
||||
@@ -687,6 +701,58 @@ function colModelGridEodSwap() {
|
||||
return colModelGrid;
|
||||
}
|
||||
|
||||
// EQD-7084 新框架合约:复用旧列定义,只替换新口径字段并追加新增列。
|
||||
function colModelGridEodSwapNew() {
|
||||
var colModelGrid = colModelGridEodSwap().map(function (col) {
|
||||
return Object.assign({}, col);
|
||||
});
|
||||
|
||||
// 替换后仍保留旧字段 index:后端沿用旧查询处理排序,NewFields 只是显示用的计算字段。
|
||||
function replaceColumn(oldName, newName) {
|
||||
var column = colModelGrid.find(function (col) { return col.name === oldName; });
|
||||
if (column) {
|
||||
column.name = newName;
|
||||
// 新字段在服务端计算,沿用旧列的数据库排序字段,保持分页/排序请求有效。
|
||||
column.index = oldName;
|
||||
}
|
||||
}
|
||||
|
||||
function newColumn(name, label, formatter, index) {
|
||||
return {
|
||||
name: name,
|
||||
label: label,
|
||||
index: index || name,
|
||||
width: 150,
|
||||
align: 'center',
|
||||
formatter: formatter,
|
||||
sortable: false
|
||||
};
|
||||
}
|
||||
|
||||
// 插入点必须在原“名义本金”前,使新需求字段与旧字段的业务阅读顺序、标准导出顺序一致。
|
||||
var contractInfoIndex = colModelGrid.findIndex(function (col) {
|
||||
return col.name === 'position.NotionalValue';
|
||||
});
|
||||
colModelGrid.splice(contractInfoIndex, 0,
|
||||
newColumn('NewFields.UnderlyingDirection', '标的多空(浮动端)'),
|
||||
newColumn('NewFields.UnderlyingCode', '标的代码'),
|
||||
newColumn('NewFields.InitialPrice', '期初价格', InitialPriceFormat),
|
||||
newColumn('NewFields.NotionalQuantity', '名义数量', otcformat.trading.notional),
|
||||
newColumn('NewFields.ContractStartDate', '合约起始日', 'date'),
|
||||
newColumn('NewFields.ContractMaturityDate', '合约到期日', 'date'),
|
||||
newColumn('NewFields.InterestBenchmark', '利息端基准'),
|
||||
newColumn('NewFields.InterestRatePrice', '利率端价格', PercentFormat),
|
||||
newColumn('NewFields.OpeningClosingFee', '开平仓费用', StockEqvNotionalFormat));
|
||||
|
||||
replaceColumn('FloatingUnrealizedPnl', 'NewFields.FloatingUnrealizedPnl');
|
||||
replaceColumn('position.InterestPnL', 'NewFields.OrdinaryInterestPnl');
|
||||
replaceColumn('MarginInterestGain', 'NewFields.MarginInterestGain');
|
||||
replaceColumn('MarginInterestLoss', 'NewFields.MarginInterestLoss');
|
||||
replaceColumn('MaturityNettingValuation', 'NewFields.MaturityNettingValuation');
|
||||
replaceColumn('PeriodPaymentValuation', 'NewFields.PeriodPaymentValuation');
|
||||
return colModelGrid;
|
||||
}
|
||||
|
||||
//框架合约分组配置(对应需求《估值模块V1》2.2 字段定义)
|
||||
//columns 使用 colModel.name;组内列在 colModel 中必须连续
|
||||
var eodSwapGroupConfig = [
|
||||
@@ -699,14 +765,27 @@ var eodSwapGroupConfig = [
|
||||
{ title: '估值与实现收益', columns: ['position.dv01', 'InterestPaymentMethod', 'MaturityNettingValuation', 'PeriodPaymentValuation', 'position.RealizedPnL'] }
|
||||
];
|
||||
|
||||
// 新 Tab 页面不渲染可折叠分组表头(产品已要求取消界面分组);
|
||||
// 此配置只服务“导出标准格式”,因此必须与旧 Tab 分开维护而不能删除。
|
||||
var eodSwapRiskNewGroupConfig = [
|
||||
{ title: '基本信息', columns: ['position.ValueDate', 'AssetBookName', 'ClientName', 'SwapTradeNo', 'StructureType', 'SwapTradeTypeStr', 'UnderlyingType'] },
|
||||
{ title: '新增字段', columns: ['NewFields.UnderlyingDirection', 'NewFields.UnderlyingCode', 'NewFields.InitialPrice', 'NewFields.NotionalQuantity', 'NewFields.ContractStartDate', 'NewFields.ContractMaturityDate', 'NewFields.InterestBenchmark', 'NewFields.InterestRatePrice', 'NewFields.OpeningClosingFee'] },
|
||||
{ title: '名义本金', columns: ['position.NotionalValue', 'position.NotionalValueLong', 'position.NotionalValueShort'] },
|
||||
{ title: '标的市值', columns: ['position.MarketValueLong', 'position.MarketValueShort'] },
|
||||
{ title: '浮动端', columns: ['NewFields.FloatingUnrealizedPnl', 'PeriodAmount'] },
|
||||
{ title: '利息端', columns: ['NewFields.OrdinaryInterestPnl'] },
|
||||
{ title: '保证金', columns: ['position.InitMarginGain', 'position.PostionMarginGain', 'position.InitMarginLoss', 'position.PostionMarginLoss', 'NewFields.MarginInterestGain', 'NewFields.MarginInterestLoss'] },
|
||||
{ title: '估值与实现收益', columns: ['position.dv01', 'InterestPaymentMethod', 'NewFields.MaturityNettingValuation', 'NewFields.PeriodPaymentValuation', 'position.RealizedPnL'] }
|
||||
];
|
||||
|
||||
|
||||
function gridComplete() {
|
||||
var jgrid = $(this);
|
||||
if (arguments[0].Sum) {
|
||||
jgrid.footerData("set", { 'position.dv01': arguments[0].Sum["DV"] });
|
||||
}
|
||||
//框架合约Tab:列设置应用完成后补充期间付息提示。
|
||||
if (page.tabIndex == 2) {
|
||||
// 两个框架合约 Tab 均保留 DV footer 与列设置;界面使用普通单层表头。
|
||||
if (page.tabIndex == 2 || page.tabIndex == 3) {
|
||||
var defer = main.setcolumnChooser(jgrid, cloumnTargetName);
|
||||
$.when(defer).done(function () {
|
||||
jgrid.jqGrid('setLabel', 'PeriodAmount', null, null, {
|
||||
@@ -734,13 +813,21 @@ function starttradeView(id) {
|
||||
function exportVisibleColumns() {
|
||||
var jgrid = jQuery('#listGrid');
|
||||
var dateStr = $("#DateValueDate").val() || '';
|
||||
var tabName = page.tabIndex == 2 ? '框架合约' : '日终持仓';
|
||||
var tabName = page.tabIndex == 2
|
||||
? '框架合约(旧口径)'
|
||||
: page.tabIndex == 3 ? '框架合约' : '日终持仓';
|
||||
var fileName = '日终持仓风险_互换_' + tabName + (dateStr ? '_' + dateStr : '');
|
||||
if (page.tabIndex != 2) {
|
||||
if (page.tabIndex != 2 && page.tabIndex != 3) {
|
||||
main.exportVisibleColumnsToExcel(jgrid, fileName, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// 虽然新 Tab 不展示分组表头,标准格式导出仍按需求输出分组标题和固定列顺序。
|
||||
var groupConfig = page.tabIndex == 3 ? eodSwapRiskNewGroupConfig : eodSwapGroupConfig;
|
||||
var standardColumnNames = page.tabIndex == 3
|
||||
? eodSwapRiskNewExportColumnNames
|
||||
: eodSwapExportColumnNames;
|
||||
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: '选择导出方式',
|
||||
@@ -753,7 +840,7 @@ function exportVisibleColumns() {
|
||||
'</div>',
|
||||
success: function (layero, index) {
|
||||
layero.find('.js-export-eod-swap-standard').on('click', function () {
|
||||
exportEodSwapRows(jgrid, fileName, eodSwapGroupConfig, eodSwapExportColumnNames);
|
||||
exportEodSwapRows(jgrid, fileName, groupConfig, standardColumnNames);
|
||||
layer.close(index);
|
||||
});
|
||||
layero.find('.js-export-eod-swap-visible').on('click', function () {
|
||||
@@ -810,6 +897,13 @@ function RealizedPnlFormat(cellValue, options, rowObject) {
|
||||
function StockEqvNotionalFormat(cellValue, options, rowObject) {
|
||||
return otcformat.trading.StockEqvNotional(cellValue);
|
||||
}
|
||||
function InitialPriceFormat(cellValue, options, rowObject) {
|
||||
// 类型来自 NewFields(不再是旧 eodPosition 嵌套对象),以便债券按全价精度、非债券按普通价格精度展示。
|
||||
var instrumentType = rowObject
|
||||
&& rowObject.NewFields
|
||||
&& rowObject.NewFields.UnderlyingInstrumentType;
|
||||
return swapPricePrecision.format(cellValue, instrumentType, 'grossPrice');
|
||||
}
|
||||
function NullableStockEqvNotionalFormat(cellValue, options, rowObject) {
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '') {
|
||||
return '';
|
||||
|
||||
@@ -46,7 +46,9 @@ const vue = new Vue({
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.multiplier = this.deal.StructureType == '普通债券类收益互换' ? 100 : 1;
|
||||
// 是否 ×100 由浮动腿标的资产类型决定(债券价格以小数保存,展示时转为百分比),
|
||||
// 与存储层 SetPosiPrice/GetStorageDeliveryPriceRound 的 IsBond 口径一致,不依赖簿记结构类型。
|
||||
this.multiplier = this.IsBond(swapInstrumentType) ? 100 : 1;
|
||||
this.initDeal();
|
||||
this.setValueDate();
|
||||
},
|
||||
|
||||
@@ -817,17 +817,35 @@ const vue = new Vue({
|
||||
"补充协议编号": $("#SupProtocolCode").val()
|
||||
};
|
||||
this.trade.trade_extend.ExtendJson = JSON.stringify(this.trade.trade_extend.ExtendObj);
|
||||
//R4 保存环节资金校验:资金不足且系统开启"允许交易特批"时,服务端按确认/审批同一协议返回
|
||||
//AdditionalProcessing/LackOfMoney——弹"交易特批"确认,带 additionalProcessing=LackOfMoney 重提放行
|
||||
//保存环节两类拦截确认(服务端按确认/审批同一协议返回 AdditionalProcessing):
|
||||
//1) MarginCreditSplit 预付金授信不足——确认后按 剩余授信+现金差额 拆腿再保存(标准流程);
|
||||
//2) LackOfMoney 资金不足且系统开启"允许交易特批"——弹"交易特批",特批放行。
|
||||
//两类可链式发生(拆单后现金仍不足再走特批),确认标记累积在 query 上一并带上
|
||||
var thisObj = this;
|
||||
var confirmedProcessings = [];
|
||||
var doSave = function (additionalProcessing) {
|
||||
var url = "/swaptrade2/tradeEditJson";
|
||||
if (!main.isEmpty(additionalProcessing)) {
|
||||
url += "?additionalProcessing=" + additionalProcessing;
|
||||
if (!main.isEmpty(additionalProcessing) && confirmedProcessings.indexOf(additionalProcessing) < 0) {
|
||||
confirmedProcessings.push(additionalProcessing);
|
||||
}
|
||||
if (confirmedProcessings.length) {
|
||||
url += "?additionalProcessing=" + confirmedProcessings.join(",");
|
||||
}
|
||||
main.post(url, thisObj.trade).done(function (resp) {
|
||||
if (resp.obj && resp.obj.proccessType == "AdditionalProcessing") {
|
||||
if (resp.obj.type == "LackOfMoney") {
|
||||
if (resp.obj.type == "MarginCreditSplit") {
|
||||
var splitContent = '<div style="padding:10px">' + resp.obj.message + '</div>';
|
||||
main.open2("提示",
|
||||
splitContent,
|
||||
{
|
||||
area: ["460px", "260px"],
|
||||
btn: ['确认拆分', '取消'],
|
||||
yes: function (index, layero) {
|
||||
layer.close(index);
|
||||
doSave("MarginCreditSplit");
|
||||
}
|
||||
});
|
||||
} else if (resp.obj.type == "LackOfMoney") {
|
||||
var htmlContent = '<div style="padding:10px">' + resp.obj.message + '</div>';
|
||||
var lackMoneyConfirmLayer = main.open2("提示",
|
||||
htmlContent,
|
||||
|
||||
@@ -64,7 +64,7 @@ const vue = new Vue({
|
||||
marginList: [],
|
||||
initPosiNetPrice: 0,
|
||||
multiplier: 1,
|
||||
// EQD-6953 簿记模板=普通债券类收益互换 时启用 期末交割全价↔结算收益率(ExitYtm) 互算
|
||||
// EQD-6953 浮动腿标的为债券时启用 期末交割全价↔结算收益率(ExitYtm) 互算
|
||||
isBondTRS: false,
|
||||
// 平仓比例展示/输入均为"占期初(original)"语义(A):默认与每次重开都基于原始名义本金。
|
||||
// oriClosePercent = 剩余名义本金/期初名义本金 = 最多可平比例(不能平超过剩余持仓)。
|
||||
@@ -77,13 +77,15 @@ const vue = new Vue({
|
||||
minStartDate() {
|
||||
return this.deal.StartDate;
|
||||
},
|
||||
// EQD-6953:簿记模板为普通债券类收益互换 且 浮动腿标的为债券 时,才展示 源/AUTO/REV 标识并允许互算
|
||||
// EQD-6953:浮动腿标的为债券时,才展示 源/AUTO/REV 标识并允许互算
|
||||
isBondUnwindLeg() {
|
||||
return this.isBondTRS && !!this.floatPosition && this.IsBond(this.floatPosition.UnderlyingInstrumentType);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.isBondTRS = this.deal.StructureType == '普通债券类收益互换';
|
||||
// 是否债券收益互换由浮动腿标的资产类型决定(×100 展示口径与存储层 SetPosiPrice 的 IsBond 一致),
|
||||
// 不依赖簿记结构类型 StructureType。
|
||||
this.isBondTRS = this.IsBond(swapInstrumentType);
|
||||
this.multiplier = this.isBondTRS ? 100 : 1;
|
||||
this.initDeal();
|
||||
this.setValueDate(this.deal.ValueDate);
|
||||
|
||||
Reference in New Issue
Block a user