Automate issuer overlay creation from ticker searches

This commit is contained in:
2026-03-19 20:44:58 -04:00
parent 17de3dd72d
commit 391d6d34ce
79 changed files with 4746 additions and 695 deletions

View File

@@ -14,7 +14,8 @@ import type {
ResearchMemoConviction,
ResearchMemoRating,
ResearchMemoSection,
TaskStatus
TaskStatus,
TickerAutomationSource
} from '@/lib/types';
import { auth } from '@/lib/auth';
import { requireAuthenticatedSession } from '@/lib/server/auth-session';
@@ -71,6 +72,8 @@ import {
upsertWatchlistItemRecord
} from '@/lib/server/repos/watchlist';
import { answerSearchQuery, searchKnowledgeBase } from '@/lib/server/search';
import { shouldQueueTickerAutomation } from '@/lib/server/issuer-overlays';
import { ensureIssuerOverlayRow } from '@/lib/server/repos/issuer-overlays';
import {
enqueueTask,
findOrEnqueueTask,
@@ -91,6 +94,7 @@ const FINANCIAL_STATEMENT_KINDS: FinancialStatementKind[] = [
'income',
'balance',
'cash_flow',
'disclosure',
'equity',
'comprehensive_income'
];
@@ -99,6 +103,8 @@ const FINANCIAL_SURFACES: FinancialSurfaceKind[] = [
'income_statement',
'balance_sheet',
'cash_flow_statement',
'equity_statement',
'disclosures',
'ratios',
'segments_kpis',
'adjusted',
@@ -112,6 +118,7 @@ const RESEARCH_ARTIFACT_KINDS: ResearchArtifactKind[] = ['filing', 'ai_report',
const RESEARCH_ARTIFACT_SOURCES: ResearchArtifactSource[] = ['system', 'user'];
const RESEARCH_MEMO_RATINGS: ResearchMemoRating[] = ['strong_buy', 'buy', 'hold', 'sell'];
const RESEARCH_MEMO_CONVICTIONS: ResearchMemoConviction[] = ['low', 'medium', 'high'];
const TICKER_AUTOMATION_SOURCES: TickerAutomationSource[] = ['analysis', 'financials', 'search', 'graphing', 'research'];
const RESEARCH_MEMO_SECTIONS: ResearchMemoSection[] = [
'thesis',
'variant_view',
@@ -212,6 +219,10 @@ function surfaceFromLegacyStatement(statement: FinancialStatementKind): Financia
return 'balance_sheet';
case 'cash_flow':
return 'cash_flow_statement';
case 'disclosure':
return 'disclosures';
case 'equity':
return 'equity_statement';
default:
return 'income_statement';
}
@@ -296,6 +307,12 @@ function asResearchMemoSection(value: unknown) {
: undefined;
}
function asTickerAutomationSource(value: unknown) {
return TICKER_AUTOMATION_SOURCES.includes(value as TickerAutomationSource)
? value as TickerAutomationSource
: undefined;
}
function formatLabel(value: string) {
return value
.split('_')
@@ -361,6 +378,42 @@ async function queueAutoFilingSync(
}
}
async function ensureTickerAutomationTask(input: {
userId: string;
ticker: string;
source: TickerAutomationSource;
}) {
void input.source;
const ticker = input.ticker.trim().toUpperCase();
await ensureIssuerOverlayRow(ticker);
if (!(await shouldQueueTickerAutomation(ticker))) {
return {
queued: false,
task: null
};
}
const watchlistItem = await getWatchlistItemByTicker(input.userId, ticker);
const task = await findOrEnqueueTask({
userId: input.userId,
taskType: 'sync_filings',
payload: buildSyncFilingsPayload({
ticker,
limit: defaultFinancialSyncLimit(),
category: watchlistItem?.category,
tags: watchlistItem?.tags
}),
priority: 89,
resourceKey: `sync_filings:${ticker}`
});
return {
queued: true,
task
};
}
const authHandler = ({ request }: { request: Request }) => auth.handler(request);
async function checkWorkflowBackend() {
@@ -821,6 +874,45 @@ export const app = new Elysia({ prefix: '/api' })
return Response.json({ insight });
})
.post('/tickers/ensure', async ({ body }) => {
const { session, response } = await requireAuthenticatedSession();
if (response) {
return response;
}
const payload = asRecord(body);
const ticker = typeof payload.ticker === 'string' ? payload.ticker.trim().toUpperCase() : '';
const source = asTickerAutomationSource(payload.source);
if (!ticker) {
return jsonError('ticker is required');
}
if (!source) {
return jsonError('source is required');
}
try {
return Response.json(await ensureTickerAutomationTask({
userId: session.user.id,
ticker,
source
}));
} catch (error) {
return jsonError(asErrorMessage(error, `Failed to ensure ticker automation for ${ticker}`));
}
}, {
body: t.Object({
ticker: t.String({ minLength: 1 }),
source: t.Union([
t.Literal('analysis'),
t.Literal('financials'),
t.Literal('search'),
t.Literal('graphing'),
t.Literal('research')
])
})
})
.get('/research/workspace', async ({ query }) => {
const { session, response } = await requireAuthenticatedSession();
if (response) {
@@ -1492,6 +1584,8 @@ export const app = new Elysia({ prefix: '/api' })
t.Literal('income_statement'),
t.Literal('balance_sheet'),
t.Literal('cash_flow_statement'),
t.Literal('equity_statement'),
t.Literal('disclosures'),
t.Literal('ratios'),
t.Literal('segments_kpis'),
t.Literal('adjusted'),
@@ -1506,6 +1600,7 @@ export const app = new Elysia({ prefix: '/api' })
t.Literal('income'),
t.Literal('balance'),
t.Literal('cash_flow'),
t.Literal('disclosure'),
t.Literal('equity'),
t.Literal('comprehensive_income')
])),