All work

AI Platform · Machine Learning

InsightAide

AI-driven investment research & analysis platform

.NET 10ASP.NET CorePostgreSQL + pgvectorEF CoreML.NET / LightGBMClaude APIOpenAI APIPolygon.ioRailway / Docker

The problem

InsightAide is a quantitative investment-research platform built end-to-end by one person on .NET 10. The goal was a system that could ingest market data, engineer features, train and rank models, and layer LLM reasoning on top — with the architectural discipline to stay maintainable as it grew to 25 projects, and the statistical rigor to avoid fooling itself.

  • Financial ML is dangerously easy to overfit — a naive backtest leaks future information and looks brilliant until it meets real data.
  • LLM costs and vendor lock-in balloon quickly without a routing and accounting layer.
  • A solo-built system spanning data, ML, and API surface has to stay modular, or it becomes impossible to change safely.

Architecture

The solution is a strict four-tier clean architecture spanning 25 projects. Dependencies point inward — Presentation depends on the domain engines and application services, which depend on Foundation, never the reverse. Each engine is an isolated, testable unit, so the ML pipeline can evolve without touching the API and vice versa.

Presentation

Entry points
APIClient

Domain engines

11 modules
IngestionFeature EngineeringModelingEvaluationServing

Application

Services
IdentityNotificationsAnalytics

Foundation

Core + infrastructure
CoreCommonDomainInfrastructure

Dependencies point downward — inner layers never depend on outer ones.

Engineering in depth

The snippets below are illustrative of the techniques used — simplified to show the approach without exposing proprietary signal logic.

RoutingLlmProvider.cs
// Route each request to the right provider by model prefix,
// then record token usage + estimated cost for every call.
public async Task<LlmResponse> CompleteAsync(LlmRequest request, CancellationToken ct)
{
    ILlmProvider provider = request.Model switch
    {
        var m when m.StartsWith("claude-") => _anthropic,
        var m when m.StartsWith("gpt-")    => _openAi,
        _                                  => _default,
    };

    var response = await provider.CompleteAsync(request, ct);

    await _usageLog.RecordAsync(new ApiUsage(
        provider:         provider.Name,
        model:            request.Model,
        inputTokens:      response.Usage.InputTokens,
        outputTokens:     response.Usage.OutputTokens,
        estimatedCostUsd: _pricing.Estimate(request.Model, response.Usage)), ct);

    return response;
}
A single entry point routes claude-* and gpt-* models to the right vendor and logs per-call cost — so provider choice never leaks into calling code, and every token is accounted for.
WalkForwardValidator.cs
// Drop any training row whose label window overlaps the test fold,
// plus an embargo buffer on each side.
private IReadOnlyList<Sample> PurgeAndEmbargo(
    IReadOnlyList<Sample> train,
    DateRange testFold,
    TimeSpan labelWindow,
    TimeSpan embargo)
{
    var lower = testFold.Start - labelWindow - embargo;
    var upper = testFold.End + embargo;

    return train
        .Where(s => s.Timestamp < lower || s.Timestamp > upper)
        .ToList();
}
Even a correct train/test split leaks when a label's outcome window straddles the boundary. Purging removes those rows and the embargo adds a buffer — so the model can never train on information bleeding out of the test period. This is the core defense against lookahead bias.
RankingModelTrainer.cs
// Persist a model only if its out-of-sample top-vs-bottom spread
// is statistically significant — not merely positive.
var spread = oos.TopDecileReturn - oos.BottomDecileReturn;
var tStat  = spread.Mean / spread.StdError;

if (tStat < MinTStat)   // MinTStat = 1.0
{
    _log.LogInformation(
        "Rejecting model: OOS spread t-stat {T:F2} below gate", tStat);
    return SaveResult.Rejected;
}

await _registry.SaveAsync(model, metrics, ct);
Positive backtest returns are easy to get by luck. Gating on a t-statistic means a model must prove its top-vs-bottom ranking spread is unlikely to be noise on unseen data before it's ever saved.

Key decisions

No live trading, by design

Keeping the system research-and-analysis only removes execution risk and regulatory surface entirely, and lets the engineering focus stay on signal quality and evaluation rigor rather than order routing.

In-process ML.NET over a Python microservice

Running LightGBM natively in .NET keeps the whole platform a single deployable — no cross-language serialization, no extra service to operate, no model-server drift between training and inference.

A router in front of the LLMs

Abstracting Claude and OpenAI behind one provider interface avoids vendor lock-in, allows routing by capability and cost, and makes per-call accounting a first-class concern instead of an afterthought.