Files

138 lines
8.5 KiB
Markdown

# 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`).