149 lines
4.4 KiB
C#
149 lines
4.4 KiB
C#
using System;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using FinlyticCore.Dtos.TechnicalAnalysis;
|
|
using FinlyticCore.Services;
|
|
using FinlyticTechnicals.Util;
|
|
|
|
namespace FinlyticTechnicals.Services;
|
|
|
|
/// <summary>
|
|
/// Cleaned, normalized real-time tick ready for multi-timeframe aggregation.
|
|
/// </summary>
|
|
public record CleanLiveTick(
|
|
string Isin,
|
|
decimal MidPrice,
|
|
decimal Bid,
|
|
decimal Ask,
|
|
decimal LastPrice,
|
|
decimal SpreadPercent,
|
|
bool IsSpreadVolatile,
|
|
DateTime TimestampUtc
|
|
);
|
|
|
|
public interface ITradeRepublicIngestionService
|
|
{
|
|
/// <summary>
|
|
/// Event triggered when a cleaned, UTC-normalized tick arrives.
|
|
/// </summary>
|
|
event Func<CleanLiveTick, Task>? OnTickReceived;
|
|
|
|
/// <summary>
|
|
/// Processes a raw tick from Trade Republic (e.g. via WebSocket or Poller).
|
|
/// </summary>
|
|
Task<CleanLiveTick?> ProcessRawTickAsync(string isin, decimal bid, decimal ask, decimal? last, DateTime? timestamp, CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
public class TradeRepublicIngestionService : ITradeRepublicIngestionService
|
|
{
|
|
private readonly IFinlyticLogger<TradeRepublicIngestionService> _finlyticLogger;
|
|
private static readonly TimeZoneInfo BerlinTimeZone = GetBerlinTimeZone();
|
|
|
|
public event Func<CleanLiveTick, Task>? OnTickReceived;
|
|
|
|
public TradeRepublicIngestionService(IFinlyticLogger<TradeRepublicIngestionService> finlyticLogger)
|
|
{
|
|
_finlyticLogger = finlyticLogger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes a raw incoming tick with strict UTC normalization, spread check, and mid-price calculation.
|
|
/// </summary>
|
|
public async Task<CleanLiveTick?> ProcessRawTickAsync(string isin, decimal bid, decimal ask, decimal? last, DateTime? timestamp, CancellationToken cancellationToken = default)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(isin)) return null;
|
|
|
|
var cleanIsin = isin.Trim().ToUpperInvariant();
|
|
|
|
// 1. Strict UTC Normalization
|
|
DateTime utcTimestamp;
|
|
if (timestamp.HasValue)
|
|
{
|
|
var rawTime = timestamp.Value;
|
|
if (rawTime.Kind == DateTimeKind.Utc)
|
|
{
|
|
utcTimestamp = rawTime;
|
|
}
|
|
else if (rawTime.Kind == DateTimeKind.Unspecified)
|
|
{
|
|
// Trade Republic ticks typically arrive in German local market time (Europe/Berlin)
|
|
utcTimestamp = TimeZoneInfo.ConvertTimeToUtc(rawTime, BerlinTimeZone);
|
|
}
|
|
else
|
|
{
|
|
utcTimestamp = rawTime.ToUniversalTime();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
utcTimestamp = DateTime.UtcNow;
|
|
}
|
|
|
|
// 2. Clean Mid-Price Calculation: (Bid + Ask) / 2
|
|
decimal cleanMidPrice;
|
|
if (bid > 0m && ask > 0m)
|
|
{
|
|
cleanMidPrice = (bid + ask) / 2m;
|
|
}
|
|
else if (last.HasValue && last.Value > 0m)
|
|
{
|
|
cleanMidPrice = last.Value;
|
|
if (bid <= 0m) bid = cleanMidPrice;
|
|
if (ask <= 0m) ask = cleanMidPrice;
|
|
}
|
|
else
|
|
{
|
|
return null; // Invalid quote
|
|
}
|
|
|
|
// 3. Spread Calculation & Volatility Tagging
|
|
decimal spreadPercent = 0m;
|
|
bool isSpreadVolatile = false;
|
|
if (cleanMidPrice > 0m && ask >= bid)
|
|
{
|
|
spreadPercent = ((ask - bid) / cleanMidPrice) * 100m;
|
|
if (spreadPercent > 1.5m)
|
|
{
|
|
isSpreadVolatile = true;
|
|
}
|
|
}
|
|
|
|
var cleanTick = new CleanLiveTick(
|
|
Isin: cleanIsin,
|
|
MidPrice: cleanMidPrice,
|
|
Bid: bid,
|
|
Ask: ask,
|
|
LastPrice: last ?? cleanMidPrice,
|
|
SpreadPercent: spreadPercent,
|
|
IsSpreadVolatile: isSpreadVolatile,
|
|
TimestampUtc: utcTimestamp
|
|
);
|
|
|
|
if (OnTickReceived != null)
|
|
{
|
|
await OnTickReceived.Invoke(cleanTick);
|
|
}
|
|
|
|
return cleanTick;
|
|
}
|
|
|
|
private static TimeZoneInfo GetBerlinTimeZone()
|
|
{
|
|
try
|
|
{
|
|
return TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time"); // Windows ID
|
|
}
|
|
catch
|
|
{
|
|
try
|
|
{
|
|
return TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin"); // Linux IANA ID
|
|
}
|
|
catch
|
|
{
|
|
return TimeZoneInfo.Utc;
|
|
}
|
|
}
|
|
}
|
|
}
|