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,63 @@
using System;
using FinlyticEngine.Database;
using FinlyticEngine.Services.Trading;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace FinlyticEngine.Tests.TestSupport;
/// <summary>
/// Builds a real <see cref="TradeLifecycleService"/> wired against an EF Core InMemory-backed
/// <see cref="EngineDbContext"/> resolved through a genuine <see cref="IServiceScopeFactory"/> — the same
/// DI shape production code uses (a fresh scoped DbContext per call). This is deliberately NOT a fake
/// DbContext: using the real EngineDbContext against the InMemory provider means the tenant-filtering LINQ
/// predicates in TradeLifecycleService are actually evaluated by EF Core, not bypassed.
///
/// DB approach: see the "DB-Ansatz" section of the final task report for why InMemory was chosen over
/// SQLite and Testcontainers/real Postgres.
/// </summary>
public sealed class TradeLifecycleServiceHarness : IDisposable
{
private readonly ServiceProvider _provider;
public TradeLifecycleService Sut { get; }
public FakeEngineRpcClient RpcClient { get; }
public FakeSettingsService SettingsService { get; }
public IServiceScopeFactory ScopeFactory { get; }
public TradeLifecycleServiceHarness()
{
var dbName = Guid.NewGuid().ToString("N");
var services = new ServiceCollection();
services.AddDbContext<EngineDbContext>(o => o.UseInMemoryDatabase(dbName));
_provider = services.BuildServiceProvider();
ScopeFactory = _provider.GetRequiredService<IServiceScopeFactory>();
RpcClient = new FakeEngineRpcClient();
SettingsService = new FakeSettingsService();
Sut = new TradeLifecycleService(
ScopeFactory,
new NeverInvokedCompositeOpportunityScorer(),
new NeverInvokedAiReasoningGateService(),
new NeverInvokedKnockOutDerivativeResolver(),
RpcClient,
SettingsService,
new FakeFinlyticLogger<TradeLifecycleService>());
}
/// <summary>
/// Opens a fresh scope and returns its <see cref="EngineDbContext"/>, mirroring how the service itself
/// obtains a DbContext per call. Caller is responsible for disposing the returned scope via
/// <see cref="IServiceScope"/> semantics (use inside a <c>using</c> block on the returned context's
/// owning scope where needed) — for simplicity in tests we just dispose the DbContext itself, since the
/// InMemory provider keeps data keyed by database name, not by context instance.
/// </summary>
public EngineDbContext OpenDbContext()
{
var scope = ScopeFactory.CreateScope();
return scope.ServiceProvider.GetRequiredService<EngineDbContext>();
}
public void Dispose() => _provider.Dispose();
}