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>
97 lines
2.9 KiB
TypeScript
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);
|
|
}
|