feat: dealer flow, mirror portfolio (M21), options convexity, FINRA short interest, alert producers, vendor gate
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:
Investor Flow Build
2026-08-10 13:36:26 -04:00
parent 04fc11b2fd
commit ac94acf9e3
229 changed files with 32617 additions and 3934 deletions
+360 -1
View File
@@ -67,9 +67,24 @@ function runMigrations(db: DatabaseSync): void {
`ALTER TABLE rotation_state ADD COLUMN signal_since TEXT`,
`ALTER TABLE users ADD COLUMN modules TEXT NOT NULL DEFAULT '["research","settings"]'`,
`UPDATE users SET modules='["research","execution","analytics","settings"]' WHERE modules='["research","settings"]'`,
// Workspace profile (adaptive management density)
`ALTER TABLE users ADD COLUMN experience_stage TEXT`,
`ALTER TABLE users ADD COLUMN goal TEXT`,
`ALTER TABLE users ADD COLUMN horizon TEXT`,
`ALTER TABLE users ADD COLUMN density TEXT NOT NULL DEFAULT 'focused'`,
`ALTER TABLE users ADD COLUMN jargon_comfort TEXT NOT NULL DEFAULT 'plain'`,
// Backfill density / experience_stage from legacy complexity (idempotent)
`UPDATE users SET density='standard', experience_stage=COALESCE(experience_stage, 'some_stocks') WHERE complexity='intermediate' AND (experience_stage IS NULL OR density='focused')`,
`UPDATE users SET density='full', experience_stage=COALESCE(experience_stage, 'active_self_directed') WHERE complexity='advanced' AND (experience_stage IS NULL OR density IN ('focused','standard'))`,
`UPDATE users SET experience_stage=COALESCE(experience_stage, 'never_invested') WHERE complexity='beginner' AND experience_stage IS NULL`,
`ALTER TABLE watchlists ADD COLUMN kind TEXT NOT NULL DEFAULT 'user'`,
`ALTER TABLE watchlists ADD COLUMN class_key TEXT`,
`ALTER TABLE watchlists ADD COLUMN class_label TEXT`,
// System-pinned demand symbols (rotation universe / benchmarks) survive unsubscribe
`ALTER TABLE symbol_demand ADD COLUMN system_pin INTEGER NOT NULL DEFAULT 0`,
// Extended-hours quote fields (pre/post last vs RTH close)
`ALTER TABLE quotes ADD COLUMN session TEXT`,
`ALTER TABLE quotes ADD COLUMN regular_price REAL`,
`DROP INDEX IF EXISTS idx_watchlists_owner_name`,
`CREATE UNIQUE INDEX IF NOT EXISTS idx_watchlists_owner_kind_name ON watchlists(owner_id, kind, name)`,
`CREATE TABLE IF NOT EXISTS rotation_state (
@@ -114,10 +129,89 @@ function runMigrations(db: DatabaseSync): void {
acquired_at TEXT NOT NULL,
note TEXT
)`,
`CREATE INDEX IF NOT EXISTS idx_option_legs_owner_status
ON portfolio_option_legs(owner_id, status)`,
`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`,
// Mirror Portfolio (M21) — tracked funds + position records.
`CREATE TABLE IF NOT EXISTS tracked_funds (
id TEXT PRIMARY KEY,
ci_key TEXT NOT NULL,
fund_name TEXT NOT NULL,
manager_name TEXT NOT NULL,
x_handle TEXT,
paywall_status TEXT NOT NULL DEFAULT 'unknown',
enabled INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT
)`,
`CREATE UNIQUE INDEX IF NOT EXISTS uq_tracked_funds_cik ON tracked_funds(ci_key)`,
`CREATE UNIQUE INDEX IF NOT EXISTS uq_tracked_funds_name ON tracked_funds(fund_name)`,
`CREATE TABLE IF NOT EXISTS fund_position_records (
id TEXT PRIMARY KEY,
fund_id TEXT NOT NULL REFERENCES tracked_funds(id) ON DELETE CASCADE,
symbol TEXT NOT NULL,
shares REAL,
value_usd REAL,
cost_basis REAL,
as_of TEXT NOT NULL,
source TEXT NOT NULL,
evidence_url TEXT,
created_at TEXT NOT NULL
)`,
`CREATE INDEX IF NOT EXISTS idx_fund_records_fund_asof ON fund_position_records(fund_id, symbol, as_of DESC)`,
// Symbol Search Index (M22) — issuer CIK on symbols + lookup index.
`ALTER TABLE symbols ADD COLUMN cik TEXT`,
`CREATE INDEX IF NOT EXISTS idx_symbols_cik ON symbols(cik)`,
// Dealer Flow velocity snapshots (PR2)
`CREATE TABLE IF NOT EXISTS dealer_map_snapshots (
symbol TEXT NOT NULL,
as_of_date TEXT NOT NULL,
payload_json TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (symbol, as_of_date)
)`,
`CREATE INDEX IF NOT EXISTS idx_dealer_map_snapshots_symbol ON dealer_map_snapshots(symbol, as_of_date DESC)`,
`CREATE TABLE IF NOT EXISTS user_llm_endpoints (
user_id TEXT PRIMARY KEY,
base_url TEXT NOT NULL,
api_key_enc TEXT,
model TEXT NOT NULL DEFAULT 'gpt-4o-mini',
key_hint TEXT,
updated_at TEXT NOT NULL
)`,
`CREATE INDEX IF NOT EXISTS idx_alert_events_type_time ON alert_events(type, created_at DESC)`,
`CREATE INDEX IF NOT EXISTS idx_alerts_owner_type ON alerts(owner_id, alert_type)`,
`CREATE TABLE IF NOT EXISTS producer_run_log (
id TEXT PRIMARY KEY,
producer TEXT NOT NULL,
frequency TEXT NOT NULL,
events_fired INTEGER NOT NULL DEFAULT 0,
inspected INTEGER NOT NULL DEFAULT 0,
duration_ms INTEGER NOT NULL DEFAULT 0,
error TEXT,
run_at TEXT NOT NULL
)`,
`CREATE INDEX IF NOT EXISTS idx_producer_run_log_producer ON producer_run_log(producer, run_at DESC)`,
`CREATE INDEX IF NOT EXISTS idx_adapter_queue_status ON adapter_queue(status, last_attempt)`,
`CREATE INDEX IF NOT EXISTS idx_queue_errors_key_ts ON queue_errors(queue_key, ts DESC)`,
`CREATE TABLE IF NOT EXISTS notification_outbox (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
type TEXT NOT NULL,
severity TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT NOT NULL,
symbol TEXT,
created_at TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
attempt INTEGER NOT NULL DEFAULT 0,
last_error TEXT,
sent_at TEXT
)`,
`CREATE INDEX IF NOT EXISTS idx_notification_outbox_pending ON notification_outbox(status, created_at)`,
];
for (const sql of migrations) {
try { db.exec(sql); } catch { /* column already exists */ }
@@ -233,7 +327,9 @@ function runMigrations(db: DatabaseSync): void {
`);
} catch { /* ignore */ }
// 10. FINRA config table for admin-configurable download URL.
// 10. FINRA config + tables + schedule update (daily + bi-monthly).
try { db.exec(`UPDATE queue_schedules SET interval_ms=86400000 WHERE source_kind='finra-bulk'`); } catch { /* table may not exist yet */ }
try { db.exec(`INSERT OR IGNORE INTO queue_schedules (source_kind, interval_ms, last_enqueued, next_enqueue) VALUES ('finra-si', 1209600000, NULL, ?)`); } catch { /* ignore */ }
try {
db.exec(`
CREATE TABLE IF NOT EXISTS finra_config (
@@ -243,6 +339,242 @@ function runMigrations(db: DatabaseSync): void {
)
`);
} catch { /* ignore */ }
try {
db.exec(`
CREATE TABLE IF NOT EXISTS finra_short_interest_biweekly (
symbol TEXT NOT NULL, settlement_date TEXT NOT NULL, issue_name TEXT,
exchange_code TEXT, market_class TEXT, current_short_position REAL,
previous_short_position REAL, avg_daily_volume REAL, days_to_cover REAL,
change_percent REAL, change_previous REAL, revision_flag TEXT,
source_file TEXT, ingested_at TEXT NOT NULL,
PRIMARY KEY (symbol, settlement_date)
)
`);
} catch { /* ignore */ }
try { db.exec(`CREATE INDEX IF NOT EXISTS idx_finra_si_symbol ON finra_short_interest_biweekly(symbol)`); } catch { /* ignore */ }
try { db.exec(`CREATE INDEX IF NOT EXISTS idx_finra_si_settlement ON finra_short_interest_biweekly(settlement_date)`); } catch { /* ignore */ }
// 11. Strategy lab + execution redesign tables and columns.
try { db.exec(`ALTER TABLE strategies ADD COLUMN preset_id TEXT`); } catch { /* ignore */ }
try { db.exec(`ALTER TABLE strategies ADD COLUMN scope TEXT NOT NULL DEFAULT 'single'`); } catch { /* ignore */ }
try { db.exec(`ALTER TABLE strategies ADD COLUMN components TEXT NOT NULL DEFAULT '[]'`); } catch { /* ignore */ }
try { db.exec(`ALTER TABLE strategy_presets ADD COLUMN example_tickers TEXT`); } catch { /* ignore */ }
try { db.exec(`ALTER TABLE strategies ADD COLUMN exit_profile TEXT`); } catch { /* ignore */ }
try { db.exec(`ALTER TABLE strategies ADD COLUMN risk_policy TEXT`); } catch { /* ignore */ }
try { db.exec(`ALTER TABLE trades ADD COLUMN realized_pnl REAL`); } catch { /* ignore */ }
try { db.exec(`ALTER TABLE trades ADD COLUMN allocation_plan_id TEXT`); } catch { /* ignore */ }
try { db.exec(`ALTER TABLE trades ADD COLUMN allocation_role TEXT`); } catch { /* ignore */ }
try { db.exec(`CREATE TABLE IF NOT EXISTS strategy_presets (id TEXT PRIMARY KEY, name TEXT NOT NULL, category TEXT NOT NULL, complexity TEXT NOT NULL, scope TEXT NOT NULL DEFAULT 'single', description TEXT NOT NULL, entry_rules TEXT NOT NULL DEFAULT '[]', exit_profile TEXT NOT NULL DEFAULT '[]', risk_policy TEXT NOT NULL DEFAULT '[]', instrument_type TEXT NOT NULL DEFAULT 'stock', portfolio_template TEXT, conviction_tier_default TEXT, requires_options_unlock INTEGER NOT NULL DEFAULT 0, forkable INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL)`); } catch { /* ignore */ }
try { db.exec(`CREATE TABLE IF NOT EXISTS allocation_plans (id TEXT PRIMARY KEY, owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, strategy_id TEXT NOT NULL REFERENCES strategies(id) ON DELETE CASCADE, name TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'building', target_alloc TEXT NOT NULL DEFAULT '{}', created_at TEXT NOT NULL)`); } catch { /* ignore */ }
try { db.exec(`CREATE TABLE IF NOT EXISTS dividend_fundamentals (symbol TEXT PRIMARY KEY, dividend_yield REAL, payout_ratio REAL, dividend_rate REAL, ex_dividend_date TEXT, trailing_eps REAL, forward_pe REAL, growth_streak_years INTEGER, observed_at TEXT NOT NULL)`); } catch { /* ignore */ }
try { db.exec(`CREATE INDEX IF NOT EXISTS idx_strategies_preset ON strategies(preset_id)`); } catch { /* ignore */ }
try { db.exec(`CREATE INDEX IF NOT EXISTS idx_alloc_owner ON allocation_plans(owner_id)`); } catch { /* ignore */ }
// Seed strategy presets (idempotent).
const now = new Date().toISOString();
const seedPresets = [
{
id: 'dca_accumulation', name: 'DCA / Accumulation', category: 'growth', complexity: 'beginner', scope: 'single',
description: 'Build a position steadily over time by investing fixed amounts at regular intervals, regardless of price. Removes the stress of timing the market.',
entry_rules: JSON.stringify([{ conditions: ['Time-based: scheduled buy day', 'Cash available >= planned amount'] }]),
exit_profile: JSON.stringify([
{ action: 'Take profit', reason: 'Price reaches target return (e.g. +20%)', threshold: 0.2 },
{ action: 'Stop loss', reason: 'Price drops below cost basis by max drawdown tolerance', threshold: -0.15 },
]),
risk_policy: JSON.stringify({ maxPositionPct: 0.15, stopLossPct: 0.15, takeProfitPct: 0.2, maxDrawdownPct: 0.2 }),
instrument_type: 'stock', portfolio_template: null, conviction_tier_default: 'B', requires_options_unlock: 0, forkable: 1,
example_tickers: JSON.stringify([
{ symbol: 'VOO', name: 'S&P 500 ETF', role: 'Core holding', allocationPct: 60, reason: 'Broad market low-cost ETF' },
{ symbol: 'IVV', name: 'S&P 500 ETF (iShares)', role: 'Core holding', allocationPct: 40, reason: 'Alternative broad market ETF' },
]),
},
{
id: 'dividend_growth', name: 'Dividend Growth', category: 'income', complexity: 'beginner', scope: 'single',
description: 'Own companies that consistently pay and grow their dividends. Provides income while you hold and benefits from compounding over time.',
entry_rules: JSON.stringify([{ conditions: ['Dividend yield >= 2%', 'Payout ratio <= 60%', 'Dividend growth streak >= 5 years', 'Forward P/E <= 25'] }]),
exit_profile: JSON.stringify([
{ action: 'Sell', reason: 'Dividend cut or suspension', threshold: null },
{ action: 'Sell', reason: 'Payout ratio exceeds 80%', threshold: 0.8 },
{ action: 'Take profit', reason: 'Position grows beyond portfolio allocation target', threshold: null },
]),
risk_policy: JSON.stringify({ maxPositionPct: 0.1, stopLossPct: 0.2, maxDrawdownPct: 0.25 }),
instrument_type: 'stock', portfolio_template: null, conviction_tier_default: 'B', requires_options_unlock: 0, forkable: 1,
example_tickers: JSON.stringify([
{ symbol: 'SCHD', name: 'Schwab US Dividend Equity ETF', role: 'Dividend growth', allocationPct: 40, reason: 'Strong dividend growth history' },
{ symbol: 'JNJ', name: 'Johnson & Johnson', role: 'Dividend growth', allocationPct: 20, reason: '60+ years of dividend growth' },
{ symbol: 'KO', name: 'Coca-Cola', role: 'Dividend growth', allocationPct: 20, reason: '60+ years of dividend growth' },
{ symbol: 'PG', name: 'Procter & Gamble', role: 'Dividend growth', allocationPct: 20, reason: '130+ years of dividends' },
]),
},
{
id: 'core_satellite', name: 'Core + Satellite', category: 'portfolio', complexity: 'intermediate', scope: 'portfolio',
description: 'Put the majority in a broad market core (e.g. VOO, VTI) for stable growth, then add smaller satellite positions around your best ideas.',
entry_rules: JSON.stringify([{ conditions: ['Core: broad market ETF (VOO, VTI, IVV, etc.)', 'Satellites: individual stocks or sector ETFs', 'Each satellite <= target allocation %'] }]),
exit_profile: JSON.stringify([
{ action: 'Rebalance', reason: 'Satellite drifts beyond allocation band', threshold: 0.05 },
{ action: 'Sell satellite', reason: 'Thesis invalidated or position no longer conviction', threshold: null },
{ action: 'Harvest', reason: 'Satellite hits profit target', threshold: null },
]),
risk_policy: JSON.stringify({ maxPositionPct: 0.05, stopLossPct: 0.2 }),
instrument_type: 'stock', portfolio_template: JSON.stringify([
{ role: 'Core (broad market)', target: 70, min: 60, max: 80 },
{ role: 'Satellite - Growth', target: 15, min: 5, max: 25 },
{ role: 'Satellite - Value/Income', target: 15, min: 5, max: 25 },
]), conviction_tier_default: 'B', requires_options_unlock: 0, forkable: 1,
example_tickers: JSON.stringify([
{ symbol: 'VOO', name: 'S&P 500 ETF', role: 'Core', allocationPct: 50, reason: 'Broad market core holding' },
{ symbol: 'VXUS', name: 'Total International Stock ETF', role: 'Core', allocationPct: 20, reason: 'International diversification' },
{ symbol: 'QQQ', name: 'Nasdaq 100 ETF', role: 'Satellite - Growth', allocationPct: 15, reason: 'Tech-heavy growth exposure' },
{ symbol: 'SCHD', name: 'Schwab US Dividend Equity ETF', role: 'Satellite - Income', allocationPct: 15, reason: 'Dividend growth complement' },
]),
},
{
id: 'barbell_strategy', name: 'Bar-bell', category: 'portfolio', complexity: 'intermediate', scope: 'portfolio',
description: 'Keep most of your capital in very safe assets (broad market ETFs, bonds) and put a small portion in high-growth bets. Limits downside while preserving upside.',
entry_rules: JSON.stringify([{ conditions: ['Safe portion: broad market ETF or bond ETF', 'Growth portion: high-conviction growth stocks or sector bets', 'Growth allocation <= max risk budget'] }]),
exit_profile: JSON.stringify([
{ action: 'Rebalance', reason: 'Growth portion exceeds allocation band due to outsized gains', threshold: 0.05 },
{ action: 'Cut growth position', reason: 'Growth thesis breaks or stop loss triggered', threshold: null },
{ action: 'Trim', reason: 'Growth position exceeds max single-stock allocation', threshold: null },
]),
risk_policy: JSON.stringify({ maxPositionPct: 0.05, stopLossPct: 0.2, maxDrawdownPct: 0.15 }),
instrument_type: 'stock', portfolio_template: JSON.stringify([
{ role: 'Safe (broad market / bonds)', target: 80, min: 70, max: 90 },
{ role: 'Growth bets', target: 20, min: 10, max: 30 },
]), conviction_tier_default: 'B', requires_options_unlock: 0, forkable: 1,
example_tickers: JSON.stringify([
{ symbol: 'VOO', name: 'S&P 500 ETF', role: 'Safe core', allocationPct: 50, reason: 'Broad market core' },
{ symbol: 'BND', name: 'Total Bond Market ETF', role: 'Safe bonds', allocationPct: 30, reason: 'Bond diversification' },
{ symbol: 'NVDA', name: 'NVIDIA Corporation', role: 'Growth bet', allocationPct: 10, reason: 'High-growth tech leader' },
{ symbol: 'AMZN', name: 'Amazon.com Inc', role: 'Growth bet', allocationPct: 10, reason: 'High-growth consumer/cloud leader' },
]),
},
{
id: 'covered_call_wheel', name: 'Covered Call Income', category: 'income', complexity: 'advanced', scope: 'single',
description: 'Generate regular income from stocks you plan to hold by selling call options against them. Requires understanding of options basics.',
entry_rules: JSON.stringify([{ conditions: ['Own at least 100 shares of underlying', 'Stock is one you want to hold long-term', 'Option liquidity: bid-ask spread < $0.20', 'IV percentile < 70 (avoid selling into low vol)'] }]),
exit_profile: JSON.stringify([
{ action: 'Roll', reason: 'Stock approaches strike price before expiration', threshold: null },
{ action: 'Let shares get called away', reason: 'Stock closes above strike at expiration', threshold: null },
{ action: 'Close early', reason: 'Captured most of the premium, time value decayed', threshold: null },
{ action: 'Sell put', reason: 'After shares called away, sell cash-secured put to re-enter', threshold: null },
]),
risk_policy: JSON.stringify({ maxPositionPct: 0.1, stopLossPct: 0.2, maxDrawdownPct: 0.3 }),
instrument_type: 'option', portfolio_template: null, conviction_tier_default: 'C', requires_options_unlock: 1, forkable: 1,
example_tickers: JSON.stringify([
{ symbol: 'AAPL', name: 'Apple Inc.', role: 'Covered call', allocationPct: 30, reason: 'High liquidity, strong options market' },
{ symbol: 'MSFT', name: 'Microsoft Corporation', role: 'Covered call', allocationPct: 30, reason: 'High liquidity, stable price' },
{ symbol: 'NVDA', name: 'NVIDIA Corporation', role: 'Covered call', allocationPct: 20, reason: 'Active options chain' },
{ symbol: 'AMZN', name: 'Amazon.com Inc', role: 'Covered call', allocationPct: 20, reason: 'High liquidity options market' },
]),
},
{
id: 'concentrated_value', name: 'Concentrated Value', category: 'growth', complexity: 'advanced', scope: 'portfolio',
description: 'Own a small number of high-conviction positions you understand deeply. Buy at a significant discount to intrinsic value, hold through volatility, and let your best ideas drive returns. Based on the Alpine Fox Capital playbook: board-level research, contrarian entry, permanent capital mindset.',
entry_rules: JSON.stringify([{ conditions: ['Deep research completed — understand the business at board level', 'Price at significant discount to estimated intrinsic value (margin of safety)', 'Contrarian — buying when out of favor or misunderstood', 'Management quality and capital allocation verified', 'Thesis is multi-year, not a trade'] }]),
exit_profile: JSON.stringify([
{ action: 'Sell', reason: 'Thesis invalidated — core assumption broken', threshold: null },
{ action: 'Sell', reason: 'Full valuation reached with no further upside', threshold: null },
{ action: 'Hold', reason: '50%+ drawdown is expected; re-check thesis, do not panic-sell', threshold: null },
]),
risk_policy: JSON.stringify({ maxPositionPct: 0.40, stopLossPct: null, maxDrawdownPct: 0.50 }),
instrument_type: 'stock', portfolio_template: JSON.stringify([
{ role: 'Core thesis positions', target: 80, min: 60, max: 95 },
{ role: 'Satellite / ballast', target: 20, min: 5, max: 40 },
]), conviction_tier_default: 'A', requires_options_unlock: 0, forkable: 1,
example_tickers: JSON.stringify([
{ symbol: 'CIFR', name: 'Cipher Mining', role: 'Core thesis', allocationPct: 25, reason: 'Bitcoin miner pivoting to AI/HPC infrastructure — largest position' },
{ symbol: 'IREN', name: 'IREN Limited', role: 'Core thesis', allocationPct: 20, reason: 'Bitcoin miner to AI cloud, board-level insight, held since $2' },
{ symbol: 'BKKT', name: 'Bakkt Holdings', role: 'Core thesis', allocationPct: 20, reason: 'Newest high-conviction build — 865K shares added in Q2 2026' },
{ symbol: 'IBIT', name: 'iShares Bitcoin Trust', role: 'Core thesis', allocationPct: 15, reason: 'Bitcoin ETF exposure, shares + calls for capital efficiency' },
{ symbol: 'OPEN', name: 'Opendoor Technologies', role: 'Core thesis', allocationPct: 10, reason: 'Contrarian bet — massive QoQ add (+6,581%)' },
{ symbol: 'NVO', name: 'Novo Nordisk', role: 'Satellite / ballast', allocationPct: 10, reason: 'Consumer staple ballast, consistent grower' },
]),
},
];
for (const p of seedPresets) {
try {
db.prepare(`INSERT OR IGNORE INTO strategy_presets (id, name, category, complexity, scope, description, entry_rules, exit_profile, risk_policy, instrument_type, portfolio_template, conviction_tier_default, requires_options_unlock, forkable, created_at, example_tickers) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`)
.run(p.id, p.name, p.category, p.complexity, p.scope, p.description, p.entry_rules, p.exit_profile, p.risk_policy, p.instrument_type, p.portfolio_template, p.conviction_tier_default, p.requires_options_unlock, p.forkable, now, p.example_tickers);
} catch { /* row may already exist */ }
// Backfill example_tickers for rows that already existed before this column was added.
try {
db.prepare(`UPDATE strategy_presets SET example_tickers=? WHERE id=? AND example_tickers IS NULL`)
.run(p.example_tickers, p.id);
} catch { /* ignore */ }
}
// Dealer Flow Study Desk (educational practice setups + auto-grade).
try {
db.exec(`
CREATE TABLE IF NOT EXISTS dealer_study_setups (
id TEXT PRIMARY KEY,
owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
symbol TEXT NOT NULL,
hypothesis_id TEXT NOT NULL,
title TEXT NOT NULL,
bias TEXT NOT NULL,
map_reading TEXT NOT NULL,
study_question TEXT NOT NULL,
entry REAL NOT NULL,
invalidation REAL NOT NULL,
target REAL NOT NULL,
horizon_days INTEGER NOT NULL,
confidence REAL,
map_snapshot TEXT NOT NULL DEFAULT '{}',
status TEXT NOT NULL DEFAULT 'open',
grade TEXT,
score REAL,
grade_summary TEXT,
grade_path TEXT,
graded_at TEXT,
logged_at TEXT NOT NULL,
disclaimer TEXT NOT NULL
)
`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_dealer_study_owner ON dealer_study_setups(owner_id, logged_at DESC)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_dealer_study_symbol ON dealer_study_setups(owner_id, symbol)`);
} catch { /* ignore */ }
// Mentor call ledger (local path-match grades for harvested sources).
try {
db.exec(`
CREATE TABLE IF NOT EXISTS mentor_sources (
handle TEXT PRIMARY KEY,
enabled INTEGER NOT NULL DEFAULT 1,
notes TEXT,
updated_at TEXT NOT NULL
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS mentor_calls (
id TEXT PRIMARY KEY,
owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
handle TEXT NOT NULL,
post_id TEXT NOT NULL,
posted_at TEXT,
symbol TEXT,
bias TEXT NOT NULL,
claim_type TEXT NOT NULL,
entry REAL NOT NULL,
invalidation REAL NOT NULL,
target REAL NOT NULL,
horizon_days INTEGER NOT NULL,
excerpt TEXT,
extract_confidence REAL,
status TEXT NOT NULL DEFAULT 'draft',
grade TEXT,
score REAL,
grade_summary TEXT,
grade_path TEXT,
graded_at TEXT,
logged_at TEXT NOT NULL,
UNIQUE(owner_id, handle, post_id)
)
`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_mentor_calls_owner ON mentor_calls(owner_id, logged_at DESC)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_mentor_calls_handle ON mentor_calls(owner_id, handle)`);
} catch { /* ignore */ }
}
let _db: DatabaseSync | null = null;
@@ -257,10 +589,37 @@ export function db(): DatabaseSync {
_db.prepare("INSERT OR IGNORE INTO users (id, email, pw_hash, created_at) VALUES (?, ?, ?, ?)")
.run('anonymous', 'anonymous@investor-flow.local', '', new Date().toISOString());
} catch { /* table may not exist yet or already exists */ }
seedTrackedFunds(_db);
}
return _db;
}
/** Seed the operator-curated tracked-fund registry (v1: Alpine Fox Capital LLC). */
function seedTrackedFunds(db: DatabaseSync): void {
const alpine: (string | number | null)[] = [
'alpine-fox-capital',
'0002096493', // SEC CIK (filer key), Alpine Fox Capital LLC
'Alpine Fox Capital LLC',
'Mike Alfred',
'mikealfred', // manager X handle (no @)
'paywalled', // verified: subscribe-only capture posts return tombstones
1,
new Date().toISOString(),
null,
];
try {
db.prepare(`INSERT OR IGNORE INTO tracked_funds
(id, ci_key, fund_name, manager_name, x_handle, paywall_status, enabled, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?)`).run(...alpine);
} catch { /* row may already exist */ }
// Keep the hero row (fund) in sync with changes we may want to ship.
try {
db.prepare(`UPDATE tracked_funds SET
x_handle='mikealfred', manager_name='Mike Alfred', paywall_status='paywalled', enabled=1
WHERE id='alpine-fox-capital'`).run();
} catch { /* ignore */ }
}
/** CLI entry: `node src/db/client.ts` initializes the on-disk database. */
const isMain = process.argv[1] ? resolve(process.argv[1]) === fileURLToPath(import.meta.url) : false;
if (isMain) {