Ship Node production images, Unraid compose, and Gitea CI/CD (test then push registry images; cron script if no runner). Rebuild dealer flow as a heatmap-first map with integrity gates and chart helpers. Add confluence zone rules, session clock, capture evidence, and tighter 13F/queue/options paths, plus the matching UI and tests.
91 lines
3.9 KiB
TypeScript
91 lines
3.9 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[], weights?: ReadonlyMap<string, number>): number {
|
|
if (firingSlotIds.length === 0) return 0;
|
|
let w = 0;
|
|
let margin = 1;
|
|
for (const id of firingSlotIds) {
|
|
const slotW = weights?.get(id) ?? 1;
|
|
w += margin * slotW;
|
|
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'; |