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:
@@ -0,0 +1,50 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace YLErp.DBModels
|
||||
{
|
||||
public enum PushStateEnum
|
||||
{
|
||||
待推送 = 0,
|
||||
成功 = 1,
|
||||
失败 = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 外发推送失败状态。只记录定位信息,不保存报文。
|
||||
/// </summary>
|
||||
[Table("push_status")]
|
||||
public class PushStatus
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public long id { get; set; }
|
||||
|
||||
[Column("value_date")]
|
||||
public DateTime ValueDate { get; set; }
|
||||
|
||||
[Column("push_type")]
|
||||
public int PushType { get; set; }
|
||||
|
||||
[Column("record_id")]
|
||||
public long RecordId { get; set; }
|
||||
|
||||
[Column("state")]
|
||||
public PushStateEnum State { get; set; }
|
||||
|
||||
[Column("retry_count")]
|
||||
public int RetryCount { get; set; }
|
||||
|
||||
[Column("last_error")]
|
||||
public string LastError { get; set; }
|
||||
|
||||
[Column("push_time")]
|
||||
public DateTime? PushTime { get; set; }
|
||||
|
||||
[Column("create_time")]
|
||||
public DateTime CreateTime { get; set; }
|
||||
|
||||
[Column("update_time")]
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
using YLErp.Abstract;
|
||||
using YLErp.Helpers;
|
||||
using YLErp.Modules.EodModule;
|
||||
|
||||
namespace YLErp.Modules.EodModuleTests
|
||||
{
|
||||
[TestClass]
|
||||
public class TrsContractKafkaPushServiceTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void Push_空日快照_发送一条空消息并使用业务日期作为Key()
|
||||
{
|
||||
var valueDate = new DateTime(2026, 8, 24);
|
||||
var producer = new RecordingKafkaProducer();
|
||||
var service = CreateService(producer, valueDate);
|
||||
|
||||
service.Push(valueDate);
|
||||
|
||||
Assert.AreEqual(1, producer.Messages.Count);
|
||||
Assert.AreEqual("onederiv.trs.contract.v1", producer.Messages[0].Topic);
|
||||
Assert.AreEqual("2026-08-24", producer.Messages[0].Key);
|
||||
var payload = JsonHelper.Deserialize<TrsContractSnapshot>(producer.Messages[0].Message);
|
||||
Assert.AreEqual("2026-08-24", payload.ValueDate);
|
||||
Assert.AreEqual(0, payload.ContractCount);
|
||||
Assert.AreEqual(0, payload.Contracts.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Push_区间内每天分别调用_每个日期各发送一条快照()
|
||||
{
|
||||
var valueDates = new[]
|
||||
{
|
||||
new DateTime(2026, 8, 20),
|
||||
new DateTime(2026, 8, 21),
|
||||
new DateTime(2026, 8, 24)
|
||||
};
|
||||
var producer = new RecordingKafkaProducer();
|
||||
var service = new TestableTrsContractKafkaPushService(producer, valueDates.ToDictionary(x => x, CreateEmptySnapshot));
|
||||
|
||||
foreach (var valueDate in valueDates)
|
||||
{
|
||||
service.Push(valueDate);
|
||||
}
|
||||
|
||||
CollectionAssert.AreEqual(
|
||||
new[] { "2026-08-20", "2026-08-21", "2026-08-24" },
|
||||
producer.Messages.Select(x => x.Key).ToArray());
|
||||
CollectionAssert.AreEqual(
|
||||
new[] { "2026-08-20", "2026-08-21", "2026-08-24" },
|
||||
producer.Messages.Select(x => JsonHelper.Deserialize<TrsContractSnapshot>(x.Message).ValueDate).ToArray());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Push_首次失败后成功_停止重试且不记录最终失败()
|
||||
{
|
||||
var valueDate = new DateTime(2026, 8, 24);
|
||||
var producer = new RecordingKafkaProducer { FailuresBeforeSuccess = 1 };
|
||||
var service = CreateService(producer, valueDate);
|
||||
|
||||
service.Push(valueDate);
|
||||
|
||||
Assert.AreEqual(2, producer.AttemptCount);
|
||||
Assert.AreEqual(1, producer.Messages.Count);
|
||||
Assert.AreEqual(0, service.FailureRecords.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Push_连续失败三次_记录最终失败和三次尝试()
|
||||
{
|
||||
var valueDate = new DateTime(2026, 8, 24);
|
||||
var producer = new RecordingKafkaProducer { FailuresBeforeSuccess = int.MaxValue };
|
||||
var service = CreateService(producer, valueDate);
|
||||
|
||||
service.Push(valueDate);
|
||||
|
||||
Assert.AreEqual(3, producer.AttemptCount);
|
||||
Assert.AreEqual(0, producer.Messages.Count);
|
||||
Assert.AreEqual(1, service.FailureRecords.Count);
|
||||
Assert.AreEqual(valueDate, service.FailureRecords[0].ValueDate);
|
||||
Assert.AreEqual(3, service.FailureRecords[0].RetryCount);
|
||||
Assert.IsInstanceOfType(service.FailureRecords[0].Exception, typeof(InvalidOperationException));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void BuildContract_字段使用日终快照和约定来源()
|
||||
{
|
||||
var valueDate = new DateTime(2026, 8, 24);
|
||||
var eodSwap = new eod_swap
|
||||
{
|
||||
id = 10,
|
||||
ValueDate = valueDate,
|
||||
SwapTradeId = 7,
|
||||
SwapTradeNo = "TRS-001",
|
||||
BookId = 3,
|
||||
ClientId = 8,
|
||||
NotionalValue = 1000000m,
|
||||
dv01 = 12.34m,
|
||||
InitMarginGain = 100m,
|
||||
InitMarginLoss = 0m
|
||||
};
|
||||
var trade = new trade
|
||||
{
|
||||
id = 7,
|
||||
UnderlyingCode = "600000.SH",
|
||||
UnderlyingAssetName = "浦发银行",
|
||||
UnderlyingInstrumentType = "Stock",
|
||||
StartDate = new DateTime(2026, 8, 1),
|
||||
ExerciseDate = new DateTime(2027, 8, 1)
|
||||
};
|
||||
var positions = new List<eod_swap_position>
|
||||
{
|
||||
new() { SwapTradeId = 7, PositionId = 101, UnderlyingCode = "600000.SH", PositionType = 1 },
|
||||
new() { SwapTradeId = 7, PositionId = 102, InterestMode = (int)InterestModeEnum.固定值, InterestRateDefault = 0.0123m, InterestDirection = 2 }
|
||||
};
|
||||
var swapPositions = new Dictionary<long, swap_position>
|
||||
{
|
||||
[101] = new() { id = 101, category_tag = "互换利率" },
|
||||
[102] = new() { id = 102, category_tag = "互换利率" }
|
||||
};
|
||||
|
||||
var item = TrsContractKafkaPushService.BuildContract(
|
||||
eodSwap,
|
||||
new Dictionary<int, trade> { [7] = trade },
|
||||
positions,
|
||||
swapPositions);
|
||||
|
||||
Assert.AreEqual("2026-08-24", item.TradeDate);
|
||||
Assert.AreEqual(3, item.BookId);
|
||||
Assert.AreEqual("TRS-001", item.SwapTradeNo);
|
||||
Assert.AreEqual(8, item.ClientId);
|
||||
Assert.AreEqual("600000.SH", item.UnderlyingCode);
|
||||
Assert.AreEqual("浦发银行", item.UnderlyingName);
|
||||
Assert.AreEqual("Stock", item.UnderlyingInstrumentType);
|
||||
Assert.AreEqual(1000000m, item.NotionalValue);
|
||||
Assert.AreEqual("2026-08-01", item.StartDate);
|
||||
Assert.AreEqual("2027-08-01", item.MaturityDate);
|
||||
Assert.AreEqual(12.34m, item.Dv01);
|
||||
Assert.AreEqual(0.0123m, item.FixedRate);
|
||||
Assert.AreEqual(2, item.InterestDirection);
|
||||
Assert.AreEqual(1, item.FloatingDirection);
|
||||
Assert.AreEqual(100m, item.InitMarginGain);
|
||||
Assert.AreEqual(0m, item.InitMarginLoss);
|
||||
}
|
||||
|
||||
private static TestableTrsContractKafkaPushService CreateService(RecordingKafkaProducer producer, DateTime valueDate)
|
||||
{
|
||||
return new TestableTrsContractKafkaPushService(
|
||||
producer,
|
||||
new Dictionary<DateTime, TrsContractSnapshot> { [valueDate] = CreateEmptySnapshot(valueDate) });
|
||||
}
|
||||
|
||||
private static TrsContractSnapshot CreateEmptySnapshot(DateTime valueDate)
|
||||
{
|
||||
return new TrsContractSnapshot
|
||||
{
|
||||
SchemaVersion = "v1",
|
||||
ValueDate = valueDate.ToString("yyyy-MM-dd"),
|
||||
PushTime = "2026-08-24 12:00:00",
|
||||
ContractCount = 0,
|
||||
Contracts = new List<TrsContractSnapshotItem>()
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class TestableTrsContractKafkaPushService : TrsContractKafkaPushService
|
||||
{
|
||||
private readonly IReadOnlyDictionary<DateTime, TrsContractSnapshot> _snapshots;
|
||||
|
||||
public List<(DateTime ValueDate, int RetryCount, Exception Exception)> FailureRecords { get; } = new();
|
||||
|
||||
public TestableTrsContractKafkaPushService(IKafkaProduce producer, IReadOnlyDictionary<DateTime, TrsContractSnapshot> snapshots)
|
||||
: base(new YLContext(), producer, "onederiv.trs.contract.v1")
|
||||
{
|
||||
_snapshots = snapshots;
|
||||
}
|
||||
|
||||
protected override TrsContractSnapshot BuildSnapshot(DateTime valueDate)
|
||||
{
|
||||
return _snapshots[valueDate];
|
||||
}
|
||||
|
||||
protected override void RecordFailures(DateTime valueDate, int retryCount, Exception exception)
|
||||
{
|
||||
FailureRecords.Add((valueDate, retryCount, exception));
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RecordingKafkaProducer : IKafkaProduce
|
||||
{
|
||||
public int FailuresBeforeSuccess { get; set; }
|
||||
public int AttemptCount { get; private set; }
|
||||
public List<(string Topic, string Key, string Message)> Messages { get; } = new();
|
||||
|
||||
public void Produce(string topic, string message)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void Produce(string topic, string key, string message)
|
||||
{
|
||||
AttemptCount++;
|
||||
if (AttemptCount <= FailuresBeforeSuccess)
|
||||
{
|
||||
throw new InvalidOperationException("Kafka unavailable");
|
||||
}
|
||||
|
||||
Messages.Add((topic, key, message));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,5 +9,7 @@ namespace YLErp.Abstract
|
||||
public interface IKafkaProduce
|
||||
{
|
||||
void Produce(string topic, string message);
|
||||
|
||||
void Produce(string topic, string key, string message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,6 +354,7 @@ namespace YLErp.BLL
|
||||
public DbSet<eod_swap_position> eod_swap_position { get; set; }
|
||||
public DbSet<swap_event> swap_event { get; set; }
|
||||
public DbSet<eod_swap> eod_swap { get; set; }
|
||||
public DbSet<PushStatus> push_status { get; set; }
|
||||
public DbSet<TradeObervation> trade_obervation { get; set; }
|
||||
|
||||
public DbSet<SystemLog> SystemLogs { get; set; }
|
||||
|
||||
@@ -63,19 +63,29 @@ namespace YLErp.Helpers
|
||||
|
||||
public void Produce(string topic,string message)
|
||||
{
|
||||
var kafkaMessage = new Message<string, string>
|
||||
{
|
||||
Key=null,
|
||||
Value = message
|
||||
};
|
||||
try
|
||||
{
|
||||
_producer.ProduceAsync(topic, kafkaMessage).GetAwaiter().GetResult();
|
||||
ProduceCore(topic, null, message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Topic:{topic} send failed",ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void Produce(string topic, string key, string message)
|
||||
{
|
||||
ProduceCore(topic, key, message);
|
||||
}
|
||||
|
||||
private void ProduceCore(string topic, string key, string message)
|
||||
{
|
||||
var kafkaMessage = new Message<string, string>
|
||||
{
|
||||
Key = key,
|
||||
Value = message
|
||||
};
|
||||
_producer.ProduceAsync(topic, kafkaMessage).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace YLErp.Model
|
||||
/// <summary>
|
||||
/// TRS合约数据推送topic(对外,如onebp等)
|
||||
/// </summary>
|
||||
public string ContractTopic { get; set; } = "onederi.trs.onebp.contract.v1";
|
||||
public string ContractTopic { get; set; } = "onederiv.trs.contract.v1";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@
|
||||
"CompressionType": 0, // 消息压缩方式,可以是 None(0)、Gzip(1)、Snappy(2)、Lz4(3)、Zstd(4) 中的一种,
|
||||
"MessageTimeoutMs": 3000, // 控制生产者等待消息确认的时间,单位是毫秒
|
||||
"ClientRateTopic": "ylClientRateTopic", //客户互换费率生产topic
|
||||
"ContractTopic": "onederiv.trs.contract.v1", //TRS合约日终推送topic
|
||||
"HedgingAccountTopic": "ylHedgingAccountTopic", //对冲账户生产topic
|
||||
"ReqAccountCapitalTopic": "ReqAccountCapital", //账户资金请求topic
|
||||
"OnRspAccountCapitalTopic": "OnRspAccountCapital", //账户资金请求返回topic
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"CompressionType": 0, // 消息压缩方式,可以是 None(0)、Gzip(1)、Snappy(2)、Lz4(3)、Zstd(4) 中的一种,
|
||||
"MessageTimeoutMs": 3000, // 控制生产者等待消息确认的时间,单位是毫秒
|
||||
"ClientRateTopic": "ylClientRateTopic", //客户互换费率生产topic
|
||||
"ContractTopic": "onederiv.trs.contract.v1", //TRS合约日终推送topic
|
||||
"HedgingAccountTopic": "ylHedgingAccountTopic", //对冲账户生产topic
|
||||
"ReqAccountCapitalTopic": "ReqAccountCapital", //账户资金请求topic
|
||||
"OnRspAccountCapitalTopic": "OnRspAccountCapital", //账户资金请求返回topic
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"CompressionType": 0, // 消息压缩方式,可以是 None(0)、Gzip(1)、Snappy(2)、Lz4(3)、Zstd(4) 中的一种,
|
||||
"MessageTimeoutMs": 3000, // 控制生产者等待消息确认的时间,单位是毫秒
|
||||
"ClientRateTopic": "ylClientRateTopic", //客户互换费率生产topic
|
||||
"ContractTopic": "onederiv.trs.contract.v1", //TRS合约日终推送topic
|
||||
"HedgingAccountTopic": "ylHedgingAccountTopic", //对冲账户生产topic
|
||||
"AccountCapitalTopicGroupId": "YiLian_OnRspAccountCapitalConsumer", //账户资金消费组
|
||||
"ReqAccountCapitalTopic": "ReqAccountCapital", //账户资金请求topic
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"CompressionType": 0, // 消息压缩方式,可以是 None(0)、Gzip(1)、Snappy(2)、Lz4(3)、Zstd(4) 中的一种,
|
||||
"MessageTimeoutMs": 3000, // 控制生产者等待消息确认的时间,单位是毫秒
|
||||
"ClientRateTopic": "ylClientRateTopic", //客户互换费率生产topic
|
||||
"ContractTopic": "onederiv.trs.contract.v1", //TRS合约日终推送topic
|
||||
"HedgingAccountTopic": "ylHedgingAccountTopic", //对冲账户生产topic
|
||||
"AccountCapitalTopicGroupId": "YiLian_OnRspAccountCapitalConsumer", //账户资金消费组
|
||||
"ReqAccountCapitalTopic": "ReqAccountCapital", //账户资金请求topic
|
||||
|
||||
Reference in New Issue
Block a user