mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-04-17 15:20:58 +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.
91 lines
3.3 KiB
Markdown
91 lines
3.3 KiB
Markdown
---
|
|
phase: 3
|
|
title: "Portfolio Data Layer"
|
|
status: Pending
|
|
priority: P2
|
|
effort: 45m
|
|
---
|
|
|
|
# Phase 3: Portfolio Data Layer
|
|
|
|
## Context
|
|
|
|
- [KV interface](../../src/db/kv-store-interface.js) — `getJSON`/`putJSON`
|
|
- [Symbols](phase-01-symbols-and-format.md)
|
|
|
|
## 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}`
|
|
|
|
```js
|
|
{
|
|
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 user
|
|
- `savePortfolio(db, userId, portfolio)` — writes to KV
|
|
- `addCurrency(portfolio, currency, amount)` — mutates + returns portfolio
|
|
- `deductCurrency(portfolio, currency, amount)` — returns `{ ok, portfolio, balance }`. `ok=false` if insufficient
|
|
- `addAsset(portfolio, symbol, qty)` — adds to correct category bucket
|
|
- `deductAsset(portfolio, symbol, qty)` — returns `{ ok, portfolio, held }`. `ok=false` if insufficient
|
|
- `emptyPortfolio()` — returns fresh empty portfolio object
|
|
|
|
### Implementation steps
|
|
|
|
1. Create `src/modules/trading/portfolio.js`
|
|
2. `emptyPortfolio()` — returns deep clone of default shape
|
|
3. `getPortfolio(db, userId)`:
|
|
- `db.getJSON("user:" + userId)`
|
|
- If null, return `emptyPortfolio()`
|
|
- Validate shape: ensure all category keys exist (migration-safe)
|
|
4. `savePortfolio(db, userId, portfolio)`:
|
|
- `db.putJSON("user:" + userId, portfolio)`
|
|
5. `addCurrency(portfolio, currency, amount)`:
|
|
- `portfolio.currency[currency] += amount`
|
|
- Return portfolio
|
|
6. `deductCurrency(portfolio, currency, amount)`:
|
|
- Check `portfolio.currency[currency] >= amount`
|
|
- If not, return `{ ok: false, portfolio, balance: portfolio.currency[currency] }`
|
|
- Deduct, return `{ ok: true, portfolio }`
|
|
7. `addAsset(portfolio, symbol, qty)`:
|
|
- Lookup category from SYMBOLS
|
|
- `portfolio[category][symbol] = (portfolio[category][symbol] || 0) + qty`
|
|
8. `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 -> `getPortfolio` returns empty, no KV write until explicit `savePortfolio`
|
|
- 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
|
|
|
|
- [ ] `getPortfolio` returns empty portfolio for new user
|
|
- [ ] `addCurrency` + `deductCurrency` correctly modify balances
|
|
- [ ] `deductCurrency` returns `ok: false` when insufficient
|
|
- [ ] `addAsset` / `deductAsset` work across all 3 categories
|
|
- [ ] `deductAsset` to zero removes key from category object
|
|
- [ ] File under 120 lines
|