Files
MosaicIQ/apps/desktop/src/dataRefresh.ts

193 lines
6.2 KiB
TypeScript

/**
* Background data refresh service
* Updates prices, filings, and earnings data on a schedule
*/
import type { BrowserWindow } from "electron";
import type { Db } from "@mosaiciq/shared/db";
import { fetchQuote, fetchFilings } from "@mosaiciq/shared/data";
import { upsertCompany } from "@mosaiciq/shared/db";
import { listFilings } from "@mosaiciq/shared/db";
export interface DataRefreshOptions {
priceInterval?: number; // minutes
filingInterval?: number; // minutes
earningsInterval?: number; // minutes
}
export function startDataRefresh(
db: Db,
mainWindow: BrowserWindow,
options: DataRefreshOptions = {}
): () => void {
const {
priceInterval = 5,
filingInterval = 60,
earningsInterval = 1440, // daily
} = options;
let priceTimeout: NodeJS.Timeout | null = null;
let filingTimeout: NodeJS.Timeout | null = null;
let earningsTimeout: NodeJS.Timeout | null = null;
// Get all portfolio holdings
function getPortfolioTickers(): string[] {
const stmt = db.prepare(`
SELECT DISTINCT ticker FROM holdings
UNION
SELECT DISTINCT ticker FROM companies
`);
const rows = stmt.all() as Array<{ ticker: string }>;
return rows.map((r) => r.ticker);
}
// Refresh prices for all tickers
async function refreshPrices() {
try {
const tickers = getPortfolioTickers();
if (tickers.length === 0) return;
console.log(`[DataRefresh] Refreshing prices for ${tickers.length} tickers`);
for (const ticker of tickers) {
const quote = await fetchQuote(ticker);
if (quote) {
// Update company price in database
const company = db.prepare("SELECT * FROM companies WHERE ticker = ?").get(ticker) as any;
if (company) {
db.prepare(`
UPDATE companies
SET price = ?, change_pct = ?, updated_at = datetime('now')
WHERE ticker = ?
`).run(quote.price, quote.changePercent, ticker);
}
}
}
// Notify UI of price updates
mainWindow?.webContents.send("data:prices-updated", {
timestamp: new Date().toISOString(),
});
console.log("[DataRefresh] Price refresh complete");
} catch (error) {
console.error("[DataRefresh] Error refreshing prices:", error);
}
// Schedule next refresh
priceTimeout = setTimeout(refreshPrices, priceInterval * 60 * 1000);
}
// Check for new filings
async function refreshFilings() {
try {
const tickers = getPortfolioTickers();
if (tickers.length === 0) return;
console.log(`[DataRefresh] Checking for new filings for ${tickers.length} tickers`);
for (const ticker of tickers) {
// Get most recent filing date from database
const existingFilings = listFilings(db, ticker);
const since = existingFilings.length > 0
? existingFilings[0].filedDate
: undefined;
const newFilings = await fetchFilings(ticker, { limit: 10, since });
for (const filing of newFilings) {
// Check if filing already exists
const exists = db.prepare(`
SELECT id FROM filings WHERE company_id = ? AND filed_date = ? AND form_type = ?
`).get(ticker, filing.filedDate, filing.formType);
if (!exists) {
db.prepare(`
INSERT INTO filings (id, company_id, form_type, filed_date, title)
VALUES (?, ?, ?, ?, ?)
`).run(
`${ticker}-${filing.formType}-${filing.filedDate}`,
ticker,
filing.formType,
filing.filedDate,
filing.title
);
// Notify UI of new filing
mainWindow?.webContents.send("alert:new-filing", {
ticker,
formType: filing.formType,
title: filing.title,
filedDate: filing.filedDate,
});
}
}
}
console.log("[DataRefresh] Filing refresh complete");
} catch (error) {
console.error("[DataRefresh] Error refreshing filings:", error);
}
// Schedule next refresh
filingTimeout = setTimeout(refreshFilings, filingInterval * 60 * 1000);
}
// Refresh earnings dates
async function refreshEarnings() {
try {
const tickers = getPortfolioTickers();
if (tickers.length === 0) return;
console.log(`[DataRefresh] Updating earnings dates for ${tickers.length} tickers`);
// Import dynamically to avoid circular dependency
const { getEarningsDate, getQuarterString } = await import("@mosaiciq/shared/data");
for (const ticker of tickers) {
const earningsDate = await getEarningsDate(ticker);
if (earningsDate) {
// Check if earnings schedule exists
const existing = db.prepare(`
SELECT id FROM earnings_schedules WHERE company_id = ? AND expected_date = ?
`).get(ticker, earningsDate.toISOString());
if (!existing) {
const quarter = getQuarterString(earningsDate);
db.prepare(`
INSERT INTO earnings_schedules (id, company_id, quarter, expected_date)
VALUES (?, ?, ?, ?)
`).run(
`earnings-${ticker}-${Date.now()}`,
ticker,
quarter,
earningsDate.toISOString()
);
}
}
}
console.log("[DataRefresh] Earnings refresh complete");
} catch (error) {
console.error("[DataRefresh] Error refreshing earnings:", error);
}
// Schedule next refresh
earningsTimeout = setTimeout(refreshEarnings, earningsInterval * 60 * 1000);
}
// Start all refresh cycles
console.log("[DataRefresh] Starting data refresh service");
priceTimeout = setTimeout(refreshPrices, 1000); // Start immediately
filingTimeout = setTimeout(refreshFilings, 5000); // Start after 5s
earningsTimeout = setTimeout(refreshEarnings, 10000); // Start after 10s
// Return cleanup function
return () => {
if (priceTimeout) clearTimeout(priceTimeout);
if (filingTimeout) clearTimeout(filingTimeout);
if (earningsTimeout) clearTimeout(earningsTimeout);
console.log("[DataRefresh] Stopped data refresh service");
};
}