// Investor Flow — Confluence Library (M22, slice 2) // // Redundancy modeling for the Confluence Signal Engine. Correlated slots that // measure the same underlying phenomenon ("golden cross" and "trend alignment" // both read the same moving-average structure) are grouped so a rack does not // over-count them as independent evidence. // // Pure data + accessors. NO I/O. ADR-0007: these are evidence-accounting rules, // never recommendations. import { CONFLUENCE_SLOTS, type ConfluenceSlot, type SlotFamily } from './confluenceSlots.ts'; /** * A redundancy group: slots that corroborate each other because they consume * overlapping inputs. Within a group, the first firing slot contributes full * evidence and each additional firing slot contributes a discounted margin * (`REDUNDANCY_DISCOUNT`, applied multiplicatively) so three correlated * bullish slots are not counted as three independent bullish facts. */ export interface RedundancyGroup { family: SlotFamily; slots: string[]; } /** Default discount applied per additional firing slot within a group. */ export const REDUNDANCY_DISCOUNT = 0.5; /** Evidence redundancy groups keyed by the phenomenon they all measure. */ export const REDUNDANCY_GROUPS: RedundancyGroup[] = [ { family: 'technical', slots: ['goldenCross', 'trendAlignment', 'pullbackToEMA21'] }, { family: 'technical', slots: ['timeSeriesMomentum12_1', 'relativeStrengthVsSpy', 'momentumForward'] }, { family: 'technical', slots: ['macdBullish', 'rsiOversold'] }, { family: 'technical', slots: ['macdBearish', 'rsiOverbought'] }, { family: 'technical', slots: ['relVolume', 'volumeProfileShelf'] }, { family: 'institutional', slots: ['instNetActivePositive', 'insiderInformedBuy30d', 'new13da'] }, { family: 'macro', slots: ['ratesRegime', 'macroRegimeUp'] }, { family: 'macro', slots: ['consumerSentimentLow', 'breadthThrust'] }, { family: 'macro', slots: ['corridorEntryCheap', 'corridorUpsideHigh'] }, { family: 'macro', slots: ['corridorEntryStretched', 'corridorUpsideLow'] }, { family: 'seasonal', slots: ['seasonalFavorableMonth', 'winterHalfOn', 'electionCycleFavorableYear'] }, { family: 'flows', slots: ['etfFlowPositive', 'cotPositioning'] }, ]; /** Map slot id -> the group it belongs to (a slot may be in one group). */ export const slotGroupIndex: ReadonlyMap = new Map( REDUNDANCY_GROUPS.flatMap((g) => g.slots.map((id) => [id, g] as const)), ); /** The group a slot belongs to, if any. */ export function redundancyGroupFor(slotId: string): RedundancyGroup | undefined { return slotGroupIndex.get(slotId); } /** * Effective evidence weight for a set of firing slots *within one group*. * First firing slot contributes 1.0; each additional contributes * `REDUNDANCY_DISCOUNT` of the previous marginal — so N same-group firings * weigh 1 + 0.5 + 0.25 + ... , never N. */ export function groupWeight(firingSlotIds: string[]): number { if (firingSlotIds.length === 0) return 0; let w = 0; let margin = 1; for (let i = 0; i < firingSlotIds.length; i++) { w += margin; margin *= REDUNDANCY_DISCOUNT; } return w; } /** A flat description of every slot for explain-note / docs rendering. */ export function slotCatalog(): { id: string; name: string; family: SlotFamily; body: string; explain: string }[] { return CONFLUENCE_SLOTS.map((s) => ({ id: s.id, name: s.name, family: s.family, body: s.body, explain: s.explain, })); } /** Convenience: number of slots per family. */ export function familyCounts(): Record { const counts: Record = { technical: 0, institutional: 0, macro: 0, seasonal: 0, flows: 0, sentiment: 0 }; for (const s of CONFLUENCE_SLOTS) counts[s.family] += 1; return counts; } /** Re-export the slot type for consumers of the library. */ export type { ConfluenceSlot } from './confluenceSlots.ts';