Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions web/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
dist
*.local
.DS_Store
159 changes: 159 additions & 0 deletions web/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# Azoth Finance — web dashboard

A Google-Finance-style web dashboard for Azoth's Vietnam-market data (HOSE/HNX/UPCOM).
It reuses Azoth's existing data layer (`src/data/sources/**`, DNSE/SSI/VNDirect/CafeF)
through a thin Node API server, and renders a React frontend that clones Google
Finance's information architecture and visual language. The UI defaults to the
**light** theme (dark is available via the theme toggle; tokens flip on `[data-theme]`).

## What's here

- **`server/index.ts`** — a dependency-light Node `http` API server. It imports Azoth's
`src/**` data clients directly and exposes JSON under `/api/*`. Data is TTL-cached via
Azoth's SQLite cache (`~/.azoth/azoth.db`). No secrets required — all sources are public.
- **`server/store.ts`** — the SQLite persistence layer for user-managed watchlists and manual
portfolios. Tables are prefixed `web_*` and share the same better-sqlite3 handle as the rest
of Azoth; `ensureWebSchema()` creates them (and seeds a default watchlist) at server startup.
- **`src/`** — Vite + React + TypeScript frontend.
- `pages/Home.tsx` — a row of **compact index cards** (small name, medium value, % badge,
sparkline), a **Discover** strip (bluechip/sector shortcuts), and a **market summary**
news block. Movers no longer live here — they moved to the **`/markets`** trends page.
- `pages/Quote.tsx` — price header (with **add-to-list**), interactive chart, then
**Overview / Financials / News tabs**.
- `pages/WatchlistPage.tsx` — a single user watchlist: live quote rows with add/remove a
symbol, rename, and delete the list.
- `pages/Portfolio.tsx` — a manually-entered portfolio (Google-Finance style, **not**
broker-linked): holdings table with per-position and total gain/loss, day change, and weights;
add/edit/remove holdings; create/rename/delete and switch between multiple portfolios.
- `pages/MarketTrends.tsx` — market-trends tabs (Active / Gainers / Losers / Indexes) with a
Liquid/VN30 universe toggle.
- `components/` — TopNav (search + theme toggle), Sidebar (persistent watchlists + portfolios
with inline add/remove, plus a **SectorRail** mini index-list of stock sectors),
ResearchPanel, MarketStrip (compact IndexCards), MarketSummary (home news block),
NewsList, QuoteHeader (perf summary line +
add-to-list popover), DiscoverStrip, PriceChart (lightweight-charts, with **dropdown**
chart-type / indicators / compare menus above a range-button row, and a **Compare**
multi-ticker % overlay), StatsGrid (dense label→value grid), RelatedStocks, AboutCompany,
FinancialsTab (annual key metrics + chart), PortfolioSummary / HoldingsTable / AddHoldingForm,
plus Sparkline + ChangeBadge primitives.
- `lib/` — the shared API contract (`types.ts`), formatters (`format.ts`), fetch client (`api.ts`),
and `userData.tsx` (a `UserDataProvider` + `useWatchlists()` / `usePortfolios()` hooks that
hold watchlist/portfolio state for the sidebar and pages).
- `index.css` — the design system (Google-Finance-style tokens; **light theme by default**,
dark via `[data-theme="dark"]`).

## Features

- **Persistent watchlists** — create multiple named lists, add/remove symbols from the sidebar,
the watchlist page, or the stock header's add-to-list popover. Lists persist across restarts.
- **Manual portfolios** — track holdings (quantity + average cost) entered by hand and see live
market value, total and per-position gain/loss, day change, and weights. Portfolios are
**manual-entry only** and are not linked to any broker account.
- **Market trends** — dedicated pages for the most active stocks, top gainers/losers, and the
market indices, with a liquid/VN30 universe toggle.
- **Add-to-list from the stock header** and a **Discover** strip on the home page for quick
navigation into sectors and bluechips.

Watchlists and portfolios are persisted in the shared Azoth SQLite database (`~/.azoth/azoth.db`)
under `web_*` tables — no separate datastore, no broker credentials.

## Prerequisites

Install the **Azoth root** dependencies first (the server imports `../src`):

```bash
# from the repo root
pnpm install
```

Then install the web app's own dependencies:

```bash
# from web/
pnpm install
```

## Run

```bash
# from web/ — starts the API server (:8787) and Vite (:5273) together
pnpm dev
```

Open http://localhost:5273. Vite proxies `/api` → `http://localhost:8787`.

Run the pieces individually if you prefer:

```bash
pnpm server # API server only (:8787)
pnpm client # Vite dev server only (:5273)
```

## Build

```bash
pnpm build # tsc + vite build → dist/
pnpm typecheck # type-check the frontend
```

## API endpoints

| Endpoint | Description |
| --- | --- |
| `GET /api/indices` | VN-Index, VN30, HNX-Index, HNX30, UPCOM with sparklines |
| `GET /api/movers?kind=gainers\|losers\|active&universe=vn30` | Top movers (used by `/markets`) |
| `GET /api/sectors` | Stock sectors for the sidebar rail: per-sector avg daily % change, a synthetic rebased-index sparkline, and top leaders, sorted by % change |
| `GET /api/watchlist` | Sidebar watchlist rows |
| `GET /api/market-news` | Aggregated market news feed (home market summary) |
| `GET /api/search?q=` | Ticker/name search |
| `GET /api/quote/:ticker` | Quote header + stats (incl. `change_pct_1m` / `_3m` / `_ytd` perf fields) |
| `GET /api/ohlcv/:ticker?range=1D..MAX` | Candles/area chart data |
| `GET /api/indicators/:ticker?range=` | SMA/EMA/Bollinger/RSI/MACD |
| `GET /api/news/:ticker` | Ticker news |
| `GET /api/about/:ticker` | Company profile + related stocks |
| `GET /api/financials/:ticker?period=annual` | Annual key metrics (EPS, BVPS, ROE, ROA, margins, P/E) from CafeF |

### Persistent (SQLite-backed `web_*` tables)

User watchlists:

| Endpoint | Description |
| --- | --- |
| `GET /api/watchlists` | All watchlists (id, name, tickers) |
| `POST /api/watchlists` `{name}` | Create a watchlist |
| `GET /api/watchlists/:id` | One watchlist with live quote rows |
| `PATCH /api/watchlists/:id` `{name}` | Rename a watchlist |
| `DELETE /api/watchlists/:id` | Delete a watchlist |
| `POST /api/watchlists/:id/items` `{ticker}` | Add a symbol |
| `DELETE /api/watchlists/:id/items/:ticker` | Remove a symbol |

Manual portfolios (not broker-linked):

| Endpoint | Description |
| --- | --- |
| `GET /api/portfolios` | All portfolios (id, name) |
| `POST /api/portfolios` `{name}` | Create a portfolio |
| `GET /api/portfolios/:id` | One portfolio with computed holdings + totals |
| `PATCH /api/portfolios/:id` `{name}` | Rename a portfolio |
| `DELETE /api/portfolios/:id` | Delete a portfolio |
| `POST /api/portfolios/:id/holdings` `{ticker,quantity,avgCostVnd}` | Add a holding |
| `PATCH /api/holdings/:id` `{quantity?,avgCostVnd?}` | Edit a holding |
| `DELETE /api/holdings/:id` | Remove a holding |

## Notes on units (Vietnam market)

- Board prices (last/ref/ceiling/floor/open/high/low/EPS/BVPS) are in **thousand VND**
(28.5 = 28,500 VND). The frontend formatters scale to full VND (`28,500 ₫`).
- SSI iBoard returns ref/ceiling/floor in **plain VND**; the server normalizes them to
board units (÷1000) so everything shares one scale.
- Market cap and portfolio money aggregates (market value, cost basis, gain, day change — every
field ending in `Vnd`) are in **plain VND**; volume is in shares. Portfolio holding cost
(`avgCostVnd`) is entered and stored as plain VND per share (a buy at 64,800 ₫ is `64800`).
- The intraday chart shifts timestamps +7h so the axis shows Vietnam exchange time (ICT).

## Scope

This is a data/visualization surface. The "Research" (AI) panel is a visual preview —
Azoth's AI analyst, ordering, backtesting, and broker workflows live in the terminal CLI.
The Portfolio page is **manual entry** (holdings you type in, priced with live market data);
broker-linked positions and cash (which need broker auth) are intentionally not wired here.
33 changes: 33 additions & 0 deletions web/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%231a73e8' d='M4 20V10h3v10H4zm6.5 0V4h3v16h-3zM17 20v-7h3v7h-3z'/%3E%3C/svg%3E"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&family=Roboto+Mono:wght@400;500&display=swap"
rel="stylesheet"
/>
<title>Azoth Finance</title>
<script>
// Set theme synchronously to avoid a dark flash on load.
// Light is the default unless the user explicitly chose a theme.
(function () {
var stored = localStorage.getItem("azoth-theme");
document.documentElement.setAttribute(
"data-theme",
stored === "dark" || stored === "light" ? stored : "light",
);
})();
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
36 changes: 36 additions & 0 deletions web/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@toreleon/azoth-web",
"private": true,
"version": "0.1.0",
"type": "module",
"description": "Google-Finance-style web dashboard for Azoth (Vietnam market data)",
"scripts": {
"dev": "concurrently -n server,web -c cyan,magenta \"node --import tsx server/index.ts\" \"vite\"",
"server": "node --import tsx server/index.ts",
"server:watch": "tsx watch server/index.ts",
"client": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"typecheck": "tsc -b --noEmit"
},
"dependencies": {
"lightweight-charts": "^4.2.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.28.0",
"technicalindicators": "^3.1.0"
},
"devDependencies": {
"@types/node": "^22.7.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"concurrently": "^9.1.0",
"tsx": "^4.19.1",
"typescript": "^5.6.3",
"vite": "^5.4.11"
},
"pnpm": {
"onlyBuiltDependencies": ["esbuild"]
}
}
Loading
Loading