2Branches0Tags
GL
glucryptoFix trade candle loading via gateway
4620100a month ago52Commits
typescript
import type { DailySummaryConfig } from "./config"; import { readConfig } from "./config"; import { openpondRequest } from "./openpond"; export type JsonValue = | Record<string, unknown> | Array<unknown> | string | number | boolean | null; export type DailySummarySubAgentResult = { name: string; ok: boolean; data?: JsonValue | null; missing?: boolean; }; export type DailyNewsSection = { summary?: string; stories: Array<{ title: string; link: string; source?: string; reason?: string; }>; }; export type DailyImportantDatesSection = { summary?: string; events: Array<{ id?: string; series: string; category?: string; referencePeriod?: string; releaseAt: string; source?: string; sourceUrl?: string; reason?: string; }>; }; export type StrategySnapshot = { name: string; netPnl?: number | null; trades?: number | null; lastFilledAt?: string | null; }; export type StrategyChangeItem = { name: string; status: "new" | "updated" | "removed"; netPnlDelta?: number | null; tradesDelta?: number | null; }; export type StrategyChangeSummary = { summary: string; items: StrategyChangeItem[]; }; export type DailyDigestBriefing = { headline: string; overview?: string | null; changes: string[]; previousRunAt?: string | null; }; type DailyPerformanceSummary = { netPnl?: number | null; trades?: number | null; winRate?: number | null; lastFilledAt?: string | null; }; export type DailyDigestHistoryItem = { messageId: string; content: string; runAt: string; createdAt: string; updatedAt: string; metadata?: Record<string, unknown>; }; type DailyDigestHistoryContext = { item: DailyDigestHistoryItem | null; strategies: StrategySnapshot[]; performance: JsonValue | null; }; const MAX_NEWS_STORIES = 3; const MAX_IMPORTANT_DATES = 3; const STRATEGY_DELTA_EPSILON = 0.005; function isRecord(value: unknown): value is Record<string, unknown> { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function mapLegacyNewsStories(topStories: unknown[]): DailyNewsSection["stories"] { return topStories.slice(0, MAX_NEWS_STORIES).map((story) => { const record = isRecord(story) ? story : {}; return { title: typeof record.title === "string" ? record.title : "Untitled story", link: typeof record.link === "string" ? record.link : "", source: typeof record.source === "string" ? record.source : undefined, reason: typeof record.reason === "string" ? record.reason : undefined, }; }); } function collectWindowTopArticles(summary: Record<string, unknown>): unknown[] { const windowSummaries = Array.isArray(summary.windowSummaries) ? summary.windowSummaries : []; const collected: unknown[] = []; for (const windowSummary of windowSummaries) { if (!isRecord(windowSummary) || !Array.isArray(windowSummary.topArticles)) { continue; } for (const article of windowSummary.topArticles) { collected.push(article); if (collected.length >= MAX_NEWS_STORIES) { return collected; } } } return collected; } function mapRoundupNewsStories(topArticles: unknown[]): DailyNewsSection["stories"] { return topArticles.slice(0, MAX_NEWS_STORIES).map((article) => { const record = isRecord(article) ? article : {}; return { title: typeof record.title === "string" ? record.title : "Untitled story", link: typeof record.canonicalUrl === "string" ? record.canonicalUrl : "", source: typeof record.sourceName === "string" ? record.sourceName : undefined, reason: typeof record.reason === "string" ? record.reason : undefined, }; }); } function hasMechanicalRoundupSummary(value: string | undefined) { const summary = value?.trim(); if (!summary) return false; return ( /^Fetched \d+ items from .+ across \d+ lookback windows\.$/i.test(summary) || /^No relevant items were found from .+ in the requested windows\.$/i.test(summary) ); } function joinLabelList(values: string[]) { if (values.length === 0) return ""; if (values.length === 1) return values[0]!; if (values.length === 2) return `${values[0]} and ${values[1]}`; return `${values.slice(0, -1).join(", ")}, and ${values[values.length - 1]}`; } function buildFallbackNewsSummary(stories: DailyNewsSection["stories"]) { if (stories.length === 0) return undefined; const sources = Array.from( new Set( stories .map((story) => story.source?.trim()) .filter((source): source is string => Boolean(source)), ), ).slice(0, 3); if (sources.length === 0) { return `${stories.length} recent geopolitical ${stories.length === 1 ? "story is" : "stories are"} in focus.`; } return `Recent geopolitical context is coming from ${joinLabelList(sources)}.`; } export function extractNewsSection( subAgentResults: DailySummarySubAgentResult[], ): DailyNewsSection | undefined { for (const result of subAgentResults) { if (!isRecord(result.data)) continue; const summary = isRecord(result.data.summary) ? result.data.summary : null; if (!summary) continue; const hasLegacyShape = Array.isArray(summary.topStories); if (hasLegacyShape) { return { summary: typeof summary.summary === "string" ? summary.summary : undefined, stories: mapLegacyNewsStories(summary.topStories as unknown[]), }; } const hasRoundupShape = Array.isArray(summary.topArticles) || Array.isArray(summary.windowSummaries); if (hasRoundupShape) { const topArticles = Array.isArray(summary.topArticles) ? summary.topArticles : collectWindowTopArticles(summary); const stories = mapRoundupNewsStories(topArticles); const overallSummary = typeof summary.overallSummary === "string" ? summary.overallSummary : undefined; return { summary: hasMechanicalRoundupSummary(overallSummary) ? buildFallbackNewsSummary(stories) : overallSummary, stories, }; } } return undefined; } function mapImportantDateEvent(value: unknown): DailyImportantDatesSection["events"][number] | null { if (!isRecord(value)) return null; const series = typeof value.series === "string" ? value.series : ""; const releaseAt = typeof value.releaseAt === "string" ? value.releaseAt : ""; if (!series || !releaseAt) { return null; } return { id: typeof value.id === "string" ? value.id : undefined, series, category: typeof value.category === "string" ? value.category : undefined, referencePeriod: typeof value.referencePeriod === "string" ? value.referencePeriod : undefined, releaseAt, source: typeof value.source === "string" ? value.source : undefined, sourceUrl: typeof value.sourceUrl === "string" ? value.sourceUrl : undefined, reason: typeof value.reason === "string" ? value.reason : undefined, }; } function hasImportantDateEvents(value: unknown) { return Array.isArray(value) && value.some((event) => mapImportantDateEvent(event) !== null); } function isImportantDatesResult(result: DailySummarySubAgentResult) { const normalizedName = result.name.trim().toLowerCase(); if ( normalizedName.includes("important-dates") || normalizedName.includes("important dates") ) { return true; } if (!isRecord(result.data)) return false; const summary = isRecord(result.data.summary) ? result.data.summary : null; return ( typeof result.data.daysAhead === "number" && (hasImportantDateEvents(result.data.events) || hasImportantDateEvents(summary?.topEvents)) ); } export function extractImportantDatesSection( subAgentResults: DailySummarySubAgentResult[], ): DailyImportantDatesSection | undefined { for (const result of subAgentResults) { if (!isImportantDatesResult(result)) continue; if (!isRecord(result.data)) continue; const summary = isRecord(result.data.summary) ? result.data.summary : null; const topEvents: unknown[] = hasImportantDateEvents(summary?.topEvents) ? (summary?.topEvents as unknown[]) : hasImportantDateEvents(result.data.events) ? (result.data.events as unknown[]) : []; const events = topEvents .map((event: unknown) => mapImportantDateEvent(event)) .filter( (event): event is DailyImportantDatesSection["events"][number] => event !== null, ) .slice(0, MAX_IMPORTANT_DATES); const summaryText = typeof summary?.overallSummary === "string" && summary.overallSummary.trim().length > 0 ? summary.overallSummary.trim() : undefined; if (summaryText || events.length > 0) { return { ...(events.length === 0 && summaryText ? { summary: summaryText } : {}), events, }; } } return undefined; } export function extractStrategies(performance: JsonValue | null): StrategySnapshot[] { if (!isRecord(performance)) return []; const strategies = Array.isArray(performance.strategies) ? performance.strategies : []; return strategies .map((strategy) => ({ name: typeof strategy?.name === "string" ? strategy.name : "Unnamed strategy", netPnl: typeof strategy?.netPnl === "number" ? strategy.netPnl : undefined, trades: typeof strategy?.trades === "number" ? strategy.trades : undefined, lastFilledAt: typeof strategy?.lastFilledAt === "string" ? strategy.lastFilledAt : null, })) .slice(0, 8); } function extractPerformanceSummary(performance: JsonValue | null): DailyPerformanceSummary | null { if (!isRecord(performance) || !isRecord(performance.summary)) { return null; } return { netPnl: typeof performance.summary.netPnl === "number" ? performance.summary.netPnl : null, trades: typeof performance.summary.trades === "number" ? performance.summary.trades : null, winRate: typeof performance.summary.winRate === "number" ? performance.summary.winRate : null, lastFilledAt: typeof performance.summary.lastFilledAt === "string" ? performance.summary.lastFilledAt : null, }; } function normalizeStrategyName(name: string): string { return name.trim().toLowerCase(); } function normalizeTimestamp(value?: string | null): number | null { if (!value) return null; const timestamp = Date.parse(value); return Number.isFinite(timestamp) ? timestamp : null; } function formatStrategyCount(count: number): string { return `${count} ${count === 1 ? "strategy" : "strategies"}`; } function summarizeStrategyChangeCount(params: { newCount: number; updatedCount: number; removedCount: number; }): string { const parts: string[] = []; if (params.newCount > 0) { parts.push(`${params.newCount} new`); } if (params.updatedCount > 0) { parts.push(`${params.updatedCount} updated`); } if (params.removedCount > 0) { parts.push(`${params.removedCount} removed`); } if (parts.length === 0) { return "No strategy changes since the previous snapshot."; } const total = params.newCount + params.updatedCount + params.removedCount; return `${total} strategy change${total === 1 ? "" : "s"}: ${parts.join(", ")}.`; } export function buildStrategyChanges(params: { currentStrategies: StrategySnapshot[]; previousStrategies: StrategySnapshot[]; }): StrategyChangeSummary | undefined { const currentStrategies = params.currentStrategies; const previousStrategies = params.previousStrategies; if (currentStrategies.length === 0 && previousStrategies.length === 0) { return undefined; } if (previousStrategies.length === 0) { return { summary: `Initial snapshot with ${formatStrategyCount(currentStrategies.length)}.`, items: currentStrategies.map((strategy) => ({ name: strategy.name, status: "new" as const, })), }; } const previousByName = new Map( previousStrategies.map((strategy) => [ normalizeStrategyName(strategy.name), strategy, ]), ); const currentByName = new Map( currentStrategies.map((strategy) => [ normalizeStrategyName(strategy.name), strategy, ]), ); const items: StrategyChangeItem[] = []; for (const strategy of currentStrategies) { const previous = previousByName.get(normalizeStrategyName(strategy.name)); if (!previous) { items.push({ name: strategy.name, status: "new", }); continue; } const netPnlDelta = typeof strategy.netPnl === "number" && typeof previous.netPnl === "number" ? strategy.netPnl - previous.netPnl : null; const tradesDelta = typeof strategy.trades === "number" && typeof previous.trades === "number" ? strategy.trades - previous.trades : null; const lastFilledAtChanged = normalizeTimestamp(strategy.lastFilledAt) !== normalizeTimestamp(previous.lastFilledAt); if ( (netPnlDelta != null && Math.abs(netPnlDelta) >= STRATEGY_DELTA_EPSILON) || (tradesDelta != null && tradesDelta !== 0) || lastFilledAtChanged ) { items.push({ name: strategy.name, status: "updated", ...(netPnlDelta != null ? { netPnlDelta } : {}), ...(tradesDelta != null ? { tradesDelta } : {}), }); } } for (const strategy of previousStrategies) { if (currentByName.has(normalizeStrategyName(strategy.name))) continue; items.push({ name: strategy.name, status: "removed", }); } const newCount = items.filter((item) => item.status === "new").length; const updatedCount = items.filter((item) => item.status === "updated").length; const removedCount = items.filter((item) => item.status === "removed").length; return { summary: summarizeStrategyChangeCount({ newCount, updatedCount, removedCount, }), items, }; } function formatUsd(value: number): string { const sign = value < 0 ? "-" : ""; return `${sign}$${Math.abs(value).toFixed(2)}`; } function formatUsdDelta(value: number): string { return `${value >= 0 ? "+" : "-"}$${Math.abs(value).toFixed(2)}`; } function formatTradesDelta(value: number): string { return `${value >= 0 ? "+" : ""}${value}`; } function describeStrategyChange(item: StrategyChangeItem): string { if (item.status === "new") { return `${item.name} was added to the snapshot.`; } if (item.status === "removed") { return `${item.name} dropped out of the snapshot.`; } const parts: string[] = []; if (typeof item.netPnlDelta === "number" && Math.abs(item.netPnlDelta) >= STRATEGY_DELTA_EPSILON) { parts.push(`${formatUsdDelta(item.netPnlDelta)} net PnL`); } if (typeof item.tradesDelta === "number" && item.tradesDelta !== 0) { parts.push(`${item.tradesDelta >= 0 ? "+" : ""}${item.tradesDelta} trades`); } if (parts.length === 0) { return `${item.name} changed since the prior snapshot.`; } return `${item.name}: ${parts.join(", ")}.`; } function buildFallbackBriefing(params: { currentStrategies: StrategySnapshot[]; strategyChanges?: StrategyChangeSummary; news?: DailyNewsSection; importantDates?: DailyImportantDatesSection; previousRunAt?: string | null; }): DailyDigestBriefing { const strategyCount = params.currentStrategies.length; const headline = params.strategyChanges?.summary ?? (strategyCount > 0 ? `Active snapshot with ${formatStrategyCount(strategyCount)}.` : "No active strategies were detected in this snapshot."); const changes = params.strategyChanges?.items.slice(0, 4).map(describeStrategyChange) ?? []; return { headline, overview: null, changes, previousRunAt: params.previousRunAt ?? null, }; } function normalizeHistoryItem(value: unknown): DailyDigestHistoryItem | null { if (!isRecord(value)) return null; const messageId = typeof value.messageId === "string" ? value.messageId : ""; const content = typeof value.content === "string" ? value.content : ""; const runAt = typeof value.runAt === "string" ? value.runAt : ""; const createdAt = typeof value.createdAt === "string" ? value.createdAt : ""; const updatedAt = typeof value.updatedAt === "string" ? value.updatedAt : ""; if (!messageId || !runAt || !createdAt || !updatedAt) { return null; } return { messageId, content, runAt, createdAt, updatedAt, metadata: isRecord(value.metadata) ? value.metadata : undefined, }; } function normalizeStoredStrategySnapshot(value: unknown): StrategySnapshot | null { if (!isRecord(value)) { return null; } return { name: typeof value.name === "string" ? value.name : "Unnamed strategy", netPnl: typeof value.netPnl === "number" ? value.netPnl : null, trades: typeof value.trades === "number" ? value.trades : null, lastFilledAt: typeof value.lastFilledAt === "string" ? value.lastFilledAt : null, }; } export async function fetchDigestHistory(limit: number): Promise<DailyDigestHistoryItem[]> { const response = await openpondRequest(`/apps/agent/history?limit=${limit}`, { method: "GET", }); if (!response.ok) { throw new Error(`Daily summary history request failed (${response.status})`); } const items = isRecord(response.data) && Array.isArray(response.data.items) ? response.data.items : []; return items .map((item) => normalizeHistoryItem(item)) .filter((item): item is DailyDigestHistoryItem => item !== null); } function extractHistoryContext( historyItems: DailyDigestHistoryItem[], ): DailyDigestHistoryContext { const item = historyItems[0] ?? null; if (!item?.metadata) { return { item, strategies: [], performance: null }; } const strategies = Array.isArray(item.metadata.strategies) ? item.metadata.strategies .map((strategy) => normalizeStoredStrategySnapshot(strategy)) .filter((strategy): strategy is StrategySnapshot => strategy !== null) : extractStrategies( isRecord(item.metadata.performance) ? item.metadata.performance as JsonValue : null, ); return { item, strategies, performance: isRecord(item.metadata.performance) ? (item.metadata.performance as JsonValue) : null, }; } export async function buildDigestBriefing(params: { runAt: string; config: DailySummaryConfig; performance: JsonValue | null; currentStrategies: StrategySnapshot[]; subAgentResults: DailySummarySubAgentResult[]; news?: DailyNewsSection; importantDates?: DailyImportantDatesSection; historyItems: DailyDigestHistoryItem[]; }): Promise<{ briefing: DailyDigestBriefing; previousStrategies: StrategySnapshot[]; strategyChanges?: StrategyChangeSummary; }> { const historyContext = extractHistoryContext(params.historyItems); const previousRunAt = historyContext.item?.runAt ?? null; const previousStrategies = historyContext.strategies; const strategyChanges = buildStrategyChanges({ currentStrategies: params.currentStrategies, previousStrategies, }); return { briefing: buildFallbackBriefing({ currentStrategies: params.currentStrategies, strategyChanges, news: params.news, importantDates: params.importantDates, previousRunAt, }), previousStrategies, strategyChanges, }; } function formatWinRate(value?: number | null): string | null { if (typeof value !== "number") return null; return `${value.toFixed(1)}%`; } function formatStrategyLine( strategy: StrategySnapshot, strategyChange?: StrategyChangeItem, ): string { const parts = [strategy.name]; if (typeof strategy.netPnl === "number") { parts.push(`net PnL ${formatUsd(strategy.netPnl)}`); } if ( typeof strategyChange?.netPnlDelta === "number" && Math.abs(strategyChange.netPnlDelta) >= STRATEGY_DELTA_EPSILON ) { parts.push(`Δ ${formatUsdDelta(strategyChange.netPnlDelta)}`); } if (typeof strategy.trades === "number") { parts.push(`${strategy.trades} trades`); } if (typeof strategyChange?.tradesDelta === "number" && strategyChange.tradesDelta !== 0) { parts.push(`Δ ${formatTradesDelta(strategyChange.tradesDelta)} trades`); } return `- ${parts.join(" | ")}`; } function formatReleaseAt(value: string): string { const timestamp = Date.parse(value); if (!Number.isFinite(timestamp)) return value; return new Intl.DateTimeFormat("en-US", { timeZone: "America/New_York", month: "short", day: "numeric", hour: "numeric", minute: "2-digit", timeZoneName: "short", }).format(new Date(timestamp)); } export function formatDigest(params: { runAt: string; briefing: DailyDigestBriefing; performance: JsonValue | null; strategies: StrategySnapshot[]; strategyChanges?: StrategyChangeSummary; subAgentResults: DailySummarySubAgentResult[]; news?: DailyNewsSection; importantDates?: DailyImportantDatesSection; }) { const lines: string[] = []; const strategyChangesByName = new Map( (params.strategyChanges?.items ?? []).map((item) => [ normalizeStrategyName(item.name), item, ]), ); lines.push(`Daily Snapshot - ${params.runAt}`); lines.push(""); if (params.news) { lines.push("News:"); if (params.news.summary) { lines.push(params.news.summary); } for (const story of params.news.stories) { const parts = [story.title]; if (story.source) { parts.push(story.source); } if (story.link) { parts.push(story.link); } lines.push(`- ${parts.join(" | ")}`); } lines.push(""); } if (params.importantDates && params.importantDates.events.length > 0) { lines.push("Upcoming dates:"); for (const event of params.importantDates.events.slice(0, MAX_IMPORTANT_DATES)) { const title = event.referencePeriod ? `${event.series} (${event.referencePeriod})` : event.series; const parts = [title, formatReleaseAt(event.releaseAt)]; if (event.sourceUrl) { parts.push(event.sourceUrl); } lines.push(`- ${parts.join(" | ")}`); } lines.push(""); } if (params.strategies.length > 0) { lines.push("Strategies:"); for (const strategy of params.strategies) { lines.push( formatStrategyLine( strategy, strategyChangesByName.get(normalizeStrategyName(strategy.name)), ), ); } lines.push(""); } return lines.join("\n").trim(); }