feat: dealer flow, mirror portfolio (M21), options convexity, FINRA short interest, alert producers, vendor gate
CI / Test & Type-Check (push) Canceled after 0s
CI / Test & Type-Check (push) Canceled after 0s
Snapshot of in-progress module work across multiple slices: - Dealer Flow: dealerExposureEngine, dealerMapService, dealerMapExplain, dealerMapIntegrity, dealerMapReplay, dealerStudyEngine, hanStyleLevels - Mirror Portfolio (M21): fundRepository, captureIngest, mirrorAlertProducers, fund holdings strip, live book, position capture ingest - Options: BSM, NormalizedOptionSurface types, OptionsChainRouter, ConvexityGate, option legs panel - Alert producers: vixLevel, rotation, thesis, unlock, portfolioRisk, mirror (fund_capture, fund_13f, mirror_diff) - FINRA short interest adapter + queue integration - SEC company tickers adapter + ingest (symbol search index seed) - Vendor gate (rate-limit-first data plane, ADR-0009) - CUSIP registry, reverse 13F refresh, stock float service - LRU cache, portfolio backtest engine - Frontend: dealer-flow, funds, journal, lab, monitor, plan, portfolio, reports, screener, strategies, theses, guided-start, exits, more pages - Volume profile, workspace profile, visibility-aware poll - ADRs 0010 (mirror math not advice), 0011 (symbol search index) - VENDOR_INTEGRATIONS.md, END_USER_TEST.md - .gitignore: exclude DBs, .DS_Store, local config, agent scratch
This commit is contained in:
@@ -20,6 +20,32 @@ export interface CreateSubscriptionInput {
|
||||
params?: string;
|
||||
}
|
||||
|
||||
/** Catalog of alert types shown in the per-type toggle list. */
|
||||
export const ALERT_TYPE_CATALOG: Array<{ type: string; label: string; description: string }> = [
|
||||
{ type: 'informed_buy', label: 'Insider Buy', description: 'Insider Form 4 purchases outside 10b5-1 plans.' },
|
||||
{ type: 'informed_sell', label: 'Insider Sell', description: 'Insider Form 4 sales outside 10b5-1 plans.' },
|
||||
{ type: 'new_13da', label: 'New 13D/G Filing', description: 'New institutional 13D/13G positions and 13F-HR filings.' },
|
||||
{ type: 'rotation_incipient', label: 'Rotation Signal', description: 'Incipient sector rotation detected in the rotation engine.' },
|
||||
{ type: 'regime_shift', label: 'Regime Shift', description: 'Market regime transition affecting portfolio risk.' },
|
||||
{ type: 'conviction_unlock', label: 'Conviction Unlock', description: 'A new conviction tier unlocks from trading history.' },
|
||||
{ type: 'thesis_broken', label: 'Thesis Broken', description: 'Thesis invalidation criteria met.' },
|
||||
{ type: 'thesis_weakening', label: 'Thesis Weakening', description: 'Signals suggest a thesis may be weakening.' },
|
||||
{ type: 'cluster_breach', label: 'Cluster Breach', description: 'Cluster exposure exceeds the recommended cap.' },
|
||||
{ type: 'drawdown_halt', label: 'Drawdown Halt', description: 'Drawdown tolerance breached; circuit breaker paused entries.' },
|
||||
{ type: 'asymmetry_warning', label: 'Asymmetry Warning', description: 'Portfolio reward-to-risk below threshold.' },
|
||||
{ type: 'fund_capture', label: 'Fund Capture', description: 'A tracked fund posted a position update.' },
|
||||
{ type: 'fund_13f', label: 'Tracked Fund 13F', description: 'A tracked fund filed a new 13F.' },
|
||||
{ type: 'mirror_diff', label: 'Mirror Diff', description: 'The mirror target book changed materially.' },
|
||||
{ type: 'vix_level', label: 'VIX Level', description: 'VIX moves into a new historical volatility band.' },
|
||||
];
|
||||
|
||||
export interface AlertTypeToggle {
|
||||
type: string;
|
||||
label: string;
|
||||
description: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
function stmts(db: DatabaseSync) {
|
||||
return {
|
||||
insert: db.prepare(
|
||||
@@ -171,3 +197,103 @@ export function getEffectiveAlertSubscription(
|
||||
if (globalRow) return mapRow(globalRow);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed a global default subscription (symbol NULL, watchlist NULL) for every
|
||||
* catalog alert type for admin users (is_admin=1). Idempotent: only creates a
|
||||
* default when the user has zero enabled subscriptions of that type, so a toggled
|
||||
* preference (any ticker/list/global row) is never overwritten. Runs on every
|
||||
* producer tick (cheap: no-op once seeded).
|
||||
*/
|
||||
export function seedAdminDefaultAlertSubscriptions(db: DatabaseSync): number {
|
||||
let inserted = 0;
|
||||
try {
|
||||
const admins = db.prepare(
|
||||
`SELECT id FROM users
|
||||
WHERE is_admin = 1
|
||||
AND id NOT IN ('anonymous','system')
|
||||
AND email NOT LIKE 'anonymous@%'`,
|
||||
).all() as Array<{ id: string }>;
|
||||
if (admins.length === 0) return 0;
|
||||
|
||||
const insert = db.prepare(
|
||||
`INSERT OR IGNORE INTO alerts (id, owner_id, watchlist_id, symbol, alert_type, enabled, params, created_at)
|
||||
VALUES (?, ?, NULL, NULL, ?, 1, '{}', ?)`,
|
||||
);
|
||||
const hasEnabled = db.prepare(
|
||||
`SELECT COUNT(*) AS n FROM alerts WHERE owner_id = ? AND alert_type = ? AND enabled = 1`,
|
||||
);
|
||||
const now = new Date().toISOString();
|
||||
|
||||
for (const admin of admins) {
|
||||
for (const { type } of ALERT_TYPE_CATALOG) {
|
||||
const { n } = hasEnabled.get(admin.id, type) as { n: number };
|
||||
if (n > 0) continue;
|
||||
const id = `auto-global-${admin.id}-${type}`;
|
||||
const r = insert.run(id, admin.id, type, now);
|
||||
if (Number(r.changes) > 0) inserted += 1;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* schema may lack tables in tests */
|
||||
}
|
||||
return inserted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-type enable state for the current user. A type is "on" if any enabled
|
||||
* subscription of that type exists (ticker-level, list-level, or global).
|
||||
*/
|
||||
export function listAlertTypeToggles(db: DatabaseSync, userId: string): AlertTypeToggle[] {
|
||||
let rows: Array<{ alert_type: string; max_enabled: number }> = [];
|
||||
try {
|
||||
rows = db.prepare(
|
||||
`SELECT alert_type, MAX(enabled) AS max_enabled
|
||||
FROM alerts WHERE owner_id = ?
|
||||
GROUP BY alert_type`,
|
||||
).all(userId) as Array<{ alert_type: string; max_enabled: number }>;
|
||||
} catch {
|
||||
/* schema may lack alerts table in tests */
|
||||
}
|
||||
const enabledByType = new Map<string, boolean>();
|
||||
for (const r of rows) enabledByType.set(r.alert_type, r.max_enabled === 1);
|
||||
return ALERT_TYPE_CATALOG.map(({ type, label, description }) => ({
|
||||
type,
|
||||
label,
|
||||
description,
|
||||
enabled: enabledByType.get(type) ?? false,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle a whole alert type for a user. Flipping ON creates a global default
|
||||
* subscription (symbol NULL, watchlist NULL) unless one exists; flipping OFF
|
||||
* disables every subscription the user has of that type (ticker + list +
|
||||
* global) so the toggle is authoritative. Never deletes rows.
|
||||
*/
|
||||
export function toggleAlertType(
|
||||
db: DatabaseSync,
|
||||
userId: string,
|
||||
alertType: string,
|
||||
enabled: boolean,
|
||||
): AlertTypeToggle[] {
|
||||
const catalog = ALERT_TYPE_CATALOG.find((c) => c.type === alertType);
|
||||
if (!catalog) throw new Error(`Unknown alert type: ${alertType}`);
|
||||
|
||||
if (enabled) {
|
||||
const existing = db.prepare(
|
||||
`SELECT id FROM alerts WHERE owner_id = ? AND alert_type = ? AND symbol IS NULL AND watchlist_id IS NULL LIMIT 1`,
|
||||
).get(userId, alertType) as { id: string } | undefined;
|
||||
if (existing) {
|
||||
db.prepare('UPDATE alerts SET enabled = 1 WHERE id = ?').run(existing.id);
|
||||
} else {
|
||||
createAlertSubscription(db, userId, { alertType });
|
||||
}
|
||||
} else {
|
||||
db.prepare(
|
||||
'UPDATE alerts SET enabled = 0 WHERE owner_id = ? AND alert_type = ?',
|
||||
).run(userId, alertType);
|
||||
}
|
||||
|
||||
return listAlertTypeToggles(db, userId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user