2026-03-12 23:45:46 -07:00
// LLM-Powered Trade Ideas — generates actionable ideas from sweep data + delta context
/ * *
* Generate LLM - enhanced trade ideas from sweep data .
* @ param { LLMProvider } provider - configured LLM provider
* @ param { object } sweepData - synthesized dashboard data
* @ param { object | null } delta - delta from last sweep
* @ param { Array } previousIdeas - ideas from previous runs ( for dedup )
* @ returns { Promise < Array > } - array of idea objects
* /
export async function generateLLMIdeas ( provider , sweepData , delta , previousIdeas = [ ] ) {
if ( ! provider ? . isConfigured ) return null ;
let context ;
try {
context = compactSweepForLLM ( sweepData , delta , previousIdeas ) ;
} catch ( err ) {
console . error ( '[LLM Ideas] Failed to compact sweep data:' , err . message ) ;
return null ;
}
const systemPrompt = ` You are a quantitative analyst at a macro intelligence firm. You receive structured OSINT + economic data from 25 sources and produce 5-8 actionable trade ideas.
Rules :
- Each idea must cite specific data points from the input
- Include entry rationale , risk factors , and time horizon
- Blend geopolitical , economic , and market signals — cross - correlate across domains
- Be specific : name instruments ( tickers , futures , ETFs ) , not vague sectors
- If delta shows significant changes , lead with those
- Do NOT repeat ideas from the "previous ideas" list unless conditions have materially changed
- Rate confidence : HIGH ( multiple confirming signals ) , MEDIUM ( thesis supported ) , LOW ( speculative )
Output ONLY valid JSON array . Each object :
{
"title" : "Short title (max 10 words)" ,
"type" : "LONG|SHORT|HEDGE|WATCH|AVOID" ,
"ticker" : "Primary instrument" ,
"confidence" : "HIGH|MEDIUM|LOW" ,
"rationale" : "2-3 sentence explanation citing specific data" ,
"risk" : "Key risk factor" ,
"horizon" : "Intraday|Days|Weeks|Months" ,
"signals" : [ "signal1" , "signal2" ]
} ` ;
try {
2026-07-04 12:11:28 +02:00
const maxTokens = Number . isFinite ( provider . maxTokens ) && provider . maxTokens > 0
? provider . maxTokens
: 4096 ;
const timeout = Number . isFinite ( provider . timeoutMs ) && provider . timeoutMs > 0
? provider . timeoutMs
: 90000 ;
const result = await provider . complete ( systemPrompt , context , { maxTokens , timeout } ) ;
2026-03-12 23:45:46 -07:00
const ideas = parseIdeasResponse ( result . text ) ;
if ( ideas && ideas . length > 0 ) {
return ideas ;
}
console . warn ( '[LLM Ideas] No valid ideas parsed from response' ) ;
return null ;
} catch ( err ) {
console . error ( '[LLM Ideas] Generation failed:' , err . message ) ;
return null ;
}
}
/ * *
* Compact sweep data to ~ 8 KB for token efficiency .
* /
function compactSweepForLLM ( data , delta , previousIdeas ) {
const sections = [ ] ;
// Economic indicators
if ( data . fred ? . length ) {
const key = data . fred . filter ( f => [ 'VIXCLS' , 'DFF' , 'DGS10' , 'DGS2' , 'T10Y2Y' , 'BAMLH0A0HYM2' , 'DTWEXBGS' , 'MORTGAGE30US' ] . includes ( f . id ) ) ;
sections . push ( ` ECONOMIC: ${ key . map ( f => ` ${ f . id } = ${ f . value } ${ f . momChange ? ` ( ${ f . momChange > 0 ? '+' : '' } ${ f . momChange } ) ` : '' } ` ) . join ( ', ' ) } ` ) ;
}
// Energy
if ( data . energy ) {
sections . push ( ` ENERGY: WTI= $ ${ data . energy . wti } , Brent= $ ${ data . energy . brent } , NatGas= $ ${ data . energy . natgas } , CrudeStocks= ${ data . energy . crudeStocks } bbl ` ) ;
}
2026-03-28 22:53:23 -07:00
// Metals
if ( data . metals ? . gold != null || data . metals ? . silver != null ) {
const gold = data . metals ? . gold != null ? ` $ ${ data . metals . gold } ` : 'n/a' ;
const silver = data . metals ? . silver != null ? ` $ ${ data . metals . silver } ` : 'n/a' ;
const goldChg = data . metals ? . goldChangePct != null ? ` ( ${ data . metals . goldChangePct >= 0 ? '+' : '' } ${ data . metals . goldChangePct } %) ` : '' ;
const silverChg = data . metals ? . silverChangePct != null ? ` ( ${ data . metals . silverChangePct >= 0 ? '+' : '' } ${ data . metals . silverChangePct } %) ` : '' ;
sections . push ( ` METALS: Gold= ${ gold } ${ goldChg } , Silver= ${ silver } ${ silverChg } ` ) ;
}
2026-03-12 23:45:46 -07:00
// BLS
if ( data . bls ? . length ) {
sections . push ( ` LABOR: ${ data . bls . map ( b => ` ${ b . id } = ${ b . value } ` ) . join ( ', ' ) } ` ) ;
}
// Treasury
if ( data . treasury ) {
sections . push ( ` TREASURY: totalDebt= $ ${ data . treasury } T ` ) ;
}
// Supply chain
if ( data . gscpi ) {
sections . push ( ` SUPPLY_CHAIN: GSCPI= ${ data . gscpi . value } ( ${ data . gscpi . interpretation } ) ` ) ;
}
2026-03-23 12:57:37 -04:00
// Geopolitical signals (cap total OSINT text to ~1500 chars to keep prompt compact)
2026-03-12 23:45:46 -07:00
const urgentPosts = ( data . tg ? . urgent || [ ] ) . slice ( 0 , 5 ) ;
if ( urgentPosts . length ) {
2026-03-23 12:57:37 -04:00
const MAX _OSINT _CHARS = 1500 ;
let remaining = MAX _OSINT _CHARS ;
const lines = [ ] ;
for ( const p of urgentPosts ) {
const text = p . text || '' ;
if ( remaining <= 0 ) break ;
const trimmed = text . length > remaining ? text . substring ( 0 , remaining ) + '…' : text ;
lines . push ( ` - ${ trimmed } ` ) ;
remaining -= trimmed . length ;
}
sections . push ( ` URGENT_OSINT: \n ${ lines . join ( '\n' ) } ` ) ;
2026-03-12 23:45:46 -07:00
}
// Thermal / fire detections
if ( data . thermal ? . length ) {
const hotRegions = data . thermal . filter ( t => t . det > 10 ) . map ( t => ` ${ t . region } : ${ t . det } detections ( ${ t . hc } high-conf) ` ) ;
if ( hotRegions . length ) sections . push ( ` THERMAL: ${ hotRegions . join ( ', ' ) } ` ) ;
}
// Air activity
if ( data . air ? . length ) {
const airSum = data . air . map ( a => ` ${ a . region } : ${ a . total } aircraft ` ) ;
sections . push ( ` AIR_ACTIVITY: ${ airSum . join ( ', ' ) } ` ) ;
}
// Nuclear
if ( data . nuke ? . length ) {
const anomalies = data . nuke . filter ( n => n . anom ) ;
if ( anomalies . length ) sections . push ( ` NUCLEAR_ANOMALY: ${ anomalies . map ( n => ` ${ n . site } : ${ n . cpm } cpm ` ) . join ( ', ' ) } ` ) ;
}
// WHO alerts
if ( data . who ? . length ) {
sections . push ( ` WHO_ALERTS: ${ data . who . slice ( 0 , 3 ) . map ( w => w . title ) . join ( '; ' ) } ` ) ;
}
// Defense spending
if ( data . defense ? . length ) {
const topContracts = data . defense . slice ( 0 , 3 ) . map ( d => ` $ ${ ( ( d . amount || 0 ) / 1e6 ) . toFixed ( 0 ) } M to ${ d . recipient } ` ) ;
sections . push ( ` DEFENSE_CONTRACTS: ${ topContracts . join ( ', ' ) } ` ) ;
}
// Delta context
if ( delta ? . summary ) {
sections . push ( ` \n DELTA_SINCE_LAST_SWEEP: direction= ${ delta . summary . direction } , changes= ${ delta . summary . totalChanges } , critical= ${ delta . summary . criticalChanges } ` ) ;
if ( delta . signals ? . escalated ? . length ) {
sections . push ( ` ESCALATED: ${ delta . signals . escalated . map ( s => ` ${ s . label } : ${ s . previous } → ${ s . current } ( ${ ( s . changePct || 0 ) > 0 ? '+' : '' } ${ ( s . changePct || 0 ) . toFixed ( 1 ) } %) ` ) . join ( ', ' ) } ` ) ;
}
if ( delta . signals ? . new ? . length ) {
sections . push ( ` NEW_SIGNALS: ${ delta . signals . new . map ( s => s . label || s . text ? . substring ( 0 , 60 ) ) . join ( '; ' ) } ` ) ;
}
}
// Previous ideas (for dedup)
if ( previousIdeas . length ) {
sections . push ( ` \n PREVIOUS_IDEAS (avoid repeating): \n ${ previousIdeas . map ( i => ` - ${ i . title } [ ${ i . type } ] ` ) . join ( '\n' ) } ` ) ;
}
return sections . join ( '\n' ) ;
}
/ * *
* Parse LLM response into ideas array . Handles markdown code blocks .
* /
function parseIdeasResponse ( text ) {
if ( ! text ) return null ;
// Strip markdown code block wrappers
let cleaned = text . trim ( ) ;
if ( cleaned . startsWith ( '```' ) ) {
cleaned = cleaned . replace ( /^```(?:json)?\n?/ , '' ) . replace ( /\n?```$/ , '' ) ;
}
try {
const parsed = JSON . parse ( cleaned ) ;
if ( ! Array . isArray ( parsed ) ) return null ;
// Validate each idea has required fields
return parsed . filter ( idea =>
idea . title && idea . type && idea . confidence
) . map ( idea => ( {
title : idea . title ,
type : idea . type ,
ticker : idea . ticker || '' ,
confidence : idea . confidence ,
rationale : idea . rationale || '' ,
risk : idea . risk || '' ,
horizon : idea . horizon || '' ,
signals : idea . signals || [ ] ,
source : 'llm' ,
} ) ) ;
} catch {
// Try to extract JSON array from mixed text
const match = cleaned . match ( /\[[\s\S]*\]/ ) ;
if ( match ) {
try {
const arr = JSON . parse ( match [ 0 ] ) ;
return arr . filter ( i => i . title && i . type ) . map ( idea => ( {
... idea ,
source : 'llm' ,
} ) ) ;
} catch { /* give up */ }
}
return null ;
}
}