193 lines
6.2 KiB
TypeScript
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");
|
|
};
|
|
}
|