Vietnam-first financial analytics workspace for serious equity research, market scanning, and agent-assisted workflows
VNIBB is a dashboard-driven research platform for Vietnamese equities.
It combines:
- a high-density widget workspace for fundamentals, technicals, quant, sector rotation, and company intelligence
- a FastAPI backend that normalizes messy local-market data into product-ready responses
- a dedicated
vnibb-mcpsidecar so VniAgent and other trusted clients can read VNIBB data through a curated MCP surface - bank-aware analytics, market-structure tools, and Vietnam-specific workflows that generic global terminals usually miss
- a repo structure and documentation style that make it practical for AI agents to continue development, debugging, and deployment work
The goal is not to be just another screener or charting page. VNIBB is meant to feel like a serious research cockpit for Vietnam-focused investors, builders, and agents.
- Vietnam-first modeling: the app is designed around Vietnamese equities, not retrofitted from a US-market product
- OpenBB-inspired workflow: dense, modular, multi-widget research surfaces instead of shallow page-by-page navigation
- VniAgent: database-first context, validated source citations, evidence panels, and reasoning/status events
- Bank-aware analytics: banks are treated as a distinct analytical class, not forced into industrial-company ratios
- Fallback-first backend: provider instability, missing values, and schema quirks are handled in the backend instead of leaking directly into the UI
- Agent-friendly repo: phased planning, ops notes, AGENTS guidance, and docs make handoff to other coding agents much easier
Run from vnibb/.
# 1. Install dependencies
pnpm install --frozen-lockfile
python -m pip install -e "apps/api[dev]"
# 2. Add env values to apps/web/.env.local
NEXT_PUBLIC_API_URL=http://localhost:8000
NEXT_PUBLIC_WS_URL=ws://localhost:8000/api/v1/ws/prices
# 3. Start backend
python -m uvicorn vnibb.api.main:app --reload --app-dir apps/api
# 4. Optional: start the dedicated read-only MCP sidecar for VniAgent/local MCP clients
vnibb-mcp --transport streamable-http --host 127.0.0.1 --port 8001
# 5. Start frontend
pnpm --filter frontend dev
# 6. Open: http://localhost:3000 (frontend), http://localhost:8000/docs (API docs), and http://127.0.0.1:8001/health (MCP health)If you want VniAgent to use the MCP sidecar locally, add this backend env:
VNIBB_MCP_URL=http://127.0.0.1:8001/mcpYou are working on VNIBB, a Vietnam-first financial analytics monorepo.
Start here:
1. Read `AGENTS.md`
2. Read `docs/README.md` (docs hub)
3. Work from `vnibb/`
Install:
- `pnpm install --frozen-lockfile`
- `python -m pip install -e "apps/api[dev]"`
Run:
- frontend: `pnpm --filter frontend dev`
- backend: `python -m uvicorn vnibb.api.main:app --reload --app-dir apps/api`
Validate:
- `pnpm --filter frontend exec tsc --noEmit`
- `pnpm --filter frontend lint`
- `python -m ruff check apps/api`
- `python -m pytest apps/api/tests -v`
- `pnpm run ci:gate`
Key context:
- Active product code lives in `vnibb/`
- Docs and planning live in `docs/`, `../docs/`, and `.agent/`
- Backend is FastAPI, frontend is Next.js 16 + React 19
- Primary configs: `package.json`, `scripts/ci-gate.mjs`, `apps/api/pyproject.toml`
- Never commit secrets, tokens, keys, or `.env*` files
# 1. Run narrow test first
pnpm --filter frontend test -- --runTestsByPath src/lib/financialPeriods.test.ts -t "bug description"
python -m pytest apps/api/tests/test_api/test_news_service.py -v -k "test_name"
# 2. Fix the code
# 3. Validate
pnpm --filter frontend exec tsc --noEmit
python -m ruff check apps/api
pnpm --filter frontend lint
# 4. Run broader test
pnpm --filter frontend test
python -m pytest apps/api/tests -v
# 5. Full gate before commit
pnpm run ci:gate# 1. Read relevant existing code patterns
# 2. Implement smallest coherent change
# 3. Add test
# 4. Validate
pnpm --filter frontend exec tsc --noEmit
pnpm --filter frontend lint
python -m ruff check apps/api
# 5. Full gate
pnpm run ci:gate# TypeScript
pnpm --filter frontend exec tsc --noEmit
pnpm --filter frontend lint --fix
# Python
python -m ruff check apps/api --fixpnpm run ci:gate # Full gate: lint β build β test β compile β pytest
pnpm run gate:no502 # Widget health probe (5 repeats, 10s timeout)- maintainer docs hub:
docs/README.md - deployment and operations:
docs/DEPLOYMENT_AND_OPERATIONS.md - auto update strategy:
docs/AUTO_UPDATE_STRATEGY.md - database schema:
docs/DATABASE_SCHEMA.md - read-only MCP server:
docs/VNIBB_MCP_READONLY.md - development journal:
docs/DEVELOPMENT_JOURNAL.md - project-level overview docs:
../docs/README.md - agent instructions:
AGENTS.md
MIT. See LICENSE.
VNIBB is a monorepo with a frontend, a backend, and a dedicated read-only MCP sidecar for agent-facing data access:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β End Users / Agents β
β (Investors, Quants, Research Agents, VniAgent, MCP Clients) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β apps/web (Next.js 16) β
β Dashboard UI, Widgets, VniAgent Sidebar, Routing β
β Port: 3000 β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
β
ββββββββββββββ΄ββββββββββββββ
βΌ βΌ
βββββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββ
β apps/api (FastAPI) β β vnibb-mcp (read-only MCP) β
β Port: 8000 β β Port: 8001 β
β β β β
β Middleware β Routes β β β Curated tools/resources β
β Services β Providers β β for database-backed reads β
β β β β
βββββββββββββββββ¬ββββββββββββββββ βββββββββββββββββ¬ββββββββββββββββ
β β
ββββββββββββββββ¬βββββββββββββββββββββ
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β External Services β
β Self-hosted database stack (system of record + cache) Β· VNStock β
β Scrapers / Provider fallbacks β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
apps/web/src/
βββ app/ # Next.js App Router pages
βββ components/
β βββ widgets/ # 50+ research widgets (FinancialsWidget, etc.)
β β βββ charts/ # Chart sub-components
β βββ ui/ # Shared UI (WidgetContainer, WidgetSkeleton, etc.)
β βββ common/ # ExportButton, ProtectedRoute, etc.
βββ contexts/ # React contexts
β βββ DashboardContext # Widget layout state
β βββ SymbolContext # Current stock symbol
β βββ ThemeContext # Light/dark mode
β βββ AuthContext # Auth/session state
β βββ ...
βββ hooks/ # Custom hooks (usePeriodState, useWebSocket, etc.)
βββ lib/
β βββ api.ts # fetchAPI() wrapper with error handling
β βββ queries.ts # TanStack Query hooks (useFinancialRatios, etc.)
β βββ appwrite.ts # Appwrite client
β βββ supabase.ts # Supabase client
βββ types/ # TypeScript interfaces
βββ styles/ # Global CSS
Widget (tsx)
β
ββ useFinancialRatios(symbol, { period })
β
βββΊ TanStack Query
ββ queryKey: ['financialRatios', 'VNM', 'FY']
ββ staleTime: 60 * 60 * 1000 (1 hour)
ββ queryFn: api.getFinancialRatios(symbol, params, signal)
β
βββΊ fetchAPI('/equity/{symbol}/ratios')
ββ Adds Authorization header
ββ Handles timeout (default 30s)
ββ Returns typed response
fetchAPI<T>()- Central fetch wrapper with:- Configurable timeout (default 30s)
- Automatic query parameter handling
- Authorization token management (database-stack session token)
- Structured error handling (
APIError,RateLimitError)
useQuery({
queryKey: queryKeys.financialRatios(symbol, period),
queryFn: ({ signal }) => api.getFinancialRatios(symbol, { period }, signal),
staleTime: 60 * 60 * 1000, // 1 hour
});FastAPI application with middleware stack (outer to inner):
1. CORSMiddleware # CORS headers
2. CORSErrorMiddleware # CORS on exceptions
3. APIVersionMiddleware # API version headers
4. RequestLoggingMiddleware # Request ID + error logging
5. ResponseCacheControlMiddleware # Cache headers by endpoint
6. GZipMiddleware # Compression
7. RequestTimeoutMiddleware # Global timeout
8. PerformanceLoggingMiddleware # Latency logging
9. MetricsMiddleware # Sentry performance
10. RateLimitMiddleware # Rate limiting (120 req/min)
Lifespan Events: Startup validation β cache connect β Scheduler start β WebSocket broadcaster β vnstock pre-init
apps/api/vnibb/
βββ api/
β βββ main.py # FastAPI app factory, middleware, exception handlers
β βββ router.py # Route aggregation
β βββ deps.py # Dependency injection
β βββ v1/
β βββ equity.py # /equity/{symbol}/* endpoints
β βββ screener.py # /screener endpoints
β βββ financials.py # /financials endpoints
β βββ dashboard.py # /dashboard endpoints
β βββ data_sync.py # /data pipeline triggers
β βββ realtime.py # /stream real-time
β βββ technical.py # /analysis technical
β βββ market.py # /market indices, sectors
β βββ news.py # /news, /market/news
β βββ websocket.py # WebSocket /ws/prices
β βββ quant.py # /quant analytics
β βββ comparison.py # /compare, /analysis
β βββ rs_rating.py # /rs relative strength
β βββ copilot.py # /copilot AI
β βββ health.py # /health checks
β βββ ...
βββ core/
β βββ config.py # Pydantic Settings, env validation
β βββ database.py # SQLAlchemy async engine
β βββ cache.py # Cache tier + memory fallback, @cached decorator
β βββ auth.py # JWT validation
β βββ exceptions.py # VniBBException hierarchy
β βββ rate_limiter.py # Slowapi configuration
β βββ logging_config.py # Structured JSON logging
βββ models/ # Pydantic response models
β βββ financials.py
β βββ market_news.py
β βββ stock.py
β βββ ...
βββ providers/
β βββ base.py # BaseFetcher abstract class
β βββ retry.py # Retry logic
β βββ errors.py # Provider exceptions
β βββ vnstock/ # VNStock API fetchers (50+)
β βββ equity_historical.py
β βββ financials.py
β βββ financial_ratios.py
β βββ ...
βββ services/ # Business logic
β βββ financial_service.py # TTM calculation, period normalization
β βββ screener_service.py # Screener data sync
β βββ market_service.py # Market indices, top movers
β βββ comparison_service.py # Stock comparison
β βββ news_service.py # News fetching
β βββ technical_analysis.py # Technical indicators
β βββ rs_rating_service.py # Relative Strength
β βββ cache_manager.py # Multi-tier cache
β βββ fallback_resolver.py # Provider fallback chain
β βββ ...
βββ utils/
βββ validators.py
1. REQUEST
β
βΌ
2. MIDDLEWARE STACK
ββ CORS check
ββ Rate limit check (120 req/min)
ββ Request logging
ββ Timeout check (30s global)
β
βΌ
3. ROUTER β ROUTE HANDLER
ββ @cached(ttl=86400, key_prefix="ratios_v3")
β
βΌ
4. SERVICE LAYER
β
βββΊ Check Cache Tier
β ββ HIT β Return cached data
β ββ MISS β Continue
β
βββΊ Provider Chain
β
βββΊ Primary: VNStock API
β ββ SUCCESS β Cache + Return
β ββ FAIL β Continue
β
βββΊ Secondary: Scraper Fallback
β ββ SUCCESS β Cache + Return
β ββ FAIL β Continue
β
βββΊ Tertiary: Database Stack
ββ SUCCESS β Return
ββ FAIL β Return stale cache or DataNotFoundError
β
βΌ
5. RESPONSE
ββ Cache-Control header set
ββ Response logged
ββ Return to client
| Backend | Use Case | TTL |
|---|---|---|
| Cache tier | Primary cache | 30s - 24h |
| Memory | Fallback when the cache tier is unavailable | Same as cache tier |
v:sc:<hash> # screener
v:q:<hash> # quote
v:r:<hash> # ratios
v:f:<hash> # financials
v:is:<hash> # income statement
v:n:<hash> # news
| Policy | Endpoints | Header |
|---|---|---|
real_time |
/health, /equity/*/quote |
no-store, max-age=0 |
near_real_time |
/screener, /sectors, /historical |
public, max-age=30, stale-while-revalidate=90 |
staticish |
/profile, /ratios, /financials |
public, max-age=300, stale-while-revalidate=1800 |
Request
β
βΌ
1. Check Cache Tier
β
βΌ
2. Try Primary Provider (VNStock)
β ββ API: VNStock (KBS, VCI, DNSE sources)
β
βΌ
3. Try Scraper Fallback
β ββ cophieu68 historical scraper
β
βΌ
4. Try Database Stack
β ββ Price data archival
β
βΌ
5. Return Stale Cache (if available)
β
βΌ
6. Raise DataNotFoundError
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β External Sources β
β VNStock API ββbatch syncββ> Database ββserveββ> FastAPI ββ> UI β
β βββββserveββββ> vnibb-mcp β
β VniAgent server reads: UI β> FastAPI β> vnibb-mcp β> Database β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Database Collections (26 total):
βββ stocks, stock_prices, stock_indices
βββ income_statements, balance_sheets, cash_flows, financial_ratios
βββ foreign_trading, order_flow_daily, orderbook_snapshots
βββ dividends, company_events, insider_deals
βββ company_news, shareholders, officers, subsidiaries
βββ market_sectors, sector_performance, screener_snapshots
βββ user_dashboards, dashboard_widgets, system_dashboard_templates
βββ intraday_trades, derivative_prices
Cache Tier: Session, rate limits, API response cache (TTL: 30s - 24h)
VniAgent now has a dedicated server-side MCP path for selected reads.
VniAgent UI
-> POST /api/v1/copilot/chat/stream
-> apps/api runtime context assembly
-> vnibb-mcp (when VNIBB_MCP_URL is configured)
-> database curated read tools
-> source-validated answer + SSE events back to UI
Current MCP-backed VniAgent reads:
get_market_snapshotget_symbol_snapshot
If vnibb-mcp is unavailable, the backend falls back to direct database-stack context assembly.
VNIBB uses a self-hosted database stack as its system of record with 26 collections.
For detailed schema with all attributes and relationships, see:
| Collection | Purpose | Key Attributes |
|---|---|---|
stocks |
Stock master data | symbol, exchange, industry, sector |
stock_prices |
Historical OHLCV | symbol, time, interval, open, high, low, close, volume |
income_statements |
Income data | symbol, period, revenue, net_income, eps |
balance_sheets |
Balance sheet | symbol, period, total_assets, total_liabilities, total_equity |
cash_flows |
Cash flow | symbol, period, operating_cash_flow, free_cash_flow |
financial_ratios |
Ratios | symbol, period, pe_ratio, pb_ratio, roe, roa |
foreign_trading |
Foreign trades | symbol, trade_date, buy_value, sell_value, net_value |
screener_snapshots |
Daily screener | symbol, snapshot_date, price, volume, market_cap, pe, pb |
orderbook_snapshots |
Price depth | symbol, snapshot_time, bid1-3, ask1-3 |
company_news |
News | symbol, title, source, published_date |
user_dashboards |
User layouts | user_id, name, layout_config |
dashboard_widgets |
Widget configs | dashboard_id, widget_type, layout, widget_config |
stocks (1) βββ¬ββ (*) stock_prices # Historical prices
βββ (*) financial_ratios # Ratio history
βββ (*) income_statements # Quarterly/annual income
βββ (*) balance_sheets # Balance sheet data
βββ (*) cash_flows # Cash flow data
βββ (*) dividends # Dividend history
βββ (*) foreign_trading # Daily foreign trades
βββ (*) company_news # News articles
βββ (*) insider_deals # Insider transactions
βββ (*) screener_snapshots # Daily snapshots
companies (1) βββ¬ββ (*) shareholders # Major shareholders
βββ (*) officers # Company officers
βββ (*) subsidiaries # Subsidiary companies
user_dashboards (1) ββ (*) dashboard_widgets
VNStock API ββbatch syncββ> Database Collections
β
βββ stocks
βββ stock_prices
βββ financial_ratios
βββ income_statements
βββ balance_sheets
βββ cash_flows
βββ foreign_trading
βββ dividends
βββ company_news
| Router | Prefix | Description |
|---|---|---|
| equity | /equity |
Stock data: profile, ratios, financials, historical |
| screener | /screener |
Stock screening with filters |
| financials | /financials |
Unified financial statements |
| dashboard | /dashboard |
User dashboards & widgets |
| data_sync | /data |
Data pipeline triggers |
| realtime | /stream |
Real-time streaming |
| technical | /analysis |
Technical analysis |
| market | /market |
Market indices, sectors |
| news | /news |
News feeds |
| listing | /listing |
Stock listings, symbols |
| search | /search |
Ticker search |
| trading | /trading |
Top movers, price boards |
| quant | /quant |
Quant analytics |
| comparison | /compare |
Stock comparison |
| rs_rating | /rs |
Relative Strength ratings |
| websocket | /ws |
WebSocket for real-time |
| copilot | /copilot |
AI copilot |
| health | /health |
Health checks |
GET /api/v1/equity/{symbol}/profile # Company profile
GET /api/v1/equity/{symbol}/quote # Real-time quote
GET /api/v1/equity/{symbol}/historical # Historical OHLCV
GET /api/v1/equity/{symbol}/ratios # Financial ratios
GET /api/v1/equity/{symbol}/financials # Financial statements
GET /api/v1/equity/{symbol}/income-statement
GET /api/v1/equity/{symbol}/balance-sheet
GET /api/v1/equity/{symbol}/cash-flow
GET /api/v1/equity/{symbol}/dividends
GET /api/v1/equity/{symbol}/news
GET /api/v1/equity/{symbol}/foreign-trading
GET /api/v1/market/indices # Vietnam indices
GET /api/v1/market/world-indices # Global indices
GET /api/v1/market/top-gainers # Top gaining stocks
GET /api/v1/market/top-losers # Top losing stocks
GET /api/v1/market/sector-performance # Sector performance
Each data fetcher follows a 3-step pattern:
class VnstockFinancialsFetcher(BaseFetcher):
def transform_query(self, params: FinancialsQueryParams) -> dict:
# Convert Pydantic params to provider format
pass
def extract_data(self, transformed: dict) -> RawData:
# Make API call, return raw data
pass
def transform_data(self, raw: RawData) -> List[FinancialStatementData]:
# Convert raw data to Pydantic models
passexport class APIError extends Error {
status?: number;
statusText?: string;
}
export class RateLimitError extends APIError {
retryAfter: number; // seconds
}VniBBException (base)
βββ ProviderError
β βββ ProviderTimeoutError
β βββ ProviderRateLimitError
β βββ ProviderAuthError
βββ DataNotFoundError
βββ DataValidationError
βββ StaleDataError
βββ DatabaseError
βββ CacheError
βββ InvalidParameterError
- run the narrowest relevant test for the touched area
- run package-level validation if the change is broader
- finish with
pnpm run ci:gatefor substantial work
Example flows:
- frontend UI change ->
pnpm --filter frontend exec tsc --noEmit+ relevant Jest test - backend service fix -> focused
pytesttarget + broader backend tests - cross-stack change -> package checks +
pnpm run ci:gate
- inspired by OpenBB
- shaped by Vietnamese market workflows and local data realities
- informed by iterative parity checks against both OpenBB Pro and Vietnamese market products
- built for both human researchers and agentic development workflows
MIT. See LICENSE.
