Files
stock-agent/src/agent/watchlistStore.ts
Gahow Wang 870b9cb2b1 feat(data): extend market data provider with charts, fundamentals, and quant analysis
Adds a stock data cache layer and indicator helpers (MA/RSI/MACD), expands
the MarketDataProvider interface with candles, fundamentals, related-data,
and quant analysis, and threads tags + source attribution through the
shared types. Updates affected agent tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 23:20:42 +08:00

97 lines
2.9 KiB
TypeScript

import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname } from "node:path";
import { normalizeSymbol } from "./symbols";
import type { WatchStock } from "../shared/types";
export class WatchlistStore {
constructor(private readonly path: string) {}
async list(): Promise<WatchStock[]> {
return this.read();
}
async add(symbolInput: string, name?: string, tags: string[] = []): Promise<WatchStock> {
const normalized = normalizeSymbol(symbolInput);
const stocks = await this.read();
const existing = stocks.find((stock) => stock.display === normalized.display);
if (existing) {
return existing;
}
const stock: WatchStock = {
...normalized,
id: normalized.display,
name: name?.trim() || undefined,
tags: normalizeTags(tags),
addedAt: new Date().toISOString()
};
await this.write([...stocks, stock]);
return stock;
}
async updateTags(symbolInput: string, tags: string[]): Promise<WatchStock> {
const normalized = normalizeSymbol(symbolInput);
const stocks = await this.read();
const stock = stocks.find((item) => item.display === normalized.display);
if (!stock) {
throw new Error("Stock is not in watchlist");
}
const updated = { ...stock, tags: normalizeTags(tags) };
await this.write(stocks.map((item) => (item.display === normalized.display ? updated : item)));
return updated;
}
async remove(symbolInput: string): Promise<void> {
const normalized = normalizeSymbol(symbolInput);
const stocks = await this.read();
await this.write(stocks.filter((stock) => stock.display !== normalized.display));
}
private async read(): Promise<WatchStock[]> {
try {
const content = await readFile(this.path, "utf8");
const parsed = JSON.parse(content) as WatchStock[];
if (!Array.isArray(parsed)) {
return [];
}
const byDisplay = new Map<string, WatchStock>();
for (const stock of parsed) {
const normalized = normalizeSymbol(stock.display ?? stock.symbol);
byDisplay.set(normalized.display, {
...stock,
...normalized,
id: normalized.display,
tags: normalizeTags(stock.tags ?? [])
});
}
return Array.from(byDisplay.values());
} catch (error) {
if (isMissingFile(error)) {
return [];
}
throw error;
}
}
private async write(stocks: WatchStock[]): Promise<void> {
await mkdir(dirname(this.path), { recursive: true });
await writeFile(this.path, `${JSON.stringify(stocks, null, 2)}\n`, "utf8");
}
}
function isMissingFile(error: unknown): boolean {
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
}
function normalizeTags(tags: string[]): string[] {
return Array.from(
new Set(
tags
.map((tag) => tag.trim())
.filter(Boolean)
.map((tag) => tag.slice(0, 24))
)
).slice(0, 8);
}