feat(confluence): add15-symbol universe seed + 3 system rack presets (M22 slice 10)
Confluence startup seed: pins the15-symbol research universe + SPY benchmark into the permanent demand set, and creates three system rack presets (Full Confluence, Technical Momentum, Macro+Flows+Sentiment) if absent. Called once from index.ts; idempotent on every restart.
This commit is contained in:
@@ -0,0 +1,96 @@
|
|||||||
|
// Investor Flow — confluenceSeed.test.ts (M22 slice 10)
|
||||||
|
// Integration test: pin symbols and create system racks against an in-memory DB.
|
||||||
|
|
||||||
|
import { describe, it, beforeEach } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
import { createDb, initSchema } from '../../db/client.ts';
|
||||||
|
import { ConfluenceRepository } from '../../db/confluenceRepository.ts';
|
||||||
|
import { createCacheRepository, type CacheRepository } from '../../cache/CacheRepository.ts';
|
||||||
|
import { FakeSourceAdapter } from '../../adapters/SourceAdapter.ts';
|
||||||
|
import { AdapterQueue } from '../../queue/AdapterQueue.ts';
|
||||||
|
import {
|
||||||
|
seedConfluence,
|
||||||
|
CONFLUENCE_UNIVERSE,
|
||||||
|
BENCHMARK_SYMBOL,
|
||||||
|
defineSystemRackPresets,
|
||||||
|
} from '../confluenceSeed.ts';
|
||||||
|
import { CONFLUENCE_SLOT_IDS } from '../confluenceSlots.ts';
|
||||||
|
|
||||||
|
let db: ReturnType<typeof createDb>;
|
||||||
|
let cache: CacheRepository;
|
||||||
|
let repo: ConfluenceRepository;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
db = createDb({ path: ':memory:' });
|
||||||
|
initSchema(db);
|
||||||
|
repo = new ConfluenceRepository(db);
|
||||||
|
const adapter = new FakeSourceAdapter('yfinance');
|
||||||
|
const queue = new AdapterQueue({ db, adapters: new Map([['yfinance', adapter as any]]) });
|
||||||
|
cache = createCacheRepository({ db, scheduler: queue });
|
||||||
|
(queue as any).cache = cache;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('CONFLUENCE_UNIVERSE', () => {
|
||||||
|
it('contains exactly15 symbols plus benchmark', () => {
|
||||||
|
assert.equal(CONFLUENCE_UNIVERSE.length, 15);
|
||||||
|
assert.equal(BENCHMARK_SYMBOL, 'SPY');
|
||||||
|
const symbols = CONFLUENCE_UNIVERSE.map((s) => s.symbol);
|
||||||
|
assert.equal(new Set(symbols).size, symbols.length, 'no duplicate symbols');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SMH is the only ETF; the rest are equities', () => {
|
||||||
|
const etfs = CONFLUENCE_UNIVERSE.filter((s) => s.kind === 'etf');
|
||||||
|
assert.deepEqual(etfs.map((s) => s.symbol), ['SMH']);
|
||||||
|
for (const s of CONFLUENCE_UNIVERSE.filter((s) => s.symbol !== 'SMH')) {
|
||||||
|
assert.equal(s.kind, 'equity', `${s.symbol} should be equity`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('defineSystemRackPresets', () => {
|
||||||
|
it('returns exactly 3 presets', () => {
|
||||||
|
const presets = defineSystemRackPresets();
|
||||||
|
assert.equal(presets.length, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('all slot ids in presets are valid', () => {
|
||||||
|
const validIds = new Set(CONFLUENCE_SLOT_IDS);
|
||||||
|
for (const preset of defineSystemRackPresets()) {
|
||||||
|
for (const id of preset.slotIds) {
|
||||||
|
assert.ok(validIds.has(id), `${preset.name} has unknown slot id: ${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('"Full Confluence" uses all34 slots', () => {
|
||||||
|
const full = defineSystemRackPresets().find((p) => p.id === 'confluence-full')!;
|
||||||
|
assert.equal(full.slotIds.length, 34);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('"Technical Momentum" has 15 slots', () => {
|
||||||
|
const tech = defineSystemRackPresets().find((p) => p.id === 'confluence-technical')!;
|
||||||
|
assert.equal(tech.slotIds.length, 15);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('"Macro + Flows + Sentiment" has 14 slots', () => {
|
||||||
|
const macro = defineSystemRackPresets().find((p) => p.id === 'confluence-macro-flows')!;
|
||||||
|
assert.equal(macro.slotIds.length, 14);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('seedConfluence', () => {
|
||||||
|
it('pins16 symbols (15 universe + benchmark) and creates 3 system racks', async () => {
|
||||||
|
const { symbolsPinned, racksCreated } = await seedConfluence(db, cache);
|
||||||
|
assert.equal(symbolsPinned, 16);
|
||||||
|
assert.equal(racksCreated, 3);
|
||||||
|
const systemRacks = repo.listSystemRacks();
|
||||||
|
assert.equal(systemRacks.length, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is idempotent: running twice does not duplicate racks', async () => {
|
||||||
|
await seedConfluence(db, cache);
|
||||||
|
await seedConfluence(db, cache);
|
||||||
|
assert.equal(repo.listSystemRacks().length, 3);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
// Investor Flow — Confluence Starter Seed (M22, slice 10)
|
||||||
|
//
|
||||||
|
// Defines the 15-symbol confluence universe, three system rack presets, and the
|
||||||
|
// startup function that pins the universe into the demand set and persists the
|
||||||
|
// system racks if they don't exist yet.
|
||||||
|
//
|
||||||
|
// Called once on server start from index.ts. All operations are idempotent:
|
||||||
|
// running twice is a no-op.
|
||||||
|
|
||||||
|
import type { DatabaseSync } from 'node:sqlite';
|
||||||
|
import type { CacheRepository, TickerKind } from '../cache/CacheRepository.ts';
|
||||||
|
import { ConfluenceRepository, rackFromSlots } from '../db/confluenceRepository.ts';
|
||||||
|
import { CONFLUENCE_SLOTS, CONFLUENCE_SLOT_IDS } from './confluenceSlots.ts';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Universe
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface ConfluenceSymbol {
|
||||||
|
symbol: string;
|
||||||
|
kind: TickerKind;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The15-symbol confluence research universe + SPY benchmark. Every confluence
|
||||||
|
* evaluation and backtest assumes these symbols are warm. SMH is the
|
||||||
|
* semiconductor ETF; the rest are single-name equities.
|
||||||
|
*/
|
||||||
|
export const CONFLUENCE_UNIVERSE: ConfluenceSymbol[] = [
|
||||||
|
{ symbol: 'PLTR', kind: 'equity', label: 'Palantir' },
|
||||||
|
{ symbol: 'NVDA', kind: 'equity', label: 'NVIDIA' },
|
||||||
|
{ symbol: 'AMD', kind: 'equity', label: 'AMD' },
|
||||||
|
{ symbol: 'AAPL', kind: 'equity', label: 'Apple' },
|
||||||
|
{ symbol: 'MSFT', kind: 'equity', label: 'Microsoft' },
|
||||||
|
{ symbol: 'SMH', kind: 'etf', label: 'Semiconductor ETF' },
|
||||||
|
{ symbol: 'XOM', kind: 'equity', label: 'ExxonMobil' },
|
||||||
|
{ symbol: 'JPM', kind: 'equity', label: 'JPMorgan' },
|
||||||
|
{ symbol: 'UNH', kind: 'equity', label: 'UnitedHealth' },
|
||||||
|
{ symbol: 'COST', kind: 'equity', label: 'Costco' },
|
||||||
|
{ symbol: 'AMZN', kind: 'equity', label: 'Amazon' },
|
||||||
|
{ symbol: 'CAT', kind: 'equity', label: 'Caterpillar' },
|
||||||
|
{ symbol: 'LMT', kind: 'equity', label: 'Lockheed Martin' },
|
||||||
|
{ symbol: 'LIN', kind: 'equity', label: 'Linde' },
|
||||||
|
{ symbol: 'NEE', kind: 'equity', label: 'NextEra Energy' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const BENCHMARK_SYMBOL = 'SPY';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// System rack presets
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
interface RackPreset {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
slotIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALL_IDS = CONFLUENCE_SLOT_IDS as readonly string[];
|
||||||
|
|
||||||
|
function familySlots(...families: string[]): string[] {
|
||||||
|
return CONFLUENCE_SLOTS.filter((s) => families.includes(s.family)).map((s) => s.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Three curated system rack presets. Each is a different lens on the same
|
||||||
|
* symbol data, expressed as a subset of the 34-slot catalog:
|
||||||
|
*
|
||||||
|
* 1. "Full Confluence" — all 34 slots (the default every-picture view)
|
||||||
|
* 2. "Technical Momentum" — the15 technical slots only (price-action focus)
|
||||||
|
* 3. "Macro + Flows + Sentiment" — macro 5 + seasonal 5 + flows 3 +
|
||||||
|
* sentiment 1 = 14 slots (the macro/structural lens)
|
||||||
|
*/
|
||||||
|
export function defineSystemRackPresets(): RackPreset[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'confluence-full',
|
||||||
|
name: 'Full Confluence',
|
||||||
|
description: 'All 34 slots. The broadest evidence view of a symbol\'s picture.',
|
||||||
|
slotIds: [...ALL_IDS],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'confluence-technical',
|
||||||
|
name: 'Technical Momentum',
|
||||||
|
description: 'The 15 technical slots: trend, momentum, mean-reversion, and volume.',
|
||||||
|
slotIds: familySlots('technical'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'confluence-macro-flows',
|
||||||
|
name: 'Macro + Flows + Sentiment',
|
||||||
|
description: 'Macro regime, seasonal calendar, ETF/COT flows, and informed-commentator sentiment (14 slots).',
|
||||||
|
slotIds: familySlots('macro', 'seasonal', 'flows', 'sentiment'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Seed routine
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pin the confluence universe into the demand set and create the three system
|
||||||
|
* rack presets if they don't already exist. Safe to call on every startup.
|
||||||
|
*/
|
||||||
|
export async function seedConfluence(
|
||||||
|
db: DatabaseSync,
|
||||||
|
cache: CacheRepository,
|
||||||
|
): Promise<{ symbolsPinned: number; racksCreated: number }> {
|
||||||
|
// 1. Pin universe symbols + benchmark into the permanent demand set.
|
||||||
|
const allSymbols: ConfluenceSymbol[] = [
|
||||||
|
...CONFLUENCE_UNIVERSE,
|
||||||
|
{ symbol: BENCHMARK_SYMBOL, kind: 'etf', label: 'S&P 500 benchmark' },
|
||||||
|
];
|
||||||
|
let symbolsPinned = 0;
|
||||||
|
for (const { symbol, kind } of allSymbols) {
|
||||||
|
try {
|
||||||
|
await cache.pinSystemSymbol(symbol, kind);
|
||||||
|
symbolsPinned++;
|
||||||
|
} catch { /* symbol already pinned — ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Create system rack presets that don't already exist.
|
||||||
|
const repo = new ConfluenceRepository(db);
|
||||||
|
const existing = new Set(repo.listSystemRacks().map((r) => r.id));
|
||||||
|
let racksCreated = 0;
|
||||||
|
for (const preset of defineSystemRackPresets()) {
|
||||||
|
if (existing.has(preset.id)) continue;
|
||||||
|
const rack = rackFromSlots(preset.id, preset.name, preset.slotIds, {
|
||||||
|
isSystem: true,
|
||||||
|
description: preset.description,
|
||||||
|
});
|
||||||
|
repo.saveRack(rack);
|
||||||
|
racksCreated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { symbolsPinned, racksCreated };
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import cryptoMod from './lib/crypto.ts';
|
|||||||
import { AdapterQueue } from './queue/AdapterQueue.ts';
|
import { AdapterQueue } from './queue/AdapterQueue.ts';
|
||||||
import { seedCuratedCusips } from './services/cusipRegistry.ts';
|
import { seedCuratedCusips } from './services/cusipRegistry.ts';
|
||||||
import { seedAdminDefaultAlertSubscriptions } from './db/alertSubscriptionRepository.ts';
|
import { seedAdminDefaultAlertSubscriptions } from './db/alertSubscriptionRepository.ts';
|
||||||
|
import { seedConfluence } from './confluence/confluenceSeed.ts';
|
||||||
import { makeCreateContext } from './trpc/context.ts';
|
import { makeCreateContext } from './trpc/context.ts';
|
||||||
import { appRouter } from './trpc/router.ts';
|
import { appRouter } from './trpc/router.ts';
|
||||||
import type { Alert } from './alerts/AlertEngine.ts';
|
import type { Alert } from './alerts/AlertEngine.ts';
|
||||||
@@ -100,6 +101,16 @@ try {
|
|||||||
console.error('[investor-flow] admin alert seed failed', e);
|
console.error('[investor-flow] admin alert seed failed', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Confluence starter seed: pin15-symbol universe + 3 system rack presets
|
||||||
|
try {
|
||||||
|
const { symbolsPinned, racksCreated } = await seedConfluence(database, cache);
|
||||||
|
if (symbolsPinned > 0 || racksCreated > 0) {
|
||||||
|
console.log(`[investor-flow] confluence seed: ${symbolsPinned} symbols pinned, ${racksCreated} system racks created`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[investor-flow] confluence seed failed', e);
|
||||||
|
}
|
||||||
|
|
||||||
// Demand hygiene: junk test symbols + inflated refcounts from page-view subscribe spam
|
// Demand hygiene: junk test symbols + inflated refcounts from page-view subscribe spam
|
||||||
try {
|
try {
|
||||||
const hygiene = queue.cleanupDemandHygiene();
|
const hygiene = queue.cleanupDemandHygiene();
|
||||||
|
|||||||
Reference in New Issue
Block a user