Files
Finlytic/CLAUDE.md
T

8.5 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this is

Finlytic is a personal financial analysis/trading platform: a fleet of .NET 10 microservices communicating over MQTT, one HTTP/WebSocket gateway (FinlyticBackend), and a Flutter app (FinlyticApp) as the only client. There is no CI/multi-developer setup — this is a single-developer repo backed by one shared PostgreSQL instance ("OmniDB") and one MQTT broker, both external to this repo.

Commands

Backend (.NET)

dotnet build Finlytic.sln              # build everything
dotnet build FinlyticEngine            # build a single project
dotnet test FinlyticEngine.Tests       # run one test project
dotnet test FinlyticEngine.Tests --filter "FullyQualifiedName~TradeLifecycleServiceTests"
  • Solution-wide dotnet test will also try to run FinlyticEngine.Tests/_Verify/PostgresVerificationTests.cs, which connects to a real, throwaway local Postgres container on localhost:55987 and has no [Skip]/trait to exclude it. It fails outside that one-off setup — run test projects individually, or filter it out with --filter "FullyQualifiedName!~_Verify", rather than running the whole solution's tests.
  • FinlyticBot.Tests currently has no test files (scaffolding only).
  • EF Core migrations are created via the CLI only (never hand-written) from inside the owning service project, e.g.: dotnet ef migrations add <Name> --project FinlyticAssets.

Frontend (Flutter, FinlyticApp/)

flutter pub get
flutter run                            # or -d chrome for the web dashboard
flutter test
flutter analyze

Docker Compose (compose.yaml)

The compose file only defines this repo's own services — Postgres and the MQTT broker are external infrastructure (see the comment block at the top of compose.yaml for required env vars: DB_HOST, DB_PORT, DB_PASSWORD, MQTT_HOST, MQTT_PORT, plus per-service secrets like JWT_SECRET_KEY, ADMIN_DEFAULT_PASSWORD, ALPACA_KEY_ID/ALPACA_SECRET_KEY). Set these in a local .env file.

FinlyticNews and FinlyticFundamentals build from a shared Playwright base image that must exist first:

docker compose --profile build-base build finlytic-playwright-base
docker compose build

Keep the PLAYWRIGHT_VERSION build arg in compose.yaml in sync with the Microsoft.Playwright NuGet package version in FinlyticCore/FinlyticCore.csproj.

Architecture

Service topology

Nine .NET projects share one solution (Finlytic.sln): FinlyticCore (shared library, no entry point) plus eight deployable services — FinlyticAssets, FinlyticNews, FinlyticFundamentals, FinlyticSentiment, FinlyticTechnicals, FinlyticEngine, FinlyticSimulation, FinlyticBot, and FinlyticBackend. Each non-Core service is its own Docker image, has its own PostgreSQL database (finlytic_assets, finlytic_news, ... one per service — never shared tables across services), and runs its own EF Core migrations independently at startup via DbContext.MigrateWithBootstrapAsync (FinlyticCore/Database/DatabaseBootstrapper.cs).

  • FinlyticBackend is the only service allowed to host Kestrel/HTTP/WebSocket endpoints. It is a thin aggregation gateway for FinlyticApp: its controllers and SignalR hubs (Hubs/) largely proxy to the backing microservices over MQTT RPC and rebroadcast their MQTT events to connected clients via SignalR (BackendMqttBridge). All other services are Microsoft.NET.Sdk.Worker background services with no HTTP surface at all — this is an enforced rule (Rules.md §5), not a stylistic default. See the compose.yaml comment block explaining why only finlyticbackend has a Docker HEALTHCHECK.
  • Every other service follows the same internal shape: Database/ (DbContext + migrations), Entities/ (EF entities), Services/ (business logic + the service's MQTT client), Util/, and a Program.cs that wires DI, registers the service's MQTT client as a hosted service, and migrates the DB at startup.

Inter-service communication (MQTT-only)

All cross-service calls go over MQTT — never direct HTTP between backend services. FinlyticCore/Util/MqttTopics.cs is the single source of truth for every topic and RPC channel name; add new channels there rather than inlining topic strings at call sites. Two patterns are used:

  1. RPC (services/request/{channel}/{correlationId}services/response/{channel}/{correlationId}), implemented by FinlyticCore/Util/ManagedMqttClient.cs (SendRpcRequestAsync / SubscribeRpcAsync). Used for synchronous-style query/command calls, e.g. FinlyticBackend asking FinlyticAssets to resolve an ISIN, or FinlyticEngine asking FinlyticSentiment for a sentiment summary.
  2. Pub/sub event streams (e.g. finlytic/news/stream/{isin}, finlytic/engine/proposals/created, finlytic/bot/trades/stream, finlytic/logs/{service}) for fire-and-forget notifications. FinlyticBackend's BackendMqttBridge subscribes to the wildcard form of most of these (*Wildcard constants) purely to relay them onward to SignalR clients.

Every service also implements a shared health_Ping RPC channel and exposes runtime-configurable settings via a {service}_settings_GetAll / {service}_settings_Update channel pair (backed by ISettingsService and IOptionsMonitor-style dynamic config — see FinlyticCore/Services/Settings) — changing a setting does not require a service restart, and channel-level logging enable/disable follows the same mechanism (see IFinlyticLogger<T> / FinlyticLogBroadcaster in FinlyticCore/Services/FinlyticLogger).

Domain flow (roughly upstream → downstream)

FinlyticAssets (asset/ISIN resolution, Trade Republic price feed) and FinlyticNews/FinlyticFundamentals (scraped/ingested data, using Playwright) feed FinlyticSentiment (FinBERT analysis) and FinlyticTechnicals (indicators/strategy setups), which feed FinlyticEngine (trade proposal generation, AI-assisted validation via an n8n webhook, trade lifecycle management) and FinlyticSimulation (backtesting/strategy reliability scoring). FinlyticBot executes accepted proposals as paper trades (via Alpaca or a synthetic ledger). FinlyticBackend sits above all of them as the gateway FinlyticApp talks to.

Flutter app (FinlyticApp/)

Feature-folder structure under lib/features/* (auth, dashboard, discovery, trades, bot, simulation, news, calendar, favorites, search, asset_detail, proposals, admin), with lib/core/ for cross-cutting network (Dio + interceptors), theme, and services, and lib/shared/ for shared widgets. State management is flutter_bloc. Every outgoing request must carry the JWT via a central Dio interceptor, and a 401/403 must trigger an immediate client-side logout — this is an enforced rule (Rules.md §8), not optional handling.

Project-wide rules (Rules.md)

Rules.md is a binding rules document for this repo, not a style guide — treat every rule as an architectural constraint to actively check against, not background reading. Full text is in Rules.md; key points, since they shape most non-trivial changes:

  • Every service interface + implementation lives in one file, named after the implementation class.
  • Every method (any visibility) needs XML doc comments in English; implementations of interface/base methods use /// <inheritdoc /> (custom helper methods still need their own explicit docs).
  • Strongly-typed data classes only for state passing, API payloads, and MQTT messages — no Dictionary<string, object>, dynamic, or raw JObject/JsonDocument in internal logic. Shared DTOs/entities/enums used by more than one service belong in FinlyticCore; service-specific models (e.g. raw third-party API shapes) stay in that service.
  • No mock/demo/fallback data anywhere, backend or frontend. Empty results are either a real empty set or an explicit exception — the Flutter UI must show an explicit empty state, never placeholder content.
  • Async all the way (no .Result/.Wait()/.GetAwaiter().GetResult()); async DB/MQTT/network methods take a trailing CancellationToken and pass it through.
  • Every FinlyticBackend HTTP/WebSocket endpoint requires [Authorize] unless explicitly [AllowAnonymous] (only /api/v1/auth/login-style endpoints and the /health check are exempt).
  • No hardcoded secrets; bind config via IOptions<T>/IOptionsMonitor<T>, and FinlyticBackend fails fast at startup if JWT_SECRET_KEY/ADMIN_DEFAULT_PASSWORD are missing or weak (see Program.cs).