An interactive web app for managing Hong Kong Equity-Linked Investment (ELI) deals with live price updates from Yahoo Finance via a Flask API. Create and edit ELI deals (single or basket up to 4 HK stocks), visualize allocations and performance with charts, and track barrier risks and alerts. Backend persists deals in MySQL.
- UI built with a clean dashboard, deals management, analytics, alerts, and settings views.
- Add, edit, delete HK ELI deals (1–4 underlying HK stocks per deal)
- Live HK stock price refresh from Yahoo Finance (via
yfinance) - Resilient price fetching — exponential backoff on rate limits
- Finnhub fallback for HK stock prices when yfinance is unavailable
- Persistent price cache (SQLite) survives server restarts
- Automatic deal status calculation — barrier breach, approaching maturity, settled
- API key protection for write endpoints (optional, configurable)
- Restricted CORS — configurable allowed origins
- Risk analytics: HK market concentration, barrier risk exposure, diversification
- Charts: allocation doughnut, performance bars, stock-count distribution
- Alerts: barrier proximity/breach, approaching maturity, notifications popover
- Import/Export: CSV and JSON; sync and load deals from MySQL
- Docker support for easy deployment
- Frontend: Vanilla JS (
app.js), HTML (index.html), CSS (style.css), Chart.js, PapaParse, Font Awesome - Backend: Python Flask (
server.py), Flask-CORS, SQLAlchemy, MySQL,yfinance, Finnhub API
ELI_DEAL/
index.html # App shell and views (Dashboard, Deals, Analytics, Alerts, Settings)
style.css # Design system + application styles
app.js # Frontend state, UI rendering, charts, alerts, API calls
server.py # Flask API: deals CRUD, price endpoints, DB models
yf_resilient.py # yfinance rate-limit resilient wrapper with Finnhub fallback
finnhub_client.py # Finnhub API client for HK stock price fallback
tests/
test_api.py # Unit tests for API endpoints
Dockerfile # Docker image for the API server
docker-compose.yml # Full stack (API + MySQL) deployment
.env.example # Environment variable template
requirements.txt # Python dependencies
- Python 3.10+
- MySQL 8.x (or compatible)
- Node is NOT required (static frontend)
Install with pip (recommended to use a virtualenv):
pip install -r requirements.txtThe backend reads config from environment variables. Copy .env.example and fill in your values:
cp .env.example .env| Variable | Required | Description |
|---|---|---|
DB_USER |
✅ | MySQL username |
DB_PASS |
✅ | MySQL password |
DB_HOST |
✅ | MySQL host (e.g. 127.0.0.1) |
DB_PORT |
❌ | MySQL port (default 3306) |
DB_NAME |
✅ | Database name (e.g. hk_eli_db) |
CORS_ORIGINS |
❌ | Comma-separated allowed origins |
ELI_API_KEY |
❌ | API key for write protection |
CACHE_TTL_SECONDS |
❌ | Price cache TTL (default 180s) |
FINNHUB_API_KEY |
❌ | Finnhub API key for price fallback |
CREATE DATABASE IF NOT EXISTS hk_eli_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;docker-compose up --buildThis starts both MySQL and the Flask API on port 8000.
# 1. Set environment variables
export DB_USER=myuser
export DB_PASS=mypassword
export DB_HOST=127.0.0.1
export DB_PORT=3306
export DB_NAME=hk_eli_db
# 2. Install dependencies
pip install -r requirements.txt
# 3. Start the Flask API
python server.pyThis starts the API at http://localhost:8000 and auto-creates tables if they don't exist.
Open index.html in a browser (double-click or serve statically). By default, the frontend points to http://localhost:8000.
If you need to change the API base URL, edit app.js:
const API_BASE_URL = 'http://localhost:8000';For hosting the frontend elsewhere, set API_BASE_URL accordingly or switch to window.location.origin when both are served together.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/status |
No | Health check |
| GET | /api/price?symbol=0700.HK |
No | Single price |
| POST | /api/prices |
No | Batch prices {symbols: [...]} |
| GET | /api/deals |
No | List deals |
| POST | /api/deals |
✅ Key | Create deal |
| PUT | /api/deals/:id |
✅ Key | Update deal |
| DELETE | /api/deals/:id |
✅ Key | Delete deal |
| POST | /api/deals/sync |
✅ Key | Batch sync |
| POST | /api/deals/import |
✅ Key | Import JSON/CSV |
Auth: If
ELI_API_KEYis set, write endpoints requireX-API-Keyheader orapi_keyquery param.
Generate a secure key:
python -c "import secrets; print(secrets.token_hex(32))"# Health check
curl http://localhost:8000/api/status
# Batch prices
curl -X POST http://localhost:8000/api/prices \
-H 'Content-Type: application/json' \
-d '{"symbols":["0700.HK","9988.HK"]}'
# Create deal (with API key)
curl -X POST http://localhost:8000/api/deals \
-H 'Content-Type: application/json' \
-H 'X-API-Key: your-secret-key' \
-d '{
"dealName":"ELI Demo",
"investmentDate":"2025-08-29",
"maturityDate":"2026-05-01",
"nominalAmount":100000,
"purchasePrice":98000,
"numberOfStocks":2,
"couponRate":12.5,
"settlementMethod":"Worst Performer Physical",
"issuer":"Credit Suisse",
"underlyingAssets":[
{"symbol":"0941.HK","name":"China Mobile","strikePrice":86.35,"barrierLevel":86.35,"weight":0.5},
{"symbol":"2883.HK","name":"China Oilfield Services Limited","strikePrice":6.74,"barrierLevel":6.74,"weight":0.5}
]
}'- Go to the "ELI Deals" view and click "Add New HK ELI Deal".
- Select 1–4 HK stocks, input strike and barrier. Weights are supported for baskets.
- Save — the app persists to DB and refreshes live prices.
- Use "Portfolio Analytics" for charts; "Alerts" for barrier/maturity alerts; "Settings" to refresh now, change intervals, and import/export.
Import/Export:
- Export CSV/JSON from Settings.
- Import CSV/JSON using the "Import" buttons. The app will parse and refresh prices.
- "Save to Database" and "Load from Database" sync UI with MySQL.
Auto-refresh:
- Prices auto-refresh based on the "Auto Refresh Interval" (default 4 hours). Manual refresh is available in Settings.
HKEX Market hours indicator:
- Shows whether HKEX is currently open based on HKT time.
cd /path/to/ELI_DEAL
python -m pytest tests/ -vTests use an in-memory SQLite database — no real MySQL needed.
Deal
id(int, PK)dealName(string)investmentDate(date)maturityDate(date)nominalAmount(float)purchasePrice(float)numberOfStocks(int, 1–4)couponRate(float)settlementMethod(string)issuer(string)status(string: auto-calculated)
DealAsset
deal_id(FK -> Deal, ondelete=CASCADE)symbol(e.g.0700.HK)name(string)strikePrice(float)barrierLevel(float)weight(float 0..1)
Note: Frontend runtime fields (
currentPrice,currentValue,pnl) are calculated client-side from live prices.
Status is automatically calculated based on:
- Maturity date passed →
Settled - Any underlying stock at or below barrier level →
Knock-in Triggered - Within 30 days of maturity →
Approaching Maturity - Otherwise →
Active
- Never commit real DB credentials. Use environment variables or
.envfile. - API key: Set
ELI_API_KEYto protect write endpoints from unauthorized access. - CORS: Restrict via
CORS_ORIGINSenv var (default: localhost:8000). - debug=False: Production-mode Flask (no debug console).
- Backend runs on port 8000 (frontend connects via API).
- Frontend: Any static host (GitHub Pages, Netlify, S3) can serve
index.html,style.css, andapp.js. - Backend: Use Docker for easy deployment, or run behind gunicorn/uvicorn + nginx.
- MySQL: Ensure network connectivity between API server and MySQL instance.
MIT
This application is for educational and portfolio tracking purposes only and does not constitute financial advice. Market data may be delayed or inaccurate; verify independently before making investment decisions.