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:
+132
-1
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user