Files
investor-flow/app/server/src/confluence/confluenceLibrary.ts
T
Investor Flow Build 9b41890d33 fix: IREN/ASTS/IRE stuck-pending + corridor feature
Fix stuck adapter_queue jobs (ASTS/IRE/IREN stuck pending forever):

1. fetchSpec early-return paths (cooldown checks) now update job status
   to 'backoff' with last_attempt set and 30s backoff_until, instead of
   returning without any status change. Prevents jobs from being
   re-processed every drain cycle indefinitely.

2. Wrap adapter.fetchOne() in 30s Promise.race timeout. A hung HTTP
   request no longer blocks the entire per-source promise chain
   forever, preventing all subsequent jobs for that source.

Also includes corridor/confluence feature, tiered quote schedules,
cache improvements, and HoldingsBookView refinements.
2026-08-11 22:25:17 -04:00

90 lines
3.8 KiB
TypeScript

// 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<string, RedundancyGroup> = 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<SlotFamily, number> {
const counts: Record<SlotFamily, number> = { 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';