fix: backfill symbol_demand for sidebar-added symbols + analyst ratings schema fix

- Add await ctx.cache.subscribe() to addSymbol mutation so symbols
  added via the sidebar get registered in symbol_demand and yfinance
  jobs are queued immediately
- Backfill PEP, WYNN, STZ, CELH into symbol_demand + adapter_queue
- Upgrade yahoo-finance2 3.15.3 -> 3.15.4 and pass validateResult:false
  to quoteSummary() to handle Yahoo schema drift
- Add error detail logging for analyst ratings schema failures
- Update .gitignore with common ignores
This commit is contained in:
Investor Flow Build
2026-07-23 18:02:24 -04:00
parent 5ef2b2f060
commit e262187c3c
204 changed files with 25014 additions and 2934 deletions
+132 -1
View File
@@ -35,6 +35,12 @@ export function createDb(opts: DbOptions = {}): DatabaseSync {
} catch {
/* readOnly or disallowed — ignore */
}
// Concurrent queue drain + request handlers write often; wait instead of failing immediately.
try {
database.exec('PRAGMA busy_timeout = 5000;');
} catch {
/* ignore */
}
return database;
}
@@ -44,15 +50,140 @@ export function initSchema(database: DatabaseSync): void {
database.exec(sql);
}
/** Idempotent migrations for existing databases (new columns, tables). */
/** Idempotent migrations for existing databases (new columns, tables, dedupes). */
function runMigrations(db: DatabaseSync): void {
// 1. New columns (ignore "already exists").
const migrations: string[] = [
`ALTER TABLE adapter_queue ADD COLUMN error TEXT`,
`ALTER TABLE adapter_queue ADD COLUMN scheduled_for TEXT`,
`ALTER TABLE insider_transactions ADD COLUMN accession TEXT`,
`ALTER TABLE x_credentials ADD COLUMN fred_api_key_enc TEXT`,
`ALTER TABLE users ADD COLUMN status TEXT NOT NULL DEFAULT 'active'`,
// Onboarding / risk posture fields added after early installs (CREATE IF NOT EXISTS does not alter).
`ALTER TABLE users ADD COLUMN drawdown_tolerance REAL`,
`ALTER TABLE users ADD COLUMN backup_codes_hashed TEXT`,
`ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE rotation_state ADD COLUMN signal_since TEXT`,
`CREATE TABLE IF NOT EXISTS rotation_state (
id TEXT PRIMARY KEY DEFAULT 'singleton',
signal TEXT NOT NULL DEFAULT 'none',
spread REAL,
inflow_avg REAL,
outflow_avg REAL,
sub_group TEXT,
checked_at TEXT,
signal_since TEXT
)`,
`CREATE TABLE IF NOT EXISTS rotation_rank_snapshots (
as_of_date TEXT NOT NULL,
symbol TEXT NOT NULL,
name TEXT,
grp TEXT,
rank_1m INTEGER,
rs_1m REAL,
rank_1w INTEGER,
rs_1w REAL,
leadership TEXT,
early_watch INTEGER NOT NULL DEFAULT 0,
strength TEXT,
PRIMARY KEY (as_of_date, symbol)
)`,
`CREATE INDEX IF NOT EXISTS idx_rotation_rank_date ON rotation_rank_snapshots(as_of_date DESC)`,
// Option legs book (MVP risk contribution; M17 sleeve later).
`CREATE TABLE IF NOT EXISTS portfolio_option_legs (
id TEXT PRIMARY KEY,
owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
underlying TEXT NOT NULL,
right TEXT NOT NULL,
side TEXT NOT NULL,
strike REAL NOT NULL,
expiry TEXT NOT NULL,
contracts REAL NOT NULL,
premium REAL NOT NULL,
multiplier INTEGER NOT NULL DEFAULT 100,
role TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'open',
acquired_at TEXT NOT NULL,
note TEXT
)`,
`CREATE INDEX IF NOT EXISTS idx_option_legs_owner_status
ON portfolio_option_legs(owner_id, status)`,
// 13F put/call indicator for options positions.
`ALTER TABLE institution_filings ADD COLUMN put_call TEXT`,
];
for (const sql of migrations) {
try { db.exec(sql); } catch { /* column already exists */ }
}
// 2. De-dupe legacy institution_filings rows (keep the oldest rowid per group).
// Uses rowid (SQLite built-in) rather than tuple NOT IN which can hit limits
// with large duplicate sets. This must succeed before creating the unique index.
try {
db.exec(`
DELETE FROM institution_filings
WHERE rowid NOT IN (
SELECT MIN(rowid)
FROM institution_filings
GROUP BY filer_cik, symbol, reported_quarter, form
)
`);
} catch { /* empty / locked — safe to skip */ }
// 3. Idempotency index for institution_filings (now safe after de-dupe).
try {
db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS uq_inst_filings ON institution_filings(filer_cik, symbol, reported_quarter, form)`);
} catch { /* ignore */ }
// 4. Data-quality lint table.
try {
db.exec(`
CREATE TABLE IF NOT EXISTS data_quality (
symbol TEXT NOT NULL,
kind TEXT NOT NULL,
last_checked_at TEXT,
stored_count INTEGER,
discovered_count INTEGER,
missing_count INTEGER,
stale INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL,
detail TEXT,
PRIMARY KEY (symbol, kind)
)
`);
} catch { /* ignore */ }
// 5. Analyst ratings / upgrades & downgrades (yahoo-finance2).
try { db.exec(`ALTER TABLE analyst_ratings ADD COLUMN target_from REAL`); } catch { /* ignore */ }
try { db.exec(`ALTER TABLE analyst_ratings ADD COLUMN target_to REAL`); } catch { /* ignore */ }
try {
db.exec(`
CREATE TABLE IF NOT EXISTS analyst_ratings (
symbol TEXT NOT NULL,
firm TEXT NOT NULL,
action TEXT,
grade_from TEXT,
grade_to TEXT,
target_from REAL,
target_to REAL,
rating_date TEXT NOT NULL,
fetched_at TEXT NOT NULL,
PRIMARY KEY (symbol, firm, rating_date)
)
`);
} catch { /* ignore */ }
try {
db.exec(`
CREATE TABLE IF NOT EXISTS analyst_consensus (
symbol TEXT PRIMARY KEY,
strong_buy INTEGER NOT NULL DEFAULT 0,
buy INTEGER NOT NULL DEFAULT 0,
hold INTEGER NOT NULL DEFAULT 0,
sell INTEGER NOT NULL DEFAULT 0,
strong_sell INTEGER NOT NULL DEFAULT 0,
fetched_at TEXT NOT NULL
)
`);
} catch { /* ignore */ }
}
let _db: DatabaseSync | null = null;