# 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 --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` / `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 `/// ` (custom helper methods still need their own explicit docs). - Strongly-typed data classes only for state passing, API payloads, and MQTT messages — no `Dictionary`, `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`/`IOptionsMonitor`, and `FinlyticBackend` fails fast at startup if `JWT_SECRET_KEY`/`ADMIN_DEFAULT_PASSWORD` are missing or weak (see `Program.cs`).