const https = require("https"); const http = require("http"); async function runSignalWorker(archiveDb, intelligenceDb, config) { const loopDelay = config.workers?.signalLoopDelayMs ?? 120000; const llmConfig = config.openRouter || {}; const getCompanies = intelligenceDb.prepare("SELECT * FROM tracked_companies ORDER BY id"); const getLastSignal = intelligenceDb.prepare(` SELECT generated_at FROM trade_signals WHERE company_id = ? ORDER BY generated_at DESC LIMIT 1 `); const getFacts = intelligenceDb.prepare(` SELECT claim, type, confidence, confirmation_count FROM company_facts WHERE company_id = ? ORDER BY confirmation_count DESC LIMIT 40 `); const getPredictions = intelligenceDb.prepare(` SELECT type, direction, magnitude, timeframe, rationale, event_date, id FROM event_predictions WHERE company_id = ? AND created_at >= datetime('now', '-90 days') ORDER BY created_at DESC LIMIT 50 `); const getRelationships = intelligenceDb.prepare(` SELECT relationship_type, to_entity, confidence, confirmation_count FROM company_relationships WHERE from_company_id = ? ORDER BY confirmation_count DESC LIMIT 20 `); const deleteSignal = intelligenceDb.prepare( "DELETE FROM trade_signals WHERE company_id = ?" ); const insertSignal = intelligenceDb.prepare(` INSERT INTO trade_signals (company_id, signal, confidence, timeframe, risk_level, risk_factors, summary, key_drivers, supporting_prediction_ids, window_days) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 90) `); const recordEvent = intelligenceDb.prepare( `INSERT INTO worker_events (worker) VALUES ('signal')` ); const pruneEvents = intelligenceDb.prepare( `DELETE FROM worker_events WHERE worker = 'signal' AND completed_at < datetime('now', '-1 hour')` ); let pruneCounter = 0; while (true) { try { const companies = getCompanies.all(); // pick the company with the oldest (or missing) signal that has enough predictions let target = null; let oldestTs = null; const companiesByAge = []; for (const company of companies) { const row = getLastSignal.get(company.id); companiesByAge.push({ company, ts: row ? row.generated_at : null }); } // null ts (never generated) first, then oldest companiesByAge.sort((a, b) => { if (a.ts === null && b.ts === null) return 0; if (a.ts === null) return -1; if (b.ts === null) return 1; return a.ts < b.ts ? -1 : 1; }); let predictions = null; for (const entry of companiesByAge) { const preds = getPredictions.all(entry.company.id); if (preds.length >= 3) { target = entry.company; predictions = preds; break; } } if (!target) { await sleep(loopDelay); continue; } const facts = getFacts.all(target.id); const relationships = getRelationships.all(target.id); const prompt = buildPrompt(target.name, facts, relationships, predictions); let result; try { result = await callLlm(llmConfig, prompt); } catch (err) { console.error(`[signal] LLM error for ${target.name}:`, err.message); await sleep(loopDelay); continue; } if (!result) { console.log(`[signal] ${target.name} — LLM returned null, skipping`); await sleep(loopDelay); continue; } const predictionIds = predictions.map(p => p.id); const write = intelligenceDb.transaction(() => { deleteSignal.run(target.id); insertSignal.run( target.id, result.signal, result.confidence, result.timeframe, result.risk_level, JSON.stringify(result.risk_factors || []), result.summary, JSON.stringify(result.key_drivers || []), JSON.stringify(predictionIds) ); }); write(); recordEvent.run(); pruneCounter++; if (pruneCounter >= 20) { pruneEvents.run(); pruneCounter = 0; } console.log(`[signal] ${target.name} — ${result.signal} (${result.confidence} confidence, ${result.risk_level} risk)`); } catch (err) { console.error("[signal] cycle error:", err.message); } await sleep(loopDelay); } } function buildPrompt(companyName, facts, relationships, predictions) { const factsBlock = facts.length > 0 ? facts.map(f => `- ${f.claim} (confirmed ${f.confirmation_count}x)`).join("\n") : "No known facts yet."; const relBlock = relationships.length > 0 ? relationships.map(r => `- ${r.relationship_type}: ${r.to_entity} (${r.confidence})`).join("\n") : "No known relationships."; const predBlock = predictions.map((p, i) => `${i + 1}. [${p.type}] ${p.direction} / ${p.magnitude} / ${p.timeframe} — ${p.rationale || "no rationale"}` ).join("\n"); return `You are a financial intelligence analyst generating a trade signal for ${companyName}. Known facts about ${companyName} (most confirmed first): ${factsBlock} Known relationships: ${relBlock} Recent event predictions (last 90 days): ${predBlock} Generate a trade signal as JSON with this exact shape: { "signal": "BUY | HOLD | SELL", "confidence": "low | medium | high | very_high", "timeframe": "short | medium | long", "risk_level": "low | medium | high | very_high", "risk_factors": ["string", ...], "key_drivers": ["string", ...], "summary": "2-3 sentence plain English summary" } Risk factors should be derived from: - Supply chain concentration (heavy dependence on single suppliers) - Geopolitical exposure (relationships with entities in sensitive regions) - Competitive threats (strong competitors gaining ground) - Regulatory exposure (themes mentioning regulation or export controls) - Negative prediction patterns in recent events Only output valid JSON. Always respond in English.`; } async function callLlm(llmConfig, prompt) { const body = JSON.stringify({ model: llmConfig.llmModel || llmConfig.model, messages: [{ role: "user", content: prompt }], temperature: 0.1, }); const url = new URL("https://openrouter.ai/api/v1/chat/completions"); const responseText = await httpPost(url, body, { "Content-Type": "application/json", "Authorization": `Bearer ${llmConfig.apiKey || ""}`, }); let parsed; try { parsed = JSON.parse(responseText); } catch (_) { throw new Error(`LLM response not JSON: ${responseText.slice(0, 300)}`); } const content = parsed.choices?.[0]?.message?.content; if (!content) return null; const stripped = content.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "").trim(); return JSON.parse(stripped); } function httpPost(url, body, headers) { return new Promise((resolve, reject) => { const lib = url.protocol === "https:" ? https : http; const req = lib.request({ hostname: url.hostname, port: url.port || (url.protocol === "https:" ? 443 : 80), path: url.pathname + url.search, method: "POST", headers: { ...headers, "Content-Length": Buffer.byteLength(body) }, }, (res) => { let data = ""; res.on("data", chunk => data += chunk); res.on("end", () => { if (res.statusCode >= 200 && res.statusCode < 300) resolve(data); else reject(new Error(`LLM ${res.statusCode}: ${data.slice(0, 300)}`)); }); }); req.on("error", reject); req.write(body); req.end(); }); } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } module.exports = { runSignalWorker };