Files
miti99bot/plans/260414-1457-trading-module/phase-01-symbols-and-format.md
tiennm99 c9270764f2 feat: add fake trading module with crypto, stocks, forex and gold
Paper trading system with 5 commands (trade_topup, trade_buy,
trade_sell, trade_convert, trade_stats). Supports VN stocks via TCBS,
crypto via CoinGecko, forex via ER-API, and gold via PAX Gold proxy.
Per-user portfolio stored in KV with 60s price caching. 54 new tests.
2026-04-14 15:16:53 +07:00

103 lines
3.7 KiB
Markdown

---
phase: 1
title: "Symbol Registry + Formatters"
status: Pending
priority: P2
effort: 45m
---
# Phase 1: Symbol Registry + Formatters
## Context
- [Module pattern](../../docs/adding-a-module.md)
- [KV interface](../../src/db/kv-store-interface.js)
## Overview
Two pure-data/pure-function files with zero side effects. Foundation for all other phases.
## File: `src/modules/trading/symbols.js`
### Requirements
- Export `SYMBOLS` — frozen object keyed by uppercase symbol name
- Export `CURRENCIES` — frozen Set of supported fiat: `VND`, `USD`
- Export helper `getSymbol(name)` — case-insensitive lookup, returns entry or `undefined`
- Export helper `listSymbols()` — returns formatted string of all symbols grouped by category
### Data shape
```js
export const SYMBOLS = Object.freeze({
// crypto
BTC: { category: "crypto", apiId: "bitcoin", label: "Bitcoin" },
ETH: { category: "crypto", apiId: "ethereum", label: "Ethereum" },
SOL: { category: "crypto", apiId: "solana", label: "Solana" },
// stock (Vietnam)
TCB: { category: "stock", apiId: "TCB", label: "Techcombank" },
VPB: { category: "stock", apiId: "VPB", label: "VPBank" },
FPT: { category: "stock", apiId: "FPT", label: "FPT Corp" },
VNM: { category: "stock", apiId: "VNM", label: "Vinamilk" },
HPG: { category: "stock", apiId: "HPG", label: "Hoa Phat" },
// others
GOLD: { category: "others", apiId: "pax-gold", label: "Gold (troy oz)" },
});
```
### Implementation steps
1. Create `src/modules/trading/symbols.js`
2. Define `SYMBOLS` constant with all entries above
3. Define `CURRENCIES = Object.freeze(new Set(["VND", "USD"]))`
4. `getSymbol(name)``SYMBOLS[name.toUpperCase()]` with guard for falsy input
5. `listSymbols()` — group by category, format as `SYMBOL — Label` per line
6. Keep under 60 lines
---
## File: `src/modules/trading/format.js`
### Requirements
- `formatVND(n)` — integer, dot thousands separator, suffix ` VND`. Example: `15.000.000 VND`
- `formatUSD(n)` — 2 decimals, comma thousands, prefix `$`. Example: `$1,234.56`
- `formatCrypto(n)` — up to 8 decimals, strip trailing zeros. Example: `0.00125`
- `formatStock(n)` — integer (Math.floor), no decimals. Example: `150`
- `formatAmount(n, symbol)` — dispatcher: looks up symbol category, calls correct formatter
- `formatCurrency(n, currency)` — VND or USD formatter based on currency string
### Implementation steps
1. Create `src/modules/trading/format.js`
2. `formatVND`: `Math.round(n).toLocaleString("vi-VN")` + ` VND` — verify dot separator (or manual impl for CF Workers locale support)
3. `formatUSD`: `n.toFixed(2)` with comma grouping + `$` prefix
4. `formatCrypto`: `parseFloat(n.toFixed(8)).toString()` to strip trailing zeros
5. `formatStock`: `Math.floor(n).toString()`
6. `formatAmount`: switch on `getSymbol(sym).category`
7. `formatCurrency`: switch on currency string
8. Keep under 80 lines
### Edge cases
- `formatVND(0)` -> `0 VND`
- `formatCrypto(1.00000000)` -> `1`
- `formatAmount` with unknown symbol -> return raw number string
- CF Workers may not have full locale support — implement manual dot-separator for VND
### Failure modes
| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|------------|
| `toLocaleString` not available in CF Workers runtime | Medium | Medium | Manual formatter fallback: split on groups of 3, join with `.` |
## Success criteria
- [ ] `SYMBOLS` has 9 entries across 3 categories
- [ ] `getSymbol("btc")` returns BTC entry (case-insensitive)
- [ ] `getSymbol("NOPE")` returns `undefined`
- [ ] `formatVND(15000000)` === `"15.000.000 VND"`
- [ ] `formatCrypto(0.001)` === `"0.001"` (no trailing zeros)
- [ ] `formatStock(1.7)` === `"1"`
- [ ] Both files under 200 lines