CFD instrument technical and fundamental analysis application. Provides market data retrieval with multi-provider fallback, technical indicators, and analysis endpoints via a REST API.
- Report bugs and feature ideas through GitHub Issues: https://github.com/Finfinder/Investment-Assistant/issues
- Larger goals are tracked with milestones and the pinned roadmap issue.
- Collaboration notes live in CONTRIBUTING.md.
- Python 3.13 / FastAPI / Pydantic 2
- SQLAlchemy 2 (async, SQLite for MVP)
- Redis (caching with fallback to in-memory)
- Data providers: yfinance (primary), Twelve Data, Financial Modeling Prep
- Analysis: pandas-ta, TA-Lib
- Next.js 15 (App Router) / TypeScript 5
- TailwindCSS 3.4 with CSS custom properties (dark theme)
- lightweight-charts v5 for interactive candlestick charts
- Docker Compose for full-stack deployment
- Python 3.12β3.13
- TA-Lib C library installed on the system
cd backend
python -m venv .venv
# Windows
.venv\Scripts\activate
# Linux/macOS
source .venv/bin/activate
pip install -e ".[dev]"
cp .env.example .env
# Edit .env with your API keys (optional β yfinance works without keys)
uvicorn app.main:app --reloadcd frontend
npm install
npm run devThe frontend is available at http://localhost:3000.
cp backend/.env.example backend/.env
# Edit backend/.env with your API keys
docker compose up --buildThe application is available at http://localhost (nginx reverse proxy). Health check: GET /api/v1/health. API documentation: http://localhost/api/v1/docs.
The local docker compose stack builds the same backend, frontend, and nginx Dockerfiles that are published during tagged releases.
Tagged releases publish three container images to GHCR:
ghcr.io/<owner>/<repo>-backendghcr.io/<owner>/<repo>-frontendghcr.io/<owner>/<repo>-nginx
Each image is published with a semver tag matching the release, an immutable sha-<commit> tag, and latest only for stable tags without a prerelease suffix.
All three base images are pinned by their @sha256 digest (build arguments NODE_IMAGE_DIGEST in frontend/Dockerfile, PYTHON_IMAGE_DIGEST in backend/Dockerfile, and NGINX_IMAGE_DIGEST in nginx/Dockerfile) to guard against supply-chain attacks and ensure reproducible builds (IA-163 / #220, IA-164 / #222). The digests are the single source of truth, so release.yml and docker-compose.yml inherit them without passing build arguments. The mutable image tags are parameterized via build arguments whose defaults come from single-source-of-truth files (IA-167 / #228): frontend/.nvmrc (NODE_VERSION), backend/.python-version (PYTHON_VERSION, suffixed with -slim for the image), and nginx/.nginxrc (NGINX_VERSION). release.yml reads these files and passes --build-arg, so release builds track version bumps automatically. docker-compose.yml also passes them via build.args, but uses hardcoded fallback defaults (e.g. ${NGINX_VERSION:-1.27-alpine}) that must be updated in the compose file (or overridden by exporting NGINX_VERSION/PYTHON_VERSION/NODE_VERSION) to keep local builds aligned with the SSOT files. Rotate a digest when its upstream image is rebuilt with a security fix:
docker buildx imagetools inspect node:$(cat frontend/.nvmrc)-alpine # frontend
docker buildx imagetools inspect python:$(cat backend/.python-version)-slim # backend
docker buildx imagetools inspect nginx:$(cat nginx/.nginxrc) # nginx
# use the value from the "Digest:" line (manifest list / index digest)To bump a base image version, edit the corresponding single-source-of-truth file (and rotate its digest); the Dockerfile default and CI consistency checks follow automatically. For local docker compose build, also update the hardcoded fallback defaults in docker-compose.yml (or export NGINX_VERSION/PYTHON_VERSION/NODE_VERSION) so local builds stay aligned.
GitHub Release notes are generated from CHANGELOG.md. The supported release artifacts are container images only; this repository does not publish a frontend npm package or a backend PyPI package.
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β Frontend β β Backend β β Redis β
β Next.js 15 βββββββΊβ FastAPI βββββββΊβ Cache β
β :3000 β β :8000 β β :6379 β
ββββββββ¬ββββββββ ββββββββ¬ββββββββ ββββββββββββββββ
β β
ββββββββββ¬βββββββββββββ
β
ββββββββΌβββββββ
β nginx β
β :80 β
βββββββββββββββ
The backend is organized into independent domain modules that communicate through core models:
backend/app/
βββ api/v1/ # REST + WebSocket endpoints
βββ core/ # Settings, database, shared models, logging
βββ modules/
βββ data_acquisition/ # Multi-provider market data (yfinance, Twelve Data, FMP)
βββ technical_analysis/ # 9 oscillators, 12 MAs, 5 pivot types
βββ pattern_recognition/ # Candlestick, S/R, Fibonacci, IKI, geometric
βββ fundamental_analysis/ # Forex/commodity/index macro analysis (FRED, OECD SDMX, BLS, StatCan, BFS, FMP)
βββ signal_aggregation/ # Weighted signal scoring and consolidation
βββ strategy_generator/ # Entry/exit scenarios with SL/TP levels
Import boundaries are enforced by import-linter contracts defined in backend/pyproject.toml ([tool.importlinter]). The formal domain contract documenting bounded contexts, responsibilities, public APIs and the dependency direction of these modules lives in backend/domain.md β it is the single source of truth for module boundaries and review.
The mutation testing quality gate enforces a single, shared mutation score threshold for both the backend (mutmut) and the frontend (Stryker). The threshold is the single source of truth in mutation-threshold.json at the repository root:
{ "mutationScoreThreshold": 70 }- Backend (mutmut):
backend/scripts/run_mutmut.pyresolves the threshold viaload_default_min_score(), which readsmutationScoreThresholdfrom the shared file. It can be overridden locally by theMUTATION_SCORE_THRESHOLDenvironment variable (seebackend/.env.example) or the--min-scoreCLI argument (highest priority). - Frontend (Stryker):
frontend/stryker.conf.jsreads the samemutationScoreThresholdvalue for itsthresholds.breaksetting.
To change the gate, edit the single value in mutation-threshold.json β both gates pick it up automatically. The reusable mutation-testing workflow (reusable-mutation-testing.yml) reads the file when no min-score input is provided, so no literal 70 remains hardcoded in CI.
The application is deployed as a single Docker Compose stack of four services. nginx is the only service publishing a port to the host; backend, frontend and redis communicate exclusively over the internal Compose network.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Docker Compose Stack β
β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββ β
β β nginx β β backend β β redis β β
β β :80 (host) βββββΊβ :8000 βββββΊβ :6379 β β
β β reverse proxyβ β FastAPI β β cache β β
β ββββββββ¬ββββββββ ββββββββ¬ββββββββ ββββββββββββββββββββββββ β
β β β β
β β ββββββββββββββΌβββββββββββββ β
β βββββββΊβ frontend β β
β β :3000 β β
β β Next.js 15 β β
β ββββββββββββββββββββββββββββ β
β β
β Volumes: redis-data β backend-data β nginx-logs β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
nginx terminates all inbound traffic on :80 and proxies /api/ and /api/v1/ws/ to backend:8000, while everything else is served by frontend:3000. redis is reached only by the backend (the nginx β backend β redis arrow in the diagram denotes the backend's cache dependency, not a direct proxy path from nginx). backend depends on redis:6379 for caching (with in-memory fallback). Startup order is enforced by depends_on conditions: redis (healthy) β backend (healthy) β frontend (started) β nginx. Note that nginx.depends_on.frontend uses condition: service_started (not a health condition), while the other dependencies wait on service_healthy.
Production note: the stack terminates plain HTTP on
:80. The HTTPS redirect innginx/nginx.confis commented out and must be enabled (with TLS certificates provisioned) before exposing the stack to production traffic. See the Security section for the security headers applied at the proxy layer.
| Service | Container port | Published to host | Notes |
|---|---|---|---|
| nginx | 80 | Yes (:80) |
Only externally reachable entry point |
| backend | 8000 | No | Internal only, behind nginx |
| frontend | 3000 | No | Internal only, behind nginx |
| redis | 6379 | No | Internal only, protected by REDIS_PASSWORD |
| Volume | Mount point | Purpose |
|---|---|---|
redis-data |
/data (redis) |
Persistent Redis cache data |
backend-data |
/app/data (backend) |
Persistent backend data (SQLite DB, etc.) |
nginx-logs |
/var/log/nginx (nginx) |
Nginx access/error logs |
| Service | Check | Interval | Timeout | Retries | Start period |
|---|---|---|---|---|---|
| redis | redis-cli ping (with REDISCLI_AUTH) |
10s | 5s | 3 | β |
| nginx | wget -qO- http://127.0.0.1/api/v1/health |
30s | 5s | 3 | 10s |
| backend | urllib.request.urlopen('http://localhost:8000/api/v1/health') |
30s | 10s | 3 | 10s |
| frontend | wget -qO- http://127.0.0.1:3000 |
30s | 5s | 3 | β |
All services use restart: unless-stopped.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/health |
Health check |
| GET | /api/v1/market-data/{symbol} |
Fetch OHLCV data for a CFD symbol |
| POST | /api/v1/technical-analysis |
Run full technical analysis |
| POST | /api/v1/patterns |
Detect chart and candlestick patterns |
| POST | /api/v1/fundamental-analysis |
Run fundamental analysis for a symbol |
| POST | /api/v1/analysis |
Trigger full analysis pipeline (async) |
| GET | /api/v1/analysis/{id} |
Get analysis result or status |
| GET | /api/v1/analysis/{id}/status |
Get analysis progress status |
| WS | /api/v1/ws/analysis/{id} |
WebSocket for live status updates |
GET /api/v1/market-data/EURUSD?timeframe=H1&period=30d
Supported symbols include forex pairs (EURUSD, GBPUSD, β¦), commodities (GOLD, SILVER, β¦), indices (US500, US30, β¦), and more. Timeframes: M15, H1, H4, D1. Period format: {n}d, {n}m, {n}y.
POST /api/v1/technical-analysis
{"symbol": "EURUSD", "timeframe": "H1", "period": "90d"}
Returns 9 oscillator/momentum indicators (RSI, MACD, Stochastic, CCI, ADX, AO, Momentum, Williams %R, Ultimate Oscillator), 12 moving averages (SMA + EMA for periods 5β200), 5 pivot point types (Classic, Fibonacci, Camarilla, Woodie, DeMark), and an aggregated signal summary.
POST /api/v1/patterns
{"symbol": "EURUSD", "timeframe": "H1", "period": "180d"}
Detects candlestick patterns (28 types via TA-Lib, scanned across last 10 candles) with reliability rating (β ββ β β ), signal indication and Polish description. Also detects support/resistance levels with strength scoring, Fibonacci retracement levels, IKI (Impulse-Correction-Impulse) patterns, and geometric chart patterns (triangle, wedge, flag, pennant).
POST /api/v1/fundamental-analysis
{"symbol": "EURUSD"}
Automatically routes to the correct analyzer based on instrument type. Forex pairs compare interest rate and inflation differentials between base and quote currencies. Commodities analyze COT positioning, USD strength, and rate environment. Indices evaluate regional macro data (rates, unemployment). Data sourced from FRED API, OECD SDMX (monthly Japan CPI YoY), country CPI fallback APIs (BLS for US, Statistics Canada for CA, BFS/FSO for CH), and Financial Modeling Prep.
POST /api/v1/analysis
{"symbol": "EURUSD", "timeframe": "H1"}
Triggers an asynchronous 6-step pipeline: data fetch β technical analysis β pattern recognition β fundamental analysis β signal aggregation β strategy generation. Returns an analysis_id to poll via GET /api/v1/analysis/{id} or subscribe via WS /api/v1/ws/analysis/{id} for live progress updates. The final report includes weighted signal scoring, entry point scenarios (aggressive and conservative), SL/TP levels, and confidence percentages.
Copy backend/.env.example to backend/.env. For production see .env.production.example.
| Variable | Description | Default |
|---|---|---|
DEBUG |
Enable debug mode | true |
LOG_LEVEL |
Logging level (DEBUG, INFO, WARNING, ERROR) | INFO |
DATABASE_URL |
SQLAlchemy database URL | sqlite+aiosqlite:///./data/investment.db |
CORS_ORIGINS |
Allowed CORS origins (JSON array) | ["http://localhost:3000"] |
TWELVE_DATA_API_KEY |
Twelve Data API key (optional) | β |
FMP_API_KEY |
Financial Modeling Prep API key (optional) | β |
FRED_API_KEY |
FRED API key (optional) | β |
CACHE_TTL_INTRADAY |
Cache TTL for intraday data (seconds) | 300 |
CACHE_TTL_DAILY |
Cache TTL for daily data (seconds) | 3600 |
# Backend β unit & integration tests
cd backend
python -m pytest tests/ -v
# Architecture boundary checks
lint-imports
# Frontend lint & type-check
cd frontend
npm run lint
npm run build
# E2E tests (requires running frontend)
cd frontend
npm run test:e2e
# Performance tests (requires k6 CLI)
k6 run tests/performance/analysis.k6.jsContributions are welcome! Please read the Contributing Guide before submitting a pull request.
To report a security vulnerability, please see SECURITY.md for instructions.
The API applies per-client rate limits (via slowapi). Client identity is resolved with spoof-resistance in mind:
- Authenticated requests are limited by the verified JWT subject (
user:<sub>), which is stable per user and cannot be rotated by an attacker. - Requests behind a trusted proxy use the rightmost untrusted IP from
X-Forwarded-For. The header is only trusted when the direct peer belongs to a network listed inTRUSTED_PROXIES(CIDR list). Configure it to match your reverse proxy (e.g. nginx in Docker:TRUSTED_PROXIES=["127.0.0.1","172.16.0.0/12"]). - Requests without a trusted proxy ignore
X-Forwarded-Forand fall back to the direct connection peer, logging a warning when the header chain looks suspicious.
See Issue #112 for the original finding.
In production (DEBUG=false) the backend injects the following security headers into every API response via SecurityHeadersMiddleware (app/core/security_headers.py):
Strict-Transport-Security: max-age=31536000; includeSubDomainsContent-Security-Policy: default-src 'none'(API is not browser-rendered)X-Frame-Options: DENYX-Content-Type-Options: nosniffReferrer-Policy: strict-origin-when-cross-origin
The nginx reverse proxy adds X-Frame-Options, X-Content-Type-Options, Referrer-Policy and Strict-Transport-Security (on the HTTPS server block) at the proxy layer, and scopes a browser-oriented Content-Security-Policy to the frontend location / so the API keeps its minimal CSP. Headers are intentionally skipped in local development to avoid breaking hot reload.
See Issue #114 for the original finding.
See CHANGELOG.md for a detailed history of changes.
This project is licensed under the MIT License.