feat(technicals): add technical analysis microservice with indicator engines, pattern detectors, and strategies
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user