2026-07-23 18:02:24 -04:00
// Pure helpers for focused-ticker context vs market, sector, and peers.
// ADR-0007: describes relative position — never buy/sell directives.
import {
totalReturnPct ,
HORIZON_MS ,
type Horizon ,
type CandlePoint ,
} from './marketRotationRs.ts' ;
export type Stance = 'outperforming' | 'underperforming' | 'inline' | 'unknown' ;
export interface HorizonReturns {
oneWeek : number | null ;
oneMonth : number | null ;
threeMonth : number | null ;
sixMonth : number | null ;
oneYear : number | null ;
threeYear : number | null ;
fiveYear : number | null ;
}
export function returnsBundle ( candles : CandlePoint []) : HorizonReturns {
return {
oneWeek : totalReturnPct ( candles , HORIZON_MS . oneWeek ),
oneMonth : totalReturnPct ( candles , HORIZON_MS . oneMonth ),
threeMonth : totalReturnPct ( candles , HORIZON_MS . threeMonth ),
sixMonth : totalReturnPct ( candles , HORIZON_MS . sixMonth ),
oneYear : totalReturnPct ( candles , HORIZON_MS . oneYear ),
threeYear : totalReturnPct ( candles , HORIZON_MS . threeYear ),
fiveYear : totalReturnPct ( candles , HORIZON_MS . fiveYear ),
};
}
function relativeBundle ( subject : HorizonReturns , bench : HorizonReturns ) : HorizonReturns {
return {
oneWeek : relativeTo ( subject . oneWeek , bench . oneWeek ),
oneMonth : relativeTo ( subject . oneMonth , bench . oneMonth ),
threeMonth : relativeTo ( subject . threeMonth , bench . threeMonth ),
sixMonth : relativeTo ( subject . sixMonth , bench . sixMonth ),
oneYear : relativeTo ( subject . oneYear , bench . oneYear ),
threeYear : relativeTo ( subject . threeYear , bench . threeYear ),
fiveYear : relativeTo ( subject . fiveYear , bench . fiveYear ),
};
}
export function relativeTo (
subject : number | null ,
benchmark : number | null ,
) : number | null {
if ( subject === null || benchmark === null ) return null ;
return subject - benchmark ;
}
export function stanceFromRs ( rs : number | null , threshold = 2 ) : Stance {
if ( rs === null ) return 'unknown' ;
if ( rs > threshold ) return 'outperforming' ;
if ( rs < - threshold ) return 'underperforming' ;
return 'inline' ;
}
export function avgReturn ( values : Array < number | null >) : number | null {
const nums = values . filter (( v ) : v is number => v !== null && Number . isFinite ( v ));
if ( nums . length === 0 ) return null ;
return nums . reduce (( a , b ) => a + b , 0 ) / nums . length ;
}
/** Map Yahoo sector labels → liquid sector ETF. */
export function sectorToEtf ( sector : string | null | undefined ) : { etf : string ; label : string } | null {
if ( ! sector ) return null ;
const s = sector . toLowerCase ();
const table : Array < { match : RegExp ; etf : string ; label : string } > = [
{ match : /technolog|information technology|software/ , etf : 'XLK' , label : 'Technology' },
{ match : /financial|bank/ , etf : 'XLF' , label : 'Financials' },
{ match : /energy|oil|gas/ , etf : 'XLE' , label : 'Energy' },
{ match : /industrial/ , etf : 'XLI' , label : 'Industrials' },
{ match : /health|pharma|biotech|biotechnology/ , etf : 'XLV' , label : 'Healthcare' },
{ match : /consumer cycl|consumer discretionary|retail/ , etf : 'XLY' , label : 'Consumer Discretionary' },
{ match : /consumer defen|consumer staple|food|beverage/ , etf : 'XLP' , label : 'Consumer Staples' },
{ match : /utilit/ , etf : 'XLU' , label : 'Utilities' },
{ match : /real estate|reit/ , etf : 'XLRE' , label : 'Real Estate' },
{ match : /communicat|media|telecom/ , etf : 'XLC' , label : 'Communication Services' },
{ match : /material|basic material|mining|chemical/ , etf : 'XLB' , label : 'Materials' },
];
for ( const row of table ) {
if ( row . match . test ( s )) return { etf : row.etf , label : row.label };
}
return null ;
}
/** Industry / description keywords → thematic ETF (optional overlay). */
export function industryToTheme (
industry : string | null | undefined ,
description : string | null | undefined ,
) : { etf : string ; label : string } | null {
const text = ` ${ industry ?? '' } ${ description ?? '' } ` . toLowerCase ();
if ( ! text . trim ()) return null ;
// Order matters: compute / AI infra and crypto-mining before generic finance.
const table : Array < { match : RegExp ; etf : string ; label : string } > = [
{ match : /ai data.?center|hyperscal|gpu cloud|hpc|high.?performance comput|ai infrastructure|ai infra|neocloud/ , etf : 'SMH' , label : 'AI / HPC infrastructure' },
{ match : /bitcoin.?min|crypto.?min|digital.?asset.?min|cryptocurrency min|btc min/ , etf : 'BLOK' , label : 'Digital-asset mining' },
{ match : /data.?center|colocation|power for compute/ , etf : 'SRVR' , label : 'Data centers' },
{ match : /semiconductor|chip|gpu|foundry|fabless/ , etf : 'SMH' , label : 'Semiconductors' },
{ match : /biotech|biotechnology|genomic/ , etf : 'XBI' , label : 'Biotech' },
{ match : /software|saas|cloud|enterprise software/ , etf : 'IGV' , label : 'Software' },
{ match : /bank|banking|regional bank/ , etf : 'KBE' , label : 'Banks' },
{ match : /insurance/ , etf : 'KIE' , label : 'Insurance' },
{ match : /aerospace|defense|weapon/ , etf : 'ITA' , label : 'Defense' },
{ match : /airline|aviation/ , etf : 'JETS' , label : 'Airlines' },
];
for ( const row of table ) {
if ( row . match . test ( text )) return { etf : row.etf , label : row.label };
}
return null ;
}
/** Crypto miners / AI power-compute names often mis-bucketed by Yahoo under Financials. */
export const CRYPTO_MINING_PEERS = [
'IREN' , 'CIFR' , 'RIOT' , 'MARA' , 'CLSK' , 'WULF' , 'CORZ' , 'HUT' , 'BITF' , 'HIVE' , 'BTDR' , 'BTBT' ,
] as const ;
/** Hyperscaler / GPU-cloud / AI data-center operating peers (not crypto-miner beta). */
export const AI_INFRA_PEERS = [
'CRWV' , 'NBIS' , 'IREN' , 'APLD' , 'VRT' , 'SMCI' , 'ANET' , 'EQIX' , 'DLR' , 'GDS' , 'VNET' ,
] as const ;
export interface SymbolContextOverride {
/** Operating peer set (excludes self at use site). */
peers : string [];
/** Preferred GICS-style comparison ETF (may override Yahoo sector). */
sectorEtf : string ;
sectorLabel : string ;
themeEtf : string | null ;
themeLabel : string | null ;
/** Shown when Yahoo classification disagrees with operating profile. */
classificationNote : string ;
}
/**
* Explicit operating-profile overrides. Yahoo still lists many power/compute
* and crypto-mining names under Financial Services / Capital Markets.
*/
export const SYMBOL_CONTEXT_OVERRIDES : Record < string , SymbolContextOverride > = {
IREN : {
peers : [ 'CRWV' , 'NBIS' , 'APLD' , 'VRT' , 'SMCI' , 'ANET' , 'EQIX' , 'DLR' ],
sectorEtf : 'XLK' ,
sectorLabel : 'Technology (AI infrastructure)' ,
2026-07-25 11:38:23 -04:00
themeEtf : null ,
2026-07-23 18:02:24 -04:00
themeLabel : 'AI / hyperscaler infrastructure' ,
classificationNote :
'Vendor feeds often label IREN as Financial Services / Capital Markets (crypto-era bucket). Operating comparison uses AI infrastructure / GPU-cloud peers (e.g. CRWV, NBIS), not banks or pure crypto miners.' ,
},
2026-07-25 11:38:23 -04:00
SLNH : {
peers : [ 'IREN' , 'CIFR' , 'WULF' , 'CORZ' , 'APLD' ],
sectorEtf : 'XLK' ,
sectorLabel : 'Technology (green data centers)' ,
themeEtf : null ,
themeLabel : null ,
classificationNote :
'Vendor feeds label SLNH as Financial Services; operating profile is green-energy data centers and crypto-mining infrastructure.' ,
},
2026-07-23 18:02:24 -04:00
CRWV : {
peers : [ 'NBIS' , 'IREN' , 'APLD' , 'VRT' , 'SMCI' , 'ANET' , 'EQIX' , 'DLR' ],
sectorEtf : 'XLK' ,
sectorLabel : 'Technology (AI infrastructure)' ,
themeEtf : 'SMH' ,
themeLabel : 'AI / hyperscaler infrastructure' ,
classificationNote : 'GPU-cloud / AI infrastructure peer set (with IREN, NBIS, etc.).' ,
},
NBIS : {
peers : [ 'CRWV' , 'IREN' , 'APLD' , 'VRT' , 'SMCI' , 'ANET' , 'EQIX' , 'DLR' ],
sectorEtf : 'XLK' ,
sectorLabel : 'Technology (AI infrastructure)' ,
themeEtf : 'SMH' ,
themeLabel : 'AI / hyperscaler infrastructure' ,
classificationNote : 'AI infrastructure peer set (with CRWV, IREN, etc.).' ,
},
CIFR : {
peers : [ 'RIOT' , 'MARA' , 'CLSK' , 'WULF' , 'CORZ' , 'HUT' , 'BITF' , 'IREN' ],
2026-07-25 11:38:23 -04:00
sectorEtf : 'XLK' ,
sectorLabel : 'Technology (digital assets)' ,
2026-07-23 18:02:24 -04:00
themeEtf : null ,
themeLabel : 'Digital-asset mining' ,
classificationNote :
'Often classified under Financial Services; primary peer set is crypto miners (IREN retained as a related power/compute name).' ,
},
RIOT : {
peers : [... CRYPTO_MINING_PEERS ],
2026-07-25 11:38:23 -04:00
sectorEtf : 'XLK' ,
sectorLabel : 'Technology (digital assets)' ,
2026-07-23 18:02:24 -04:00
themeEtf : null ,
themeLabel : 'Digital-asset mining' ,
classificationNote : 'Yahoo may list under Financials; peer set is crypto miners.' ,
},
MARA : {
peers : [... CRYPTO_MINING_PEERS ],
2026-07-25 11:38:23 -04:00
sectorEtf : 'XLK' ,
sectorLabel : 'Technology (digital assets)' ,
2026-07-23 18:02:24 -04:00
themeEtf : null ,
themeLabel : 'Digital-asset mining' ,
classificationNote : 'Yahoo may list under Financials; peer set is crypto miners.' ,
},
CLSK : {
peers : [... CRYPTO_MINING_PEERS ],
2026-07-25 11:38:23 -04:00
sectorEtf : 'XLK' ,
sectorLabel : 'Technology (digital assets)' ,
2026-07-23 18:02:24 -04:00
themeEtf : null ,
themeLabel : 'Digital-asset mining' ,
classificationNote : 'Yahoo may list under Financials; peer set is crypto miners.' ,
},
WULF : {
peers : [... CRYPTO_MINING_PEERS ],
sectorEtf : 'XLK' ,
sectorLabel : 'Technology (compute infrastructure)' ,
2026-07-25 11:38:23 -04:00
themeEtf : null ,
2026-07-23 18:02:24 -04:00
themeLabel : 'AI / HPC infrastructure' ,
classificationNote : 'Mining + HPC/AI data-center transition; not a traditional financial.' ,
},
CORZ : {
peers : [... CRYPTO_MINING_PEERS ],
sectorEtf : 'XLK' ,
sectorLabel : 'Technology (compute infrastructure)' ,
2026-07-25 11:38:23 -04:00
themeEtf : null ,
2026-07-23 18:02:24 -04:00
themeLabel : 'AI / HPC infrastructure' ,
classificationNote : 'Mining + AI hosting; not a traditional financial.' ,
},
APLD : {
peers : [ 'IREN' , 'CIFR' , 'CRWV' , 'NBIS' , 'EQIX' , 'DLR' , 'VRT' , 'SMCI' ],
sectorEtf : 'XLK' ,
sectorLabel : 'Technology (data centers)' ,
2026-07-25 11:38:23 -04:00
themeEtf : null ,
2026-07-23 18:02:24 -04:00
themeLabel : 'AI / HPC infrastructure' ,
classificationNote : 'AI data-center developer/operator peer set.' ,
},
};
export interface ResolvedBusinessContext {
sectorEtf : string | null ;
sectorLabel : string | null ;
themeEtf : string | null ;
themeLabel : string | null ;
/** Curated peers when available; empty means fall back to vendor peers / ETF holdings. */
peers : string [];
classificationNote : string | null ;
/** When true, do not pull peers from sector ETF holdings (XLF etc.). */
blockSectorEtfPeers : boolean ;
/** Yahoo-reported sector (may be misleading). */
vendorSector : string | null ;
vendorIndustry : string | null ;
}
/**
* Resolve operating sector/theme/peers. Prefer symbol overrides and text signals
* over vendor GICS when those conflict (e.g. IREN → not XLF).
*/
export function resolveBusinessContext ( opts : {
symbol : string ;
sector : string | null | undefined ;
industry : string | null | undefined ;
description : string | null | undefined ;
}) : ResolvedBusinessContext {
const symbol = opts . symbol . toUpperCase ();
const vendorSector = opts . sector ?? null ;
const vendorIndustry = opts . industry ?? null ;
const text = ` ${ opts . sector ?? '' } ${ opts . industry ?? '' } ${ opts . description ?? '' } ` . toLowerCase ();
const override = SYMBOL_CONTEXT_OVERRIDES [ symbol ];
if ( override ) {
return {
sectorEtf : override.sectorEtf ,
sectorLabel : override.sectorLabel ,
themeEtf : override.themeEtf ,
themeLabel : override.themeLabel ,
peers : override.peers.filter (( p ) => p !== symbol ),
classificationNote : override.classificationNote ,
blockSectorEtfPeers : true ,
vendorSector ,
vendorIndustry ,
};
}
// Heuristic: crypto mining / AI power-compute mislabeled as Financial Services.
const isCryptoOrComputeInfra =
/bitcoin.?min|crypto.?min|digital.?asset.?min|cryptocurrency|data.?center|hyperscal|ai infrastructure|ai infra|hpc|gpu cloud|neocloud/ . test ( text );
const vendorIsFinance =
/financial|capital markets|asset management/ . test ( ` ${ vendorSector ?? '' } ${ vendorIndustry ?? '' } ` . toLowerCase ());
if ( isCryptoOrComputeInfra && vendorIsFinance ) {
const aiTilt = /data.?center|hyperscal|ai infrastructure|ai infra|hpc|gpu|neocloud/ . test ( text );
const peers = ( aiTilt ? AI_INFRA_PEERS : CRYPTO_MINING_PEERS ). filter (( p ) => p !== symbol );
return {
sectorEtf : 'XLK' ,
sectorLabel : aiTilt ? 'Technology (compute infrastructure)' : 'Technology (digital assets)' ,
themeEtf : aiTilt ? 'SMH' : null ,
themeLabel : aiTilt ? 'AI / HPC infrastructure' : 'Digital-asset mining' ,
peers : [... peers ],
classificationNote :
`Vendor sector is " ${ vendorSector ?? 'n/a' } " / " ${ vendorIndustry ?? 'n/a' } ", which is a common bucket for crypto-era names. Operating comparison uses compute/digital-infrastructure peers instead of financials.` ,
blockSectorEtfPeers : true ,
vendorSector ,
vendorIndustry ,
};
}
const sectorMap = sectorToEtf ( vendorSector );
const themeMap = industryToTheme ( vendorIndustry , opts . description );
return {
sectorEtf : sectorMap?.etf ?? null ,
sectorLabel : sectorMap?.label ?? vendorSector ,
themeEtf : themeMap?.etf ?? null ,
themeLabel : themeMap?.label ?? null ,
peers : [],
classificationNote : null ,
blockSectorEtfPeers : false ,
vendorSector ,
vendorIndustry ,
};
}
export interface PeerRow {
symbol : string ;
name : string | null ;
returns : HorizonReturns ;
rsVsMarket1M : number | null ;
}
export interface TickerContextInput {
symbol : string ;
name : string | null ;
sector : string | null ;
industry : string | null ;
tickerKind : string ;
symbolReturns : HorizonReturns ;
marketReturns : HorizonReturns ;
marketRegime : 'trending-up' | 'trending-down' | 'range-bound' | null ;
marketRegimeConfidence : number | null ;
sectorEtf : string | null ;
sectorLabel : string | null ;
sectorReturns : HorizonReturns | null ;
sectorLeadership : 'leading' | 'lagging' | 'inline' | 'unknown' | null ;
themeEtf : string | null ;
themeLabel : string | null ;
themeReturns : HorizonReturns | null ;
peers : PeerRow [];
/** When vendor GICS disagrees with operating profile. */
classificationNote? : string | null ;
vendorSector? : string | null ;
vendorIndustry? : string | null ;
}
export interface TickerContextResult {
symbol : string ;
name : string | null ;
sector : string | null ;
industry : string | null ;
tickerKind : string ;
performance : {
symbol : HorizonReturns ;
vsMarket : HorizonReturns ;
vsSector : HorizonReturns | null ;
vsPeers1M : number | null ;
marketStance1M : Stance ;
sectorStance1M : Stance ;
peerStance1M : Stance ;
};
market : {
regime : string | null ;
confidence : number | null ;
spyReturns : HorizonReturns ;
};
sectorContext : {
label : string | null ;
etf : string | null ;
etfReturns : HorizonReturns | null ;
leadership : string | null ;
};
themeContext : {
label : string | null ;
etf : string | null ;
etfReturns : HorizonReturns | null ;
};
classification : {
note : string | null ;
vendorSector : string | null ;
vendorIndustry : string | null ;
};
peers : Array < {
symbol : string ;
name : string | null ;
oneMonth : number | null ;
rsVsMarket1M : number | null ;
} > ;
/** Short professional paragraphs for the UI. */
summary : {
headline : string ;
market : string ;
sector : string ;
peers : string ;
synthesis : string ;
};
}
function fmtPct ( v : number | null ) : string {
if ( v === null || ! Number . isFinite ( v )) return 'n/a' ;
const sign = v > 0 ? '+' : '' ;
return ` ${ sign }${ v . toFixed ( 1 ) } %` ;
}
function stancePhrase ( s : Stance ) : string {
if ( s === 'outperforming' ) return 'outperforming' ;
if ( s === 'underperforming' ) return 'underperforming' ;
if ( s === 'inline' ) return 'broadly in line with' ;
return 'insufficient data versus' ;
}
export function buildTickerContext ( input : TickerContextInput ) : TickerContextResult {
const vsMarket = relativeBundle ( input . symbolReturns , input . marketReturns );
const vsSector = input . sectorReturns
? relativeBundle ( input . symbolReturns , input . sectorReturns )
: null ;
const peerAvg1M = avgReturn ( input . peers . map (( p ) => p . returns . oneMonth ));
const vsPeers1M = relativeTo ( input . symbolReturns . oneMonth , peerAvg1M );
const marketStance1M = stanceFromRs ( vsMarket . oneMonth );
const sectorStance1M = stanceFromRs ( vsSector ? . oneMonth ?? null );
const peerStance1M = stanceFromRs ( vsPeers1M );
const name = input . name ?? input . symbol ;
const regime =
input . marketRegime === 'trending-up' ? 'trending up'
: input . marketRegime === 'trending-down' ? 'trending down'
: input . marketRegime === 'range-bound' ? 'range-bound'
: null ;
const headline = ` ${ input . symbol }${ input . name ? ` ( ${ input . name } )` : '' } — relative context versus market, sector, and peers.` ;
const market =
regime
? `Broad market regime is currently ${ regime } ` +
( input . marketRegimeConfidence != null ? ` (~ ${ input . marketRegimeConfidence } % classifier confidence). ` : '. ' ) +
`SPY 1M ${ fmtPct ( input . marketReturns . oneMonth ) } ; ${ input . symbol } 1M ${ fmtPct ( input . symbolReturns . oneMonth ) } ` +
`( ${ fmtPct ( vsMarket . oneMonth ) } relative). On a one-month basis the name is ${ stancePhrase ( marketStance1M ) } the broad market.`
: `SPY 1M ${ fmtPct ( input . marketReturns . oneMonth ) } ; ${ input . symbol } 1M ${ fmtPct ( input . symbolReturns . oneMonth ) } ` +
`( ${ fmtPct ( vsMarket . oneMonth ) } relative). Market regime data is incomplete.` ;
let sector = 'Sector classification is unavailable for this symbol.' ;
if ( input . sectorLabel || input . sector ) {
const label = input . sectorLabel ?? input . sector ?? 'sector' ;
const ind = input . industry ? ` Industry: ${ input . industry } .` : '' ;
const vendorNote =
input . classificationNote
? ` ${ input . classificationNote } `
: ( input . vendorSector && input . sectorLabel && ! input . sectorLabel . toLowerCase (). includes (( input . vendorSector ?? '' ). toLowerCase (). split ( ' ' )[ 0 ] ?? '___' )
? ` Vendor feed lists sector as ${ input . vendorSector }${ input . vendorIndustry ? ` / ${ input . vendorIndustry } ` : '' } .`
: '' );
const etfBit = input . sectorEtf
? ` Comparison proxy ${ input . sectorEtf } 1M ${ fmtPct ( input . sectorReturns ? . oneMonth ?? null ) } ` +
( vsSector ? `; name vs proxy ${ fmtPct ( vsSector . oneMonth ) } ( ${ stancePhrase ( sectorStance1M ) } that group).`
: '.' )
: '' ;
const themeBit = input . themeLabel
? ` Theme: ${ input . themeLabel }${ input . themeEtf ? ` ( ${ input . themeEtf } )` : '' } ` +
( input . themeReturns ? ` 1M ${ fmtPct ( input . themeReturns . oneMonth ) } .` : '.' )
: '' ;
const lead =
input . sectorLeadership === 'leading' ? ' The comparison group is currently outperforming SPY.'
: input . sectorLeadership === 'lagging' ? ' The comparison group is currently underperforming SPY.'
: input . sectorLeadership === 'inline' ? ' The comparison group is roughly in line with SPY.'
: '' ;
sector = `Operating comparison group: ${ label } . ${ ind }${ vendorNote }${ etfBit }${ themeBit }${ lead } ` ;
}
let peers = 'Peer comparison is limited — peer list or peer price history is incomplete.' ;
if ( input . peers . length > 0 ) {
const ranked = [... input . peers ]
. filter (( p ) => p . returns . oneMonth !== null )
. sort (( a , b ) => ( b . returns . oneMonth ?? - Infinity ) - ( a . returns . oneMonth ?? - Infinity ));
const top = ranked . slice ( 0 , 3 ). map (( p ) => ` ${ p . symbol } ${ fmtPct ( p . returns . oneMonth ) } ` ). join ( ', ' );
peers =
`Compared with ${ input . peers . length } peer ${ input . peers . length === 1 ? '' : 's' } ` +
`(1M peer average ${ fmtPct ( peerAvg1M ) } ), ${ input . symbol } is ${ stancePhrase ( peerStance1M ) } that set ` +
`( ${ fmtPct ( vsPeers1M ) } relative).` +
( top ? ` Stronger peer prints on 1M include: ${ top } .` : '' );
}
const synthesisParts : string [] = [];
if ( marketStance1M === 'outperforming' && ( sectorStance1M === 'outperforming' || sectorStance1M === 'unknown' )) {
synthesisParts . push ( 'Relative price action is constructive versus the broad market' );
if ( sectorStance1M === 'outperforming' ) synthesisParts . push ( 'and its sector group' );
} else if ( marketStance1M === 'underperforming' ) {
synthesisParts . push ( 'Relative price action is lagging the broad market' );
if ( sectorStance1M === 'underperforming' ) synthesisParts . push ( 'and its sector group' );
} else {
synthesisParts . push ( 'Relative price action is mixed or roughly in line with the market' );
}
if ( peerStance1M === 'outperforming' ) synthesisParts . push ( 'with a lead versus available peers on one month' );
else if ( peerStance1M === 'underperforming' ) synthesisParts . push ( 'with a lag versus available peers on one month' );
const synthesis =
` ${ name } : ${ synthesisParts . join ( ', ' ) } . ` +
'This is a relative-performance snapshot for research context — not a forecast and not an investment recommendation.' ;
return {
symbol : input . symbol ,
name : input.name ,
sector : input.sector ,
industry : input.industry ,
tickerKind : input.tickerKind ,
performance : {
symbol : input . symbolReturns ,
vsMarket ,
vsSector ,
vsPeers1M ,
marketStance1M ,
sectorStance1M ,
peerStance1M ,
},
market : {
regime : input.marketRegime ,
confidence : input.marketRegimeConfidence ,
spyReturns : input.marketReturns ,
},
sectorContext : {
label : input.sectorLabel ?? input . sector ,
etf : input.sectorEtf ,
etfReturns : input.sectorReturns ,
leadership : input.sectorLeadership ,
},
themeContext : {
label : input.themeLabel ,
etf : input.themeEtf ,
etfReturns : input.themeReturns ,
},
classification : {
note : input.classificationNote ?? null ,
vendorSector : input.vendorSector ?? input . sector ,
vendorIndustry : input.vendorIndustry ?? input . industry ,
},
peers : input.peers.map (( p ) => ({
symbol : p . symbol ,
name : p.name ,
oneMonth : p.returns.oneMonth ,
rsVsMarket1M : p.rsVsMarket1M ,
})),
summary : { headline , market , sector , peers , synthesis },
};
}
// re-export horizon type for callers
export type { Horizon , CandlePoint };