243 lines
10 KiB
C#
243 lines
10 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)
|
|
.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)).ToList();
|
|
var interestCandidates = positions.Where(x => string.IsNullOrWhiteSpace(x.UnderlyingCode)
|
|
&& ConsTrade.InterestModels.Contains(x.InterestMode)
|
|
&& swapPositions.TryGetValue(x.PositionId, out var swapPosition)
|
|
&& (swapPosition.category_tag == InterestCategory || string.IsNullOrWhiteSpace(swapPosition.category_tag)))
|
|
.ToList();
|
|
|
|
// 互换利率腿优先;同类别多腿按当前查询顺序取第一条。历史类别为空时保留利息方向,
|
|
// 但 fixedRate 按约定置 0,避免把未标注类别的历史值当作已确认利率。
|
|
var interest = interestCandidates.FirstOrDefault(x =>
|
|
swapPositions.TryGetValue(x.PositionId, out var swapPosition)
|
|
&& swapPosition.category_tag == InterestCategory);
|
|
var isUncategorizedInterest = interest == null && interestCandidates.Count > 0;
|
|
interest ??= interestCandidates.FirstOrDefault();
|
|
|
|
if (floating.Count != 1 || interest == null)
|
|
{
|
|
throw new InvalidOperationException($"TRS legs invalid, swapTradeId:{eodSwap.SwapTradeId}, floating:{floating.Count}, interest:{(interest == null ? 0 : 1)}");
|
|
}
|
|
|
|
var interestLeg = interest;
|
|
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 = isUncategorizedInterest ? 0 : 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; }
|
|
}
|
|
}
|