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 testwill also try to runFinlyticEngine.Tests/_Verify/PostgresVerificationTests.cs, which connects to a real, throwaway local Postgres container onlocalhost:55987and 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.Testscurrently 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).
FinlyticBackendis the only service allowed to host Kestrel/HTTP/WebSocket endpoints. It is a thin aggregation gateway forFinlyticApp: 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 areMicrosoft.NET.Sdk.Workerbackground services with no HTTP surface at all — this is an enforced rule (Rules.md§5), not a stylistic default. See thecompose.yamlcomment block explaining why onlyfinlyticbackendhas a DockerHEALTHCHECK.- Every other service follows the same internal shape:
Database/(DbContext + migrations),Entities/(EF entities),Services/(business logic + the service's MQTT client),Util/, and aProgram.csthat 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:
- RPC (
services/request/{channel}/{correlationId}→services/response/{channel}/{correlationId}), implemented byFinlyticCore/Util/ManagedMqttClient.cs(SendRpcRequestAsync/SubscribeRpcAsync). Used for synchronous-style query/command calls, e.g.FinlyticBackendaskingFinlyticAssetsto resolve an ISIN, orFinlyticEngineaskingFinlyticSentimentfor a sentiment summary. - 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'sBackendMqttBridgesubscribes to the wildcard form of most of these (*Wildcardconstants) 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 rawJObject/JsonDocumentin internal logic. Shared DTOs/entities/enums used by more than one service belong inFinlyticCore; 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 trailingCancellationTokenand pass it through. - Every
FinlyticBackendHTTP/WebSocket endpoint requires[Authorize]unless explicitly[AllowAnonymous](only/api/v1/auth/login-style endpoints and the/healthcheck are exempt). - No hardcoded secrets; bind config via
IOptions<T>/IOptionsMonitor<T>, andFinlyticBackendfails fast at startup ifJWT_SECRET_KEY/ADMIN_DEFAULT_PASSWORDare missing or weak (seeProgram.cs).