Case Study · AI-First Build

Inside GMI: Claude and ML.NET in production

Grade My Investments — the SaaS Leopard Data built to prove its AI-first thesis — runs two very different kinds of intelligence side by side: ML.NET for deterministic, repeatable financial math, and Anthropic Claude for language: document analysis, interpretation, and narrative. This is how they fit together in production, with the real services and numbers from the codebase — the same patterns we bring to client work.

In plain English

GMI puts AI to work on real stock data in two ways. First, machine learning (ML.NET) is baked into the stock statistics — it forecasts each company’s revenue, debt, cash flow, and equity, flags anomalies, and rolls everything into a letter-grade health score. Second, Claude lets you ask plain-language questions and get answers about your uploaded report files and the AI report dashboard. This page shows how both are built.

The big picture

A report starts deterministic and ends conversational. Gmi.ReportEngine.Console orchestrates jobs (four concurrent) and spawns Gmi.ReportGeneratorAlpha.Console per report. That generator pulls fundamentals from Financial Modeling Prep, runs the ML.NET forecasting layer, and writes Excel and JSON outputs to Azure Blob Storage. Only afterward, on demand, does Claude enter — analyzing the documents a user chooses, under a hard cost cap.

flowchart TD
    ENGINE["Gmi.ReportEngine.Console -- orchestrator, 4 concurrent"] --> ALPHA["Gmi.ReportGeneratorAlpha.Console -- one per report"]
    ALPHA -->|"13 calls per symbol, 3000 per min token bucket"| FMP["Financial Modeling Prep API"]
    FMP --> ALPHA
    ALPHA --> ML["ML.NET Forecasting Service"]
    ML --> OUT["ml-forecasts.xlsx plus JSON companions"]
    ALPHA --> BLOB["Azure Blob Storage"]
    OUT --> BLOB
    BLOB --> NOTIFY["Email and SMS notify user"]
    BLOB -.->|"on demand, user selected files"| CLAUDE["Claude analysis layer"]
                    
End-to-end: ML.NET runs inside every report; Claude is an on-demand layer on top.

The ML.NET engine — the deterministic grade

The grade is not an LLM opinion. ML.NET (Microsoft.ML 5.0 with Microsoft.ML.TimeSeries, seeded at 0 for reproducibility) drives every number that has to be repeatable. Singular Spectrum Analysis (SSA) forecasts revenue, debt, cash flow, and shareholder equity four quarters out with 95% confidence intervals; if SSA can’t train on the available history, it falls back to linear regression (1.96 × standard error). A separate SDCA multiclass classifier turns daily prices plus RSI, MACD, and volatility into a Bullish / Neutral / Bearish sentiment signal, and SSA spike detection flags anomalies in debt-to-equity. Those feed a composite 0–100 health score and the familiar A+ to F rating.

flowchart TD
    Q["Quarterly financials"] --> SSA["SSA forecast -- 4 quarters, 95 percent CI"]
    SSA -->|"if training fails"| LR["Linear regression fallback -- 1.96 x std error"]
    SSA --> F["Revenue, Debt, Cash Flow, Equity forecasts"]
    LR --> F
    P["Daily prices plus RSI, MACD, volatility"] --> SDCA["SDCA multiclass classifier"]
    SDCA --> SENT["Bullish, Neutral or Bearish"]
    DE["Debt-to-equity ratios"] --> SPIKE["SSA spike anomaly detection"]
    F --> HS["Composite health score, 0 to 100"]
    SENT --> HS
    SPIKE --> HS
    HS --> RATING["Green above 70, Yellow 40 to 70, Red below 40"]
                    
Deterministic by design: the same inputs always produce the same grade.
GMI per-symbol ML forecasts for AAPL — revenue, cash flow, debt, and equity with 95 percent confidence bands, an ML health score of 100 grade A, and a Strong Buy ML signal
Per-symbol ML.NET forecasts with 95% confidence bands, the composite health score, and the ML signal.
GMI AI report dashboard — 25 symbols analyzed, ML average health score 90.7, zero anomalies detected, 24 of 25 ML buy recommendations, and portfolio forward projections
Portfolio-level rollup: average health score, anomaly detection, ML buy recommendations, and forward projections.

The Claude layer — language and analysis

Where judgment and language help, Claude takes over. A user right-clicks any file from a finished report — PDF, DOCX, XLSX, or an image (handled by the vision model) — and asks for analysis. Cost transparency is the whole point: the API returns a token estimate first, the job is capped at $45, and nothing runs until the user approves the spend. The background Gmi.ClaudeAnalysis.Console then works the queue: polling every 15 seconds, a batch of 5, three concurrent calls behind a SemaphoreSlim, three retries with exponential backoff (5s, 15s, 45s), and graceful 429 handling that honors Anthropic’s Retry-After. Each model can be chosen per job — Haiku for cost-optimized passes, Sonnet as the analysis default, Opus for the deepest reasoning — and results are saved as DOCX and HTML alongside the original file.

sequenceDiagram
    participant U as User
    participant Web as Gmi.ClientWeb.Blazor
    participant Api as Gmi.Api.AzureFunctions
    participant Db as MySQL job queue
    participant Svc as Gmi.ClaudeAnalysis.Console
    participant Cl as Anthropic API
    participant Blob as Azure Blob Storage
    U->>Web: Select report files to analyze
    Web->>Api: POST claude-analysis estimate
    Api-->>Web: Token and cost estimate, 45 dollar cap
    U->>Web: Approve spend
    Web->>Api: POST claude-analysis submit
    Api->>Db: Create job plus per-file requests
    loop Poll 15s, batch of 5, 3 concurrent
        Svc->>Db: Claim pending requests
        Svc->>Cl: Analyze file, max 8192 tokens
        Cl-->>Svc: Structured analysis
        Svc->>Blob: Save DOCX and HTML
    end
    Note over Svc,Cl: 3 retries with backoff 5s 15s 45s, and 429 responses honor Retry-After
    Svc->>Db: Record input and output tokens and cost
    Svc-->>U: Notify complete
                    
Estimate, approve, then process — a metered, queue-driven analysis pipeline.

The same estimate-approve-meter pattern powers the conversational “Ask Claude” flow below: a token cost and the chosen model are shown up front, the job runs server-side, and Claude returns a structured result grounded in the report’s numbers.

GMI Ask Claude dialog — a user poses the question 'What would you characterize this portfolio type as?' against a 25-symbol report
Ask Claude — pose a question against the report data.
GMI Ask Claude cost estimate — estimated cost $0.0870 using model claude-sonnet-4-6, with a Confirm and Analyze button, shown before the call runs
A token cost estimate and the model (claude-sonnet-4-6) are shown before you commit.
GMI Ask Claude processing state — 'Claude is analyzing... this may take 15 to 30 seconds'
The approved request is processed server-side and metered as it runs.
GMI Ask Claude result — a structured markdown 'Portfolio Characterization' analysis with overview and key characteristics sections
Claude returns a structured, markdown analysis grounded in the report’s numbers.

Cost, safety, and billing

Every Claude call is metered server-side. Token usage is written to a ledger (ClaudeAnalysisJob and ClaudeAnalysisRequest track input tokens, output tokens, and actual cost), priced with a fixed markup, and reconciled against payment by Gmi.BillingEngine.Console and Stripe webhooks guarded by idempotency keys so a retry never double-charges. On the safety side, a strict system prompt keeps Claude in bounds — it is explicitly not an investment advisor, never gives personalized buy/sell recommendations, and appends a compliance disclaimer to every response. The deterministic ML.NET grade, not the model, remains the source of truth.

flowchart TD
    EST["Server-side token estimate"] --> CAP["Enforce 45 dollar per-job cap"]
    CAP --> CALL["Metered Claude calls"]
    CALL --> LEDGER["Usage ledger -- input tokens, output tokens, actual cost"]
    LEDGER --> BILL["Gmi.BillingEngine.Console"]
    BILL --> STRIPE["Stripe"]
    STRIPE -->|"webhook with idempotency key"| RECON["Reconcile payment and usage -- no double charge"]
                    
AI spend is always estimated, capped, metered, and reconciled.

The components behind it

Gmi.ReportEngine.Console

Job orchestrator; claims and runs report jobs four at a time.

Gmi.ReportGeneratorAlpha.Console

Per-report worker: FMP download, ML.NET forecasting, Excel/JSON output.

Gmi.ClaudeAnalysis.Console

Queue-driven Claude file analysis with batching, retries, and the $45 cap.

Gmi.ReportComparison.Console

Claude-written narrative diff of two report runs, rendered to DOCX.

Gmi.BillingEngine.Console

Queue-driven billing that reconciles usage with Stripe, idempotently.

Gmi.Api.AzureFunctions

The .NET-isolated REST surface, including the estimate and submit endpoints.

.NET 10 / C# ML.NET 5.0 ML.NET TimeSeries (SSA) SDCA Classifier Anthropic Claude Claude Haiku / Sonnet / Opus Azure Functions Azure Blob Storage MySQL / EF Core Stripe Pulumi Financial Modeling Prep

How we’d apply this to your systems

This is the pattern we bring to client work: deterministic code where repeatability matters, AI where language and judgment help, and a metering-and-approval layer so AI spend is always visible and bounded. ML.NET owns the grade; Claude explains it. Neither one is asked to do the other’s job — and that separation is exactly what makes the system safe to put in front of paying users and auditors alike.

Putting AI into a product or platform?

Leopard Data architects AI/ML integrations that ship to production — with the cost controls and governance to match.