feat(engine): add FinlyticEngine microservice with trade lifecycle, AI reasoning gate, composite scoring, and unit tests

This commit is contained in:
2026-08-24 21:37:05 +02:00
parent a4959658a2
commit 5c95dd182c
49 changed files with 7709 additions and 0 deletions
@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace FinlyticEngine.Database.Entities;
/// <summary>
/// Minimal per-cycle audit record for <c>OpportunityPollerBackgroundService</c>: which technical top-picks
/// FinlyticTechnicals returned for a given scan cycle, before <c>ITradeLifecycleService.EvaluateAssetAsync</c>
/// was called for each of them. This intentionally captures only the ENGINE-SIDE candidate set (the
/// already-filtered <c>ta_GetSetups</c> response, capped by <see cref="RequestedLimit"/> and
/// <see cref="RequestedMinScore"/>) - not the full FinlyticTechnicals scan universe (favorites/discovery/
/// sentiment-spike ISINs it monitors before that filter is even applied). See the Task 3 findings in the
/// implementing task report for why the broader, pre-filter universe is out of scope here: it lives entirely
/// inside FinlyticTechnicals (<c>TechnicalUniverseManager</c>), which this task was not scoped to touch.
/// </summary>
[Table("engine_scan_cycles")]
public class EngineScanCycleEntity
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
public DateTime CycleStartedAtUtc { get; set; } = DateTime.UtcNow;
/// <summary>The <c>Limit</c> the poller requested from FinlyticTechnicals' <c>ta_GetSetups</c> for this cycle.</summary>
public int RequestedLimit { get; set; }
/// <summary>The <c>MinScore</c> the poller requested from FinlyticTechnicals' <c>ta_GetSetups</c> for this cycle, if any.</summary>
[Column(TypeName = "decimal(6,2)")]
public decimal? RequestedMinScore { get; set; }
/// <summary>Number of candidates FinlyticTechnicals actually returned (i.e. <c>CandidateIsins.Count</c>).</summary>
public int CandidatesReturnedCount { get; set; }
/// <summary>
/// ISINs of the technical top-picks returned for this cycle - exactly the set
/// <c>OpportunityPollerBackgroundService</c> went on to call <c>EvaluateAssetAsync</c> for, in the order
/// FinlyticTechnicals returned them (best quality-score first). Persisted as a JSON array (see
/// <c>EngineDbContext</c>'s <c>List&lt;string&gt;</c> value converter) rather than a delimited string, so it
/// stays a real typed collection on this side of the mapping (Rules.md §3).
/// </summary>
public List<string> CandidateIsins { get; set; } = new();
}