This document covers this repository's own architecture: its components, data flow, and data stores, as they exist right now. For the wider Growthvine Capital product suite this app is one piece of, see README.md.
app.py)Single-process FastAPI app. In production it also serves the built frontend as static files (StaticFiles(html=True) mounted at /, registered last so it never shadows an API route — see the comment at app.py:1136). In dev, Vite serves the UI and proxies /api/* to http://127.0.0.1:8000.
Key routes:
| Route | Purpose |
|---|---|
GET /api/run (SSE) |
Runs the pipeline for one category, streaming progress |
GET /api/run_multi_category |
Same, fanned out across several categories |
POST /api/rank |
Computes a live ranking (FundRanker.rank_funds) without persisting |
GET /api/categories, GET /api/categories/grouped |
Category list for the picker — unions Growthvine's live categories, custom subcategories (src/subcategory_mapper.py), and whatever's already in the DB |
GET /api/capture_comparison |
Old-vs-new Up/Down Capture comparison (src/capture_comparison.py) |
POST /api/upload_benchmark, GET /api/get_benchmark_columns |
Manual benchmark file upload path (still used for ad-hoc/one-off benchmarks not in the external DB) |
POST /api/export_excel |
Re-generates the Excel workbook (src/exporter.py) |
POST /api/overrides, POST /api/overrides/reset |
Manual per-fund score overrides |
GET /api/fund_history, POST /api/calculate_sip |
Tear-sheet NAV history + SIP return calculator |
main.py::run_pipeline)One call = one category. Steps, in order:
SubcategoryMapper.classify); drops Direct plans; dedupes Regular/Growth variants.GrowthvineNAVDownloader, with MFAPIDownloader (mfapi.in) as a per-scheme fallback for schemes Growthvine can't serve, including a hardcoded rebrand/merger map (PREDECESSOR_MAP, e.g. IDBI → LIC MF schemes).benchmark_prices straight from Postgres (the normal path, including the external-DB-sourced series — see §5) or fall back to a manually uploaded file, which then gets persisted into benchmark_prices and extended via src/benchmark_updater.py (BharatFinTrack) so subsequent runs don't need the file again.AnalyticsEngine.calculate_monthly_rolling_returns) — rolling returns, Sharpe, Up/Down Capture, quartiles; DB-cached so only genuinely new dates are recomputed.DatabaseManager.save_pipeline_to_db (raw + analytics tables) and, after that commits, FundRanker.rank_funds + DatabaseManager.save_ranking_to_db (fund_rankings — the table the tax-harvesting app's recommendation engine reads cross-database, read-only).ExcelExporter).GET /api/run callsrc/ranking.py::FundRanker)Decile composite score, default equity weights — see README.md's full calculation methodology for every formula (decile scoring, time-decay, per-category weight tables, coverage/age/AUM penalties, stars/borderline) verified line-by-line against this file:
Debt/Hybrid categories use different weight profiles (_DEBT_CATEGORY_WEIGHT_OVERRIDES, plus Gilt/Credit Risk-specific overrides); the Configure Weights tab also offers these as one-click presets. An in-process TTL cache (_PEER_METADATA_CACHE, 5 min) avoids re-authenticating to Growthvine/Investwell on every /api/rank call.
EXTERNAL_BENCHMARK_DB_URL, src/external_benchmark_db.py, run_all_mapped_categories.py) — monthly returns sourced from NSE's own "Index Dashboard" PDFs, one row per index per month (nifty_equity_index_data/nifty_fi_index_data). This is the primary path today: it removes the need to manually upload a benchmark file. Returns are converted to a synthetic price index (returns_to_price_index, base=100, cumulative compounding — lossless for any period-over-period ratio, which is all every downstream consumer ever computes) and upserted into benchmark_prices, so the rest of the pipeline (step 7 above) needs no changes to consume it.NSETRI) — a separate, real-daily-TRI third-party source, used two ways: (a) src/benchmark_updater.py extends benchmark_prices with fresh daily closes when a manually-uploaded benchmark is used, (b) src/capture_comparison.py fetches it live as the "old method" ground truth to compare against the external-DB-sourced ("new method") Up/Down Capture, surfaced in the Fund Rankings tab's "Compare Up/Down Capture" panel. Both sides are truncated to an identical set of overlapping months before any ratio is computed, and the point count (N) used is guaranteed identical on both sides.frontend/, Vite + vanilla JS, no framework)Single page, tab-based (frontend/src/tabs.js), each tab backed by its own module:
| Tab | Module | Backs |
|---|---|---|
| 📊 Run Pipeline | pipeline.js |
/api/run, /api/run_multi_category |
| 📥 Upload Data Sheets | files.js |
/api/upload_benchmark, /api/get_benchmark_columns |
| 🗄️ Database Preview | database.js |
/api/get_results |
| 🏆 Fund Rankings | rankings.js, capture-comparison.js |
/api/rank, /api/capture_comparison, /api/aum_curve |
| 🔧 Configure Weights | weights.js |
weight-preset state, persisted client-side |
category-picker.js and state.js are shared across tabs; tearsheet.js drives the per-fund NAV/SIP modal; api.js is the single fetch wrapper every module calls through.
mutual_funds (Postgres, this app's own DB) — historical_data, rolling_returns_3y, fund_rankings, benchmark_prices, absolute_capture_details/absolute_capture_summary, performance_summary. See database-schema.md for full column-level detail.
External benchmark DB (separate Postgres instance) — nifty_equity_index_data, nifty_fi_index_data, nifty_dashboard_runs (tracks source PDF URLs). Read-only from this app's perspective.
Cross-app consumer — the tax-harvesting app reads fund_rankings read-only, cross-database, as its sole source of fund ranking data. It never calls /api/rank or triggers a pipeline run itself; freshness of fund_rankings is exactly whatever the last pipeline run for that category left behind.
Growthvine Capital SEBI-Ready System Architecture.pdf (v1.0, 2026-07-01) defines a target architecture for all five Growthvine Capital products. This repo is that document's "Fund Analysis Dashboard" product (§4): internal-team-only, Medium/Medium sensitivity/criticality, named risks "unauthorized internal access, stale data, ranking integrity". This section audits the current code against every control that document requires for this specific product — line-referenced, so it stays a checklist rather than prose. CAS/PII-specific controls (§8.3, §19 retention) are out of scope: this component never touches client CAS/transaction data, only fund-universe and ranking data.
| # | Target requirement (PDF §) | Current state | Severity |
|---|---|---|---|
| 1 | MFA + authenticated roles for internal users (§10.1, Roadmap Priority 5); "MFA for team users" is a named required control for this exact product (§13.3) | No authentication of any kind. app.py registers every route (/api/run, /api/rank, /api/overrides, /api/export_excel, …) with no auth dependency, no session, no role check. Anyone with network reach to the app can trigger pipeline runs, export data, or mutate rankings. |
Critical |
| 2 | Ranking-parameter changes and dashboard exports must be audited (§13.3); structured, append-only business audit events for admin/config changes (§3.3, §14.2) | No audit trail exists anywhere. Logging (logger.info/.error throughout app.py) goes to console/SSE only — not structured event_id/actor_id/correlation_id records, and isn't append-only. /api/overrides (app.py:1100-1117) and /api/export_excel perform exactly the two actions §13.3 requires be audited, and neither is. |
Critical |
| 3 | Ranking integrity — the PDF's own named top risk for this product (§4) | manual_score_overrides (database.py:262-268) stores only scheme_code, metric_key, override_value, updated_at — no actor/user column. Combined with #1 (no auth), any override to fund_rankings — the table the separate tax-harvesting app reads read-only as its sole ranking source (§6) — is silently unattributed and unattributable after the fact. |
Critical |
| 4 | Edge protection — rate limiting, WAF, bot controls even for internal-only routes (§6.2, §11) | No rate limiting or WAF anywhere. IS_DEV CORS (app.py:73-79) is allow_origins=["*"] — acceptable only because it's dev-gated, but there is no edge-control layer in prod either, just same-origin static serving. |
High |
| 5 | Secrets in Secrets Manager/SSM, never in .env/code (§15.2, Roadmap Priority 10) |
GROWTHVINE_*_CLIENT_SECRET, INVESTWELL_PASSWORD, DATABASE_URL are all loaded via python-dotenv from a flat .env file (app.py:25-26). docker-compose.yml also hardcodes POSTGRES_PASSWORD=postgres for local dev — fine locally, but confirm the same pattern isn't mirrored into the RDS deployment's actual secret handling. |
High |
| 6 | Cache versioned by upstream sync version, invalidated only after validated sync (§12.1-12.2); staged/validated sync into production tables (§8.2, Roadmap Priority 11) — "stale data" is this product's own named risk | Caching is three separate in-process TTL dicts (_PEER_METADATA_CACHE 5 min in ranking.py:23-24, _GV_SCHEMES_CACHE 30 min and _AMFI_ISIN_MAP_CACHE in app.py:57-63) — wall-clock expiry only, no tie to Growthvine/Investwell sync version, and not shared across processes/workers. save_pipeline_to_db also writes straight into production tables per category (delete-then-append) with no staging table, checksum, or row-count validation step first. |
Medium |
| 7 | Batch jobs need job_id, idempotency key, per-item status, dead-letter/replay state (§13.7); "Weekly fund-ranking computation" is explicitly named as an in-scope batch workload | run_all_mapped_categories.py:159-167 loops categories with a bare try/except, logging success/fail to console only. No persisted job execution record, no job_id/attempt_count/status fields, no dead-letter queue — a crash mid-run leaves no queryable record of which categories actually completed. HTTP-level retry (urllib3.Retry w/ backoff in investwell_api.py/growthvine_api.py) is solid and matches §13.7's transient-failure baseline, but that's transport-level, not job-level. |
Medium |
| 8 | Least-privilege DB access per service (§3.1, §8.1) | Positive finding. Production RDS (docs/rds_schema_fix.sql) already scopes the app to a dedicated fund_app user with only SELECT/INSERT/UPDATE/DELETE — no ALTER/CREATE/ownership. This is exactly the least-privilege posture §3.1 asks for and should be preserved (not "fixed" by the broad GRANT ALL option that script's own comments offer as an alternative). |
— (keep as-is) |
| 9 | Cloud/DB resilience evidence — Multi-AZ, PITR, encryption, deletion protection (§8.1, Roadmap Priority 7) | Not documented anywhere in this repo either way. Not necessarily absent — just not verifiable from the codebase, and this repo isn't where that evidence would live (RDS console/IaC is). Flagged as an open item for whoever owns the RDS instance, not a code fix. | Info / needs owner confirmation |
| 10 | Trust boundary between this app and cross-database consumers (§3.2) | tax-harvesting reads fund_rankings cross-database, described as "read-only" in this doc's own §7, but nothing in this repo shows a DB-grant-level read-only role enforcing that — it may just be convention today. |
Medium |
Unlike the PDF's company-wide 15-item roadmap, this product's own risk profile (no CAS/PII, but real ranking-integrity exposure) means the highest-value fixes here are, in order:
/api/* route — closes findings #1 and (mostly) #3.actor_id to manual_score_overrides and emit a structured audit event on every override write/reset and every /api/export_excel call — closes #2 and the rest of #3.save_pipeline_to_db writes before they land in production tables, and tie the three in-process caches' invalidation to a validated-sync marker instead of wall-clock TTL — closes #6.run_all_mapped_categories.py a persisted per-category job record (job_id, status, attempt_count) instead of console-only logging — closes #7..env secrets to Secrets Manager/SSM for the RDS deployment — closes #5.Three changes have been decided in principle but not yet implemented, captured here so the target shape is documented ahead of the work.
src/benchmark_updater.py and src/capture_comparison.py depend on BharatFinTrack's NSETRI client, which scrapes the NSE indices website rather than calling a licensed API. Sources in this shape (this includes yfinance-style unofficial scrapers generally, even where not currently used in this repo) are unreliable from production infrastructure — cloud/datacenter IPs get rate-limited or blocked by NSE in ways that don't show up from a developer's home connection, so failures only surface after deploy, not in local testing.
Decision: move off scraper-based sources for anything running in production. The external-benchmark-database path (src/external_benchmark_db.py, §5 above — sourced from NSE's published Index Dashboard PDFs rather than live scraping) is already the primary path for exactly this reason and should keep absorbing responsibility; BharatFinTrack/NSETRI should shrink to a manual/dev-only fallback (or be replaced with a licensed data vendor) rather than something the daily/weekly production pipeline depends on.
Open: which paid/licensed provider replaces it is not yet chosen — this is a data-sourcing decision for whoever owns the vendor relationship, not a code change to make speculatively.
Per §6, this app's frontend is a single-page Vite + vanilla JS app with no framework (frontend/index.html, frontend/src/*.js, tab modules). The main Growthvine Capital website is built on Node.js + React. Shipping the Fund Analysis Dashboard in a different stack than the rest of the company's web properties means no shared components, no shared build tooling, and every contributor needs to context-switch between two different frontend approaches.
Decision: flagged as a mismatch worth resolving — likely a migration of frontend/ to React (kept on Vite as the bundler, since Vite-with-React is standard and the issue is vanilla-JS-vs-React, not Vite-vs-something-else) so this product shares tooling and component patterns with the rest of the org's Node.js/React properties.
Open: this is a rewrite, not a patch — needs its own scoping (incremental tab-by-tab port vs. full rewrite) before work starts; not undertaken as part of this audit.
Today, main.py::run_pipeline, src/analytics.py::AnalyticsEngine, src/ranking.py::FundRanker, and all the ingestion clients (src/growthvine_api.py, src/growthvine_nav.py, src/investwell_api.py, src/mfapi.py, src/amfi.py) run inside this FastAPI backend — the same process that serves the dashboard UI and its API. Fetching, computing, and persisting fund analytics is a distinct concern from presenting them, and already has its own downstream consumer (tax-harvesting, reading fund_rankings cross-database) that has nothing to do with this app's UI.
Decision: pull data ingestion, analytics computation, and ranking out of this repo into a separate data-layer service/library. This app's backend should become a thin consumer of that layer's output (via its DB tables or an internal API) rather than the thing that runs the pipeline itself. This also directly helps §8's audit findings above: a dedicated data layer is a natural place to add the staged/validated sync (#6) and batch job tracking (#7) this document already flags as missing, without those concerns living inside a dashboard's web backend.
Open: target shape (shared library vs. standalone service with its own API, and whether run_all_mapped_categories.py/scheduled jobs move with it) needs its own design pass — not resolved here.
docker-compose.yml bundles a Postgres 16 instance (db, host port 5433) and the app itself (app, host port 8000 by default). DATABASE_URL is overridden inside Compose to always point at the bundled db service; set it yourself in .env only for a non-Docker run. See README.md §6 for exact commands.