mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-04-17 13:21:31 +00:00
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.
3.3 KiB
3.3 KiB
phase, title, status, priority, effort
| phase | title | status | priority | effort |
|---|---|---|---|---|
| 3 | Portfolio Data Layer | Pending | P2 | 45m |
Phase 3: Portfolio Data Layer
Context
- KV interface —
getJSON/putJSON - Symbols
Overview
CRUD operations on per-user portfolio KV objects. Pure data logic, no Telegram ctx dependency.
File: src/modules/trading/portfolio.js
Data shape — KV key user:{telegramId}
{
currency: { VND: 0, USD: 0 },
stock: {}, // e.g. { TCB: 10, FPT: 5 }
crypto: {}, // e.g. { BTC: 0.5, ETH: 2.1 }
others: {}, // e.g. { GOLD: 0.1 }
totalvnd: 0 // cumulative VND topped up (cost basis)
}
Exports
getPortfolio(db, userId)— returns portfolio object; inits empty if first-time usersavePortfolio(db, userId, portfolio)— writes to KVaddCurrency(portfolio, currency, amount)— mutates + returns portfoliodeductCurrency(portfolio, currency, amount)— returns{ ok, portfolio, balance }.ok=falseif insufficientaddAsset(portfolio, symbol, qty)— adds to correct category bucketdeductAsset(portfolio, symbol, qty)— returns{ ok, portfolio, held }.ok=falseif insufficientemptyPortfolio()— returns fresh empty portfolio object
Implementation steps
- Create
src/modules/trading/portfolio.js emptyPortfolio()— returns deep clone of default shapegetPortfolio(db, userId):db.getJSON("user:" + userId)- If null, return
emptyPortfolio() - Validate shape: ensure all category keys exist (migration-safe)
savePortfolio(db, userId, portfolio):db.putJSON("user:" + userId, portfolio)
addCurrency(portfolio, currency, amount):portfolio.currency[currency] += amount- Return portfolio
deductCurrency(portfolio, currency, amount):- Check
portfolio.currency[currency] >= amount - If not, return
{ ok: false, portfolio, balance: portfolio.currency[currency] } - Deduct, return
{ ok: true, portfolio }
- Check
addAsset(portfolio, symbol, qty):- Lookup category from SYMBOLS
portfolio[category][symbol] = (portfolio[category][symbol] || 0) + qty
deductAsset(portfolio, symbol, qty):- Lookup category, check held >= qty
- If not, return
{ ok: false, portfolio, held: portfolio[category][symbol] || 0 } - Deduct (remove key if 0), return
{ ok: true, portfolio }
Edge cases
- First-time user ->
getPortfolioreturns empty, no KV write until explicitsavePortfolio - Deduct exactly full balance -> ok, set to 0
- Deduct asset to exactly 0 -> delete key from object (keep portfolio clean)
- Portfolio shape migration: if old KV entry missing a category key, fill with
{}
Failure modes
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Race condition (two concurrent commands) | Low (paper trading) | Low | Acceptable for paper trading; last write wins |
| KV write fails | Low | Medium | Command handler catches, reports error to user |
Success criteria
getPortfolioreturns empty portfolio for new useraddCurrency+deductCurrencycorrectly modify balancesdeductCurrencyreturnsok: falsewhen insufficientaddAsset/deductAssetwork across all 3 categoriesdeductAssetto zero removes key from category object- File under 120 lines