feat(technicals): add technical analysis microservice with indicator engines, pattern detectors, and strategies

This commit is contained in:
2026-08-24 21:36:05 +02:00
parent 12e7b57b16
commit f43ce2b7e9
36 changed files with 6792 additions and 0 deletions
@@ -0,0 +1,47 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace FinlyticTechnicals.Entities;
[Table("fta_candles")]
public class FtaCandleEntity
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
[Required]
[MaxLength(20)]
public string Isin { get; set; } = string.Empty;
[MaxLength(30)]
public string Symbol { get; set; } = string.Empty;
[Required]
[MaxLength(10)]
public string Timeframe { get; set; } = "15m";
[Required]
public DateTime TimestampUtc { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal Open { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal High { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal Low { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal Close { get; set; }
public long Volume { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal? Bid { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal? Ask { get; set; }
}
@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace FinlyticTechnicals.Entities;
[Table("fta_detected_patterns")]
public class FtaDetectedPatternEntity
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
[MaxLength(20)]
public string Isin { get; set; } = string.Empty;
[MaxLength(10)]
public string Timeframe { get; set; } = "15m";
[Required]
[MaxLength(50)]
public string PatternType { get; set; } = string.Empty;
[MaxLength(30)]
public string Category { get; set; } = string.Empty;
[MaxLength(20)]
public string Bias { get; set; } = "Neutral";
[MaxLength(100)]
public string Name { get; set; } = string.Empty;
[Column(TypeName = "decimal(18,4)")]
public decimal KeyPriceLevel { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal UpperBoundary { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal LowerBoundary { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal InvalidationLevel { get; set; }
[Column(TypeName = "decimal(6,2)")]
public decimal QualityScore { get; set; }
public string Description { get; set; } = string.Empty;
public Dictionary<string, object>? ExtraData { get; set; }
[Required]
public DateTime DetectedAtUtc { get; set; }
}
@@ -0,0 +1,37 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace FinlyticTechnicals.Entities;
/// <summary>
/// Persisted backing store for <c>TechnicalUniverseManager</c>'s continuously-scanned asset universe (one row
/// per monitored ISIN). Deliberately NOT meant to survive a service restart - <c>Program.cs</c> clears this
/// table on every startup, since the universe is fully rebuilt within minutes from
/// <c>RefreshFavoritesAsync</c>/<c>RefreshDiscoveryAsync</c> and fresh sentiment-spike events, and a stale row
/// that never got TTL-swept because the process was down is worse than starting from an empty universe.
/// </summary>
[Table("fta_monitored_universe_assets")]
public class FtaMonitoredUniverseAssetEntity
{
[Key]
[MaxLength(20)]
public string Isin { get; set; } = string.Empty;
[MaxLength(30)]
public string? Symbol { get; set; }
/// <summary>String form of <c>FinlyticCore.Dtos.TechnicalAnalysis.UniverseSource</c>.</summary>
[Required]
[MaxLength(20)]
public string Source { get; set; } = string.Empty;
/// <summary>Lower value = higher scan priority (SentimentSpike=1, UserFavorite=2, Discovery=3).</summary>
public int Priority { get; set; }
[Required]
public DateTime AddedAtUtc { get; set; } = DateTime.UtcNow;
/// <summary><see langword="null"/> for favorites/discovery entries, which never expire by TTL.</summary>
public DateTime? ExpiresAtUtc { get; set; }
}
@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using FinlyticCore.Dtos.TechnicalAnalysis;
namespace FinlyticTechnicals.Entities;
[Table("fta_technical_setups")]
public class FtaTechnicalSetupEntity
{
[Key]
public Guid SetupId { get; set; } = Guid.NewGuid();
[Required]
[MaxLength(20)]
public string Isin { get; set; } = string.Empty;
[MaxLength(30)]
public string Symbol { get; set; } = string.Empty;
[Required]
[MaxLength(10)]
public string Timeframe { get; set; } = "15m";
[Required]
[MaxLength(50)]
public string StrategyKey { get; set; } = string.Empty;
[MaxLength(100)]
public string StrategyName { get; set; } = string.Empty;
[MaxLength(10)]
public string Direction { get; set; } = "Buy";
[Column(TypeName = "decimal(6,2)")]
public decimal QualityScore { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal CurrentPrice { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal EntryPrice { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal InvalidationPrice { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal CurrentAtr { get; set; }
[Column(TypeName = "decimal(8,2)")]
public decimal EstimatedRiskRewardRatio { get; set; }
public ExitPlan ExitPlan { get; set; } = null!;
public string TechnicalRationale { get; set; } = string.Empty;
public List<PatternResultDto> TriggeringPatterns { get; set; } = [];
public Dictionary<string, decimal> IndicatorSnapshot { get; set; } = [];
public bool IsTopPick { get; set; }
[MaxLength(5)]
public string Rating { get; set; } = "B";
public bool IsActive { get; set; } = true;
[Required]
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
[Required]
public DateTime ExpiresAtUtc { get; set; }
/// <summary>
/// String form of the <c>UniverseSource</c> this ISIN was being monitored under when this setup was
/// computed (favorite/discovery/sentiment-spike), or <see langword="null"/> for an ad hoc analysis (e.g. a
/// manual "Analyze now" call for an ISIN not currently in the scan universe). See
/// <c>FinlyticCore.Dtos.TechnicalAnalysis.StrategyResultDto.UniverseSource</c>.
/// </summary>
[MaxLength(20)]
public string? UniverseSource { get; set; }
/// <summary>When the ISIN above entered that scan universe, alongside <see cref="UniverseSource"/>.</summary>
public DateTime? UniverseEnteredAtUtc { get; set; }
/// <summary>String form of the <c>MarketRegime</c> at analysis time. See <c>StrategyResultDto.Regime</c>.</summary>
[MaxLength(30)]
public string? Regime { get; set; }
}