munger-engine / lib /sync-service.ts
dromerosm's picture
feat: integrate real Alpaca orders in detail page and chart with layer toggles
bf9d545
import { MungerEngine } from '@/lib/munger-engine';
import { JsonStore } from '@/lib/store';
import fs from 'fs';
import path from 'path';
import Alpaca from '@alpacahq/alpaca-trade-api';
// Load initial symbols (same logic as scripts/find_forever_stocks.ts)
const loadSymbols = () => {
try {
const jsonPath = path.resolve(process.cwd(), 'scripts/sp500_symbols.json');
if (fs.existsSync(jsonPath)) {
return JSON.parse(fs.readFileSync(jsonPath, 'utf-8')) as string[];
}
} catch {
console.warn("Failed to load symbols from file, using fallback");
}
return ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'META', 'BRK-B', 'V', 'JNJ', 'WMT', 'PG'];
};
// Fetch portfolio symbols from Alpaca
const getPortfolioSymbols = async (): Promise<string[]> => {
try {
if (!process.env.ALPACA_KEY || !process.env.ALPACA_SECRET) {
logToFile('getPortfolioSymbols: No Alpaca credentials found');
return [];
}
const alpaca = new (Alpaca as any)({
keyId: process.env.ALPACA_KEY,
secretKey: process.env.ALPACA_SECRET,
paper: true,
});
const positions = await alpaca.getPositions();
const portfolioSymbols = positions.map((p: any) => p.symbol as string);
logToFile(`getPortfolioSymbols: Found ${portfolioSymbols.length} symbols in portfolio: ${portfolioSymbols.join(', ')}`);
return portfolioSymbols;
} catch (error) {
logToFile(`getPortfolioSymbols: Failed to fetch - ${(error as Error).message}`);
console.warn('Failed to fetch portfolio symbols:', error);
return [];
}
};
const logToFile = (msg: string) => {
try {
const logPath = path.resolve(process.cwd(), 'debug_sync.log');
fs.appendFileSync(logPath, `${new Date().toISOString()}: ${msg}\n`);
} catch { }
};
export async function runSync(options?: { force?: boolean }) {
logToFile("runSync: Started");
const currentStatus = JsonStore.getStatus();
if (currentStatus.status === 'SYNCING') {
logToFile("runSync: Already syncing, aborting");
throw new Error('Sync already in progress');
}
// Get S&P 500 symbols
const sp500Symbols = loadSymbols();
logToFile(`runSync: Loaded ${sp500Symbols.length} S&P 500 symbols`);
// Get portfolio symbols from Alpaca
const portfolioSymbols = await getPortfolioSymbols();
// Combine symbols (unique set)
const allSymbols = [...new Set([...sp500Symbols, ...portfolioSymbols])];
logToFile(`runSync: Total unique symbols to analyze: ${allSymbols.length} (S&P500: ${sp500Symbols.length}, Portfolio: ${portfolioSymbols.length})`);
JsonStore.updateStatus({
status: 'SYNCING',
totalAssets: allSymbols.length,
assetsAnalyzed: 0,
lastSyncTimestamp: new Date().toISOString()
});
logToFile("runSync: Status set to SYNCING. Starting background process.");
(async () => {
try {
logToFile('runSync: Async block started');
const results = await MungerEngine.processBatch(allSymbols, portfolioSymbols, (analyzed, total) => {
// Log every 10 items or so to avoid huge IO
if (analyzed % 10 === 0) logToFile(`runSync: Progress ${analyzed}/${total}`);
JsonStore.updateStatus({
assetsAnalyzed: analyzed,
totalAssets: total
});
});
logToFile(`runSync: Finished processing. Saving ${results.length} results.`);
JsonStore.saveStocks(results);
logToFile("runSync: Updating status to IDLE");
JsonStore.updateStatus({
status: 'IDLE',
lastSyncTimestamp: new Date().toISOString(),
buyTriggers: results.filter(s => s.signal === 'BUY_TRIGGER').length
});
} catch (error) {
logToFile(`runSync: FAILED with error: ${(error as Error).message}`);
console.error('Sync failed:', error);
JsonStore.updateStatus({
status: 'ERROR',
errors: [(error as Error).message]
});
}
})();
logToFile("runSync: Returning success to caller");
return { success: true, totalAssets: allSymbols.length };
}