diff --git a/src/tools/portfolio.ts b/src/tools/portfolio.ts index 406d74a..5446023 100644 --- a/src/tools/portfolio.ts +++ b/src/tools/portfolio.ts @@ -18,34 +18,41 @@ async function lastClose(ticker: string): Promise { export async function shapeBrokerPortfolio( snap: BrokerSnapshot, - priceFor: (ticker: string) => Promise, + priceFor: (tickers: string[]) => Promise>, ) { - const positions = await Promise.all( - snap.positions.map(async (p) => { - const px = p.lastPrice ?? await priceFor(p.ticker); - const cost_basis_vnd = p.avgCost * p.quantity * 1000; - const market_value_vnd = p.marketValueVnd ?? (px != null ? px * p.quantity * 1000 : null); - const unrealized_pnl_vnd = - p.unrealizedPnlVnd ?? (px != null ? (px - p.avgCost) * p.quantity * 1000 : null); - const unrealized_pnl_pct = - p.unrealizedPnlPct ?? (px != null && p.avgCost > 0 - ? ((px - p.avgCost) / p.avgCost) * 100 - : null); - return { - ticker: p.ticker, - quantity: p.quantity, - sub_account_id: p.subAccountId ?? null, - custody_code: p.custodyCode ?? null, - avg_cost_thousand_vnd: p.avgCost, - last_close_thousand_vnd: px, - cost_basis_vnd, - market_value_vnd, - unrealized_pnl_vnd, - unrealized_pnl_pct, - }; - }), + const tickersNeedingPrice = Array.from( + new Set(snap.positions.filter((p) => p.lastPrice == null).map((p) => p.ticker)), ); + let fetchedPrices: Record = {}; + if (tickersNeedingPrice.length > 0) { + fetchedPrices = await priceFor(tickersNeedingPrice); + } + + const positions = snap.positions.map((p) => { + const px = p.lastPrice ?? fetchedPrices[p.ticker] ?? null; + const cost_basis_vnd = p.avgCost * p.quantity * 1000; + const market_value_vnd = p.marketValueVnd ?? (px != null ? px * p.quantity * 1000 : null); + const unrealized_pnl_vnd = + p.unrealizedPnlVnd ?? (px != null ? (px - p.avgCost) * p.quantity * 1000 : null); + const unrealized_pnl_pct = + p.unrealizedPnlPct ?? (px != null && p.avgCost > 0 + ? ((px - p.avgCost) / p.avgCost) * 100 + : null); + return { + ticker: p.ticker, + quantity: p.quantity, + sub_account_id: p.subAccountId ?? null, + custody_code: p.custodyCode ?? null, + avg_cost_thousand_vnd: p.avgCost, + last_close_thousand_vnd: px, + cost_basis_vnd, + market_value_vnd, + unrealized_pnl_vnd, + unrealized_pnl_pct, + }; + }); + const totals = positions.reduce( (a, p) => { a.cost_basis_vnd += p.cost_basis_vnd; @@ -76,6 +83,11 @@ export const listPositionsTool = tool( const ok = await requireBrokerConsent("portfolio_list", "read cash, positions, and exposure"); if (!ok) return asText({ ok: false, error: "user_declined" }); const snap = await getBroker().snapshot(); - return asText(await shapeBrokerPortfolio(snap, lastClose)); + return asText( + await shapeBrokerPortfolio(snap, async (tickers: string[]) => { + const prices = await Promise.all(tickers.map((t) => lastClose(t))); + return Object.fromEntries(tickers.map((t, i) => [t, prices[i] ?? null])); + }) + ); }, ); diff --git a/test-batch-dnse.ts b/test-batch-dnse.ts new file mode 100644 index 0000000..04fffa9 --- /dev/null +++ b/test-batch-dnse.ts @@ -0,0 +1,13 @@ +import { request } from "undici"; + +async function fetchOhlcs(symbol: string) { + const url = `https://services.entrade.com.vn/chart-api/v2/ohlcs/stock?symbol=${encodeURIComponent( + symbol, + )}&resolution=1D&from=1700000000&to=1800000000`; + const { statusCode, body } = await request(url, { + method: "GET", + headers: { accept: "application/json" }, + }); + console.log(statusCode, await body.text()); +} +fetchOhlcs("FPT,SSI"); diff --git a/test-ssi-batch.ts b/test-ssi-batch.ts new file mode 100644 index 0000000..45301f5 --- /dev/null +++ b/test-ssi-batch.ts @@ -0,0 +1,11 @@ +import { request } from "undici"; + +async function fetchSsi(symbol: string) { + const url = `https://iboard-query.ssi.com.vn/stock/${encodeURIComponent(symbol)}`; + const { statusCode, body } = await request(url, { + method: "GET", + headers: { accept: "application/json" }, + }); + console.log(statusCode, await body.text()); +} +fetchSsi("FPT,SSI"); diff --git a/tests/portfolio.test.ts b/tests/portfolio.test.ts index 2e2e037..0bc80bc 100644 --- a/tests/portfolio.test.ts +++ b/tests/portfolio.test.ts @@ -9,7 +9,7 @@ describe("portfolio units", () => { cashVnd: 1_000_000, positions: [{ ticker: "FPT", quantity: 100, avgCost: 25 }], }, - async () => 30, + async (tickers) => Object.fromEntries(tickers.map(t => [t, 30])), ); expect(shaped.cash_vnd).toBe(1_000_000);