Files
zszq-trs/YLErpDAL/Modules/EodModule/TrsContractKafkaPushService.cs
T
tengyufan 336cc3d88b feat: 新增TRS合约日终Kafka推送
- 在日终任务的逐日循环中,于当日收盘、风险监控和日终文件生成完成后,
  按 valueDate 推送一条 TRS 合约全量快照;区间收盘逐日推送,空日推送空快照。
- 基于 eod_swap、trade、eod_swap_position、swap_position 组装合约字段,
  Kafka Key 使用 yyyy-MM-dd 格式的 valueDate。
- 新增带 Key 的 Kafka 发送重载;保留原有无 Key 发送及其吞异常行为,避免影响既有调用。
- 推送失败最多尝试 3 次;最终失败记录 Error 日志,并按 eod_swap.id 写入 push_status,
  空快照失败使用 record_id=0。
- 增加 ContractTopic 配置,默认及各部署环境使用 onederiv.trs.contract.v1。
- 增加定向单测,覆盖空快照、区间逐日推送、重试成功、三次失败和字段映射。
2026-08-25 16:07:21 +08:00

234 lines
9.7 KiB
C#

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; }
}
}