- 在日终任务的逐日循环中,于当日收盘、风险监控和日终文件生成完成后, 按 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。 - 增加定向单测,覆盖空快照、区间逐日推送、重试成功、三次失败和字段映射。
234 lines
9.7 KiB
C#
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; }
|
|
}
|
|
}
|