Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/tools/finance/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface ApiResponse {
export async function callApi(
endpoint: string,
params: Record<string, string | number | string[] | undefined>,
options?: { cacheable?: boolean }
options?: { cacheable?: boolean; cacheTtlMs?: number }
): Promise<ApiResponse> {
const label = describeRequest(endpoint, params);

Expand Down Expand Up @@ -70,7 +70,7 @@ export async function callApi(

// Persist for future requests when the caller marked the response as cacheable
if (options?.cacheable) {
writeCache(endpoint, params, data, url.toString());
writeCache(endpoint, params, data, url.toString(), options.cacheTtlMs);
}

return { data, url: url.toString() };
Expand Down
3 changes: 2 additions & 1 deletion src/tools/finance/company_facts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import { callApi } from './api.js';
import { CACHE_TTL_MONTHLY } from '../../utils/cache.js';
import { formatToolResult } from '../types.js';

const CompanyFactsInputSchema = z.object({
Expand All @@ -14,7 +15,7 @@ export const getCompanyFacts = new DynamicStructuredTool({
description: `Retrieves company facts and metadata for a given ticker, including sector, industry, market cap, number of employees, listing date, exchange, location, weighted average shares, website. Useful for getting an overview of a company's profile and basic information.`,
schema: CompanyFactsInputSchema,
func: async (input) => {
const { data, url } = await callApi('/company/facts', { ticker: input.ticker });
const { data, url } = await callApi('/company/facts', { ticker: input.ticker }, { cacheable: true, cacheTtlMs: CACHE_TTL_MONTHLY });
return formatToolResult(data.company_facts || {}, [url]);
},
});
5 changes: 3 additions & 2 deletions src/tools/finance/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import { callApi } from './api.js';
import { CACHE_TTL_WEEKLY, CACHE_TTL_QUARTERLY } from '../../utils/cache.js';
import { formatToolResult } from '../types.js';

const CryptoPriceSnapshotInputSchema = z.object({
Expand All @@ -17,7 +18,7 @@ export const getCryptoPriceSnapshot = new DynamicStructuredTool({
schema: CryptoPriceSnapshotInputSchema,
func: async (input) => {
const params = { ticker: input.ticker };
const { data, url } = await callApi('/crypto/prices/snapshot/', params);
const { data, url } = await callApi('/crypto/prices/snapshot/', params, { cacheable: true, cacheTtlMs: CACHE_TTL_WEEKLY });
return formatToolResult(data.snapshot || {}, [url]);
},
});
Expand Down Expand Up @@ -56,7 +57,7 @@ export const getCryptoPrices = new DynamicStructuredTool({
const endDate = new Date(input.end_date + 'T00:00:00');
const today = new Date();
today.setHours(0, 0, 0, 0);
const { data, url } = await callApi('/crypto/prices/', params, { cacheable: endDate < today });
const { data, url } = await callApi('/crypto/prices/', params, { cacheable: endDate < today, cacheTtlMs: CACHE_TTL_QUARTERLY });
return formatToolResult(data.prices || [], [url]);
},
});
Expand Down
3 changes: 2 additions & 1 deletion src/tools/finance/estimates.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import { callApi } from './api.js';
import { CACHE_TTL_MONTHLY } from '../../utils/cache.js';
import { formatToolResult } from '../types.js';

const AnalystEstimatesInputSchema = z.object({
Expand All @@ -24,7 +25,7 @@ export const getAnalystEstimates = new DynamicStructuredTool({
ticker: input.ticker,
period: input.period,
};
const { data, url } = await callApi('/analyst-estimates/', params);
const { data, url } = await callApi('/analyst-estimates/', params, { cacheable: true, cacheTtlMs: CACHE_TTL_MONTHLY });
return formatToolResult(data.analyst_estimates || [], [url]);
},
});
Expand Down
9 changes: 5 additions & 4 deletions src/tools/finance/filings.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import { callApi } from './api.js';
import { CACHE_TTL_QUARTERLY } from '../../utils/cache.js';
import { formatToolResult } from '../types.js';

// Types for filing item metadata
Expand Down Expand Up @@ -63,7 +64,7 @@ export const getFilings = new DynamicStructuredTool({
limit: input.limit,
filing_type: input.filing_type,
};
const { data, url } = await callApi('/filings/', params);
const { data, url } = await callApi('/filings/', params, { cacheable: true, cacheTtlMs: CACHE_TTL_QUARTERLY });
return formatToolResult(data.filings || [], [url]);
},
});
Expand Down Expand Up @@ -95,7 +96,7 @@ export const get10KFilingItems = new DynamicStructuredTool({
item: input.items, // API expects 'item' not 'items'
};
// SEC filings are legally immutable once filed
const { data, url } = await callApi('/filings/items/', params, { cacheable: true });
const { data, url } = await callApi('/filings/items/', params, { cacheable: true, cacheTtlMs: CACHE_TTL_QUARTERLY });
return formatToolResult(data, [url]);
},
});
Expand Down Expand Up @@ -127,7 +128,7 @@ export const get10QFilingItems = new DynamicStructuredTool({
item: input.items, // API expects 'item' not 'items'
};
// SEC filings are legally immutable once filed
const { data, url } = await callApi('/filings/items/', params, { cacheable: true });
const { data, url } = await callApi('/filings/items/', params, { cacheable: true, cacheTtlMs: CACHE_TTL_QUARTERLY });
return formatToolResult(data, [url]);
},
});
Expand All @@ -152,7 +153,7 @@ export const get8KFilingItems = new DynamicStructuredTool({
accession_number: input.accession_number,
};
// SEC filings are legally immutable once filed
const { data, url } = await callApi('/filings/items/', params, { cacheable: true });
const { data, url } = await callApi('/filings/items/', params, { cacheable: true, cacheTtlMs: CACHE_TTL_QUARTERLY });
return formatToolResult(data, [url]);
},
});
Expand Down
9 changes: 5 additions & 4 deletions src/tools/finance/fundamentals.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import { callApi } from './api.js';
import { CACHE_TTL_QUARTERLY } from '../../utils/cache.js';
import { formatToolResult } from '../types.js';

const FinancialStatementsInputSchema = z.object({
Expand Down Expand Up @@ -60,7 +61,7 @@ export const getIncomeStatements = new DynamicStructuredTool({
schema: FinancialStatementsInputSchema,
func: async (input) => {
const params = createParams(input);
const { data, url } = await callApi('/financials/income-statements/', params);
const { data, url } = await callApi('/financials/income-statements/', params, { cacheable: true, cacheTtlMs: CACHE_TTL_QUARTERLY });
return formatToolResult(data.income_statements || {}, [url]);
},
});
Expand All @@ -71,7 +72,7 @@ export const getBalanceSheets = new DynamicStructuredTool({
schema: FinancialStatementsInputSchema,
func: async (input) => {
const params = createParams(input);
const { data, url } = await callApi('/financials/balance-sheets/', params);
const { data, url } = await callApi('/financials/balance-sheets/', params, { cacheable: true, cacheTtlMs: CACHE_TTL_QUARTERLY });
return formatToolResult(data.balance_sheets || {}, [url]);
},
});
Expand All @@ -82,7 +83,7 @@ export const getCashFlowStatements = new DynamicStructuredTool({
schema: FinancialStatementsInputSchema,
func: async (input) => {
const params = createParams(input);
const { data, url } = await callApi('/financials/cash-flow-statements/', params);
const { data, url } = await callApi('/financials/cash-flow-statements/', params, { cacheable: true, cacheTtlMs: CACHE_TTL_QUARTERLY });
return formatToolResult(data.cash_flow_statements || {}, [url]);
},
});
Expand All @@ -93,7 +94,7 @@ export const getAllFinancialStatements = new DynamicStructuredTool({
schema: FinancialStatementsInputSchema,
func: async (input) => {
const params = createParams(input);
const { data, url } = await callApi('/financials/', params);
const { data, url } = await callApi('/financials/', params, { cacheable: true, cacheTtlMs: CACHE_TTL_QUARTERLY });
return formatToolResult(data.financials || {}, [url]);
},
});
Expand Down
3 changes: 2 additions & 1 deletion src/tools/finance/insider_trades.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import { callApi } from './api.js';
import { CACHE_TTL_QUARTERLY } from '../../utils/cache.js';
import { formatToolResult } from '../types.js';

const InsiderTradesInputSchema = z.object({
Expand Down Expand Up @@ -47,7 +48,7 @@ export const getInsiderTrades = new DynamicStructuredTool({
filing_date_gt: input.filing_date_gt,
filing_date_lt: input.filing_date_lt,
};
const { data, url } = await callApi('/insider-trades/', params);
const { data, url } = await callApi('/insider-trades/', params, { cacheable: true, cacheTtlMs: CACHE_TTL_QUARTERLY });
return formatToolResult(data.insider_trades || [], [url]);
},
});
5 changes: 3 additions & 2 deletions src/tools/finance/key-ratios.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import { callApi } from './api.js';
import { CACHE_TTL_WEEKLY, CACHE_TTL_QUARTERLY } from '../../utils/cache.js';
import { formatToolResult } from '../types.js';

const KeyRatiosSnapshotInputSchema = z.object({
Expand All @@ -17,7 +18,7 @@ export const getKeyRatiosSnapshot = new DynamicStructuredTool({
schema: KeyRatiosSnapshotInputSchema,
func: async (input) => {
const params = { ticker: input.ticker };
const { data, url } = await callApi('/financial-metrics/snapshot/', params);
const { data, url } = await callApi('/financial-metrics/snapshot/', params, { cacheable: true, cacheTtlMs: CACHE_TTL_WEEKLY });
return formatToolResult(data.snapshot || {}, [url]);
},
});
Expand Down Expand Up @@ -79,7 +80,7 @@ export const getKeyRatios = new DynamicStructuredTool({
report_period_lt: input.report_period_lt,
report_period_lte: input.report_period_lte,
};
const { data, url } = await callApi('/financial-metrics/', params);
const { data, url } = await callApi('/financial-metrics/', params, { cacheable: true, cacheTtlMs: CACHE_TTL_QUARTERLY });
return formatToolResult(data.financial_metrics || [], [url]);
},
});
3 changes: 2 additions & 1 deletion src/tools/finance/news.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import { callApi } from './api.js';
import { CACHE_TTL_WEEKLY } from '../../utils/cache.js';
import { formatToolResult } from '../types.js';

const NewsInputSchema = z.object({
Expand Down Expand Up @@ -29,7 +30,7 @@ export const getNews = new DynamicStructuredTool({
start_date: input.start_date,
end_date: input.end_date,
};
const { data, url } = await callApi('/news/', params);
const { data, url } = await callApi('/news/', params, { cacheable: true, cacheTtlMs: CACHE_TTL_WEEKLY });
return formatToolResult(data.news || [], [url]);
},
});
5 changes: 3 additions & 2 deletions src/tools/finance/prices.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import { callApi } from './api.js';
import { CACHE_TTL_WEEKLY, CACHE_TTL_QUARTERLY } from '../../utils/cache.js';
import { formatToolResult } from '../types.js';

const PriceSnapshotInputSchema = z.object({
Expand All @@ -17,7 +18,7 @@ export const getPriceSnapshot = new DynamicStructuredTool({
schema: PriceSnapshotInputSchema,
func: async (input) => {
const params = { ticker: input.ticker };
const { data, url } = await callApi('/prices/snapshot/', params);
const { data, url } = await callApi('/prices/snapshot/', params, { cacheable: true, cacheTtlMs: CACHE_TTL_WEEKLY });
return formatToolResult(data.snapshot || {}, [url]);
},
});
Expand Down Expand Up @@ -56,7 +57,7 @@ export const getPrices = new DynamicStructuredTool({
const endDate = new Date(input.end_date + 'T00:00:00');
const today = new Date();
today.setHours(0, 0, 0, 0);
const { data, url } = await callApi('/prices/', params, { cacheable: endDate < today });
const { data, url } = await callApi('/prices/', params, { cacheable: endDate < today, cacheTtlMs: CACHE_TTL_QUARTERLY });
return formatToolResult(data.prices || [], [url]);
},
});
Expand Down
3 changes: 2 additions & 1 deletion src/tools/finance/segments.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import { callApi } from './api.js';
import { CACHE_TTL_QUARTERLY } from '../../utils/cache.js';
import { formatToolResult } from '../types.js';

const SegmentedRevenuesInputSchema = z.object({
Expand All @@ -27,7 +28,7 @@ export const getSegmentedRevenues = new DynamicStructuredTool({
period: input.period,
limit: input.limit,
};
const { data, url } = await callApi('/financials/segmented-revenues/', params);
const { data, url } = await callApi('/financials/segmented-revenues/', params, { cacheable: true, cacheTtlMs: CACHE_TTL_QUARTERLY });
return formatToolResult(data.segmented_revenues || {}, [url]);
},
});
Expand Down
84 changes: 82 additions & 2 deletions src/utils/cache.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs';
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs';
import { join } from 'path';
import { buildCacheKey, readCache, writeCache } from './cache.js';
import { buildCacheKey, readCache, writeCache, CACHE_TTL_WEEKLY, CACHE_TTL_MONTHLY, CACHE_TTL_QUARTERLY } from './cache.js';

const TEST_CACHE_DIR = '.dexter/cache';

Expand Down Expand Up @@ -107,4 +107,84 @@ describe('readCache / writeCache', () => {
expect(cached).toBeNull();
expect(existsSync(filepath)).toBe(false);
});

test('respects per-entry quarterly TTL — fresh entry is returned', () => {
const endpoint = '/financials/income-statements/';
const params = { ticker: 'AAPL', period: 'annual', limit: 5 };
const data = { income_statements: [{ revenue: 400_000_000_000 }] };
const url = 'https://api.financialdatasets.ai/financials/income-statements/?ticker=AAPL';

writeCache(endpoint, params, data, url, CACHE_TTL_QUARTERLY);
const cached = readCache(endpoint, params);

expect(cached).not.toBeNull();
expect(cached!.data).toEqual(data);
});

test('quarterly TTL entry survives past the default weekly window', () => {
const endpoint = '/institutional-ownership/';
const params = { ticker: 'AAPL', limit: 10 };
const data = { 'institutional-ownership': [{ investor: 'Vanguard', shares: 1_000_000 }] };
const url = 'https://api.financialdatasets.ai/institutional-ownership/?ticker=AAPL';

// Write the entry, then manually backdate it to 30 days ago
writeCache(endpoint, params, data, url, CACHE_TTL_QUARTERLY);

const key = buildCacheKey(endpoint, params);
const filepath = join(TEST_CACHE_DIR, key);
const raw = JSON.parse(readFileSync(filepath, 'utf-8'));
raw.cachedAt = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
writeFileSync(filepath, JSON.stringify(raw, null, 2));

// Should still be valid — 30 days < 90 day TTL
const cached = readCache(endpoint, params);
expect(cached).not.toBeNull();
expect(cached!.data).toEqual(data);
});

test('quarterly TTL entry expires after 90 days', () => {
const endpoint = '/institutional-ownership/';
const params = { ticker: 'MSFT', limit: 10 };
const data = { 'institutional-ownership': [{ investor: 'BlackRock', shares: 500_000 }] };
const url = 'https://api.financialdatasets.ai/institutional-ownership/?ticker=MSFT';

writeCache(endpoint, params, data, url, CACHE_TTL_QUARTERLY);

const key = buildCacheKey(endpoint, params);
const filepath = join(TEST_CACHE_DIR, key);
const raw = JSON.parse(readFileSync(filepath, 'utf-8'));
raw.cachedAt = new Date(Date.now() - 91 * 24 * 60 * 60 * 1000).toISOString();
writeFileSync(filepath, JSON.stringify(raw, null, 2));

// Should be expired — 91 days > 90 day TTL
const cached = readCache(endpoint, params);
expect(cached).toBeNull();
expect(existsSync(filepath)).toBe(false);
});

test('entry without ttlMs falls back to default weekly TTL', () => {
const endpoint = '/prices/snapshot/';
const params = { ticker: 'AAPL' };
const data = { snapshot: { price: 195.5 } };
const url = 'https://api.financialdatasets.ai/prices/snapshot/?ticker=AAPL';

// Write without ttlMs (legacy behavior)
writeCache(endpoint, params, data, url);

const key = buildCacheKey(endpoint, params);
const filepath = join(TEST_CACHE_DIR, key);
const raw = JSON.parse(readFileSync(filepath, 'utf-8'));
// Backdate to 8 days ago — past the weekly default
raw.cachedAt = new Date(Date.now() - 8 * 24 * 60 * 60 * 1000).toISOString();
writeFileSync(filepath, JSON.stringify(raw, null, 2));

const cached = readCache(endpoint, params);
expect(cached).toBeNull();
});

test('TTL constants have correct values', () => {
expect(CACHE_TTL_WEEKLY).toBe(7 * 24 * 60 * 60 * 1000);
expect(CACHE_TTL_MONTHLY).toBe(30 * 24 * 60 * 60 * 1000);
expect(CACHE_TTL_QUARTERLY).toBe(90 * 24 * 60 * 60 * 1000);
});
});
Loading
Loading