Technical note
A high-level description of the Finaim architecture: the technology stack, data flows, API endpoints and performance optimisations.
Technology stack
Backend
- Flask 3.0 + Jinja2 — WSGI framework and template engine
- SQLAlchemy 2.0 + SQLite — ORM and storage for user data
- PyPortfolioOpt — portfolio optimisation (CLA + EfficientFrontier)
- CatBoost + SHAP — ML scoring of private companies
- sentence-transformers — semantic embeddings of descriptions
Frontend and data
- d3.js v7 — interactive charts (efficient frontier, heatmap, radar)
- Tailwind CSS CDN — utility-first styling
- yfinance — NYSE/NASDAQ quotes and fundamentals
- MOEX ISS API — quotes and price history for Russian equities
- Clearbit — logos of public companies, looked up by domain
Backend layers
┌─────────────────────────────────────────────┐
│ Flask Application (src/server/app.py) │ HTTP / Jinja2 rendering
│ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Page routes │ │ REST API /api/* │ │
│ └──────┬──────┘ └──────────┬──────────┘ │
│ │ │ │
│ ┌──────▼────────────────────▼──────────┐ │
│ │ Business Logic │ │
│ │ src/portfolio/optimizer.py (CLA) │ │
│ │ src/portfolio/profiler.py (quiz) │ │
│ │ src/portfolio/company_info.py │ │
│ │ src/ml/predict.py (SHAP) │ │
│ │ src/ml/semantic.py (embeddings) │ │
│ └──────────────────┬───────────────────┘ │
│ │ │
│ ┌──────────────────▼───────────────────┐ │
│ │ Data / Storage Layer │ │
│ │ src/portfolio/data.py (yfinance) │ │
│ │ src/portfolio/moex.py (MOEX ISS) │ │
│ │ src/storage/db.py (SQLite) │ │
│ │ src/storage/community.py │ │
│ │ src/storage/portfolio_metrics.py │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
Data flow (market → cache → API → frontend)
User request (POST /portfolio/optimize)
│
▼
fetch_prices(tickers, period)
├── yfinance.download() US equities (NYSE/NASDAQ)
└── MOEX ISS API Russian equities (MOEX)
│
▼
TTL in-memory cache (30 min) ← MD5 key over tickers + weights
│
▼
optimize_portfolio(prices, bounds)
├── expected_returns.mean_historical_return(prices)
├── risk_models.sample_cov(prices)
├── EfficientFrontier.max_sharpe() max Sharpe
├── CLA.efficient_frontier(40 points) frontier
└── Sortino / MaxDD / Correlations
│
▼
JSON response → d3.js visualization (frontend)
├── scatter frontier + random cloud
├── stacked-area (transition map)
└── heatmap (correlations)
The market page relies on a background cache warm-up driven by requestIdleCallback:
on every page load (other than /market) the browser issues
GET /api/market/prewarm?regions=us while idle, so that /market opens instantly.
API endpoints
| Method | Route | Description |
|---|---|---|
| GET | / | Home page |
| GET | /portfolio | Portfolio optimiser (UI) |
| POST | /portfolio/optimize | Run the optimisation (JSON in/out) |
| GET | /score | Private-company scoring (UI) |
| POST | /score/company | SHAP scoring over 11 features |
| GET | /market | Market and company analysis (UI) |
| GET | /api/market/snapshot | Indices + top movers (RU + US) |
| GET | /api/market/quotes | Quotes for a group of tickers |
| GET | /api/market/prewarm | Warm up the quote cache |
| GET | /api/market/search | Ticker search by string |
| GET | /api/market/info/<ticker> | Snowflake + company description |
| GET | /api/market/sectors | Market sector data |
| GET | /profile | Investor risk profiling (UI) |
| GET | /api/profile/questions | List of questionnaire questions |
| POST | /api/profile/score | Compute the risk score from the answers |
| POST | /api/score/semantic | Semantic analysis of a description |
| POST | /api/waitlist | Registration / sign-in |
| GET | /community | Feed of public portfolios |
| GET | /api/community/feed | JSON feed (trending / latest) |
| POST | /api/community/follow/<handle> | Follow / unfollow (toggle) |
| GET | /api/portfolios | Portfolios of the current user |
| POST | /api/portfolios | Create / update a portfolio |
| GET | /health | Health check (version, status) |
| GET | /docs/methodology | Calculation methodology |
| GET | /docs/architecture | Technical note |
| GET | /legal/privacy | Privacy Policy |
| GET | /legal/terms | Terms of Use |
| GET | /legal/disclaimer | Investment Risk Disclosure |
Performance optimisations
In-memory TTL cache
Every network request (yfinance quotes, MOEX history) is cached in the memory of the Python
process. The TTL is 30 minutes for
quotes and 7 days for company information.
Implemented in src/portfolio/market_data.py and src/portfolio/company_info.py.
requestIdleCallback prewarm
On every page except /market, the frontend issues a background request
GET /api/market/prewarm?regions=us while the browser is idle. This fills the quote
cache ahead of time, so /market opens without waiting for data to load.
ThreadPoolExecutor for MOEX history
Historical data for several MOEX tickers is downloaded in parallel through
concurrent.futures.ThreadPoolExecutor, which cuts the wait when building history
charts for Russian equities.
lru_cache for the ML models
Loading the CatBoost model and sentence-transformers is cached with @lru_cache(maxsize=1).
The models are loaded once on first use and stay in memory for the lifetime of the process,
so there is no re-initialisation on each request.
Social graph
User ──────follows──────► User │ │ ├──owns──► Portfolio │ │ │ │ │ ├── Like │ │ │ │ │ └── Comment │ │ │ └──────────seen in Feed ◄──┘ SQLite tables: users (id, email, name, handle, avatar_url, ...) portfolios (id, user_id, name, tickers_json, weights_json, is_public, description, ...) follows (follower_id, followee_id) likes (user_id, portfolio_id) comments (id, user_id, portfolio_id, body, created_at)Public portfolios are aggregated into the “Community” feed, sorted by popularity (likes) or recency. Follows are implemented symmetrically: a single request to
POST /api/community/follow/<handle>toggles the state. For public portfolios the user grants a non-exclusive licence to display them in the feed (see the Terms of Use).