Files
Finlytic/FinlyticTechnicals/Services/TradeRepublicIngestionService.cs
T

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;
}
}
}
}