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。 - 增加定向单测,覆盖空快照、区间逐日推送、重试成功、三次失败和字段映射。
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user