using YLErp.Abstract; using YLErp.BLL; using YLErp.DBModels; using YLErp.Helpers; namespace YLErp.Modules.EodModule { /// /// 收盘后按交易日推送 TRS 合约全量快照。 /// 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 trades, IReadOnlyCollection eodPositions, IReadOnlyDictionary 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 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; } } }