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:
+360
-1
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user