Finaim
Finaim Investment intelligence
Documentation

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.

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

API endpoints

Method Route Description
GET/Home page
GET/portfolioPortfolio optimiser (UI)
POST/portfolio/optimizeRun the optimisation (JSON in/out)
GET/scorePrivate-company scoring (UI)
POST/score/companySHAP scoring over 11 features
GET/marketMarket and company analysis (UI)
GET/api/market/snapshotIndices + top movers (RU + US)
GET/api/market/quotesQuotes for a group of tickers
GET/api/market/prewarmWarm up the quote cache
GET/api/market/searchTicker search by string
GET/api/market/info/<ticker>Snowflake + company description
GET/api/market/sectorsMarket sector data
GET/profileInvestor risk profiling (UI)
GET/api/profile/questionsList of questionnaire questions
POST/api/profile/scoreCompute the risk score from the answers
POST/api/score/semanticSemantic analysis of a description
POST/api/waitlistRegistration / sign-in
GET/communityFeed of public portfolios
GET/api/community/feedJSON feed (trending / latest)
POST/api/community/follow/<handle>Follow / unfollow (toggle)
GET/api/portfoliosPortfolios of the current user
POST/api/portfoliosCreate / update a portfolio
GET/healthHealth check (version, status)
GET/docs/methodologyCalculation methodology
GET/docs/architectureTechnical note
GET/legal/privacyPrivacy Policy
GET/legal/termsTerms of Use
GET/legal/disclaimerInvestment 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.